utopia 1.9.11 → 2.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 (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