utopia 2.32.0 → 3.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 (85) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/bake/utopia/environment.rb +6 -6
  4. data/bake/utopia/server.rb +4 -1
  5. data/bake/utopia/site.rb +14 -14
  6. data/bake/utopia/static.rb +26 -13
  7. data/bake/utopia.rb +1 -1
  8. data/context/getting-started.md +7 -5
  9. data/context/index.yaml +1 -2
  10. data/context/middleware.md +42 -13
  11. data/lib/utopia/application.rb +83 -0
  12. data/lib/utopia/content/builder.rb +25 -0
  13. data/lib/utopia/content/document.rb +64 -6
  14. data/lib/utopia/content/link.rb +50 -5
  15. data/lib/utopia/content/links.rb +44 -7
  16. data/lib/utopia/content/markup.rb +74 -0
  17. data/lib/utopia/content/middleware.rb +48 -20
  18. data/lib/utopia/content/namespace.rb +10 -1
  19. data/lib/utopia/content/node.rb +66 -4
  20. data/lib/utopia/content/response.rb +12 -3
  21. data/lib/utopia/content.rb +3 -0
  22. data/lib/utopia/controller/actions.md +4 -4
  23. data/lib/utopia/controller/actions.rb +51 -3
  24. data/lib/utopia/controller/base.rb +72 -29
  25. data/lib/utopia/controller/middleware.rb +20 -15
  26. data/lib/utopia/controller/respond.rb +36 -32
  27. data/lib/utopia/controller/responder.rb +106 -45
  28. data/lib/utopia/controller/result.rb +11 -0
  29. data/lib/utopia/controller/rewrite.rb +39 -0
  30. data/lib/utopia/controller/variables.rb +15 -3
  31. data/lib/utopia/controller.rb +2 -0
  32. data/lib/utopia/exceptions/handler.rb +20 -11
  33. data/lib/utopia/exceptions/mailer.rb +56 -51
  34. data/lib/utopia/extensions/array_split.rb +6 -0
  35. data/lib/utopia/extensions/date_comparisons.rb +6 -0
  36. data/lib/utopia/http.rb +11 -48
  37. data/lib/utopia/import_map.rb +19 -11
  38. data/lib/utopia/localization/locales.rb +60 -0
  39. data/lib/utopia/localization/middleware.rb +107 -73
  40. data/lib/utopia/localization/preferences.rb +76 -0
  41. data/lib/utopia/localization/resolver.rb +47 -0
  42. data/lib/utopia/localization.rb +6 -0
  43. data/lib/utopia/middleware.rb +3 -4
  44. data/lib/utopia/path/matcher.rb +15 -0
  45. data/lib/utopia/path.rb +146 -10
  46. data/lib/utopia/redirection/client_redirect.rb +87 -0
  47. data/lib/utopia/redirection/directory_index.rb +41 -0
  48. data/lib/utopia/redirection/errors.rb +78 -0
  49. data/lib/utopia/redirection/moved.rb +52 -0
  50. data/lib/utopia/redirection/request_failure.rb +26 -0
  51. data/lib/utopia/redirection/rewrite.rb +42 -0
  52. data/lib/utopia/redirection.rb +7 -165
  53. data/lib/utopia/request.rb +202 -0
  54. data/lib/utopia/response.rb +73 -0
  55. data/lib/utopia/session/lazy_hash.rb +27 -1
  56. data/lib/utopia/session/middleware.rb +103 -30
  57. data/lib/utopia/session/serialization.rb +8 -0
  58. data/lib/utopia/session.rb +5 -0
  59. data/lib/utopia/setup.rb +23 -3
  60. data/lib/utopia/shell.rb +28 -7
  61. data/lib/utopia/static/local_file.rb +84 -61
  62. data/lib/utopia/static/middleware.rb +65 -33
  63. data/lib/utopia/static/mime_types.rb +38 -29
  64. data/lib/utopia/static.rb +2 -0
  65. data/lib/utopia/version.rb +3 -2
  66. data/lib/utopia.rb +1 -1
  67. data/license.md +1 -1
  68. data/readme.md +33 -4
  69. data/releases.md +15 -0
  70. data/setup/site/bake.rb +1 -1
  71. data/setup/site/config/application.rb +51 -0
  72. data/setup/site/config/serve.rb +8 -0
  73. data/setup/site/falcon.rb +17 -4
  74. data/setup/site/fixtures/website.rb +27 -11
  75. data/setup/site/gems.rb +1 -3
  76. data/setup/site/lib/readme.txt +1 -1
  77. data/setup/site/pages/welcome/index.xnode +3 -3
  78. data/setup/site/readme.md +0 -3
  79. data/setup/site/test/website.rb +2 -2
  80. data.tar.gz.sig +0 -0
  81. metadata +60 -34
  82. metadata.gz.sig +0 -0
  83. data/lib/utopia/localization/wrapper.rb +0 -52
  84. data/setup/site/Guardfile +0 -12
  85. data/setup/site/config.ru +0 -49
