utopia 1.9.11 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +3 -2
  3. data/.gitignore +4 -1
  4. data/.rspec +1 -0
  5. data/.travis.yml +4 -0
  6. data/.yardopts +2 -0
  7. data/Gemfile +8 -1
  8. data/README.md +2 -2
  9. data/Rakefile +10 -10
  10. data/benchmarks/call_vs_check.rb +36 -0
  11. data/benchmarks/const_vs_hash.rb +33 -0
  12. data/documentation/Gemfile +5 -0
  13. data/documentation/Guardfile +20 -0
  14. data/documentation/config.ru +6 -13
  15. data/documentation/config/puma.rb +20 -0
  16. data/documentation/pages/_editor.xnode +64 -0
  17. data/documentation/pages/_heading.xnode +2 -2
  18. data/documentation/pages/_page.xnode +1 -2
  19. data/documentation/pages/errors/exception.xnode +3 -3
  20. data/documentation/pages/errors/file-not-found.xnode +3 -3
  21. data/documentation/pages/wiki/bower-integration/content.md +1 -1
  22. data/documentation/pages/wiki/content.md +6 -8
  23. data/documentation/pages/wiki/controller.rb +3 -3
  24. data/documentation/pages/wiki/edit.xnode +7 -19
  25. data/documentation/pages/wiki/middleware/content/content.md +4 -10
  26. data/documentation/pages/wiki/{controller → middleware/controller}/actions/content.md +0 -0
  27. data/documentation/pages/wiki/{controller → middleware/controller}/links.yaml +0 -0
  28. data/documentation/pages/wiki/{controller → middleware/controller}/rewrite/content.md +3 -3
  29. data/documentation/pages/wiki/show.xnode +4 -6
  30. data/documentation/pages/wiki/updating-utopia/content.md +55 -0
  31. data/documentation/pages/wiki/your-first-page/content.md +5 -3
  32. data/documentation/public/materials +1 -0
  33. data/lib/utopia.rb +3 -4
  34. data/lib/utopia/command.rb +4 -284
  35. data/lib/utopia/command/server.rb +115 -0
  36. data/lib/utopia/command/setup.rb +78 -0
  37. data/lib/utopia/command/site.rb +183 -0
  38. data/lib/utopia/content.rb +83 -59
  39. data/lib/utopia/content/{transaction.rb → document.rb} +116 -110
  40. data/lib/utopia/content/link.rb +7 -2
  41. data/lib/utopia/content/links.rb +2 -1
  42. data/lib/utopia/content/markup.rb +7 -2
  43. data/lib/utopia/{tags/deferred.rb → content/namespace.rb} +25 -6
  44. data/lib/utopia/content/node.rb +74 -76
  45. data/lib/utopia/content/response.rb +22 -3
  46. data/lib/utopia/content/tags.rb +66 -0
  47. data/lib/utopia/controller.rb +10 -18
  48. data/lib/utopia/controller/actions.rb +10 -0
  49. data/lib/utopia/controller/base.rb +2 -1
  50. data/lib/utopia/controller/respond.rb +1 -1
  51. data/lib/utopia/controller/rewrite.rb +8 -4
  52. data/lib/utopia/exceptions.rb +1 -0
  53. data/lib/utopia/exceptions/handler.rb +7 -2
  54. data/lib/utopia/exceptions/mailer.rb +33 -12
  55. data/lib/utopia/{tags/node.rb → extensions/array_split.rb} +11 -9
  56. data/lib/utopia/{tags/environment.rb → extensions/date_comparisons.rb} +24 -14
  57. data/lib/utopia/http.rb +2 -0
  58. data/lib/utopia/locale.rb +1 -0
  59. data/lib/utopia/localization.rb +37 -28
  60. data/lib/utopia/logger.rb +1 -0
  61. data/lib/utopia/logger/compact_formatter.rb +1 -0
  62. data/lib/utopia/middleware.rb +11 -1
  63. data/lib/utopia/path.rb +1 -0
  64. data/lib/utopia/path/matcher.rb +14 -2
  65. data/lib/utopia/redirection.rb +13 -16
  66. data/lib/utopia/session.rb +14 -6
  67. data/lib/utopia/setup.rb +3 -1
  68. data/lib/utopia/static.rb +11 -12
  69. data/lib/utopia/version.rb +1 -1
  70. data/setup/server/git/hooks/post-receive +0 -4
  71. data/setup/site/.gitignore +9 -0
  72. data/setup/site/.rspec +1 -0
  73. data/setup/site/Gemfile +4 -0
  74. data/setup/site/Guardfile +17 -0
  75. data/setup/site/Rakefile +2 -2
  76. data/setup/site/config.ru +5 -12
  77. data/setup/site/pages/_heading.xnode +2 -2
  78. data/setup/site/pages/_page.xnode +1 -1
  79. data/setup/site/pages/errors/exception.xnode +3 -3
  80. data/setup/site/pages/errors/file-not-found.xnode +3 -3
  81. data/setup/site/pages/welcome/index.xnode +3 -3
  82. data/setup/site/public/_static/site.css +4 -0
  83. data/setup/site/spec/spec_helper.rb +29 -0
  84. data/setup/site/tasks/deploy.rake +13 -0
  85. data/setup/site/tasks/development.rake +34 -0
  86. data/setup/site/tasks/environment.rake +17 -0
  87. data/spec/mock_node.rb +15 -0
  88. data/spec/spec_helper.rb +29 -0
  89. data/{lib/utopia/extensions/date.rb → spec/utopia/content/document_spec.rb} +31 -21
  90. data/spec/utopia/content/markup_spec.rb +2 -2
  91. data/spec/utopia/content/{tag_spec.rb → namespace_spec.rb} +17 -10
  92. data/spec/utopia/content/tags_spec.rb +80 -0
  93. data/spec/utopia/content_spec.rb +1 -1
  94. data/spec/utopia/content_spec.ru +1 -6
  95. data/spec/utopia/content_spec/_heading.xnode +1 -1
  96. data/spec/utopia/content_spec/content/test-partial.xnode +1 -1
  97. data/spec/utopia/content_spec/index.xnode +1 -1
  98. data/spec/utopia/controller/middleware_spec.ru +1 -3
  99. data/spec/utopia/controller/respond_spec.rb +2 -22
  100. data/spec/utopia/controller/respond_spec.ru +1 -5
  101. data/spec/utopia/controller/respond_spec/errors/file-not-found.xnode +7 -6
  102. data/spec/utopia/exceptions/handler_spec.ru +1 -2
  103. data/spec/utopia/exceptions/mailer_spec.ru +1 -2
  104. data/spec/utopia/extensions_spec.rb +2 -2
  105. data/spec/utopia/localization_spec.ru +1 -2
  106. data/spec/utopia/performance_spec.rb +2 -6
  107. data/spec/utopia/performance_spec/config.ru +5 -12
  108. data/spec/utopia/performance_spec/pages/_heading.xnode +2 -2
  109. data/spec/utopia/performance_spec/pages/_page.xnode +1 -1
  110. data/spec/utopia/performance_spec/pages/errors/exception.xnode +3 -3
  111. data/spec/utopia/performance_spec/pages/errors/file-not-found.xnode +3 -3
  112. data/spec/utopia/performance_spec/pages/welcome/index.xnode +3 -3
  113. data/spec/utopia/setup_spec.rb +79 -15
  114. data/utopia.gemspec +3 -3
  115. metadata +41 -27
  116. data/.simplecov +0 -9
  117. data/documentation/pages/welcome/index.xnode +0 -41
  118. data/lib/utopia/content/tag.rb +0 -90
  119. data/lib/utopia/extensions/array.rb +0 -29
  120. data/lib/utopia/tags/override.rb +0 -33
  121. data/setup/site/.simplecov +0 -9
  122. data/setup/site/tasks/test.rake +0 -10
  123. data/setup/site/tasks/utopia.rake +0 -41
  124. data/spec/utopia/controller/respond_spec/rewrite/controller.rb +0 -12
