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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bake/utopia/environment.rb +6 -6
- data/bake/utopia/server.rb +4 -1
- data/bake/utopia/site.rb +14 -14
- data/bake/utopia/static.rb +26 -13
- data/bake/utopia.rb +1 -1
- data/context/getting-started.md +7 -5
- data/context/index.yaml +1 -2
- data/context/middleware.md +42 -13
- data/lib/utopia/application.rb +83 -0
- data/lib/utopia/content/builder.rb +25 -0
- data/lib/utopia/content/document.rb +64 -6
- data/lib/utopia/content/link.rb +50 -5
- data/lib/utopia/content/links.rb +44 -7
- data/lib/utopia/content/markup.rb +74 -0
- data/lib/utopia/content/middleware.rb +48 -20
- data/lib/utopia/content/namespace.rb +10 -1
- data/lib/utopia/content/node.rb +66 -4
- data/lib/utopia/content/response.rb +12 -3
- data/lib/utopia/content.rb +3 -0
- data/lib/utopia/controller/actions.md +4 -4
- data/lib/utopia/controller/actions.rb +51 -3
- data/lib/utopia/controller/base.rb +72 -29
- data/lib/utopia/controller/middleware.rb +20 -15
- data/lib/utopia/controller/respond.rb +36 -32
- data/lib/utopia/controller/responder.rb +106 -45
- data/lib/utopia/controller/result.rb +11 -0
- data/lib/utopia/controller/rewrite.rb +39 -0
- data/lib/utopia/controller/variables.rb +15 -3
- data/lib/utopia/controller.rb +2 -0
- data/lib/utopia/exceptions/handler.rb +20 -11
- data/lib/utopia/exceptions/mailer.rb +56 -51
- data/lib/utopia/extensions/array_split.rb +6 -0
- data/lib/utopia/extensions/date_comparisons.rb +6 -0
- data/lib/utopia/http.rb +11 -48
- data/lib/utopia/import_map.rb +19 -11
- data/lib/utopia/localization/locales.rb +60 -0
- data/lib/utopia/localization/middleware.rb +107 -73
- data/lib/utopia/localization/preferences.rb +76 -0
- data/lib/utopia/localization/resolver.rb +47 -0
- data/lib/utopia/localization.rb +6 -0
- data/lib/utopia/middleware.rb +3 -4
- data/lib/utopia/path/matcher.rb +15 -0
- data/lib/utopia/path.rb +146 -10
- data/lib/utopia/redirection/client_redirect.rb +87 -0
- data/lib/utopia/redirection/directory_index.rb +41 -0
- data/lib/utopia/redirection/errors.rb +78 -0
- data/lib/utopia/redirection/moved.rb +52 -0
- data/lib/utopia/redirection/request_failure.rb +26 -0
- data/lib/utopia/redirection/rewrite.rb +42 -0
- data/lib/utopia/redirection.rb +7 -165
- data/lib/utopia/request.rb +202 -0
- data/lib/utopia/response.rb +73 -0
- data/lib/utopia/session/lazy_hash.rb +27 -1
- data/lib/utopia/session/middleware.rb +103 -30
- data/lib/utopia/session/serialization.rb +8 -0
- data/lib/utopia/session.rb +5 -0
- data/lib/utopia/setup.rb +23 -3
- data/lib/utopia/shell.rb +28 -7
- data/lib/utopia/static/local_file.rb +84 -61
- data/lib/utopia/static/middleware.rb +65 -33
- data/lib/utopia/static/mime_types.rb +38 -29
- data/lib/utopia/static.rb +2 -0
- data/lib/utopia/version.rb +3 -2
- data/lib/utopia.rb +1 -1
- data/license.md +1 -1
- data/readme.md +33 -4
- data/releases.md +15 -0
- data/setup/site/bake.rb +1 -1
- data/setup/site/config/application.rb +51 -0
- data/setup/site/config/serve.rb +8 -0
- data/setup/site/falcon.rb +17 -4
- data/setup/site/fixtures/website.rb +27 -11
- data/setup/site/gems.rb +1 -3
- data/setup/site/lib/readme.txt +1 -1
- data/setup/site/pages/welcome/index.xnode +3 -3
- data/setup/site/readme.md +0 -3
- data/setup/site/test/website.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +60 -34
- metadata.gz.sig +0 -0
- data/lib/utopia/localization/wrapper.rb +0 -52
- data/setup/site/Guardfile +0 -12
- 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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
data/lib/utopia/content/link.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
data/lib/utopia/content/links.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|