@@ -7,11 +7,14 @@ require_relative "links"
7
7
  require_relative "response"
8
8
  require_relative "markup"
9
9
  require_relative "builder"
10
+ require_relative "../request"
10
11
 
11
12
  module Utopia
12
13
  module Content
13
14
  # This error is raised if a tag doesn't match up when parsing.
14
15
  class UnbalancedTagError < StandardError
16
+ # Initialize an error for a mismatched rendering tag.
17
+ # @parameter tag [Object] The tag.
15
18
  def initialize(tag)
16
19
  @tag = tag
17
20
 
@@ -23,12 +26,23 @@ module Utopia
23
26
 
24
27
  # A single request through content middleware. We use a struct to hide instance varibles since we instance_exec within this context.
25
28
  class Document < Response
26
- def self.render(node, request, attributes)
27
- self.new(request, attributes).render!(node, attributes)
29
+ # Render a content node into a new document.
30
+ # @parameter node [Utopia::Content::Node] The content node.
31
+ # @parameter request [Utopia::Request] The application request.
32
+ # @parameter attributes [Hash] The attributes.
33
+ # @parameter localization [Utopia::Localization::Preferences | Nil] The selected localization.
34
+ # @returns [Document] The rendered document.
35
+ def self.render(node, request, attributes, localization: request&.localization)
36
+ self.new(request, attributes, localization: localization).render!(node, attributes)
28
37
  end
29
38
 
30
- def initialize(request, attributes = {})
39
+ # Initialize a document for a protocol request.
40
+ # @parameter request [Utopia::Request] The application request.
41
+ # @parameter attributes [Hash] The attributes.
42
+ # @parameter localization [Utopia::Localization::Preferences | Nil] The selected localization.
43
+ def initialize(request, attributes = {}, localization: request&.localization)
31
44
  @request = request
45
+ @localization = localization
32
46
 
33
47
  @attributes = attributes
34
48
 
@@ -41,7 +55,7 @@ module Utopia
41
55
 
42
56
  # @returns [Path] The original request path, if known.
43
57
  def request_path
44
- Path[request.env["REQUEST_PATH"]]
58
+ request.request_path
45
59
  end
46
60
 
47
61
  protected def current_base_uri_path
@@ -60,14 +74,25 @@ module Utopia
60
74
  Path[relative_to].dirname.shortest_path(request_path)
61
75
  end
62
76
 
77
+ # Fetch a document-global attribute.
78
+ # @parameter key [String | Symbol] The lookup key.
79
+ # @returns [Object | Nil] The document attribute.
63
80
  def [] key
64
81
  @attributes[key]
65
82
  end
66
83
 
84
+ # Assign a document-global attribute.
85
+ # @parameter key [String | Symbol] The lookup key.
86
+ # @parameter value [Object] The value to assign.
87
+ # @returns [Object] The assigned value.
67
88
  def []= key, value
