amber 0.2.6

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