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
@@ -22,6 +22,7 @@ require_relative 'exceptions/handler'
22
22
  require_relative 'exceptions/mailer'
23
23
 
24
24
  module Utopia
25
+ # Middleware for handling exceptional situations.
25
26
  module Exceptions
26
27
  end
27
28
  end
@@ -20,8 +20,9 @@
20
20
 
21
21
  module Utopia
22
22
  module Exceptions
23
- # Catches exceptions and performs an internal redirect.
23
+ # A middleware which catches exceptions and performs an internal redirect.
24
24
  class Handler
25
+ # @param location [String] Peform an internal redirect to this location when an exception is raised.
25
26
  def initialize(app, location = '/errors/exception')
26
27
  @app = app
27
28
 
@@ -29,6 +30,8 @@ module Utopia
29
30
  end
30
31
 
31
32
  def freeze
33
+ return self if frozen?
34
+
32
35
  @location.freeze
33
36
 
34
37
  super
@@ -74,7 +77,9 @@ module Utopia
74
77
  end
75
78
  end
76
79
 
77
- private def write_exception_to_stream(stream, env, exception, include_backtrace = false)
80
+ private
81
+
82
+ def write_exception_to_stream(stream, env, exception, include_backtrace = false)
78
83
  buffer = []
79
84
 
80
85
  buffer << "While requesting resource #{env[Rack::PATH_INFO].inspect}, a fatal error occurred:"
@@ -23,7 +23,7 @@ require 'mail'
23
23
 
24
24
  module Utopia
25
25
  module Exceptions
26
- # Catches all exceptions raised from the app it wraps and sends a useful email with the exception, stacktrace, and contents of the environment.
26
+ # A middleware which catches all exceptions raised from the app it wraps and sends a useful email with the exception, stacktrace, and contents of the environment.
27
27
  class Mailer
28
28
  # A basic local non-authenticated SMTP server.
29
29
  LOCAL_SMTP = [:smtp, {
@@ -32,17 +32,36 @@ module Utopia
32
32
  :enable_starttls_auto => false
33
33
  }]
34
34
 
35
- def initialize(app, config = {})
35
+ DEFAULT_FROM = (ENV['USER'] || 'rack') + "@localhost"
36
+ DEFAULT_SUBJECT = '%{exception} [PID %{pid} : %{cwd}]'
37
+
38
+ # @param to [String] The address to email error reports to.
39
+ # @param from [String] The from address for error reports.
40
+ # @param subject [String] The subject template which can access attributes defined by `#attributes_for`.
41
+ # @param delivery_method [Object] The delivery method as required by the mail gem.
42
+ # @param dump_environment [Boolean] Attach `env` as `environment.yaml` to the error report.
43
+ def initialize(app, to: "postmaster", from: DEFAULT_FROM, subject: DEFAULT_SUBJECT, delivery_method: LOCAL_SMTP, dump_environment: false)
36
44
  @app = app
37
45
 
38
- @to = config[:to] || "postmaster"
39
- @from = config.fetch(:from) {(ENV['USER'] || 'rack') + "@localhost"}
40
- @subject = config[:subject] || '%{exception} [PID %{pid} : %{cwd}]'
41
- @delivery_method = config.fetch(:delivery_method, LOCAL_SMTP)
46
+ @to = to
47
+ @from = from
48
+ @subject = subject
49
+ @delivery_method = delivery_method
50
+ @dump_environment = dump_environment
51
+ end
52
+
53
+ def freeze
54
+ return self if frozen?
55
+
56
+ @to.freeze
57
+ @from.freeze
58
+ @subject.freeze
59
+ @delivery_method.freeze
60
+ @dump_environment.freeze
42
61
 
43
- @dump_environment = config.fetch(:dump_environment, false)
62
+ super
44
63
  end
45
-
64
+
46
65
  def call(env)
47
66
  begin
48
67
  return @app.call(env)
@@ -100,17 +119,19 @@ module Utopia
100
119
  return io.string
101
120
  end
102
121
 