68
89
  @attributes[key] = value
69
90
  end
70
91
 
92
+ # Render.
93
+ # @parameter node [Utopia::Content::Node] The content node.
94
+ # @parameter attributes [Hash] The attributes.
95
+ # @returns [self] This object.
71
96
  def render!(node, attributes)
72
97
  @body << render_node(node, attributes)
73
98
 
@@ -79,15 +104,20 @@ module Utopia
79
104
  @controller ||= Utopia::Controller[request]
80
105
  end
81
106
 
107
+ # Return the selected localization preferences for this document.
108
+ # @returns [Localization::Preferences | Nil] The localization preferences.
82
109
  def localization
83
- @localization ||= Utopia::Localization[request]
110
+ @localization
84
111
  end
85
112
 
113
+ # Parse markup into this document.
114
+ # @parameter markup [String] The markup.
115
+ # @returns [Nil] Parsing completes through document callbacks.
86
116
  def parse_markup(markup)
87
117
  MarkupParser.parse(markup, self)
88
118
  end
89
119
 
90
- # The Rack::Request for this document.
120
+ # The request for this document.
91
121
  attr :request
92
122
 
93
123
  # Per-document global attributes.
@@ -106,6 +136,11 @@ module Utopia
106
136
  # have appeared when evaluating nodes.
107
137
  attr :end_tags
108
138
 
139
+ # Render a complete or block-delimited tag.
140
+ # @parameter name [String] The name.
141
+ # @parameter attributes [Hash] The attributes.
142
+ # @yields {|node| ...} The node selected to render a block-delimited tag.
143
+ # @returns [Object | Nil] The completed tag result.
109
144
  def tag(name, attributes = {})
110
145
  # If we provide a block which can give inner data, we are not self-closing.
111
146
  tag = Tag.new(name, !block_given?, attributes)
@@ -119,6 +154,10 @@ module Utopia
119
154
  end
120
155
  end
121
156
 
157
+ # Render a complete tag through a matching content node or the current builder.
158
+ # @parameter tag [XRB::Tag] The tag.
159
+ # @parameter node [Utopia::Content::Node] The content node.
160
+ # @returns [Object] The builder's completion result.
122
161
  def tag_complete(tag, node = nil)
123
162
  node ||= lookup_tag(tag)
124
163
 
@@ -130,6 +169,10 @@ module Utopia
130
169
  end
131
170
  end
132
171
 
172
+ # Begin a tag through a matching content node or the current builder.
173
+ # @parameter tag [XRB::Tag] The tag.
174
+ # @parameter node [Utopia::Content::Node] The content node.
175
+ # @returns [Node | Nil] The content node selected for the tag.
133
176
  def tag_begin(tag, node = nil)
134
177
  node ||= lookup_tag(tag)
135
178
 
@@ -148,16 +191,25 @@ module Utopia
148
191
  return nil
149
192
  end
150
193
 
194
+ # Append raw content to the current builder.
195
+ # @parameter string [String] The string.
196
+ # @returns [String] The current output buffer.
151
197
  def write(string)
152
198
  @current.write(string)
153
199
  end
154
200
 
155
201
  alias cdata write
156
202
 
203
+ # Process text content.
204
+ # @parameter string [String] The string.
205
+ # @returns [Object | Nil] The builder's text result.
157
206
  def text(string)
158
207
  @current.text(string)
159
208
  end
160
209
 
210
+ # Complete the current content node or close a nested markup tag.
211
+ # @parameter tag [XRB::Tag | Nil] The nested tag to close.
212
+ # @returns [String | Nil] The completed node output, or `nil` after closing a nested tag.
161
213
  def tag_end(tag = nil)
162
214
  # Determine if the current state contains tags that need to be completed, or if the state itself is finished.
163
215
  if @current.empty?
@@ -182,6 +234,10 @@ module Utopia
182
234
  return nil
183
235
  end
184
236
 