@@ -19,8 +19,8 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require_relative 'links'
22
-
23
22
  require_relative 'response'
23
+ require_relative 'markup'
24
24
 
25
25
  module Utopia
26
26
  class Content
@@ -35,26 +35,38 @@ module Utopia
35
35
  attr :tag
36
36
  end
37
37
 
38
- DEFERRED_TAG_NAME = "deferred".freeze
39
- CONTENT_TAG_NAME = "content".freeze
40
-
41
38
  # A single request through content middleware. We use a struct to hide instance varibles since we instance_exec within this context.
42
- class Transaction < Response
39
+ class Document < Response
40
+ def self.render(node, request, attributes)
41
+ self.new(request, attributes).render!(node, attributes)
42
+ end
43
+
43
44
  def initialize(request, attributes = {})
44
45
  @request = request
45
46
 
46
47
  @attributes = attributes
47
48
 
48
- @begin_tags = []
49
+ @first = nil
50
+ @current = nil
49
51
  @end_tags = []
50
52
 
51
- # TODO: Provide way to set encoding and content type.
52
- #@encoding = Encoding::UTF_8
53
- #self.content_type = "text/html; charset=#{@encoding.name}"
54
-
55
53
  super()
56
54
  end
57
55
 
56
+ def [] key
57
+ @attributes[key]
58
+ end
59
+
60
+ def []= key, value
61
+ @attributes[key] = value
62
+ end
63
+
64
+ def render!(node, attributes)
65
+ @body << render_node(node, attributes)
66
+
67
+ return self
68
+ end
69
+
58
70
  # A helper method for accessing controller variables from view:
59
71
  def controller
60
72
  @controller ||= Utopia::Controller[request]
@@ -68,134 +80,137 @@ module Utopia
68
80
  MarkupParser.parse(markup, self)
69
81
  end
70
82
 
71
- # The Rack::Request for this transaction.
83
+ # The Rack::Request for this document.
72
84
  attr :request
73
85
 
74
- # Per-transaction global attributes.
86
+ # Per-document global attributes.
75
87
  attr :attributes
76
88
 
77
- # Begin tags represents a list from outer to inner most tag.
78
- # At any point in parsing markup, begin_tags is a list of the inner most tag,
79
- # then the next outer tag, etc. This list is used for doing dependent lookups.
80
- attr :begin_tags
89
+ # The current state, represents a list from outer to inner most tag by traversing {State#parent}.
90
+ # At any point in parsing markup, this is a list of the inner most tag,
91
+ # then the next outer tag, etc.
92
+ attr :current
93
+
94
+ # The first {State} generated by rendering this document. It contains useful information
95
+ # regarding the node and uri used to access the resource.
96
+ attr :first
81
97
 
82
98
  # End tags represents a list of execution order. This is the order that end tags
83
99
  # have appeared when evaluating nodes.
84
100
  attr :end_tags
85
101
 
86
- def tag(name, attributes = {}, &block)
102
+ def tag(name, attributes = {})
87
103
  # If we provide a block which can give inner data, we are not self-closing.
88
104
  tag = Tag.new(name, !block_given?, attributes)
89
105
 
90
- node = tag_begin(tag)
91
-
92
- yield node if block_given?
93
-
94
- tag_end(tag)
106
+ if block_given?
107
+ node = tag_begin(tag)
108
+ yield node
109
+ tag_end(tag)
110
+ else
111
+ tag_complete(tag, node)
112
+ end
95
113
  end
96
114
 
97
115
  def tag_complete(tag, node = nil)
98
- if tag.name == CONTENT_TAG_NAME
99
- current.write(content)
100
- else
101
- node ||= lookup(tag)
116
+ node ||= lookup_tag(tag)
102
117
 
