utopia 0.12.6 → 1.0.0
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.
- checksums.yaml +4 -4
- data/.travis.yml +6 -2
- data/Gemfile +6 -0
- data/README.md +48 -14
- data/Rakefile +5 -0
- data/bin/utopia +132 -15
- data/lib/utopia.rb +13 -10
- data/lib/utopia/content.rb +140 -0
- data/lib/utopia/content/link.rb +124 -0
- data/lib/utopia/content/links.rb +228 -0
- data/lib/utopia/content/node.rb +387 -0
- data/lib/utopia/content/processor.rb +128 -0
- data/lib/utopia/content/tag.rb +102 -0
- data/lib/utopia/controller.rb +137 -0
- data/lib/utopia/controller/action.rb +112 -0
- data/lib/utopia/controller/base.rb +174 -0
- data/lib/utopia/{middleware/controller → controller}/variables.rb +36 -38
- data/lib/utopia/exception_handler.rb +79 -0
- data/lib/utopia/extensions/array.rb +2 -2
- data/lib/utopia/localization.rb +143 -0
- data/lib/utopia/mail_exceptions.rb +136 -0
- data/lib/utopia/middleware.rb +7 -22
- data/lib/utopia/path.rb +150 -60
- data/lib/utopia/redirector.rb +152 -0
- data/lib/utopia/{extensions/hash.rb → session.rb} +4 -6
- data/lib/utopia/session/encrypted_cookie.rb +46 -48
- data/lib/utopia/{middleware/directory_index.rb → session/lazy_hash.rb} +44 -27
- data/lib/utopia/static.rb +255 -0
- data/lib/utopia/tags/deferred.rb +12 -8
- data/lib/utopia/tags/environment.rb +18 -6
- data/lib/utopia/tags/node.rb +12 -8
- data/lib/utopia/tags/override.rb +12 -12
- data/lib/utopia/version.rb +1 -1
- data/setup/.bowerrc +3 -0
- data/{lib/utopia/setup → setup}/Gemfile +1 -1
- data/setup/Rakefile +4 -0
- data/{lib/utopia/setup → setup}/cache/head/readme.txt +0 -0
- data/{lib/utopia/setup → setup}/cache/meta/readme.txt +0 -0
- data/setup/config.ru +64 -0
- data/{lib/utopia/setup → setup}/lib/readme.txt +0 -0
- data/{lib/utopia/setup → setup}/pages/_heading.xnode +0 -0
- data/{lib/utopia/setup → setup}/pages/_page.xnode +1 -1
- data/{lib/utopia/setup → setup}/pages/_static/icon.png +0 -0
- data/setup/pages/_static/site.css +70 -0
- data/{lib/utopia/setup → setup}/pages/errors/exception.xnode +0 -0
- data/{lib/utopia/setup → setup}/pages/errors/file-not-found.xnode +0 -0
- data/{lib/utopia/setup → setup}/pages/links.yaml +0 -0
- data/setup/pages/welcome/index.xnode +17 -0
- data/{lib/utopia/setup → setup}/public/readme.txt +0 -0
- data/spec/utopia/content/link_spec.rb +108 -0
- data/spec/utopia/content/links/foo/index.xnode +0 -0
- data/spec/utopia/content/links/foo/links.yaml +2 -0
- data/spec/utopia/content/links/foo/test.de.xnode +0 -0
- data/spec/utopia/content/links/foo/test.en.xnode +0 -0
- data/spec/utopia/content/links/links.yaml +9 -0
- data/spec/utopia/content/links/welcome.xnode +0 -0
- data/spec/utopia/content/localized/five/index.en.xnode +0 -0
- data/spec/utopia/content/localized/four/index.en.xnode +0 -0
- data/spec/utopia/content/localized/four/index.zh.xnode +0 -0
- data/spec/utopia/content/localized/four/links.yaml +4 -0
- data/spec/utopia/content/localized/links.yaml +16 -0
- data/spec/utopia/content/localized/one.xnode +0 -0
- data/spec/utopia/content/localized/three/index.xnode +0 -0
- data/spec/utopia/content/localized/two.en.xnode +0 -0
- data/spec/utopia/content/localized/two.zh.xnode +0 -0
- data/spec/utopia/content/node/ordered/first.xnode +0 -0
- data/spec/utopia/content/node/ordered/index.xnode +0 -0
- data/spec/utopia/content/node/ordered/links.yaml +4 -0
- data/spec/utopia/content/node/ordered/second.xnode +0 -0
- data/spec/utopia/content/node/related/foo.en.xnode +0 -0
- data/spec/utopia/content/node/related/foo.ja.xnode +0 -0
- data/spec/utopia/content/node/related/links.yaml +4 -0
- data/spec/utopia/content/node_spec.rb +63 -0
- data/spec/utopia/{middleware/content_spec.rb → content/processor_spec.rb} +34 -23
- data/spec/utopia/content_spec.rb +87 -0
- data/spec/utopia/content_spec.ru +10 -0
- data/spec/utopia/{middleware/controller_spec.rb → controller_spec.rb} +61 -16
- data/spec/utopia/controller_spec.ru +4 -0
- data/spec/utopia/extensions_spec.rb +6 -17
- data/spec/utopia/localization_spec.rb +60 -0
- data/spec/utopia/localization_spec.ru +11 -0
- data/{lib/utopia/tags.rb → spec/utopia/middleware_spec.rb} +8 -14
- data/spec/utopia/{middleware/content_root → pages}/_heading.xnode +0 -0
- data/spec/utopia/pages/content/_show-value.xnode +1 -0
- data/spec/utopia/pages/content/test-partial.xnode +1 -0
- data/spec/utopia/pages/controller/controller.rb +28 -0
- data/spec/utopia/pages/controller/index.xnode +1 -0
- data/spec/utopia/pages/controller/nested/controller.rb +4 -0
- data/spec/utopia/{middleware/content_root → pages}/index.xnode +0 -0
- data/spec/utopia/pages/localized.de.txt +1 -0
- data/spec/utopia/pages/localized.en.txt +1 -0
- data/spec/utopia/pages/localized.jp.txt +1 -0
- data/spec/utopia/pages/node/index.xnode +1 -0
- data/spec/utopia/pages/test.txt +1 -0
- data/spec/utopia/path_spec.rb +109 -0
- data/spec/utopia/rack_spec.rb +2 -0
- data/spec/utopia/session_spec.rb +82 -0
- data/spec/utopia/session_spec.ru +20 -0
- data/spec/utopia/spec_helper.rb +16 -0
- data/{lib/utopia/extensions/string.rb → spec/utopia/static_spec.rb} +24 -15
- data/spec/utopia/static_spec.ru +4 -0
- data/utopia.gemspec +3 -3
- metadata +138 -54
- data/lib/utopia/extensions/regexp.rb +0 -33
- data/lib/utopia/link.rb +0 -288
- data/lib/utopia/middleware/all.rb +0 -33
- data/lib/utopia/middleware/content.rb +0 -157
- data/lib/utopia/middleware/content/node.rb +0 -386
- data/lib/utopia/middleware/content/processor.rb +0 -123
- data/lib/utopia/middleware/controller.rb +0 -130
- data/lib/utopia/middleware/controller/action.rb +0 -121
- data/lib/utopia/middleware/controller/base.rb +0 -184
- data/lib/utopia/middleware/exception_handler.rb +0 -80
- data/lib/utopia/middleware/localization.rb +0 -147
- data/lib/utopia/middleware/localization/name.rb +0 -69
- data/lib/utopia/middleware/mail_exceptions.rb +0 -138
- data/lib/utopia/middleware/redirector.rb +0 -146
- data/lib/utopia/middleware/requester.rb +0 -126
- data/lib/utopia/middleware/static.rb +0 -295
- data/lib/utopia/setup.rb +0 -60
- data/lib/utopia/setup/config.ru +0 -47
- data/lib/utopia/setup/pages/_static/background.png +0 -0
- data/lib/utopia/setup/pages/_static/site.css +0 -48
- data/lib/utopia/setup/pages/welcome/index.xnode +0 -7
- data/lib/utopia/tag.rb +0 -105
- data/lib/utopia/tags/all.rb +0 -34
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
require 'yaml'
|
|
22
|
+
require 'trenni/builder'
|
|
23
|
+
|
|
24
|
+
require_relative '../content'
|
|
25
|
+
require_relative '../path'
|
|
26
|
+
|
|
27
|
+
module Utopia
|
|
28
|
+
class Content
|
|
29
|
+
class Link
|
|
30
|
+
def initialize(kind, path, info = nil)
|
|
31
|
+
path = Path.create(path)
|
|
32
|
+
|
|
33
|
+
@info = info || {}
|
|
34
|
+
@kind = kind
|
|
35
|
+
|
|
36
|
+
case @kind
|
|
37
|
+
when :file
|
|
38
|
+
@name, @variant = path.last.split('.', 2)
|
|
39
|
+
@path = path
|
|
40
|
+
when :directory
|
|
41
|
+
# raise ArgumentError unless path.last.start_with? 'index'
|
|
42
|
+
|
|
43
|
+
@name = path.dirname.last
|
|
44
|
+
@variant = path.last.split('.', 2)[1]
|
|
45
|
+
@path = path
|
|
46
|
+
when :virtual
|
|
47
|
+
@name, @variant = path.to_s.split('.', 2)
|
|
48
|
+
@path = @info[:path] ? Path.create(@info[:path]) : nil
|
|
49
|
+
else
|
|
50
|
+
raise ArgumentError.new("Unknown link kind #{@kind} with path #{path}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@title = Trenni::Strings.to_title(@name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def href
|
|
57
|
+
@href ||= @info.fetch(:uri) do
|
|
58
|
+
(@path.dirname + @path.basename.parts[0]).to_s if @path
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def [] key
|
|
63
|
+
@info[key]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
attr :kind
|
|
67
|
+
attr :name
|
|
68
|
+
attr :path
|
|
69
|
+
attr :info
|
|
70
|
+
attr :variant
|
|
71
|
+
|
|
72
|
+
def href?
|
|
73
|
+
!!href
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def relative_href(base = nil)
|
|
77
|
+
if base and href.start_with? '/'
|
|
78
|
+
Path.shortest_path(href, base)
|
|
79
|
+
else
|
|
80
|
+
href
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def title
|
|
85
|
+
@info.fetch(:title, @title)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_href(options = {})
|
|
89
|
+
Trenni::Builder.fragment(options[:builder]) do |builder|
|
|
90
|
+
if href?
|
|
91
|
+
relative_href(options[:base])
|
|
92
|
+
|
|
93
|
+
builder.inline('a', class: options.fetch(:class, 'link'), href: relative_href(options[:base])) do
|
|
94
|
+
builder.text(options[:content] || title)
|
|
95
|
+
end
|
|
96
|
+
else
|
|
97
|
+
builder.inline('span', class: options.fetch(:class, 'link')) do
|
|
98
|
+
builder.text(options[:content] || title)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def eql? other
|
|
105
|
+
if other && self.class == other.class
|
|
106
|
+
return kind.eql?(other.kind) &&
|
|
107
|
+
name.eql?(other.name) &&
|
|
108
|
+
path.eql?(other.path) &&
|
|
109
|
+
info.eql?(other.info)
|
|
110
|
+
else
|
|
111
|
+
return false
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def == other
|
|
116
|
+
return other && kind == other.kind && name == other.name && path == other.path
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def default_locale?
|
|
120
|
+
@locale == nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
require_relative 'link'
|
|
22
|
+
|
|
23
|
+
module Utopia
|
|
24
|
+
class Content
|
|
25
|
+
XNODE_EXTENSION = '.xnode'.freeze
|
|
26
|
+
|
|
27
|
+
# Links are essentially a static list of information relating to the structure of the content. They are formed from the `links.yaml` file and the actual files on disk.
|
|
28
|
+
class Links
|
|
29
|
+
def self.for(root, path, variant = nil)
|
|
30
|
+
links = self.new(root, path.dirname)
|
|
31
|
+
|
|
32
|
+
links.lookup(path.last, variant)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
DEFAULT_INDEX_OPTIONS = {
|
|
36
|
+
:directories => true,
|
|
37
|
+
:files => true,
|
|
38
|
+
:virtuals => true,
|
|
39
|
+
:indices => false,
|
|
40
|
+
:sort => :order,
|
|
41
|
+
:display => :display,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def self.index(root, path, options = {})
|
|
45
|
+
options = DEFAULT_INDEX_OPTIONS.merge(options)
|
|
46
|
+
|
|
47
|
+
ordered = self.new(root, path, options).ordered
|
|
48
|
+
|
|
49
|
+
# This option filters a link based on the display parameter.
|
|
50
|
+
if display_key = options[:display]
|
|
51
|
+
ordered.reject!{|link| link.info[display_key] == false}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Named:
|
|
55
|
+
if name = options[:name]
|
|
56
|
+
ordered.select!{|link| link.name[options[:name]]}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if variant = options[:variant]
|
|
60
|
+
variants = {}
|
|
61
|
+
|
|
62
|
+
ordered.each do |link|
|
|
63
|
+
if link.variant == variant
|
|
64
|
+
variants[link.name] = link
|
|
65
|
+
elsif link.variant == nil
|
|
66
|
+
variants[link.name] ||= link
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
ordered = variants.values
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Sort:
|
|
74
|
+
if sort_key = options[:sort]
|
|
75
|
+
# Sort by sort_key, otherwise by title.
|
|
76
|
+
ordered.sort_by!{|link| [link[sort_key] || options[:sort_default] || 0, link.title]}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
return ordered
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
XNODE_FILTER = /^(.+)#{Regexp.escape XNODE_EXTENSION}$/
|
|
83
|
+
INDEX_XNODE_FILTER = /^(index(\..+)*)#{Regexp.escape XNODE_EXTENSION}$/
|
|
84
|
+
LINKS_YAML = "links.yaml"
|
|
85
|
+
|
|
86
|
+
DEFAULT_OPTIONS = {
|
|
87
|
+
:directories => true,
|
|
88
|
+
:files => true,
|
|
89
|
+
:virtuals => true,
|
|
90
|
+
:indices => true,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def initialize(root, top = Path.new, options = DEFAULT_OPTIONS)
|
|
94
|
+
@top = top
|
|
95
|
+
@options = options
|
|
96
|
+
|
|
97
|
+
@path = File.join(root, top.components)
|
|
98
|
+
@metadata = self.class.metadata(@path)
|
|
99
|
+
|
|
100
|
+
@ordered = []
|
|
101
|
+
@named = Hash.new{|h,k| h[k] = []}
|
|
102
|
+
|
|
103
|
+
if File.directory? @path
|
|
104
|
+
load_links(@metadata.dup) do |link|
|
|
105
|
+
@ordered << link
|
|
106
|
+
@named[link.name] << link
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
attr :top
|
|
112
|
+
attr :ordered
|
|
113
|
+
attr :named
|
|
114
|
+
|
|
115
|
+
def each(variant)
|
|
116
|
+
return to_enum(:each, variant) unless block_given?
|
|
117
|
+
|
|
118
|
+
ordered.each do |links|
|
|
119
|
+
yield links.find{|link| link.variant == variant}
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def lookup(name, variant = nil)
|
|
124
|
+
# This allows generic links to serve any variant requested.
|
|
125
|
+
if links = @named[name]
|
|
126
|
+
links.find{|link| link.variant == variant} || links.find{|link| link.variant == nil}
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def self.symbolize_keys(hash)
|
|
133
|
+
# Second level attributes should be symbolic:
|
|
134
|
+
hash.each do |key, info|
|
|
135
|
+
hash[key] = info.each_with_object({}) { |(k,v),result| result[k.to_sym] = v }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
return hash
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.metadata(path)
|
|
142
|
+
links_path = File.join(path, LINKS_YAML)
|
|
143
|
+
|
|
144
|
+
hash = if File.exist?(links_path)
|
|
145
|
+
YAML::load(File.read(links_path)) || {}
|
|
146
|
+
else
|
|
147
|
+
{}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
return symbolize_keys(hash)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def indices(path, &block)
|
|
154
|
+
Dir.entries(path).reject{|filename| !filename.match(INDEX_XNODE_FILTER)}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def load_indices(name, path, metadata)
|
|
158
|
+
directory_metadata = metadata.delete(name) || {}
|
|
159
|
+
indices_metadata = Links.metadata(path)
|
|
160
|
+
|
|
161
|
+
indices_count = 0
|
|
162
|
+
|
|
163
|
+
indices(path).each do |filename|
|
|
164
|
+
index_name = File.basename(filename, XNODE_EXTENSION)
|
|
165
|
+
# Values in indices_metadata will override values in directory_metadata:
|
|
166
|
+
index_metadata = directory_metadata.merge(indices_metadata[index_name] || {})
|
|
167
|
+
|
|
168
|
+
directory_link = Link.new(:directory, @top + [name, index_name], index_metadata)
|
|
169
|
+
|
|
170
|
+
# Merge metadata from foo.en into foo/index.en
|
|
171
|
+
if directory_link.variant
|
|
172
|
+
if variant_metadata = metadata.delete(directory_link.name + '.' + directory_link.variant)
|
|
173
|
+
directory_link.info.update(variant_metadata)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
yield directory_link
|
|
178
|
+
|
|
179
|
+
indices_count += 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if indices_count == 0
|
|
183
|
+
# Specify a nil uri if no index could be found for the directory:
|
|
184
|
+
yield Link.new(:directory, top + [name, ""], {:uri => nil}.merge(directory_metadata))
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def entries(path)
|
|
189
|
+
Dir.entries(path).reject{|filename| filename.match(/^[\._]/)}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def load_links(metadata, &block)
|
|
193
|
+
# Load all metadata for a given path:
|
|
194
|
+
metadata = @metadata.dup
|
|
195
|
+
|
|
196
|
+
# Check all entries in the given directory:
|
|
197
|
+
entries(@path).each do |filename|
|
|
198
|
+
path = File.join(@path, filename)
|
|
199
|
+
|
|
200
|
+
# There are two types of filesystem based links:
|
|
201
|
+
# 1/ Named files, e.g. foo.xnode, name=foo
|
|
202
|
+
# 2/ Directories, e.g. bar/index.xnode, name=bar
|
|
203
|
+
if File.directory?(path) and @options[:directories]
|
|
204
|
+
load_indices(filename, path, metadata, &block)
|
|
205
|
+
elsif filename.match(INDEX_XNODE_FILTER) and @options[:indices] == false
|
|
206
|
+
metadata.delete($1) # We don't include indices in the list of pages.
|
|
207
|
+
elsif filename.match(XNODE_FILTER) and @options[:files]
|
|
208
|
+
yield Link.new(:file, @top + $1, metadata.delete($1))
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if @options[:virtuals]
|
|
213
|
+
# After processing all directory entries, we are left with virtual entries in the metadata:
|
|
214
|
+
metadata.each do |name, info|
|
|
215
|
+
virtual_link = Link.new(:virtual, name, info)
|
|
216
|
+
|
|
217
|
+
# Given a virtual named such as "welcome.cn", merge it with metadata from "welcome" if it exists:
|
|
218
|
+
if virtual_metadata = @metadata[virtual_link.name]
|
|
219
|
+
virtual_link.info.update(virtual_metadata)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
yield virtual_link
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
require 'set'
|
|
22
|
+
|
|
23
|
+
require_relative 'processor'
|
|
24
|
+
require_relative 'links'
|
|
25
|
+
|
|
26
|
+
module Utopia
|
|
27
|
+
class Content
|
|
28
|
+
class UnbalancedTagError < StandardError
|
|
29
|
+
def initialize(tag)
|
|
30
|
+
@tag = tag
|
|
31
|
+
|
|
32
|
+
super("Unbalanced tag #{tag.name}")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr :tag
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A single request through content middleware.
|
|
39
|
+
class Transaction
|
|
40
|
+
# The state of a single tag being rendered.
|
|
41
|
+
class State
|
|
42
|
+
def initialize(tag, node)
|
|
43
|
+
@node = node
|
|
44
|
+
|
|
45
|
+
@buffer = StringIO.new
|
|
46
|
+
@overrides = {}
|
|
47
|
+
|
|
48
|
+
@tags = []
|
|
49
|
+
@attributes = tag.to_hash
|
|
50
|
+
|
|
51
|
+
@content = nil
|
|
52
|
+
@deferred = []
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
attr :attributes
|
|
56
|
+
attr :overrides
|
|
57
|
+
attr :content
|
|
58
|
+
attr :node
|
|
59
|
+
attr :tags
|
|
60
|
+
|
|
61
|
+
attr :deferred
|
|
62
|
+
|
|
63
|
+
def defer(value = nil, &block)
|
|
64
|
+
@deferred << block
|
|
65
|
+
|
|
66
|
+
Tag.closed("deferred", :id => @deferred.size - 1).to_html
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def [](key)
|
|
70
|
+
@attributes[key.to_s]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def call(transaction)
|
|
74
|
+
@content = @buffer.string
|
|
75
|
+
@buffer = StringIO.new
|
|
76
|
+
|
|
77
|
+
if node.respond_to? :call
|
|
78
|
+
node.call(transaction, self)
|
|
79
|
+
else
|
|
80
|
+
transaction.parse_xml(@content)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
return @buffer.string
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def lookup(tag)
|
|
87
|
+
if override = @overrides[tag.name]
|
|
88
|
+
if override.respond_to? :call
|
|
89
|
+
return override.call(tag)
|
|
90
|
+
elsif String === override
|
|
91
|
+
return Tag.new(override, tag.attributes)
|
|
92
|
+
else
|
|
93
|
+
return override
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
return tag
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def cdata(text)
|
|
101
|
+
@buffer.write(text)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def markup(text)
|
|
105
|
+
cdata(text)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def tag_complete(tag)
|
|
109
|
+
tag.write_full_html(@buffer)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def tag_begin(tag)
|
|
113
|
+
@tags << tag
|
|
114
|
+
tag.write_open_html(@buffer)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def tag_end(tag)
|
|
118
|
+
raise UnbalancedTagError(tag) unless @tags.pop.name == tag.name
|
|
119
|
+
|
|
120
|
+
tag.write_close_html(@buffer)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def initialize(request, response)
|
|
125
|
+
@begin_tags = []
|
|
126
|
+
@end_tags = []
|
|
127
|
+
|
|
128
|
+
@request = request
|
|
129
|
+
@response = response
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
attr :request
|
|
133
|
+
attr :response
|
|
134
|
+
|
|
135
|
+
# A helper method for accessing controller variables from view:
|
|
136
|
+
def controller
|
|
137
|
+
@request.controller
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_xml(xml_data)
|
|
141
|
+
Processor.parse_xml(xml_data, self)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Begin tags represents a list from outer to inner most tag.
|
|
145
|
+
# At any point in parsing xml, begin_tags is a list of the inner most tag,
|
|
146
|
+
# then the next outer tag, etc. This list is used for doing dependent lookups.
|
|
147
|
+
attr :begin_tags
|
|
148
|
+
|
|
149
|
+
# End tags represents a list of execution order. This is the order that end tags
|
|
150
|
+
# have appeared when evaluating nodes.
|
|
151
|
+
attr :end_tags
|
|
152
|
+
|
|
153
|
+
def attributes
|
|
154
|
+
return current.attributes
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def current
|
|
158
|
+
@begin_tags[-1]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def content
|
|
162
|
+
@end_tags[-1].content
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def parent
|
|
166
|
+
end_tags[-2]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def first
|
|
170
|
+
@begin_tags[0]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def tag(name, attributes = {}, &block)
|
|
174
|
+
tag = Tag.new(name, attributes)
|
|
175
|
+
|
|
176
|
+
node = tag_begin(tag)
|
|
177
|
+
|
|
178
|
+
yield node if block_given?
|
|
179
|
+
|
|
180
|
+
tag_end(tag)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def tag_complete(tag, node = nil)
|
|
184
|
+
if tag.name == "content"
|
|
185
|
+
current.markup(content)
|
|
186
|
+
else
|
|
187
|
+
node ||= lookup(tag)
|
|
188
|
+
|
|
189
|
+
if node
|
|
190
|
+
tag_begin(tag, node)
|
|
191
|
+
tag_end(tag)
|
|
192
|
+
else
|
|
193
|
+
current.tag_complete(tag)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def tag_begin(tag, node = nil)
|
|
199
|
+
node ||= lookup(tag)
|
|
200
|
+
|
|
201
|
+
if node
|
|
202
|
+
state = State.new(tag, node)
|
|
203
|
+
@begin_tags << state
|
|
204
|
+
|
|
205
|
+
if node.respond_to? :tag_begin
|
|
206
|
+
node.tag_begin(self, state)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
return node
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
current.tag_begin(tag)
|
|
213
|
+
|
|
214
|
+
return nil
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def cdata(text)
|
|
218
|
+
current.cdata(text)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def partial(*args, &block)
|
|
222
|
+
if block_given?
|
|
223
|
+
current.defer(&block)
|
|
224
|
+
else
|
|
225
|
+
current.defer do
|
|
226
|
+
tag(*args)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
alias deferred_tag partial
|
|
232
|
+
|
|
233
|
+
def tag_end(tag = nil)
|
|
234
|
+
top = current
|
|
235
|
+
|
|
236
|
+
if top.tags.empty?
|
|
237
|
+
if top.node.respond_to? :tag_end
|
|
238
|
+
top.node.tag_end(self, top)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
@end_tags << top
|
|
242
|
+
buffer = top.call(self)
|
|
243
|
+
|
|
244
|
+
@begin_tags.pop
|
|
245
|
+
@end_tags.pop
|
|
246
|
+
|
|
247
|
+
if current
|
|
248
|
+
current.markup(buffer)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
return buffer
|
|
252
|
+
else
|
|
253
|
+
current.tag_end(tag)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
return nil
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def render_node(node, attributes = {})
|
|
260
|
+
state = State.new(attributes, node)
|
|
261
|
+
@begin_tags << state
|
|
262
|
+
|
|
263
|
+
return tag_end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Takes an instance of Tag
|
|
267
|
+
def lookup(tag)
|
|
268
|
+
result = tag
|
|
269
|
+
node = nil
|
|
270
|
+
|
|
271
|
+
@begin_tags.reverse_each do |state|
|
|
272
|
+
result = state.lookup(result)
|
|
273
|
+
|
|
274
|
+
node ||= state.node if state.node.respond_to? :lookup
|
|
275
|
+
|
|
276
|
+
return result if Node === result
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
@end_tags.reverse_each do |state|
|
|
280
|
+
return state.node.lookup(result) if state.node.respond_to? :lookup
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
return nil
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def method_missing(name, *args)
|
|
287
|
+
@begin_tags.reverse_each do |state|
|
|
288
|
+
if state.node.respond_to? name
|
|
289
|
+
return state.node.send(name, *args)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
super
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
class Node
|
|
298
|
+
def initialize(controller, uri_path, request_path, file_path)
|
|
299
|
+
@controller = controller
|
|
300
|
+
|
|
301
|
+
@uri_path = uri_path
|
|
302
|
+
@request_path = request_path
|
|
303
|
+
@file_path = file_path
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
attr :request_path
|
|
307
|
+
attr :uri_path
|
|
308
|
+
attr :file_path
|
|
309
|
+
|
|
310
|
+
def link
|
|
311
|
+
return Link.new(:file, uri_path)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def lookup_node(path)
|
|
315
|
+
@controller.lookup_node(path)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def local_path(path = ".", base = nil)
|
|
319
|
+
path = Path.create(path)
|
|
320
|
+
root = Pathname.new(@controller.root)
|
|
321
|
+
|
|
322
|
+
if path.absolute?
|
|
323
|
+
return root.join(*path.components)
|
|
324
|
+
else
|
|
325
|
+
base ||= uri_path.dirname
|
|
326
|
+
return root.join(*(base + path).components)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def lookup(tag)
|
|
331
|
+
from_path = parent_path
|
|
332
|
+
|
|
333
|
+
# If the current node is called 'foo', we can't lookup 'foo' in the current directory or we will have infinite recursion.
|
|
334
|
+
if tag.name == @uri_path.basename
|
|
335
|
+
from_path = from_path.dirname
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
return @controller.lookup_tag(tag.name, from_path)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def parent_path
|
|
342
|
+
uri_path.dirname
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def links(path = ".", options = {}, &block)
|
|
346
|
+
path = uri_path.dirname + Path.create(path)
|
|
347
|
+
links = Links.index(@controller.root, path, options)
|
|
348
|
+
|
|
349
|
+
if block_given?
|
|
350
|
+
links.each &block
|
|
351
|
+
else
|
|
352
|
+
links
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def related_links
|
|
357
|
+
name = @uri_path.last.split('.', 2).first
|
|
358
|
+
links = Links.index(@controller.root, uri_path.dirname, :name => name, :indices => true)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def siblings_path
|
|
362
|
+
name = @uri_path.last.split('.', 2).first
|
|
363
|
+
|
|
364
|
+
if name == "index"
|
|
365
|
+
@uri_path.dirname(2)
|
|
366
|
+
else
|
|
367
|
+
@uri_path.dirname
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def sibling_links(options = {})
|
|
372
|
+
return Links.index(@controller.root, siblings_path, options)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def call(transaction, state)
|
|
376
|
+
xml_data = @controller.fetch_xml(@file_path).evaluate(transaction)
|
|
377
|
+
|
|
378
|
+
transaction.parse_xml(xml_data)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def process!(request, response, attributes = {})
|
|
382
|
+
transaction = Transaction.new(request, response)
|
|
383
|
+
response.write(transaction.render_node(self, attributes))
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|