237
+ # Render a content node within the current builder state.
238
+ # @parameter node [Utopia::Content::Node] The content node.
239
+ # @parameter attributes [Hash] The attributes.
240
+ # @returns [String] The rendered node output.
185
241
  def render_node(node, attributes = {})
186
242
  @current = Builder.new(@current, nil, node, attributes, indent: false)
187
243
 
@@ -225,6 +281,8 @@ module Utopia
225
281
  @end_tags.last.content
226
282
  end
227
283
 
284
+ # Return the enclosing rendering state.
285
+ # @returns [Builder | Nil] The parent rendering state.
228
286
  def parent
229
287
  @end_tags[-2]
230
288
  end
@@ -6,6 +6,7 @@
6
6
  # Copyright, 2020, by Michael Adams.
7
7
 
8
8
  require "yaml"
9
+ require "protocol/url"
9
10
  require "xrb/builder"
10
11
 
11
12
  require "xrb/strings"
@@ -26,6 +27,8 @@ module Utopia
26
27
  @title = XRB::Strings.to_title(title || name)
27
28
  end
28
29
 
30
+ # Build the filesystem key, including the locale suffix when present.
31
+ # @returns [String | Nil] The link key.
29
32
  def key
30
33
  if @path
31
34
  if locale
@@ -36,16 +39,23 @@ module Utopia
36
39
  end
37
40
  end
38
41
 
42
+ # Resolve the backing content file beneath a root directory.
43
+ # @parameter root [String] The root directory.
44
+ # @parameter extension [String] The file extension.
45
+ # @returns [String | Nil] The backing file path, or `nil` for links without file paths.
39
46
  def full_path(root, extension = XNODE_EXTENSION)
40
47
  if @path&.file?
41
48
  File.join(root, @path.dirname, self.key + XNODE_EXTENSION)
42
49
  end
43
50
  end
44
51
 
52
+ # Resolve this link's target URI.
53
+ # @returns [String | Nil] The explicit target URI or one derived from the content path.
45
54
  def href
46
55
  @href ||= @info.fetch(:uri) do
47
56
  @info.fetch(:href) do
48
- (@path.dirname + @path.basename).to_s if @path
57
+ # Omit the variant suffix from inferred content links:
58
+ (@path.dirname + @path.basename).to_url_path.encoded if @path
49
59
  end
50
60
  end
51
61
  end
@@ -61,30 +71,55 @@ module Utopia
61
71
  attr :info
62
72
  attr :locale
63
73
 
74
+ # Check whether this link has a target URI.
75
+ # @returns [Boolean] Whether this link has a target URI.
64
76
  def href?
65
77
  !!href
66
78
  end
67
79
 
80
+ # Check whether this link refers to an index document.
81
+ # @returns [Boolean] Whether this link represents an index document.
68
82
  def index?
69
83
  @kind == :index
70
84
  end
71
85
 
86
+ # Check whether this link is virtual.
87
+ # @returns [Boolean] Whether this link exists only in metadata.
72
88
  def virtual?
73
89
  @kind == :virtual
74
90
  end
75
91
 
92
+ # Resolve this link's target relative to a base path.
93
+ # @parameter base [Path | String | Nil] The source path.
94
+ # @returns [String | Nil] The relative or unchanged target.
76
95
  def relative_href(base = nil)
77
- if base and href.start_with? "/"
78
- Path.shortest_path(href, base)
79
- else
80
- href
96
+ target = href
97
+ return unless target
98
+ return target unless base
99
+
100
+ url = Protocol::URL[target]
101
+ relative_url = url.relative_to(Path[base].to_url_path)
102
+
103
+ # Preserve unchanged targets exactly and avoid allocating a serialized replacement:
104
+ if relative_url.equal?(url)
105
+ return target
81
106
  end
107
+
108
+ return relative_url.to_s
82
109
  end
83
110
 
111
+ # Return the display title from metadata or the inferred title.
112
+ # @returns [String] The display title.
84
113
  def title