103
- if node
104
- tag_begin(tag, node)
105
- tag_end(tag)
106
- else
107
- current.tag_complete(tag)
108
- end
118
+ if node
119
+ tag_begin(tag, node)
120
+ tag_end(tag)
121
+ else
122
+ @current.tag_complete(tag)
109
123
  end
110
124
  end
111
125
 
112
126
  def tag_begin(tag, node = nil)
113
- node ||= lookup(tag)
127
+ node ||= lookup_tag(tag)
114
128
 
115
129
  if node
116
- state = State.new(tag, node)
117
- self.begin_tags << state
130
+ @current = State.new(@current, tag, node)
118
131
 
119
- if node.respond_to? :tag_begin
120
- node.tag_begin(self, state)
121
- end
132
+ node.tag_begin(self, state) if node.respond_to?(:tag_begin)
122
133
 
123
134
  return node
124
135
  end
125
136
 
126
- current.tag_begin(tag)
137
+ # raise ArgumentError.new("tag_begin: #{tag} is tag.self_closed?") if tag.self_closed?
138
+
139
+ @current.tag_begin(tag)
127
140
 
128
141
  return nil
129
142
  end
130
143
 
131
144
  def write(string)
132
- current.write(string)
145
+ @current.write(string)
133
146
  end
134
147
 
135
148
  alias cdata write
136
149
 
137
150
  def text(string)
138
- current.text(string)
151
+ @current.text(string)
139
152
  end
140
153
 
141
154
  def tag_end(tag = nil)
142
- # Get the current tag which we are completing/ending:
143
- top = current
144
-
145
- if top.tags.empty?
146
- if top.node.respond_to? :tag_end
147
- top.node.tag_end(self, top)
155
+ # Determine if the current state contains tags that need to be completed, or if the state itself is finished.
156
+ if @current.empty?
157
+ if node = @current.node
158
+ node.tag_end(self, @current) if node.respond_to?(:tag_end)
148
159
  end
149
160
 
150
- self.end_tags << top
151
- buffer = top.call(self)
161
+ @end_tags << @current
162
+ buffer = @current.call(self)
152
163
 
153
- self.begin_tags.pop
154
- self.end_tags.pop
164
+ @current = @current.parent
165
+ @end_tags.pop
155
166
 
156
- if current
157
- current.write(buffer)
158
- end
167
+ @current.write(buffer) if @current
159
168
 
160
169
  return buffer
161
170
  else
162
- current.tag_end(tag)
171
+ # raise ArgumentError.new("tag_begin: #{tag} is tag.self_closed?") if tag.self_closed?
172
+ @current.tag_end(tag)
163
173
  end
164
174
 
165
175
  return nil
166
176
  end
167
-
168
- def render_node(node, attributes = {})
169
- self.begin_tags << State.new(attributes, node)
170
177
 
178
+ def render_node(node, attributes = {})
179
+ @current = State.new(@current, nil, node, attributes)
180
+
181
+ # We keep track of the first thing rendered by this document.
182
+ @first ||= @current
183
+
184
+ # This returns the content of rendering the tag:
171
185
  return tag_end
172
186
  end
173
187
 
174
- # Takes an instance of Tag
175
- def lookup(tag)
176
- result = tag
177
- node = nil
178
-
179
- self.begin_tags.reverse_each do |state|
180
- result = state.lookup(result)
181
-
182
- node ||= state.node if state.node.respond_to? :lookup
183
-
184
- return result if result.is_a?(Node)
185
- end
186
-
187
- self.end_tags.reverse_each do |state|
188
- return state.node.lookup(result) if state.node.respond_to? :lookup
188
+ # Maps a tag to a node instance by asking the current node to lookup the tag name. This function is called for each tag and thus heavily affects performance.
189
+ # @return [Node] The node for the given tag.
190
+ def lookup_tag(tag)
191
+ # result = tag
192
+ #
193
+ # # This loop works from inner to outer tags, and updates the tag we are currently searching for based on any overrides:
194
+ # @begin_tags.reverse_each do |state|
195
+ # result = state.lookup(result)
196
+ #
197
+ # return result if result.is_a?(Node)
198
+ # end
199
+
200
+ # This loop looks up a tag by asking the most embedded node to look it up based on tag name. This almost always only evaluates the top state:
201
+ @end_tags.reverse_each do |state|
202
+ return state.node.lookup_tag(tag) if state.node.respond_to?(:lookup_tag)
189
203
  end
