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.
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