85
114
  @info.fetch(:title, @title)
86
115
  end
87
116
 
117
+ # Render this link as an anchor element.
118
+ # @parameter base [Path | String | Nil] The source path for relative links.
119
+ # @parameter content [String] The content.
120
+ # @parameter builder [XRB::Builder] The markup builder.
121
+ # @parameter attributes [Hash] The attributes.
122
+ # @returns [XRB::Builder::Fragment] The rendered anchor or span fragment.
88
123
  def to_anchor(base: nil, content: self.title, builder: nil, **attributes)
89
124
  attributes[:class] ||= "link"
90
125
 
@@ -106,18 +141,28 @@ module Utopia
106
141
 
107
142
  alias to_href to_anchor
108
143
 
144
+ # Convert this object to a string.
145
+ # @returns [String] The resulting string.
109
146
  def to_s
110
147
  "\#<#{self.class}(#{self.kind}) title=#{title.inspect} href=#{href.inspect}>"
111
148
  end
112
149
 
150
+ # Check whether this object is equivalent to another object.
151
+ # @parameter other [Object] The object to compare.
152
+ # @returns [Boolean] Whether both links have equivalent metadata.
113
153
  def eql? other
114
154
  self.class.eql?(other.class) and kind.eql?(other.kind) and name.eql?(other.name) and path.eql?(other.path) and info.eql?(other.info)
115
155
  end
116
156
 
157
+ # Compare this object with another object.
158
+ # @parameter other [Object] The object to compare.
159
+ # @returns [Boolean | Nil] Whether both links have the same kind, name, and path.
117
160
  def == other
118
161
  other and kind == other.kind and name == other.name and path == other.path
119
162
  end
120
163
 
164
+ # Check whether this link uses the default locale.
165
+ # @returns [Boolean] Whether this link has no locale override.
121
166
  def default_locale?
122
167
  @locale == nil
123
168
  end
@@ -13,17 +13,32 @@ module Utopia
13
13
  XNODE_EXTENSION = ".xnode"
14
14
  INDEX = "index"
15
15
 
16
+ # A collection of links resolved from the filesystem content hierarchy.
16
17
  class Links
17
- def self.for(root, path, locale = nil)
18
+ # Build links for the given root and path.
19
+ # @parameter root [String] The root directory.
20
+ # @parameter path [Utopia::Path | String] The path.
21
+ # @parameter locale [String] The locale.
22
+ # @parameter fallback [Boolean] Whether to return an unlocalized link as a fallback.
23
+ # @returns [Link | Nil] The resolved link.
24
+ def self.for(root, path, locale = nil, fallback: true)
18
25
  warn "Using uncached links metadata!"
19
- self.new(root).for(path, locale)
26
+ self.new(root).for(path, locale, fallback: fallback)
20
27
  end
21
28
 
29
+ # Build an index of links for the given path.
30
+ # @parameter root [String] The root directory.
31
+ # @parameter path [Utopia::Path | String] The path.
32
+ # @parameter options [Hash] The options.
33
+ # @returns [Array(Link)] The indexed links.
22
34
  def self.index(root, path, **options)
23
35
  warn "Using uncached links metadata!"
24
36
  self.new(root).index(path, **options)
25
37
  end
26
38
 
39
+ # Initialize cached link resolution beneath a content root.
40
+ # @parameter root [String] The root directory.
41
+ # @parameter extension [String] The file extension.
27
42
  def initialize(root, extension: XNODE_EXTENSION)
28
43
  @root = root
29
44
 
@@ -40,9 +55,12 @@ module Utopia
40
55
  attr :index_filter
41
56
 
42
57
  # Resolve a link for the specified path, which must be a path to a specific link.