190
-
204
+
191
205
  return nil
192
206
  end
193
207
 
194
- # The current tag being processed/rendered. Prefer to access state directly.
195
- def current
196
- @begin_tags.last
208
+ # Lookup a node with the given path relative to the current node.
209
+ # @return [Node] The node if could be found.
210
+ def lookup_node(path)
211
+ @current.node.lookup_node(path)
197
212
  end
198
-
213
+
199
214
  # The content of the node
200
215
  def content
201
216
  @end_tags.last.content
@@ -204,32 +219,30 @@ module Utopia
204
219
  def parent
205
220
  @end_tags[-2]
206
221
  end
207
-
208
- def first
209
- @begin_tags.first
210
- end
211
222
  end
212
223
 
213
- # The state of a single tag being rendered within a Transaction instance.
214
- class Transaction::State
215
- def initialize(tag, node, attributes = tag.to_hash)
224
+ # The state of a single tag being rendered within a document instance.
225
+ class Document::State
226
+ def initialize(parent, tag, node, attributes = tag.to_hash)
227
+ @parent = parent
228
+ @tag = tag
216
229
  @node = node
230
+ @attributes = attributes
217
231
 
218
232
  @buffer = Trenni::MarkupString.new.force_encoding(Encoding::UTF_8)
233
+ @content = nil
219
234
 
220
- @overrides = {}
235
+ @deferred = []
221
236
 
222
237
  @tags = []
223
- @attributes = attributes
224
-
225
- @content = nil
226
- @deferred = []
227
238
  end
228
239
 
240
+ attr :parent
229
241
  attr :attributes
230
- attr :overrides
231
242
  attr :content
232
243
  attr :node
244
+
245
+ # A list of all tags in order of rendering them, which have not been finished yet.
233
246
  attr :tags
234
247
 
235
248
  attr :deferred
@@ -244,28 +257,14 @@ module Utopia
244
257
  @attributes[key]
245
258
  end
246
259
 
247
- def lookup(tag)
248
- if override = @overrides[tag.name]
249
- if override.respond_to? :call
250
- return override.call(tag)
251
- elsif String === override
252
- return Tag.new(override, tag.attributes)
253
- else
254
- return override
255
- end
256
- else
257
- return tag
258
- end
259
- end
260
-
261
- def call(transaction)
260
+ def call(document)
262
261
  @content = @buffer
263
262
  @buffer = Trenni::MarkupString.new.force_encoding(Encoding::UTF_8)
264
263
 
265
264
  if node.respond_to? :call
266
- node.call(transaction, self)
265
+ node.call(document, self)
267
266
  else
268
- transaction.parse_markup(@content)
267
+ document.parse_markup(@content)
269
268
  end
270
269
 
271
270
  return @buffer
@@ -276,20 +275,27 @@ module Utopia
276
275
  end
277
276
 
278
277
  def text(string)
279
- @buffer << Trenni::MarkupString(string)
278
+ Trenni::Markup.append(@buffer, string)
280
279
  end
281
280
 
282
281
  def tag_complete(tag)
283
282
  tag.write(@buffer)
284
283
  end
284
+
285
+ # Whether this state has any nested tags.
286
+ def empty?
287
+ @tags.empty?
288
+ end
285
289
 
286
290
  def tag_begin(tag)
291
+ # raise ArgumentError.new("tag_begin: #{tag} is tag.self_closed?") if tag.self_closed?
292
+
287
293
  @tags << tag
288
294
  tag.write_opening_tag(@buffer)
289
295
  end
290
296
 
291
297
  def tag_end(tag)
292
- raise UnbalancedTagError(tag) unless @tags.pop.name == tag.name
298
+ raise UnbalancedTagError.new(tag) unless @tags.pop.name == tag.name
293
299
  tag.write_closing_tag(@buffer)
