homura-runtime 0.3.9 → 0.3.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d14417c1dedeec08cd1ee906d177d1c70dacb4d2a8e8d5979f815dd17340fc4
4
- data.tar.gz: 0d3e76edc794a886048959f63f258f581d4f8f8aae43fbe74807ab6417f2fd88
3
+ metadata.gz: c59a63b90f082b8a76fa50e58260b441620a134cec49b4b2e405e85d8a0f9d83
4
+ data.tar.gz: 345d24c902e7d4a22b36f5274cc14803bc55c49867a4b9edcf20fe9c38462df8
5
5
  SHA512:
6
- metadata.gz: a4aaa01b5c0a5953f5c5b4e57ece0344ab2de76081dc7f04c1844729fe1e3447f8e60935845808adfb8d12f7970a8a062134e9c11f02159d628d3a42f178ba4a
7
- data.tar.gz: 79be638101406487bf5409d0a6b2e4b6a516f75acf113b3b3bd7f2d9ac2e0532cd493579a22993b6dd9932d198748f3d2f3355ac9c9a58864b01c6ead3155dc8
6
+ metadata.gz: adff0dff8e7081b0ae2302dad372f5d16b5427341ed4d675121ce97f4906554967300ef232be08b8a530cd9322e79754eb9b6f7bda4885b550dfde663ca96d7d
7
+ data.tar.gz: cb1741f695090248426e9e5a57b4ec9cfecd4a294b85d20fa78d62e256d48941ef6375039360c1bdc395ab1d1c22fb3c7cd5c92411209ed06a2afb67dcf4a1f6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.10 (2026-06-25)
4
+
5
+ - Package the Phlex, Literal, and Zeitwerk Opal compatibility files in
6
+ `homura-runtime` itself. Published examples can now resolve those compat
7
+ requires from the runtime gem without depending on an unpublished
8
+ `opal-homura` stdlib change.
9
+
3
10
  ## 0.3.9 (2026-06-25)
4
11
 
5
12
  - Add the Homura Opal gem prelude for published Phlex/Literal apps. The build
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HomuraRuntime
4
- VERSION = "0.3.9"
4
+ VERSION = "0.3.10"
5
5
  end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk/opal_compat"