43
- # for(Path["/index"])
44
- def for(path, locale = nil)
45
- links(path.dirname).lookup(path.last, locale)
58
+ # @parameter path [Utopia::Path | String] The path.
59
+ # @parameter locale [String | Nil] The locale.
60
+ # @parameter fallback [Boolean] Whether to return an unlocalized link as a fallback.
61
+ # @returns [Link | Nil] The resolved link.
62
+ def for(path, locale = nil, fallback: true)
63
+ links(path.dirname).lookup(path.last, locale, fallback: fallback)
46
64
  end
47
65
 
48
66
  # Give an index of all links that can be reached from the given path.
@@ -97,12 +115,18 @@ module Utopia
97
115
 
98
116
  attr :root
99
117
 
118
+ # Load and cache metadata for a content directory.
119
+ # @parameter path [Utopia::Path | String] The path.
120
+ # @returns [Hash] The content metadata.
100
121
  def metadata(path)
101
122
  @metadata_cache.fetch_or_store(path.to_s) do
102
123
  load_metadata(path)
103
124
  end
104
125
  end
105
126
 
127
+ # Load and cache the link resolver for a content directory.
128
+ # @parameter path [Utopia::Path | String] The path.
129
+ # @returns [Resolver] The link resolver.
106
130
  def links(path)
107
131
  @links_cache.fetch_or_store(path.to_s) do
108
132
  load_links(path)
@@ -136,6 +160,9 @@ module Utopia
136
160
 
137
161
  # 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.
138
162
  class Resolver
163
+ # Resolve and order links from a content directory and its metadata.
164
+ # @parameter links [Object] The links.
165
+ # @parameter top [Object] The top.
139
166
  def initialize(links, top = Path.root)
140
167
  raise ArgumentError.new("top path must be absolute") unless top.absolute?
141
168
 
@@ -165,10 +192,15 @@ module Utopia
165
192
  attr :ordered
166
193
  attr :named
167
194
 
195
+ # Select index-document links.
196
+ # @returns [Array(Link)] The index-document links.
168
197
  def indices
169
198
  return @ordered.select{|link| link.index?}
170
199
  end
171
200
 
201
+ # Enumerate the contained values.
202
+ # @parameter locale [String] The locale.
203
+ # @returns [Enumerator] An enumerator over the resulting values.
172
204
  def each(locale)
173
205
  return to_enum(:each, locale) unless block_given?
174
206
 
@@ -177,7 +209,12 @@ module Utopia
177
209
  end
178
210
  end
179
211
 
180
- def lookup(name, locale = nil)
212
+ # Lookup.
213
+ # @parameter name [String] The name.
214
+ # @parameter locale [String] The locale.
215
+ # @parameter fallback [Boolean] Whether to return an unlocalized link as a fallback.
216
+ # @returns [Link | Nil] The exact locale match, or the unlocalized link when fallback is enabled.
217
+ def lookup(name, locale = nil, fallback: true)
181
218
  # This allows generic links to serve any locale requested.
182
219
  if links = @named[name]
183
220
  generic_link = nil
@@ -185,7 +222,7 @@ module Utopia
185
222
  links.each do |link|
186
223
  if link.locale == locale
187
224
  return link
188
- elsif link.locale.nil?
225
+ elsif fallback && link.locale.nil?
189
226
  generic_link = link
190
227
  end
191
228
  end
@@ -14,21 +14,37 @@ module Utopia
14
14
 
15
15
  # A hash which forces all keys to be symbols and fails with KeyError when strings are used.
16
16
  class SymbolicHash < Hash
17
+ # Fetch a value using a symbolic key.
18
+ # @parameter key [String | Symbol] The lookup key.
19
+ # @returns [Object | Nil] The associated value.
20
+ # @raises [KeyError] If `key` is a string.
17
21
  def [] key
18
22
  raise KeyError.new("attribute #{key} is a string, prefer a symbol") if key.is_a? String
19
23
  super key.to_sym
20
24
  end
21
25
 
26
+ # Assign a value after coercing its key to a symbol.
27
+ # @parameter key [String | Symbol] The lookup key.
28
+ # @parameter value [Object] The value to assign.
29
+ # @returns [Object] The assigned value.
22
30
  def []= key, value
