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.
@@ -0,0 +1,132 @@
1
+ #
2
+ # A class for a site's configuration.
3
+ # Site configuration file is eval'ed in the context of an instance of SiteConfiguration
4
+ #
5
+ # A site can have multiple sub sites, each with their own configurations (called mount points)
6
+ #
7
+
8
+ require 'pathname'
9
+
10
+ module Amber
11
+ class SiteConfiguration
12
+
13
+ attr_accessor :title
14
+ attr_accessor :pagination_size
15
+ attr_accessor :mount_points
16
+ attr_accessor :locales
17
+ attr_accessor :default_locale
18
+
19
+ attr_accessor :menu
20
+
21
+ attr_accessor :root_dir
22
+ attr_accessor :pages_dir
23
+ attr_accessor :dest_dir
24
+ attr_accessor :config_dir
25
+ attr_accessor :config_file
26
+ attr_accessor :layouts_dir
27
+ attr_accessor :path
28
+ attr_accessor :menu_file
29
+ attr_accessor :locales_dir
30
+ attr_accessor :timestamp
31
+
32
+ extend Forwardable
33
+ def_delegators :@site, :pages, :find_page, :find_pages, :find_page_by_path, :find_page_by_name, :continue_on_error
34
+
35
+ ##
36
+ ## CLASS METHODS
37
+ ##
38
+
39
+ def self.load(site, root_dir, options={})
40
+ SiteConfiguration.new(site, root_dir, options)
41
+ end
42
+
43
+ ##
44
+ ## INSTANCE METHODS
45
+ ##
46
+
47
+ #
48
+ # accepts a file_path to a configuration file.
49
+ #
50
+ def initialize(site, root_dir, options={})
51
+ @site = site
52
+ @root_dir = File.expand_path(find_in_directory_tree('amber', 'config.rb', root_dir))
53
+ if @root_dir == '/'
54
+ puts "Could not find amber/config.rb in the directory tree. Run `amber` from inside an amber website directory"
55
+ exit(1)
56
+ end
57
+ @pages_dir = File.join(@root_dir, 'pages')
58
+ @dest_dir = File.join(@root_dir, 'public')
59
+ @config_dir = File.join(@root_dir, 'amber')
60
+ @config_file = config_path('config.rb')
61
+ @menu_file = config_path('menu.txt')
62
+ @locales_dir = config_path('locales')
63
+ @layouts_dir = config_path('layouts')
64
+ @path = '/'
65
+ @mount_points = []
66
+ @mount_points << self
67
+ @title = "untitled"
68
+ @pagination_size = 20
69
+
70
+ @menu = Menu.new('root')
71
+ @menu.load(@menu_file) if @menu_file
72
+
73
+ self.eval
74
+ self.cleanup
75
+
76
+ reset_timestamp
77
+ Render::Layout.load(@layouts_dir)
78
+ end
79
+
80
+ #def include_site(directory_source, options={})
81
+ # @mount_points << SiteMountPoint.new(self, directory_source, options)
82
+ #end
83
+
84
+ def cleanup
85
+ @locale ||= I18n.default_locale
86
+ I18n.default_locale = @locale
87
+ @locales ||= [@locale]
88
+ @locales.map! {|locale|
89
+ if Amber::POSSIBLE_LANGUAGE_CODES.include?(locale.to_s)
90
+ locale.to_sym
91
+ else
92
+ nil
93
+ end
94
+ }.compact
95
+ end
96
+
97
+ def pages_changed?
98
+ @mount_points.detect {|mp| mp.changed?}
99
+ end
100
+
101
+ def eval
102
+ self.instance_eval(File.read(@config_file), @config_file)
103
+ end
104
+
105
+ def config_path(file)
106
+ path = File.join(@config_dir, file)
107
+ if File.exists?(path)
108
+ path
109
+ else
110
+ nil
111
+ end
112
+ end
113
+
114
+ def reset_timestamp
115
+ @timestamp = File.mtime(@pages_dir)
116
+ end
117
+
118
+ def find_in_directory_tree(target_dir_name, target_file_name, directory_tree=nil)
119
+ search_dir = directory_tree || Dir.pwd
120
+ while search_dir != "/"
121
+ Dir.foreach(search_dir) do |f|
122
+ if f == target_dir_name && File.exists?(File.join(search_dir, f,target_file_name))
123
+ return search_dir
124
+ end
125
+ end
126
+ search_dir = File.dirname(search_dir)
127
+ end
128
+ return search_dir
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,151 @@
1
+ #
2
+ # class StaticPage
3
+ #
4
+ # represents a static website page.
5
+ # see also static_page/*.rb
6
+ #
7
+
8
+ module Amber
9
+ class StaticPage
10
+
11
+ attr_accessor :path, # array of path segments
12
+ :children, # array of child pages
13
+ :name, # the name of the page
14
+ :file_path, #
15
+ :parent, # parent page (nil for root page)
16
+ :mount_point, # associated SiteConfiguration
17
+ :site, # associated Site
18
+ :locales, # currently unused
19
+ :valid # `false` if there is some problem with this page.
20
+
21
+ attr_reader :props # set of page properties (PropertySet)
22
+
23
+ alias_method :valid?, :valid
24
+
25
+ ##
26
+ ## INSTANCE METHODS
27
+ ##
28
+
29
+ FORBIDDEN_PAGE_CHARS_RE = /[A-Z\.\?\|\[\]\{\}\$\^\*~!@#%&='"<>]/
30
+
31
+ def initialize(parent, name, file_path=nil)
32
+ @valid = true
33
+ @children = PageArray.new # array of StaticPages
34
+ @nav_title = {} # key is locale
35
+ @title = {} # key is locale
36
+
37
+ @name, @suffix = parse_source_file_name(name)
38
+
39
+ # set @parent & @path
40
+ if parent
41
+ @parent = parent
42
+ @mount_point = @parent.mount_point
43
+ @parent.add_child(self)
44
+ @path = [@parent.path, @name].flatten.compact
45
+ else
46
+ @path = []
47
+ end
48
+
49
+ if @name =~ FORBIDDEN_PAGE_CHARS_RE
50
+ Amber.logger.error "Illegal page name #{@name} at path /#{self.path.join('/')} -- must not have symbols, uppercase, or periods."
51
+ @valid = false
52
+ end
53
+
54
+ # set the @file_path
55
+ if file_path
56
+ @file_path = file_path
57
+ elsif @parent && @parent.file_path
58
+ @file_path = File.join(@parent.file_path, @name)
59
+ else
60
+ raise 'file path must be specified or in parent'
61
+ end
62
+
63
+ @simple_page = !File.directory?(@file_path)
64
+
65
+ # eval the property headers, if any
66
+ @props = load_properties()
67
+ end
68
+
69
+ def add_child(page)
70
+ @children << page
71
+ end
72
+
73
+ def all_children
74
+ PageArray.new(child_tree.flatten.compact)
75
+ end
76
+
77
+ def inspect
78
+ "<'#{@path.join('/')}' #{children.inspect}>"
79
+ end
80
+
81
+ def title(locale=I18n.locale)
82
+ @title[locale] ||= begin
83
+ @props.prop_with_fallback(locale, [:title, :nav_title]) || @name
84
+ end
85
+ end
86
+
87
+ def nav_title(locale=I18n.locale)
88
+ @nav_title[locale] ||= begin
89
+ @props.prop_with_fallback(locale, [:nav_title, :title]) || @name
90
+ end
91
+ end
92
+
93
+ #
94
+ # returns title iff explicitly set.
95
+ #
96
+ def explicit_title(locale)
97
+ @props.prop_without_inheritance(locale, :title) ||
98
+ @props.prop_without_inheritance(I18n.default_locale, :title)
99
+ end
100
+
101
+ def id
102
+ self.name
103
+ end
104
+
105
+ #
106
+ # returns a child matching +name+, if any.
107
+ #
108
+ def child(name)
109
+ children.detect {|child| child.name == name}
110
+ end
111
+
112
+ def prop(*args)
113
+ @props.prop(*args)
114
+ end
115
+
116
+ #
117
+ # returns an array of normalized aliases based on the :alias property
118
+ # defined for a page.
119
+ #
120
+ # aliases are defined with a leading slash for absolute paths, or without a slash
121
+ # for relative paths. this method converts this to a format that amber uses
122
+ # (all absolute, with no leading slash).
123
+ #
124
+ # currently, we do not maintain per-locale paths or aliases.
125
+ #
126
+ def aliases
127
+ @aliases ||= begin
128
+ if @props.alias.nil?
129
+ []
130
+ else
131
+ @props.alias.collect {|alias_path|
132
+ if alias_path =~ /^\//
133
+ alias_path.sub(/^\//, '')
134
+ elsif @parent
135
+ (@parent.path + [alias_path]).join('/')
136
+ else
137
+ alias_path
138
+ end
139
+ }
140
+ end
141
+ end
142
+ end
143
+
144
+ protected
145
+
146
+ def child_tree
147
+ [self, children.collect{|child| child.child_tree}]
148
+ end
149
+
150
+ end
151
+ end
@@ -0,0 +1,270 @@
1
+ #
2
+ # StaticPage code that interfaces with the filesystem
3
+ #
4
+
5
+ require 'i18n'
6
+ require 'pathname'
7
+ require 'fileutils'
8
+
9
+ module Amber
10
+ class StaticPage
11
+
12
+ public
13
+
14
+ #
15
+ # Recursively decends the directory tree, yielding pages and directories it encounters.
16
+ #
17
+ # yield has two arguments:
18
+ #
19
+ # (1) StaticPage instance, or nil.
20
+ # (2) Directory path string if #1 is nil. Path is relative.
21
+ #
22
+ # Directory paths are dirs in the tree that don't contain any pages.
23
+ #
24
+ def self.scan_directory_tree(parent_page, absolute_dir_path, relative_dir_path, &block)
25
+ Dir.foreach(absolute_dir_path).each do |child_path|
26
+ next if child_path =~ /^\./
27
+ abs_path = File.join(absolute_dir_path, child_path)
28
+ rel_path = File.join(relative_dir_path, child_path)
29
+ if parent_page && is_directory_page?(abs_path)
30
+ child_page = StaticPage.new(parent_page, child_path)
31
+ if child_page.valid?
32
+ yield child_page, nil
33
+ scan_directory_tree(child_page, abs_path, rel_path, &block)
34
+ end
35
+ elsif parent_page && is_simple_page?(abs_path)
36
+ child_page = StaticPage.new(parent_page, child_path)
37
+ if child_page.valid?
38
+ yield child_page, nil
39
+ end
40
+ elsif File.directory?(abs_path)
41
+ yield nil, rel_path
42
+ scan_directory_tree(nil, abs_path, rel_path, &block)
43
+ end
44
+ end
45
+ end
46
+
47
+ def scan_directory_tree(&block)
48
+ StaticPage.scan_directory_tree(self, self.file_path, File.join(self.path), &block)
49
+ end
50
+
51
+ #
52
+ # e.g. /home/user/dev/leap-public-site/app/views/pages/about-us/contact
53
+ #
54
+ #def file_path
55
+ # "#{@mount_point.directory}/#{@path.join('/')}"
56
+ #end
57
+
58
+ #
59
+ # e.g. pages/about-us/contact/en
60
+ # RAILS
61
+ #def template_path(locale=I18n.locale)
62
+ # absolute_template_path(locale)
63
+ # #if @simple_page
64
+ # # "#{@mount_point.relative_directory}/#{@path.join('/')}"
65
+ # #else
66
+ # # "#{@mount_point.relative_directory}/#{@path.join('/')}/#{locale}"
67
+ # #end
68
+ #end
69
+
70
+ #
71
+ # e.g. pages/about-us/contact/en
72
+ # RAILS
73
+ #def absolute_template_path(locale=I18n.locale)
74
+ # if @simple_page
75
+ # "#{@mount_point.pages_dir}/#{@path.join('/')}"
76
+ # else
77
+ # "#{@mount_point.pages_dir}/#{@path.join('/')}/#{locale}"
78
+ # end
79
+ #end
80
+
81
+ #
82
+ # full filesystem path name of the source content file
83
+ # e.g. /home/user/mysite/pages/about-us/contact/en.md
84
+ #
85
+ def content_file(locale)
86
+ content_files[locale] || content_files[I18n.default_locale] || content_files.values.first
87
+ end
88
+
89
+ def content_file_exists?(locale)
90
+ !!content_files[locale]
91
+ end
92
+
93
+ #
94
+ # full filesystem path name of the destination rendered file
95
+ # e.g. /home/user/mysite/public/about-us/contact/index.en.html
96
+ #
97
+ def destination_file(dest_dir, locale)
98
+ if @simple_page
99
+ File.join(dest_dir, "#{File.join(@path)}.#{locale}.html")
100
+ else
101
+ File.join(dest_dir, *@path, "index.#{locale}.html")
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ # e.g. en, de, pt
108
+ LOCALES_RE = /(?<locale>#{Amber::POSSIBLE_LANGUAGE_CODES.join('|')})/
109
+ LOCALES_GLOB = "{#{Amber::POSSIBLE_LANGUAGE_CODES.join(',')}}"
110
+
111
+ # e.g. haml, md, text
112
+ PAGE_SUFFIXES_RE = /(?<suffix>#{Amber::PAGE_SUFFIXES.join('|')})/
113
+ PAGE_SUFFIXES_GLOB = "{#{Amber::PAGE_SUFFIXES.join(',')}}"
114
+
115
+ # e.g. en.haml or es.md or index.pt.text
116
+ LOCALE_FILE_MATCH_RE = /^(index\.)?#{LOCALES_RE}\.#{PAGE_SUFFIXES_RE}$/
117
+ LOCALE_FILE_MATCH_GLOB = "{index.,}#{LOCALES_GLOB}.#{PAGE_SUFFIXES_GLOB}"
118
+
119
+ SIMPLE_FILE_MATCH_RE = lambda {|name| /^(#{Regexp.escape(name)})(\.#{LOCALES_RE})?\.#{PAGE_SUFFIXES_RE}$/ }
120
+
121
+ #
122
+ # returns true if the name of a file could be a 'simple' static page
123
+ # that is not a directory.
124
+ #
125
+ # rules:
126
+ # * we include files that end in appriopriate suffixes
127
+ # * we exclude file names that are locales.
128
+ # * we exclude partials
129
+ #
130
+ def self.is_simple_page?(absolute_path)
131
+ name = File.basename(absolute_path)
132
+ name =~ /\.#{PAGE_SUFFIXES_RE}$/ && name !~ LOCALE_FILE_MATCH_RE && name !~ /^_/
133
+ end
134
+
135
+ def self.is_directory_page?(absolute_path)
136
+ if File.directory?(absolute_path)
137
+ Dir.glob(absolute_path + '/' + LOCALE_FILE_MATCH_GLOB).each do |file|
138
+ return true
139
+ end
140
+ end
141
+ return false
142
+ end
143
+
144
+ #
145
+ # returns [name, suffix]
146
+ # called on new page initialization
147
+ #
148
+ def parse_source_file_name(name)
149
+ matches = name.match(/^(?<name>.*?)(\.#{LOCALES_RE})?(\.#{PAGE_SUFFIXES_RE})$/)
150
+ if matches
151
+ [matches['name'], matches['suffix']]
152
+ else
153
+ [name, nil]
154
+ end
155
+ end
156
+
157
+ #
158
+ # returns the files that compose the content for this page,
159
+ # a different file for each locale (or no locale)
160
+ #
161
+ # returns a hash like so:
162
+ #
163
+ # {
164
+ # :en => '/path/to/page/en.haml',
165
+ # :es => '/path/to/page/index.es.md'
166
+ # }
167
+ #
168
+ # Or this, if page is simple:
169
+ #
170
+ # {
171
+ # :en => '/path/to/page.haml'
172
+ # }
173
+ #
174
+ #
175
+ def content_files
176
+ @content_files ||= begin
177
+ if @simple_page
178
+ directory = File.dirname(@file_path)
179
+ regexp = SIMPLE_FILE_MATCH_RE.call(@name)
180
+ else
181
+ directory = @file_path
182
+ regexp = LOCALE_FILE_MATCH_RE
183
+ end
184
+ hsh = {}
185
+ Dir.foreach(directory) do |file|
186
+ if file && match = regexp.match(file)
187
+ locale = match['locale'] || I18n.default_locale
188
+ hsh[locale.to_sym] = File.join(directory, file)
189
+ end
190
+ end
191
+ hsh
192
+ end
193
+ end
194
+
195
+ #
196
+ # returns an array of files in the folder that corresponds to this page
197
+ # that are not other pages. in other words, the assets in this folder
198
+ #
199
+ # file paths are relative to @file_path
200
+ #
201
+ def asset_files
202
+ if @simple_page
203
+ []
204
+ else
205
+ assets = {}
206
+ Dir.foreach(@file_path).collect { |file|
207
+ if file && file !~ /\.#{PAGE_SUFFIXES_RE}$/
208
+ file unless File.directory?(File.join(@file_path, file))
209
+ end
210
+ }.compact
211
+ end
212
+ end
213
+
214
+ #def self.relative_to_rails_view_root(absolute_path)
215
+ # if Rails.root
216
+ # absolute = Pathname.new(absolute_path)
217
+ # rails_view_root = Pathname.new(Rails.root + 'app/views')
218
+ # absolute.relative_path_from(rails_view_root).to_s
219
+ # end
220
+ #end
221
+
222
+ #
223
+ # scans the source content files for property headers in the form:
224
+ #
225
+ # @variable = 'x'
226
+ # - @variable = 'x'
227
+ #
228
+ # (with or without leading hypen works)
229
+ #
230
+ # this text is extracted and evaluated as ruby to set properties.
231
+ #
232
+ def load_properties
233
+ props = PageProperties.new(self)
234
+ content_files.each do |locale, content_file|
235
+ if File.extname(content_file) == '.haml'
236
+ props.eval(File.read(content_file, :encoding => 'UTF-8'), locale)
237
+ else
238
+ headers = []
239
+ File.open(content_file, :encoding => 'UTF-8') do |f|
240
+ while (line = f.gets) =~ /^(- |)@\w/
241
+ if line !~ /^-/
242
+ line = '- ' + line
243
+ end
244
+ headers << line
245
+ end
246
+ end
247
+ props.eval(headers.join("\n"), locale)
248
+ end
249
+ cleanup_properties(props, locale)
250
+ end
251
+ unless props.prop_without_inheritance(I18n.default_locale, :name)
252
+ props.set_prop(I18n.default_locale, :name, self.name)
253
+ end
254
+ return props
255
+ end
256
+
257
+ def cleanup_properties(props, locale)
258
+ if props.prop(locale, :alias)
259
+ props.set_prop(locale, :alias, [props.prop(locale, :alias)].flatten)
260
+ end
261
+ end
262
+
263
+ # RAILS
264
+ #def is_haml_template?(locale)
265
+ # content_file(locale) =~ /\.haml$/
266
+ # #@suffix == '.haml' || File.exists?(self.absolute_template_path(locale) + '.haml')
267
+ #end
268
+
269
+ end
270
+ end