4
+ require "literal" unless defined?(Literal)
5
+
6
+ module Literal
7
+ class OpalBuffer
8
+ def initialize(value = "")
9
+ @value = value.to_s
10
+ end
11
+
12
+ def <<(value)
13
+ @value += value.to_s
14
+ self
15
+ end
16
+
17
+ def to_s
18
+ @value
19
+ end
20
+
21
+ alias to_str to_s
22
+
23
+ def encoding
24
+ @value.encoding
25
+ end
26
+
27
+ def force_encoding(_encoding)
28
+ @value
29
+ end
30
+
31
+ def method_missing(name, ...)
32
+ if @value.respond_to?(name)
33
+ @value.public_send(name, ...)
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def respond_to_missing?(name, include_private = false)
40
+ @value.respond_to?(name, include_private) || super
41
+ end
42
+ end
43
+ end
44
+
45
+ module Literal::Properties
46
+ private def __define_literal_methods__(new_property)
47
+ extension = __literal_extension__
48
+
49
+ extension.define_method(:initialize) do |*args, **kwargs, &block|
50
+ positional_index = 0
51
+ properties = self.class.literal_properties
52
+
53
+ properties.each do |property|
54
+ value = case property.kind
55
+ when :positional
56
+ if positional_index < args.length
57
+ args[positional_index]
58
+ elsif property.default?
59
+ Literal::Undefined
60
+ elsif property.type === nil
61
+ nil
62
+ else
63
+ Literal::Undefined
64
+ end
65
+ .tap { positional_index += 1 }
66
+ when :*
67
+ args[positional_index..] || []
68
+ when :keyword
69
+ if kwargs.key?(property.name)
70
+ kwargs.delete(property.name)
71
+ elsif property.default?
72
+ Literal::Undefined
73
+ elsif property.type === nil
74
+ nil
75
+ else
76
+ Literal::Undefined
77
+ end
78
+
79
+ when :**
80
+ kwargs
81
+ when :&
82
+ block
83
+ else
84
+ raise "You should never see this error."
85
+ end
86
+
87
+ value = property.default_value(self) if property.default? && Literal::Undefined == value
88
+ value = property.coerce(value, context: self) if property.coercion
89
+ property.check_initializer(self, value)
90
+ instance_variable_set(:"@#{property.name}", value)
91
+ end
92
+
93
+ after_initialize if respond_to?(:after_initialize, true)
94
+ rescue Literal::TypeError => error
95
+ error.set_backtrace(caller(2))
96
+ raise
97
+ end
98
+
99
+ extension.define_method(:to_h) do
100
+ self.class.literal_properties.each.each_with_object({}) do |property, hash|
101
+ hash[property.name] = instance_variable_get(:"@#{property.name}")
102
+ end
103
+ end
104
+
105
+ extension.alias_method(:to_hash, :to_h)
106
+
107
+ define_literal_reader(extension, new_property) if new_property.reader
108
+ define_literal_writer(extension, new_property) if new_property.writer
109
+ define_literal_predicate(extension, new_property) if new_property.predicate
110
+ end
111
+
112
+ private def define_literal_reader(extension, property)
113
+ extension.define_method(property.name) do
114
+ instance_variable_get(:"@#{property.name}")
115
+ end
116
+
117
+ extension.__send__(property.reader, property.name)
118
+ end
119
+
120
+ private def define_literal_writer(extension, property)
121
+ method_name = :"#{property.name}="
122
+ extension.define_method(method_name) do |value|
123
+ property.check_writer(self, value)
124
+ instance_variable_set(:"@#{property.name}", value)
125
+ rescue Literal::TypeError => error
126
+ error.set_backtrace(caller(1))
127
+ raise
128
+ end
129
+
130
+ extension.__send__(property.writer, method_name)
131
+ end
132
+
133
+ private def define_literal_predicate(extension, property)
134
+ method_name = :"#{property.name}?"
135
+ extension.define_method(method_name) do
136
+ !!instance_variable_get(:"@#{property.name}")
137
+ end
138
+
139
+ extension.__send__(property.predicate, method_name)
140
+ end
141
+ end
@@ -0,0 +1,403 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "corelib/pattern_matching"
4
+ require "zeitwerk/opal_compat"
5
+ require "phlex" unless defined?(Phlex)
6
+
7
+ Phlex::SGML
8
+ Phlex::SGML::State
9
+ Phlex::SGML::Attributes
10
+ Phlex::SGML::Elements
11
+
12
+ module Phlex
13
+ class OpalBuffer
14
+ def initialize(value = "")
15
+ @value = value.to_s
16
+ end
17
+
18
+ def <<(value)
19
+ @value += value.to_s
20
+ self
21
+ end
22
+
23
+ def bytesize
24
+ @value.bytesize
25
+ end
26
+
27
+ def byteslice(offset, length = nil)
28
+ length ? @value.byteslice(offset, length) : @value.byteslice(offset)
29
+ end
30
+
31
+ def clear
32
+ @value = ""
33
+ self
34
+ end
35
+
36
+ def dup
37
+ self.class.new(@value)
38
+ end
39
+
40
+ def empty?
41
+ @value.empty?
42
+ end
43
+
44
+ def gsub(...)
45
+ @value.gsub(...)
46
+ end
47
+
48
+ def encoding
49
+ @value.encoding
50
+ end
51
+
52
+ def force_encoding(_encoding)
53
+ @value
54
+ end
55
+
56
+ def valid_encoding?
57
+ true
58
+ end
59
+
60
+ def length
61
+ @value.length
62
+ end
63
+
64
+ alias size length
65
+
66
+ def to_s
67
+ @value
68
+ end
69
+
70
+ alias to_str to_s
71
+
72
+ def inspect
73
+ @value.inspect
74
+ end
75
+
76
+ def ==(other)
77
+ @value == other.to_s
78
+ end
79
+
80
+ def eql?(other)
81
+ self == other
82
+ end
83
+
84
+ def hash
85
+ @value.hash
86
+ end
87
+
88
+ def method_missing(name, ...)
89
+ if @value.respond_to?(name)
90
+ @value.public_send(name, ...)
91
+ else
92
+ super
93
+ end
94
+ end
95
+
96
+ def respond_to_missing?(name, include_private = false)
97
+ @value.respond_to?(name, include_private) || super
98
+ end
99
+ end
100
+ end
101
+
102
+ if defined?(Phlex::SGML)
103
+ class Phlex::SGML
104
+ def call(buffer = Phlex::OpalBuffer.new, context: {}, fragments: nil, &)
105
+ buffer = Phlex::OpalBuffer.new(buffer) if buffer.is_a?(String)
106
+ state = Phlex::SGML::State.new(
107
+ user_context: context,
108
+ output_buffer: buffer,
109
+ fragments: fragments&.to_set
110
+ )
111
+
112
+ internal_call(parent: nil, state:, &)
113
+
114
+ state.output_buffer << state.buffer
115
+ end
116
+
117
+ private def __homura_normalize_comma_separated_tokens__(method_name, attributes)
118
+ case method_name
119
+ when :img
120
+ if Array === (srcset_attribute = attributes[:srcset])
121
+ attributes[:srcset] = Phlex::SGML::Attributes.generate_nested_tokens(
122
+ srcset_attribute,
123
+ ", ",
124
+ ",",
125
+ "%2C"
126
+ )
127
+ end
128
+
129
+ when :input
130
+ if Array === (accept_attribute = attributes[:accept])
131
+ type_attribute = attributes[:type] || attributes["type"]
132
+ if "file" == type_attribute || :file == type_attribute
133
+ attributes[:accept] = Phlex::SGML::Attributes.generate_nested_tokens(
134
+ accept_attribute,
135
+ ", ",
136
+ ",",
137
+ "%2C"
138
+ )
139
+ end
140
+ end
141
+
142
+ when :link
143
+ if Array === (media_attribute = attributes[:media])
144
+ attributes[:media] = Phlex::SGML::Attributes.generate_nested_tokens(
145
+ media_attribute,
146
+ ", ",
147
+ ",",
148
+ "%2C"
149
+ )
150
+ end
151
+
152
+ if Array === (sizes_attribute = attributes[:sizes])
153
+ attributes[:sizes] = Phlex::SGML::Attributes.generate_nested_tokens(
154
+ sizes_attribute,
155
+ ", ",
156
+ ",",
157
+ "%2C"
158
+ )
159
+ end
160
+
161
+ if Array === (imagesrcset_attribute = attributes[:imagesrcset])
162
+ rel_attribute = attributes[:rel] || attributes["rel"]
163
+ as_attribute = attributes[:as] || attributes["as"]
164
+ if ("preload" == rel_attribute || :preload == rel_attribute) &&
165
+ ("image" == as_attribute || :image == as_attribute)
166
+ attributes[:imagesrcset] = Phlex::SGML::Attributes.generate_nested_tokens(
167
+ imagesrcset_attribute,
168
+ ", ",
169
+ ",",
170
+ "%2C"
171
+ )
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ class Phlex::SGML::State
179
+ def initialize(user_context: {}, output_buffer:, fragments:)
180
+ @buffer = Phlex::OpalBuffer.new
181
+ @capturing = false
182
+ @user_context = user_context
183
+ @fragments = fragments
184
+ @fragment_depth = 0
185
+ @cache_stack = []
186
+ @halt_signal = nil
187
+ @output_buffer = output_buffer
188
+ end
189
+
190
+ def capture
191
+ new_buffer = Phlex::OpalBuffer.new
192
+ original_buffer = @buffer
193
+ original_capturing = @capturing
194
+ original_fragments = @fragments
195
+
196
+ begin
197
+ @buffer = new_buffer
198
+ @capturing = true
199
+ @fragments = nil
200
+ yield
201
+ ensure
202
+ @buffer = original_buffer
203
+ @capturing = original_capturing
204
+ @fragments = original_fragments
205
+ end
206
+
207
+ new_buffer
208
+ end
209
+ end
210
+
211
+ module Phlex::SGML::Elements
212
+ def register_element(method_name, tag: method_name.name.tr("_", "-"))
213
+ define_method(method_name) do |**attributes, &content|
214
+ state = @_state
215
+ buffer = state.buffer
216
+ has_content = !content.nil?
217
+
218
+ unless state.should_render?
219
+ content.call(self) if has_content
220
+ return nil
221
+ end
222
+
223
+ if attributes.length > 0
224
+ buffer << "<#{tag}"
225
+ begin
226
+ __homura_normalize_comma_separated_tokens__(method_name, attributes)
227
+ buffer << (Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes))
228
+ ensure
229
+ buffer << ">"
230
+ end
231
+
232
+ if has_content
233
+ begin
234
+ original_length = buffer.bytesize
235
+ rendered_content = content.call(self)
236
+ __implicit_output__(rendered_content) if original_length == buffer.bytesize
237
+ ensure
238
+ buffer << "</#{tag}>"
239
+ end
240
+ else
241
+ buffer << "</#{tag}>"
242
+ end
243
+ elsif has_content
244
+ buffer << "<#{tag}>"
245
+ begin
246
+ original_length = buffer.bytesize
247
+ rendered_content = content.call(self)
248
+ __implicit_output__(rendered_content) if original_length == buffer.bytesize
249
+ ensure
250
+ buffer << "</#{tag}>"
251
+ end
252
+ else
253
+ buffer << "<#{tag}></#{tag}>"
254
+ end
255
+
256
+ flush if tag == "head"
257
+ nil
258
+ end
259
+
260
+ __registered_elements__[method_name] = tag
261
+ method_name
262
+ end
263
+
264
+ def __register_void_element__(method_name, tag: method_name.name.tr("_", "-"))
265
+ define_method(method_name) do |**attributes|
266
+ state = @_state
267
+ return unless state.should_render?
268
+
269
+ buffer = state.buffer
270
+
271
+ if attributes.length > 0
272
+ buffer << "<#{tag}"
273
+ begin
274
+ __homura_normalize_comma_separated_tokens__(method_name, attributes)
275
+ buffer << (Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes))
276
+ ensure
277
+ buffer << ">"
278
+ end
279
+ else
280
+ buffer << "<#{tag}>"
281
+ end
282
+
283
+ nil
284
+ end
285
+
286
+ __registered_elements__[method_name] = tag
287
+ method_name
288
+ end
289
+ end
290
+
291
+ module Phlex::SGML::Attributes
292
+ class << self
293
+ alias __homura_generate_attributes__ generate_attributes
294
+ alias __homura_generate_nested_attributes__ generate_nested_attributes
295
+
296
+ def generate_attributes(attributes, buffer = Phlex::OpalBuffer.new)
297
+ __homura_generate_attributes__(attributes, buffer)
298
+ end
299
+
300
+ def generate_nested_attributes(attributes, base_name, buffer = Phlex::OpalBuffer.new)
301
+ __homura_generate_nested_attributes__(attributes, base_name, buffer)
302
+ end
303
+
304
+ def generate_nested_tokens(tokens, sep = " ", gsub_from = nil, gsub_to = "")
305
+ buffer = Phlex::OpalBuffer.new
306
+
307
+ i, length = 0, tokens.length
308
+
309
+ while i < length
310
+ token = tokens[i]
311
+
312
+ case token
313
+ when String
314
+ token = token.gsub(gsub_from, gsub_to) if gsub_from
315
+ i > 0 ? buffer << sep << token : buffer << token
316
+ when Symbol
317
+ value = token.name.tr("_", "-")
318
+ i > 0 ? buffer << sep << value : buffer << value
319
+ when Integer, Float, Phlex::SGML::SafeObject
320
+ i > 0 ? buffer << sep << token.to_s : buffer << token.to_s
321
+ when Array
322
+ if token.length > 0 && (value = generate_nested_tokens(token, sep, gsub_from, gsub_to))
323
+ i > 0 ? buffer << sep << value : buffer << value
324
+ end
325
+
326
+ when Set
327
+ if token.length > 0 && (value = generate_nested_tokens(token.to_a, sep, gsub_from, gsub_to))
328
+ i > 0 ? buffer << sep << value : buffer << value
329
+ end
330
+
331
+ when nil
332
+ nil
333
+ else
334
+ raise Phlex::ArgumentError.new("Invalid token type: #{token.class}.")
335
+ end
336
+
337
+ i += 1
338
+ end
339
+
340
+ return if buffer.empty?
341
+
342
+ buffer.gsub("\"", "&quot;")
343
+ end
344
+
345
+ def generate_styles(styles)
346
+ case styles
347
+ when Array, Set
348
+ styles
349
+ .filter_map do |s|
350
+ case s
351
+ when String
352
+ s == "" || s.end_with?(";") ? s : "#{s};"
353
+ when Phlex::SGML::SafeObject
354
+ value = s.to_s
355
+ value.end_with?(";") ? value : "#{value};"
356
+ when Hash
357
+ next generate_styles(s)
358
+ when nil
359
+ next nil
360
+ else
361
+ raise Phlex::ArgumentError.new("Invalid style: #{s.inspect}.")
362
+ end
363
+ end
364
+ .join(" ")
365
+ when Hash
366
+ buffer = Phlex::OpalBuffer.new
367
+ i = 0
368
+ styles.each do |k, v|
369
+ prop = case k
370
+ when String
371
+ k
372
+ when Symbol
373
+ k.name.tr("_", "-")
374
+ else
375
+ raise Phlex::ArgumentError.new("Style keys should be Strings or Symbols.")
376
+ end
377
+
378
+ value = case v
379
+ when String
380
+ v
381
+ when Symbol
382
+ v.name.tr("_", "-")
383
+ when Integer, Float, Phlex::SGML::SafeObject
384
+ v.to_s
385
+ when nil
386
+ nil
387
+ else
388
+ raise Phlex::ArgumentError.new("Invalid style value: #{v.inspect}")
389
+ end
390
+
391
+ if value
392
+ i == 0 ? buffer << prop << ": " << value << ";" : buffer << " " << prop << ": " << value << ";"
393
+ end
394
+
395
+ i += 1
396
+ end
397
+
398
+ buffer
399
+ end
400
+ end
401
+ end
402
+ end
403
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ module Zeitwerk
6
+ class << self
7
+ attr_accessor :__homura_next_gem_root
8
+
9
+ def __homura_next_loader_tag
10
+ @__homura_loader_tag_sequence ||= 0
11
+ @__homura_loader_tag_sequence += 1
12
+ "homura-#{@__homura_loader_tag_sequence}"
13
+ end
14
+ end
15
+
16
+ class Inflector
17
+ def camelize(basename, _abspath)
18
+ overrides[basename] || basename.split("_").map(&:capitalize).join
19
+ end
20
+ end
21
+
22
+ class Loader
23
+ class << self
24
+ alias __homura_original_for_gem__ for_gem
25
+
26
+ def for_gem(warn_on_extra_files: true)
27
+ if (root_file = Zeitwerk.__homura_next_gem_root)
28
+ Zeitwerk.__homura_next_gem_root = nil
29
+ Registry.loader_for_gem(
30
+ root_file,
31
+ namespace: Object,
32
+ warn_on_extra_files: false
33
+ )
34
+ else
35
+ __homura_original_for_gem__(warn_on_extra_files: warn_on_extra_files)
36
+ end
37
+ end
38
+ end
39
+
40
+ module Config
41
+ def initialize
42
+ @inflector = Zeitwerk::Inflector.new
43
+ @logger = self.class.default_logger
44
+ @tag = Zeitwerk.__homura_next_loader_tag
45
+ @initialized_at = Time.now
46
+ @roots = {}
47
+ @nsfile = nil
48
+ @ignored_glob_patterns = Set.new
49
+ @ignored_paths = Set.new
50
+ @collapse_glob_patterns = Set.new
51
+ @collapse_dirs = Set.new
52
+ @collapse_parents = Set.new
53
+ @eager_load_exclusions = Set.new
54
+ @reloading_enabled = false
55
+ @on_setup_callbacks = []
56
+ @on_load_callbacks = {}
57
+ @on_unload_callbacks = {}
58
+ end
59
+ end
60
+
61
+ private def define_autoloads_for_dir(dir, mod, external:)
62
+ @fs.ls(dir) do |basename, abspath, ftype|
63
+ if ftype == :file
64
+ if basename == @nsfile
65
+ if external
66
+ cpath = real_mod_name(mod)
67
+ location = Object.const_source_location(cpath)&.join(":")
68
+ location = nil if location&.empty?
69
+ raise(
70
+ Zeitwerk::ConflictingNamespaceDefinitionError.new(
71
+ cpath,
72
+ location: location,
73
+ conflicting_file: abspath
74
+ )
75
+ )
76
+ end
77
+
78
+ next
79
+ end
80
+
81
+ basename = basename.delete_suffix(".rb")
82
+ cref = Cref.new(mod, cname_for(basename, abspath))
83
+ visit_file(cref, abspath)
84
+ else
85
+ cref = Cref.new(mod, cname_for(basename, abspath))
86
+ visit_subdir(cref, abspath, external: external)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: homura-runtime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.9
4
+ version: 0.3.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuhiro Homma
@@ -71,7 +71,10 @@ files:
71
71
  - lib/homura_vendor_tempfile.rb
72
72
  - lib/homura_vendor_tilt.rb
73
73
  - lib/homura_vendor_zlib.rb
74
+ - lib/literal/opal_compat.rb
74
75
  - lib/opal_patches.rb
76
+ - lib/phlex/opal_compat.rb
77
+ - lib/zeitwerk/opal_compat.rb
75
78
  - runtime/patch-opal-evals.mjs
76
79
  - runtime/setup-node-crypto.mjs
77
80
  - runtime/worker.mjs