23
31
  super key.to_sym, value
24
32
  end
25
33
 
34
+ # Fetch a value after coercing the key to a symbol.
35
+ # @parameter key [String | Symbol] The lookup key.
36
+ # @parameter arguments [Array] The arguments.
37
+ # @yields {|key| ...} The missing symbolic key when no default argument is given.
38
+ # @returns [Object] The associated, default, or block-provided value.
26
39
  def fetch(key, *arguments, &block)
27
40
  key = key.to_sym
28
41
 
29
42
  super
30
43
  end
31
44
 
45
+ # Check whether this collection includes the given value.
46
+ # @parameter key [String | Symbol] The lookup key.
47
+ # @returns [Boolean] Whether the symbolic key exists.
32
48
  def include? key
33
49
  key = key.to_sym
34
50
 
@@ -40,6 +56,9 @@ module Utopia
40
56
  class MarkupParser
41
57
  # A tag generated by parsing markup.
42
58
  class ParsedTag
59
+ # Initialize a parsed opening tag at a source offset.
60
+ # @parameter name [String] The name.
61
+ # @parameter offset [Integer] The source offset.
43
62
  def initialize(name, offset)
44
63
  @offset = offset
45
64
  @tag = Tag.new(name, false, SymbolicHash.new)
@@ -48,6 +67,8 @@ module Utopia
48
67
  attr :tag
49
68
  attr :offset
50
69
 
70
+ # Convert this object to a string.
71
+ # @returns [String] The resulting string.
51
72
  def to_s
52
73
  "<#{@tag.name}#{@tag.attributes.empty? ? '' : ' ...'}>"
53
74
  end
@@ -55,6 +76,10 @@ module Utopia
55
76
 
56
77
  # The name of a closing tag fails to match up with the corresponding opening tag.
57
78
  class UnbalancedTagError < StandardError
79
+ # Initialize an error for mismatched source tags.
80
+ # @parameter buffer [String] The markup source buffer.
81
+ # @parameter opening_tag [Object] The opening tag token.
82
+ # @parameter closing_tag [Object] The closing tag token.
58
83
  def initialize(buffer, opening_tag, closing_tag = nil)
59
84
  @buffer = buffer
60
85
  @opening_tag = opening_tag
@@ -65,16 +90,22 @@ module Utopia
65
90
  attr :opening_tag
66
91
  attr :closing_tag
67
92
 
93
+ # Locate the opening tag in the source buffer.
94
+ # @returns [XRB::Location] The opening tag location.
68
95
  def start_location
69
96
  XRB::Location.new(@buffer.read, opening_tag.offset)
70
97
  end
71
98
 
99
+ # Locate the closing tag in the source buffer.
100
+ # @returns [XRB::Location | Nil] The closing tag location.
72
101
  def end_location
73
102
  if closing_tag and closing_tag.respond_to? :offset
74
103
  XRB::Location.new(@buffer.read, closing_tag.offset)
75
104
  end
76
105
  end
77
106
 
107
+ # Convert this object to a string.
108
+ # @returns [String] The resulting string.
78
109
  def to_s
79
110
  if @closing_tag
80
111
  "#{start_location}: #{@opening_tag} was not closed!"
@@ -84,6 +115,12 @@ module Utopia
84
115
  end
85
116
  end
86
117
 
118
+ # Parse markup events into a delegate.
119
+ # @parameter buffer [String | XRB::Buffer] The markup source buffer.
120
+ # @parameter delegate [Object] The delegate.
121
+ # @parameter entities [Hash] The named entity mappings.
122
+ # @returns [Nil] Parsing completes through delegate callbacks.
123
+ # @raises [UnbalancedTagError] If the markup contains mismatched tags.
87
124
  def self.parse(buffer, delegate, entities = XRB::Entities::HTML5)