103
- def generate_mail(exception, env)
104
- attributes = {
122
+ def attributes_for(exception, env)
123
+ {
105
124
  exception: exception.class.name,
106
125
  pid: $$,
107
126
  cwd: Dir.getwd,
108
127
  }
109
-
128
+ end
129
+
130
+ def generate_mail(exception, env)
110
131
  mail = Mail.new(
111
132
  :from => @from,
112
133
  :to => @to,
113
- :subject => @subject % attributes
134
+ :subject => @subject % attributes_for(exception, env)
114
135
  )
115
136
 
116
137
  mail.text_part = Mail::Part.new
@@ -19,15 +19,17 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Utopia
22
- module Tags
23
- class Node
24
- def self.call(transaction, state)
25
- path = Path[state[:path]]
26
-
27
- node = transaction.lookup_node(path)
28
-
29
- transaction.render_node(node)
22
+ module Extensions
23
+ module ArraySplit
24
+ def split_at(*args, &block)
25
+ if middle = index(*args, &block)
26
+ [self[0...middle], self[middle], self[middle+1..-1]]
27
+ else
28
+ [[], nil, []]
29
+ end
30
30
  end
31
31
  end
32
+
33
+ ::Array.prepend(ArraySplit)
32
34
  end
33
- end
35
+ end
@@ -18,24 +18,34 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
+ require 'date'
22
+
21
23
  module Utopia
22
- module Tags
23
- class Environment
24
- def self.for(environment)
25
- self.new(environment)
26
- end
27
-
28
- def initialize(environment)
29
- @environment = environment
24
+ module Extensions
25
+ # Provides comparison operator extensions.
26
+ module TimeDateComparison
27
+ def <=>(other)
28
+ if Date === other or DateTime === other
29
+ self.to_datetime <=> other
30
+ else
31
+ super
32
+ end
30
33
  end
31
-
32
- def call(transaction, state)
33
- only = state[:only].split(",").collect(&:to_sym) rescue []
34
+ end
35
+
36
+ ::Time.prepend(TimeDateComparison)
34
37
 
35
- if defined?(@environment) and only.include?(@environment)
36
- transaction.parse_markup(state.content)
38
+ # Provides comparison operator extensions.
39
+ module DateTimeComparison
40
+ def <=>(other)
41
+ if Time === other
42
+ self.to_datetime <=> other.to_datetime
43
+ else
44
+ super
37
45
  end
38
46
  end
39
47
  end
48
+
49
+ ::Date.prepend(DateTimeComparison)
40
50
  end
41
- end
51
+ end
@@ -23,7 +23,9 @@ require 'rack'
23
23
  require 'http/accept'
24
24
 
25
25
  module Utopia
26
+ # HTTP protocol implementation.
26
27
  module HTTP
28
+ # Pull in {::HTTP::Accept} for parsing.
27
29
  Accept = ::HTTP::Accept
28
30
 
29
31
  # A list of commonly used HTTP status codes.
@@ -19,6 +19,7 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Utopia
22
+ # A structured representation of locale based on RFC3066.
22
23
  Locale = Struct.new(:language, :country, :variant) do
23
24
  def to_s
24
25
  to_a.compact.join('-')
@@ -21,7 +21,7 @@
21
21
  require_relative 'middleware'
22
22
 
23
23
  module Utopia
24
- # If you request a URL which has localized content, a localized redirect would be returned based on the content requested.
24
+ # A middleware which attempts to find localized content.
25
25
  class Localization
26
26
  # A wrapper to provide easy access to locale related data in the request.
27
27
  class Wrapper
@@ -59,38 +59,42 @@ module Utopia
59
59
  LOCALIZATION_KEY = 'utopia.localization'.freeze
60
60
  CURRENT_LOCALE_KEY = 'utopia.localization.current_locale'.freeze
61
61
 
62
- DEFAULT_LOCALE = 'en'
63
-
64
- def initialize(app, **options)
62
+ # @param locales [Array<String>] An array of all supported locales.
63
+ # @param default_locale [String] The default locale if none is provided.
64
+ # @param default_locales [String] The locales to try in order if none is provided.
65
+ # @param hosts [Hash<Pattern, String>] Specify a mapping of the HTTP_HOST header to a given locale.
66
+ # @param ignore [Array<Pattern>] A list of patterns matched against PATH_INFO which will not be localized.
67
+ def initialize(app, locales:, default_locale: nil, default_locales: nil, hosts: {}, ignore: [], **options)
65
68
  @app = app
66
69
 
67
- @all_locales = HTTP::Accept::Languages::Locales.new(options[:locales])
70
+ @all_locales = HTTP::Accept::Languages::Locales.new(locales)
68
71
 
69
- # Locales here are represented as an array of strings, e.g. ['en', 'ja', 'cn', 'de'].
70
- unless @default_locales = options[:default_locales]
72
+ # Locales here are represented as an array of strings, e.g. ['en', 'ja', 'cn', 'de'] and are used in order if no locale is specified by the user.
73
+ unless @default_locales = default_locales
71
74
  # We append nil, i.e. no localization.
72
75
  @default_locales = @all_locales.names + [nil]
73
76
  end
74
77
 
75
- if @default_locale = options[:default_locale]
76
- @default_locales.unshift(default_locale)
77
- else
78
- @default_locale = @default_locales.first
79
- end
78
+ @default_locale = default_locale || @default_locales.first
80
79
 
81
- @hosts = options[:hosts] || {}
80
+ unless @default_locales.include? @default_locale
81
+ @default_locales.unshift(@default_locale)
82
+ end
82
83
 
83
- @nonlocalized = options.fetch(:nonlocalized, [])
84
+ # Select a localization based on a request host name:
85
+ @hosts = hosts
84
86
 
85
- self.freeze
87
+ @ignore = ignore || options[:nonlocalized]
86
88
  end
87
89
 
88
90
  def freeze
91
+ return self if frozen?
92
+
89
93
  @all_locales.freeze
90
94
  @default_locales.freeze
91
95
  @default_locale.freeze
92
96
  @hosts.freeze
93
- @nonlocalized.freeze
97
+ @ignore.freeze
94
98
 
95
99
  super
96
100
  end
@@ -104,7 +108,7 @@ module Utopia
104
108
  # Keep track of what locales have been tried:
105
109
  locales = Set.new
106
110
 
107
- host_preferred_locales(env).each do |locale|
111
+ host_preferred_locales(env) do |locale|
108
112
  yield env.merge(CURRENT_LOCALE_KEY => locale) if locales.add? locale
109
113
  end
110
114
 
@@ -124,16 +128,13 @@ module Utopia
124
128
  end
125
129
  end
126
130
 
127
- HTTP_HOST = 'HTTP_HOST'.freeze
128
-
129
131
  def host_preferred_locales(env)
130
132
  http_host = env[Rack::HTTP_HOST]
131
133
 
132
- # Get a list of all hosts which match the incoming htt_host:
133
- matching_hosts = @hosts.select{|host_pattern, locale| http_host =~ host_pattern}
134
-
135
- # Extract all the valid locales:
136
- matching_hosts.flat_map{|host_pattern, locale| locale}
134
+ # Yield all hosts which match the incoming http_host:
135
+ @hosts.each do |pattern, locale|
136
+ yield locale if http_host[pattern]
137
+ end
137
138
  end
138
139
 
139
140
  def request_preferred_locale(env)
@@ -163,10 +164,18 @@ module Utopia
163
164
  return []
164
165
  end
165
166
 
166
- def nonlocalized?(env)
167
+ SAFE_METHODS = ['GET', 'HEAD']
168
+
169
+ def localized?(env)
170
+ # Only SAFE_METHODS can be localized:
171
+ request_method = env[Rack::REQUEST_METHOD]
172
+ return false unless SAFE_METHODS.include?(request_method)
173
+
174
+ # Ignore requests which match the ignored paths:
167
175
  path_info = env[Rack::PATH_INFO]
176
+ return false if @ignore.any? { |pattern| path_info[pattern] != nil }
168
177
 
169
- @nonlocalized.any? { |pattern| path_info[pattern] != nil }
178
+ return true
170
179
  end
171
180
 
172
181
  # Set the Vary: header on the response to indicate that this response should include the header in the cache key.
@@ -190,8 +199,8 @@ module Utopia
190
199
  end
191
200
 
192
201
  def call(env)
193
- # Pass the request through with no localization if it is a nonlocalized path:
194
- return @app.call(env) if nonlocalized?(env)
202
+ # Pass the request through if it shouldn't be localized:
203
+ return @app.call(env) unless localized?(env)
195
204
 
196
205
  env[LOCALIZATION_KEY] = self
197
206
 
@@ -21,6 +21,7 @@
21
21
  require_relative 'logger/compact_formatter'
22
22
 
23
23
  module Utopia
24
+ # Manages an instance of Logger with useful defaults. Used in rake `tasks/log.rake`.
24
25
  module Logger
25
26
  def self.new(output: STDERR, level: ::Logger::WARN)
26
27
  log = ::Logger.new(output)
@@ -23,6 +23,7 @@ require 'rainbow'
23
23
 
24
24
  module Utopia
25
25
  module Logger
26
+ # Provides a concise log output format.
26
27
  class CompactFormatter
27
28
  def initialize
28
29
  @start = Time.now
@@ -24,12 +24,22 @@ require_relative 'http'
24
24
  require_relative 'path'
25
25
 
26
26
  module Utopia
27
+ # The default pages path for {Utopia::Content} middleware.
27
28
  PAGES_PATH = 'pages'.freeze
28
29
 
29
- # This is used for shared controller variables which get consumed by the content middleware:
30
+ # This is used for shared controller variables which get consumed by the content middleware.
30
31
  VARIABLES_KEY = 'utopia.variables'.freeze
31
32
 
33
+ # The default root directory for middleware to operate within, e.g. the web-site directory. Convention over configuration.
34
+ # @param subdirectory [String] Appended to the default root to make a more specific path.
35
+ # @param pwd [String] The working directory for the current site.
32
36
  def self.default_root(subdirectory = PAGES_PATH, pwd = Dir.pwd)
33
37
  File.expand_path(subdirectory, pwd)
34
38
  end
39
+
40
+ # The same as {default_root} but returns an instance of {Path}.
41
+ # @return [Path] The path as requested.
42
+ def self.default_path(*args)
43
+ Path[default_root(*args)]
44
+ end
35
45
  end
@@ -65,6 +65,7 @@ module Utopia
65
65
  end
66
66
  end
67
67
 
68
+ # Represents a path as an array of path components. Useful for efficient URL manipulation.
68
69
  class Path
69
70
  include Comparable
70
71
 
@@ -22,14 +22,23 @@ require_relative '../path'
22
22
 
23
23
  module Utopia
24
24
  class Path
25
+ # Performs structured, efficient, matches against {Path} instances. Supports regular expressions, type-casts and constants.
26
+ # @example
27
+ # path = Utopia::Path['users/20/edit']
28
+ # matcher = Utopia::Path::Matcher[users: /users/, id: Integer, action: String]
29
+ # match_data = matcher.match(path)
25
30
  class Matcher
31
+ # The result of matching against a {Path}.
26
32
  class MatchData
27
33
  def initialize(named_parts, post_match)
28
34
  @named_parts = named_parts
29
35
  @post_match = Path[post_match]
30
36
  end
31
37
 
38
+ # Matched components by name.
32
39
  attr :named_parts
40
+
41
+ # Any remaining part past the end of the explicitly matched components.
33
42
  attr :post_match
34
43
 
35
44
  def [] key
@@ -41,7 +50,7 @@ module Utopia
41
50
  end
42
51
  end
43
52
 
44
- # patterns = {key: /\d+/, 'foo', }
53
+ # @param patterns [Hash<Symbol,Pattern>] An ordered list of things to match.
45
54
  def initialize(patterns = [])
46
55
  @patterns = patterns
47
56
  end
@@ -64,14 +73,16 @@ module Utopia
64
73
  return nil
65
74
  end
66
75
 
67
- # This is a path prefix matching algorithm. The pattern is an array of String, Symbol, Regexp, or nil. The components is an array of String.
76
+ # This is a path prefix matching algorithm. The pattern is an array of String, Symbol, Regexp, or nil. The path is an array of String.
68
77
  def match(path)
69
78
  components = path.to_a
70
79
 
80
+ # Can't possibly match if not enough components:
71
81
  return nil if components.size < @patterns.size
72
82
 
73
83
  named_parts = {}
74
84
 
85
+ # Try to match each component against the pattern:
75
86
  @patterns.each_with_index do |(key, pattern), index|
76
87
  component = components[index]
77
88
 
@@ -83,6 +94,7 @@ module Utopia
83
94
  if result = pattern.match(component)
84
95
  named_parts[key] = result
85
96
  else
97
+ # Couldn't match:
86
98
  return nil
87
99
  end
88
100
  else