expo_turbo-rails 0.1.6 → 0.1.7
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
- data/README.md +3 -3
- data/lib/expo_turbo/rails/template_capabilities.rb +12 -2
- data/lib/expo_turbo/rails/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c37a46309254f182317989ff379dda0157142f6057027541d0efcfe57d41c1b
|
|
4
|
+
data.tar.gz: ef34d5364e16bce90e21e3a861a7347d06a8908fbc56beec0da0c6a151329e72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c40f69d53c21e181a20a8b0cedd5d8d5299ea4561dbc76588169ec075d7a1f90dcdeef01e7e39e7352421b2e664d156756c61b45cd8eae40119f6a29f4dcf6f8
|
|
7
|
+
data.tar.gz: 5b9e77f8c89554f9a6dcabe7c7c2caba3f5fb0c6ad781b1f68d2b5e58d124b38af49520fe2d5e0ffb6f4bd288cdb4163bc65b71768839723e8b124b02f7561cc
|
data/README.md
CHANGED
|
@@ -5,10 +5,10 @@ The Rails package for Expo Turbo. It registers the distinct `application/vnd.exp
|
|
|
5
5
|
The package validates rendered Expo Turbo documents structurally and rejects blank or duplicate literal IDs across the complete response, including Frame IDs. A controller must declare the components and style tokens it is allowed to render documents; when it does, the same policy also applies to its Frame, Stream, and raw controller-broadcast output. Its optional protected Cable boundary delegates all credentials and resource policy to the host.
|
|
6
6
|
|
|
7
7
|
> [!IMPORTANT]
|
|
8
|
-
> Version [`0.1.
|
|
8
|
+
> Version [`0.1.7`](https://rubygems.org/gems/expo_turbo-rails) is the stable
|
|
9
9
|
> release published on 2026-07-31. Manual VoiceOver, TalkBack, and browser
|
|
10
10
|
> screen-reader evidence remains an explicit follow-up and is not claimed by
|
|
11
|
-
> the `0.1.
|
|
11
|
+
> the `0.1.7` compatibility surface.
|
|
12
12
|
|
|
13
13
|
```ruby
|
|
14
14
|
gem "expo_turbo-rails"
|
|
@@ -54,7 +54,7 @@ class ExpoTurboController < ActionController::API
|
|
|
54
54
|
end
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
The template argument is relative to the configured root; absolute paths, traversal, missing files, and symlink escapes are rejected. The resolved `.xml.erb` source is evaluated as ERB with layouts disabled, rather than served as raw file content. Before it renders, the exact output must be a strict UTF-8 XML document: one root, valid namespaces and attributes, no DTD or processing instruction, and an optional leading UTF-8 XML declaration only. Every literal `id` must also be nonblank and unique across the complete rendered document, including nested Frames. The capability declaration then admits only its exact components (and explicit aliases), exact unprefixed `turbo-frame`, `turbo-stream`, `template`, and `turbo-cable-stream-source` wrappers (including default-namespace elements), and declared `style-tokens`. Style-token lists use the same JavaScript whitespace split, count, duplicate, component, and group-conflict rules as the native adapter. A component must opt into the `style-tokens` attribute, and style-token component lists are canonicalized through aliases. A generated registry manifest also rejects undeclared component attributes and missing required attributes. Shared protocol attributes such as `id`, `class`, `data-*`, and `xml:space` stay available. The host declaration must mirror its installed registry and style adapter. Attribute values still receive their full codec and Zod validation on the client. Validation never serializes the output, so it does not alter preserved XML text.
|
|
57
|
+
The template argument is relative to the configured root; absolute paths, traversal, missing files, and symlink escapes are rejected. The resolved `.xml.erb` source is evaluated as ERB with layouts disabled, rather than served as raw file content. Before it renders, the exact output must be a strict UTF-8 XML document: one root, valid namespaces and attributes, no DTD or processing instruction, and an optional leading UTF-8 XML declaration only. Every literal `id` must also be nonblank and unique across the complete rendered document, including nested Frames. The capability declaration then admits only its exact components (and explicit aliases), exact unprefixed `turbo-frame`, `turbo-stream`, `template`, and `turbo-cable-stream-source` wrappers (including default-namespace elements), and declared `style-tokens`. Style-token lists use the same JavaScript whitespace split, count, duplicate, component, and group-conflict rules as the native adapter. A component must opt into the `style-tokens` attribute, and style-token component lists are canonicalized through aliases. A generated registry manifest also rejects undeclared component attributes and missing required attributes. Shared protocol attributes such as `id`, `class`, `data-*`, and `xml:space` stay available. Components with `formOwner: true` also admit the form protocol attributes `action`, `enctype`, `method`, `novalidate`, and `target` without declaring them as component props. The host declaration must mirror its installed registry and style adapter. Attribute values still receive their full codec and Zod validation on the client. Validation never serializes the output, so it does not alter preserved XML text.
|
|
58
58
|
|
|
59
59
|
Do not put multiline values in XML attributes with ordinary ERB interpolation. The client XML parser changes raw tabs and line breaks in an attribute to spaces before component decoding. A later form submission can then save the changed value. Use `expo_turbo_attribute` for each value that can contain this whitespace:
|
|
60
60
|
|
|
@@ -8,6 +8,7 @@ module ExpoTurbo
|
|
|
8
8
|
MANIFEST_VERSION = 1
|
|
9
9
|
PROTOCOL_ELEMENTS = %w[turbo-cable-stream-source turbo-frame turbo-stream template].freeze
|
|
10
10
|
RESERVED_COMPONENT_NAMES = [*PROTOCOL_ELEMENTS, "expo-turbo-fragment"].freeze
|
|
11
|
+
FORM_OWNER_ATTRIBUTE_NAMES = %w[action enctype method novalidate target].freeze
|
|
11
12
|
SHARED_ATTRIBUTE_NAMES = %w[autofocus class dir dirname form id xml:space xmlns].freeze
|
|
12
13
|
TOKEN_PATTERN = /\A[a-z][a-z0-9-]*(?::[a-z][a-z0-9-]*)*\z/
|
|
13
14
|
MAX_TOKEN_LENGTH = 64
|
|
@@ -124,6 +125,9 @@ module ExpoTurbo
|
|
|
124
125
|
unless component["aliases"].all? { |alias_name| alias_name.is_a?(String) }
|
|
125
126
|
raise ConfigurationError, "Expo Turbo capability manifest aliases must be strings"
|
|
126
127
|
end
|
|
128
|
+
if component.key?("formOwner") && ![true, false].include?(component["formOwner"])
|
|
129
|
+
raise ConfigurationError, "Expo Turbo capability manifest form ownership must be boolean"
|
|
130
|
+
end
|
|
127
131
|
|
|
128
132
|
attributes = component["attributes"]
|
|
129
133
|
valid_attributes = attributes.is_a?(Array) && attributes.all? do |attribute|
|
|
@@ -145,6 +149,7 @@ module ExpoTurbo
|
|
|
145
149
|
components[component["tag"]] = {
|
|
146
150
|
aliases: component["aliases"],
|
|
147
151
|
attributes: attribute_names,
|
|
152
|
+
form_owner: component["formOwner"] == true,
|
|
148
153
|
required_attributes: attributes.filter_map { |attribute| attribute["name"] if attribute["required"] },
|
|
149
154
|
style_tokens: attribute_names.include?("style-tokens")
|
|
150
155
|
}
|
|
@@ -164,6 +169,7 @@ module ExpoTurbo
|
|
|
164
169
|
if manifest_backed
|
|
165
170
|
component_attributes[tag] = {
|
|
166
171
|
allowed: configuration[:attributes].to_h { |name| [name, true] }.freeze,
|
|
172
|
+
form_owner: configuration[:form_owner],
|
|
167
173
|
required: configuration[:required_attributes].to_h { |name| [name, true] }.freeze
|
|
168
174
|
}.freeze
|
|
169
175
|
end
|
|
@@ -176,7 +182,7 @@ module ExpoTurbo
|
|
|
176
182
|
def normalize_component_configuration(tag, configuration, manifest_backed:)
|
|
177
183
|
configuration = {} if configuration.nil?
|
|
178
184
|
allowed_keys = %i[aliases style_tokens]
|
|
179
|
-
allowed_keys.concat(%i[attributes required_attributes]) if manifest_backed
|
|
185
|
+
allowed_keys.concat(%i[attributes form_owner required_attributes]) if manifest_backed
|
|
180
186
|
unless configuration.is_a?(Hash) && (configuration.keys - allowed_keys).empty?
|
|
181
187
|
raise ConfigurationError, "Expo Turbo component #{tag.inspect} accepts only aliases and style_tokens"
|
|
182
188
|
end
|
|
@@ -197,10 +203,12 @@ module ExpoTurbo
|
|
|
197
203
|
end
|
|
198
204
|
|
|
199
205
|
attributes = manifest_backed ? configuration.fetch(:attributes) : []
|
|
206
|
+
form_owner = manifest_backed ? configuration.fetch(:form_owner) : false
|
|
200
207
|
required_attributes = manifest_backed ? configuration.fetch(:required_attributes) : []
|
|
201
208
|
{
|
|
202
209
|
aliases: aliases.freeze,
|
|
203
210
|
attributes: attributes.freeze,
|
|
211
|
+
form_owner:,
|
|
204
212
|
required_attributes: required_attributes.freeze,
|
|
205
213
|
style_tokens:
|
|
206
214
|
}.freeze
|
|
@@ -336,7 +344,9 @@ module ExpoTurbo
|
|
|
336
344
|
element.attribute_nodes.each do |attribute|
|
|
337
345
|
name = qualified_attribute_name(attribute)
|
|
338
346
|
present[name] = true
|
|
339
|
-
next if shared_attribute_name?(name) ||
|
|
347
|
+
next if shared_attribute_name?(name) ||
|
|
348
|
+
(capabilities[:form_owner] && FORM_OWNER_ATTRIBUTE_NAMES.include?(name)) ||
|
|
349
|
+
capabilities[:allowed].key?(name)
|
|
340
350
|
|
|
341
351
|
raise ValidationError, "Expo Turbo template contains an undeclared component attribute"
|
|
342
352
|
end
|