88
125
  # This is for compatibility with the existing API which passes in a string:
89
126
  buffer = XRB::Buffer(buffer)
@@ -91,6 +128,10 @@ module Utopia
91
128
  self.new(buffer, delegate, entities).parse!
92
129
  end
93
130
 
131
+ # Initialize a streaming markup parser and its delegate.
132
+ # @parameter buffer [String] The markup source buffer.
133
+ # @parameter delegate [Object] The delegate.
134
+ # @parameter entities [Hash] The named entity mappings.
94
135
  def initialize(buffer, delegate, entities = XRB::Entities::HTML5)
95
136
  @buffer = buffer
96
137
 
@@ -101,6 +142,8 @@ module Utopia
101
142
  @stack = []
102
143
  end
103
144
 
145
+ # Parse the markup buffer into delegate callbacks and validate balanced tags.
146
+ # @returns [Nil] Parsing completes through delegate callbacks.
104
147
  def parse!
105
148
  XRB::Parsers.parse_markup(@buffer, self, @entities)
106
149
 
@@ -109,14 +152,25 @@ module Utopia
109
152
  end
110
153
  end
111
154
 
155
+ # Begin collecting an opening tag.
156
+ # @parameter name [String] The name.
157
+ # @parameter offset [Integer] The source offset.
158
+ # @returns [ParsedTag] The tag being collected.
112
159
  def open_tag_begin(name, offset)
113
160
  @current = ParsedTag.new(name, offset)
114
161
  end
115
162
 
163
+ # Process a markup attribute.
164
+ # @parameter key [String | Symbol] The lookup key.
165
+ # @parameter value [Object] The value to assign.
166
+ # @returns [Object] The assigned attribute value.
116
167
  def attribute(key, value)
117
168
  @current.tag.attributes[key] = value
118
169
  end
119
170
 
171
+ # Complete an opening tag and dispatch it to the delegate.
172
+ # @parameter self_closing [Boolean] Whether the tag is self-closing.
173
+ # @returns [Nil] The current tag is cleared.
120
174
  def open_tag_end(self_closing)
121
175
  if self_closing
122
176
  @current.tag.closed = true
@@ -129,6 +183,11 @@ module Utopia
129
183
  @current = nil
130
184
  end
131
185
 
186
+ # Process a closing markup tag.
187
+ # @parameter name [String] The name.
188
+ # @parameter offset [Integer] The source offset.
189
+ # @returns [Object] The delegate's tag-end result.
190
+ # @raises [UnbalancedTagError] If the closing tag does not match its opening tag.
132
191
  def close_tag(name, offset)
133
192
  @current = @stack.pop
134
193
  tag = @current.tag
@@ -140,22 +199,37 @@ module Utopia
140
199
  @delegate.tag_end(tag)
141
200
  end
142
201
 
202
+ # Process a document type declaration.
203
+ # @parameter string [String] The string.
204
+ # @returns [Object] The delegate's write result.
143
205
  def doctype(string)
144
206
  @delegate.write(string)
145
207
  end
146
208
 
209
+ # Process a markup comment.
210
+ # @parameter string [String] The string.
211
+ # @returns [Object] The delegate's write result.
147
212
  def comment(string)
148
213
  @delegate.write(string)
149
214
  end
150
215
 
216
+ # Process a markup instruction.
217
+ # @parameter string [String] The string.
218
+ # @returns [Object] The delegate's write result.
151
219
  def instruction(string)
152
220
  @delegate.write(string)
153
221
  end
154
222
 
223
+ # Process a CDATA section.
224
+ # @parameter string [String] The string.
225
+ # @returns [Object] The delegate's write result.
155
226
  def cdata(string)
156
227
  @delegate.write(string[9..-4])
157
228
  end
158
229
 
230
+ # Process text content.
231
+ # @parameter string [String] The string.
232
+ # @returns [Object] The delegate's text result.
159
233
  def text(string)
160
234
  @delegate.text(string)
161
235
  end