294
300
  end
295
301
  end
@@ -28,6 +28,7 @@ require_relative '../locale'
28
28
 
29
29
  module Utopia
30
30
  class Content
31
+ # Represents a link to some content with associated metadata.
31
32
  class Link
32
33
  def initialize(kind, path, info = nil)
33
34
  path = Path.create(path)
@@ -91,9 +92,13 @@ module Utopia
91
92
  def to_anchor(**options)
92
93
  Trenni::Builder.fragment(options[:builder]) do |builder|
93
94
  if href?
94
- relative_href(options[:base])
95
+ a_attributes = {
96
+ :class => options.fetch(:class, 'link'),
97
+ :href => relative_href(options[:base]),
98
+ :target => options.fetch(:target, @info[:target])
99
+ }
95
100
 
96
- builder.inline('a', class: options.fetch(:class, 'link'), href: relative_href(options[:base])) do
101
+ builder.inline('a', a_attributes) do
97
102
  builder.text(options[:content] || title)
98
103
  end
99
104
  else
@@ -22,9 +22,10 @@ require_relative 'link'
22
22
 
23
23
  module Utopia
24
24
  class Content
25
+ # The file extension for markup nodes on disk.
25
26
  XNODE_EXTENSION = '.xnode'.freeze
26
27
 
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
+ # Represents a list of {Link} instances relating to the structure of the content. They are formed from the `links.yaml` file and the actual directory structure on disk.
28
29
  class Links
29
30
  def self.for(root, path, locale = nil)
30
31
  links = self.new(root, path.dirname)
@@ -21,11 +21,13 @@
21
21
  require 'trenni/parsers'
22
22
  require 'trenni/entities'
23
23
  require 'trenni/strings'
24
-
25
- require_relative 'tag'
24
+ require 'trenni/tag'
26
25
 
27
26
  module Utopia
28
27
  class Content
28
+ Tag = Trenni::Tag
29
+
30
+ # A hash which forces all keys to be symbols and fails with KeyError when strings are used.
29
31
  class SymbolicHash < Hash
30
32
  def [] key
31
33
  raise KeyError.new("attribute #{key} is a string, prefer a symbol") if key.is_a? String
@@ -49,7 +51,9 @@ module Utopia
49
51
  end
50
52
  end
51
53
 
54
+ # Provides a high level interface for parsing markup.
52
55
  class MarkupParser
56
+ # A tag generated by parsing markup.
53
57
  class ParsedTag
54
58
  def initialize(name, offset)
55
59
  @offset = offset
@@ -64,6 +68,7 @@ module Utopia
64
68
  end
65
69
  end
66
70
 
71
+ # The name of a closing tag fails to match up with the corresponding opening tag.
67
72
  class UnbalancedTagError < StandardError
68
73
  def initialize(buffer, opening_tag, closing_tag = nil)
69
74
  @buffer = buffer
@@ -19,14 +19,33 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Utopia
22
- module Tags
23
- class Deferred
24
- def self.call(transaction, state)
25
- id = state[:id].to_i
22
+ class Content
23
+ # A namespace which contains tags which can be rendered within a {Document}.
24
+ module Namespace
25
+ def self.extended(other)
26
+ other.class_exec do
27
+ @named = {}
28
+ end
29
+ end
30
+
31
+ attr :named
32
+
33
+ def freeze
34
+ return self if frozen?
26
35
 
27
- procedure = transaction.parent.deferred[id]
36
+ @named.freeze
37
+ @named.values.each(&:freeze)
28
38
 
29
- procedure.call(transaction, state)
39
+ super
40
+ end
41
+
42
+ def tag(name, klass = nil, &block)
43
+ @named[name] = klass || block
44
+ end
45
+
46
+ # @return [Node] The node which should be used to render the named tag.
47
+ def call(name, node)
48
+ @named[name]
30
49
  end
31
50
  end
32
51
  end