utopia 0.12.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -2
  3. data/Gemfile +6 -0
  4. data/README.md +48 -14
  5. data/Rakefile +5 -0
  6. data/bin/utopia +132 -15
  7. data/lib/utopia.rb +13 -10
  8. data/lib/utopia/content.rb +140 -0
  9. data/lib/utopia/content/link.rb +124 -0
  10. data/lib/utopia/content/links.rb +228 -0
  11. data/lib/utopia/content/node.rb +387 -0
  12. data/lib/utopia/content/processor.rb +128 -0
  13. data/lib/utopia/content/tag.rb +102 -0
  14. data/lib/utopia/controller.rb +137 -0
  15. data/lib/utopia/controller/action.rb +112 -0
  16. data/lib/utopia/controller/base.rb +174 -0
  17. data/lib/utopia/{middleware/controller → controller}/variables.rb +36 -38
  18. data/lib/utopia/exception_handler.rb +79 -0
  19. data/lib/utopia/extensions/array.rb +2 -2
  20. data/lib/utopia/localization.rb +143 -0
  21. data/lib/utopia/mail_exceptions.rb +136 -0
  22. data/lib/utopia/middleware.rb +7 -22
  23. data/lib/utopia/path.rb +150 -60
  24. data/lib/utopia/redirector.rb +152 -0
  25. data/lib/utopia/{extensions/hash.rb → session.rb} +4 -6
  26. data/lib/utopia/session/encrypted_cookie.rb +46 -48
  27. data/lib/utopia/{middleware/directory_index.rb → session/lazy_hash.rb} +44 -27
  28. data/lib/utopia/static.rb +255 -0
  29. data/lib/utopia/tags/deferred.rb +12 -8
  30. data/lib/utopia/tags/environment.rb +18 -6
  31. data/lib/utopia/tags/node.rb +12 -8
  32. data/lib/utopia/tags/override.rb +12 -12
  33. data/lib/utopia/version.rb +1 -1
  34. data/setup/.bowerrc +3 -0
  35. data/{lib/utopia/setup → setup}/Gemfile +1 -1
  36. data/setup/Rakefile +4 -0
  37. data/{lib/utopia/setup → setup}/cache/head/readme.txt +0 -0
  38. data/{lib/utopia/setup → setup}/cache/meta/readme.txt +0 -0
  39. data/setup/config.ru +64 -0
  40. data/{lib/utopia/setup → setup}/lib/readme.txt +0 -0
  41. data/{lib/utopia/setup → setup}/pages/_heading.xnode +0 -0
  42. data/{lib/utopia/setup → setup}/pages/_page.xnode +1 -1
  43. data/{lib/utopia/setup → setup}/pages/_static/icon.png +0 -0
  44. data/setup/pages/_static/site.css +70 -0
  45. data/{lib/utopia/setup → setup}/pages/errors/exception.xnode +0 -0
  46. data/{lib/utopia/setup → setup}/pages/errors/file-not-found.xnode +0 -0
  47. data/{lib/utopia/setup → setup}/pages/links.yaml +0 -0
  48. data/setup/pages/welcome/index.xnode +17 -0
  49. data/{lib/utopia/setup → setup}/public/readme.txt +0 -0
  50. data/spec/utopia/content/link_spec.rb +108 -0
  51. data/spec/utopia/content/links/foo/index.xnode +0 -0
  52. data/spec/utopia/content/links/foo/links.yaml +2 -0
  53. data/spec/utopia/content/links/foo/test.de.xnode +0 -0
  54. data/spec/utopia/content/links/foo/test.en.xnode +0 -0
  55. data/spec/utopia/content/links/links.yaml +9 -0
  56. data/spec/utopia/content/links/welcome.xnode +0 -0
  57. data/spec/utopia/content/localized/five/index.en.xnode +0 -0
  58. data/spec/utopia/content/localized/four/index.en.xnode +0 -0
  59. data/spec/utopia/content/localized/four/index.zh.xnode +0 -0
  60. data/spec/utopia/content/localized/four/links.yaml +4 -0
  61. data/spec/utopia/content/localized/links.yaml +16 -0
  62. data/spec/utopia/content/localized/one.xnode +0 -0
  63. data/spec/utopia/content/localized/three/index.xnode +0 -0
  64. data/spec/utopia/content/localized/two.en.xnode +0 -0
  65. data/spec/utopia/content/localized/two.zh.xnode +0 -0
  66. data/spec/utopia/content/node/ordered/first.xnode +0 -0
  67. data/spec/utopia/content/node/ordered/index.xnode +0 -0
  68. data/spec/utopia/content/node/ordered/links.yaml +4 -0
  69. data/spec/utopia/content/node/ordered/second.xnode +0 -0
  70. data/spec/utopia/content/node/related/foo.en.xnode +0 -0
  71. data/spec/utopia/content/node/related/foo.ja.xnode +0 -0
  72. data/spec/utopia/content/node/related/links.yaml +4 -0
  73. data/spec/utopia/content/node_spec.rb +63 -0
  74. data/spec/utopia/{middleware/content_spec.rb → content/processor_spec.rb} +34 -23
  75. data/spec/utopia/content_spec.rb +87 -0
  76. data/spec/utopia/content_spec.ru +10 -0
  77. data/spec/utopia/{middleware/controller_spec.rb → controller_spec.rb} +61 -16
  78. data/spec/utopia/controller_spec.ru +4 -0
  79. data/spec/utopia/extensions_spec.rb +6 -17
  80. data/spec/utopia/localization_spec.rb +60 -0
  81. data/spec/utopia/localization_spec.ru +11 -0
  82. data/{lib/utopia/tags.rb → spec/utopia/middleware_spec.rb} +8 -14
  83. data/spec/utopia/{middleware/content_root → pages}/_heading.xnode +0 -0
  84. data/spec/utopia/pages/content/_show-value.xnode +1 -0
  85. data/spec/utopia/pages/content/test-partial.xnode +1 -0
  86. data/spec/utopia/pages/controller/controller.rb +28 -0
  87. data/spec/utopia/pages/controller/index.xnode +1 -0
  88. data/spec/utopia/pages/controller/nested/controller.rb +4 -0
  89. data/spec/utopia/{middleware/content_root → pages}/index.xnode +0 -0
  90. data/spec/utopia/pages/localized.de.txt +1 -0
  91. data/spec/utopia/pages/localized.en.txt +1 -0
  92. data/spec/utopia/pages/localized.jp.txt +1 -0
  93. data/spec/utopia/pages/node/index.xnode +1 -0
  94. data/spec/utopia/pages/test.txt +1 -0
  95. data/spec/utopia/path_spec.rb +109 -0
  96. data/spec/utopia/rack_spec.rb +2 -0
  97. data/spec/utopia/session_spec.rb +82 -0
  98. data/spec/utopia/session_spec.ru +20 -0
  99. data/spec/utopia/spec_helper.rb +16 -0
  100. data/{lib/utopia/extensions/string.rb → spec/utopia/static_spec.rb} +24 -15
  101. data/spec/utopia/static_spec.ru +4 -0
  102. data/utopia.gemspec +3 -3
  103. metadata +138 -54
  104. data/lib/utopia/extensions/regexp.rb +0 -33
  105. data/lib/utopia/link.rb +0 -288
  106. data/lib/utopia/middleware/all.rb +0 -33
  107. data/lib/utopia/middleware/content.rb +0 -157
  108. data/lib/utopia/middleware/content/node.rb +0 -386
  109. data/lib/utopia/middleware/content/processor.rb +0 -123
  110. data/lib/utopia/middleware/controller.rb +0 -130
  111. data/lib/utopia/middleware/controller/action.rb +0 -121
  112. data/lib/utopia/middleware/controller/base.rb +0 -184
  113. data/lib/utopia/middleware/exception_handler.rb +0 -80
  114. data/lib/utopia/middleware/localization.rb +0 -147
  115. data/lib/utopia/middleware/localization/name.rb +0 -69
  116. data/lib/utopia/middleware/mail_exceptions.rb +0 -138
  117. data/lib/utopia/middleware/redirector.rb +0 -146
  118. data/lib/utopia/middleware/requester.rb +0 -126
  119. data/lib/utopia/middleware/static.rb +0 -295
  120. data/lib/utopia/setup.rb +0 -60
  121. data/lib/utopia/setup/config.ru +0 -47
  122. data/lib/utopia/setup/pages/_static/background.png +0 -0
  123. data/lib/utopia/setup/pages/_static/site.css +0 -48
  124. data/lib/utopia/setup/pages/welcome/index.xnode +0 -7
  125. data/lib/utopia/tag.rb +0 -105
  126. 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