homura-runtime 0.3.6 → 0.3.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/CHANGELOG.md +11 -0
- data/exe/auto-await +42 -27
- data/exe/compile-assets +46 -37
- data/exe/compile-erb +86 -61
- data/exe/homura-build +223 -119
- data/lib/homura/runtime/ai.rb +316 -22
- data/lib/homura/runtime/async_registry.rb +135 -98
- data/lib/homura/runtime/auto_await/analyzer.rb +34 -19
- data/lib/homura/runtime/auto_await/transformer.rb +1 -1
- data/lib/homura/runtime/build_support.rb +74 -38
- data/lib/homura/runtime/cache.rb +29 -22
- data/lib/homura/runtime/durable_object.rb +110 -56
- data/lib/homura/runtime/email.rb +28 -14
- data/lib/homura/runtime/http.rb +5 -4
- data/lib/homura/runtime/multipart.rb +47 -47
- data/lib/homura/runtime/queue.rb +82 -29
- data/lib/homura/runtime/scheduled.rb +29 -19
- data/lib/homura/runtime/stream.rb +30 -24
- data/lib/homura/runtime/version.rb +1 -1
- data/lib/homura/runtime.rb +330 -131
- data/lib/homura_vendor_tempfile.rb +5 -4
- data/lib/homura_vendor_tilt.rb +4 -3
- data/lib/homura_vendor_zlib.rb +20 -13
- data/lib/opal_patches.rb +196 -109
- 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: 90ff82f40bdd001631e5dd1626579eaa0c3e62b2b8569b5d33829203b44c7a17
|
|
4
|
+
data.tar.gz: 865e7c3bc07aa6b48362afb2c436eaf09c33f8dd41c98ac6c59ccacbb8b4f718
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7814e73863240b28be6da34ab8043da6e6ad64eadc29a083b6feb115b5ee55e1549c9271980bc83ef30a1b009a1e31ab9624b9fbc9abb8f43dd8f273c1ea4837
|
|
7
|
+
data.tar.gz: a7a9fd42c426e784c6938816c83c6f55924ad0256f953978dd6fbce1ce827c94634cb5d3b0b2a08bcc98375b40a261749e4884bd547c6500cb13c7b9e5847633
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.7 (2026-05-06)
|
|
4
|
+
|
|
5
|
+
- Add Ruby-shaped Workers AI helpers for the staged voice examples:
|
|
6
|
+
`ai.chat`, `ai.chat_text`, `ai.transcribe`, `ai.transcribe_text`,
|
|
7
|
+
`ai.speak`, and `ai.speak_data_url`.
|
|
8
|
+
- `ai.speak` returns a `Cloudflare::BinaryBody`, so a Sinatra route can
|
|
9
|
+
stream Aura audio directly back to the browser without exposing JS
|
|
10
|
+
response plumbing in user code.
|
|
11
|
+
- Register the new AI helpers with auto-await so example/source Ruby stays
|
|
12
|
+
sync-shaped.
|
|
13
|
+
|
|
3
14
|
## 0.3.6 (2026-05-03)
|
|
4
15
|
|
|
5
16
|
- Release the no-await-surface examples/docs baseline: public Rack,
|
data/exe/auto-await
CHANGED
|
@@ -13,34 +13,47 @@
|
|
|
13
13
|
# output directory earlier on the Opal load path and fall back to
|
|
14
14
|
# the original input for unchanged files.
|
|
15
15
|
|
|
16
|
-
require
|
|
17
|
-
require
|
|
18
|
-
require_relative
|
|
16
|
+
require "fileutils"
|
|
17
|
+
require "pathname"
|
|
18
|
+
require_relative "../lib/homura/runtime/build_support"
|
|
19
19
|
|
|
20
20
|
# homura-runtime lib path resolution (with legacy alias fallback)
|
|
21
|
-
runtime_lib = ENV[
|
|
21
|
+
runtime_lib = ENV["CFW_RUNTIME_LIB"]
|
|
22
22
|
unless runtime_lib
|
|
23
|
-
runtime_lib =
|
|
23
|
+
runtime_lib =
|
|
24
|
+
HomuraRuntime::BuildSupport
|
|
25
|
+
.runtime_root(current_file: __FILE__)
|
|
26
|
+
.join("lib")
|
|
27
|
+
.to_s
|
|
24
28
|
end
|
|
25
29
|
$LOAD_PATH.unshift(runtime_lib) unless $LOAD_PATH.include?(runtime_lib)
|
|
26
30
|
|
|
27
|
-
require
|
|
28
|
-
require
|
|
29
|
-
require
|
|
31
|
+
require "homura/runtime/async_registry"
|
|
32
|
+
require "homura/runtime/auto_await/analyzer"
|
|
33
|
+
require "homura/runtime/auto_await/transformer"
|
|
30
34
|
|
|
31
|
-
options = {
|
|
35
|
+
options = {
|
|
36
|
+
input: nil,
|
|
37
|
+
output: nil,
|
|
38
|
+
debug: ENV["CLOUDFLARE_WORKERS_AUTO_AWAIT_DEBUG"] == "1"
|
|
39
|
+
}
|
|
32
40
|
|
|
33
41
|
ARGV.each_with_index do |arg, i|
|
|
34
42
|
case arg
|
|
35
|
-
when
|
|
36
|
-
|
|
37
|
-
when
|
|
43
|
+
when "--input"
|
|
44
|
+
options[:input] = ARGV[i + 1]
|
|
45
|
+
when "--output"
|
|
46
|
+
options[:output] = ARGV[i + 1]
|
|
47
|
+
when "--debug"
|
|
48
|
+
options[:debug] = true
|
|
38
49
|
end
|
|
39
50
|
end
|
|
40
51
|
|
|
41
|
-
|
|
52
|
+
unless options[:input] && options[:output]
|
|
53
|
+
abort("Usage: auto-await --input DIR --output DIR [--debug]")
|
|
54
|
+
end
|
|
42
55
|
|
|
43
|
-
input_root
|
|
56
|
+
input_root = Pathname(options[:input]).expand_path
|
|
44
57
|
output_root = Pathname(options[:output]).expand_path
|
|
45
58
|
|
|
46
59
|
registry = HomuraRuntime::AsyncRegistry.instance
|
|
@@ -49,18 +62,19 @@ registry = HomuraRuntime::AsyncRegistry.instance
|
|
|
49
62
|
HomuraRuntime::AsyncRegistry.auto_load_gem_async_sources(debug: options[:debug])
|
|
50
63
|
|
|
51
64
|
# Load project-specific async source registrations if present.
|
|
52
|
-
project_async = File.join(Dir.pwd,
|
|
65
|
+
project_async = File.join(Dir.pwd, "lib", "homura_async_sources.rb")
|
|
53
66
|
require project_async if File.exist?(project_async)
|
|
54
67
|
|
|
55
68
|
changed = 0
|
|
56
69
|
skipped = 0
|
|
57
|
-
errors
|
|
70
|
+
errors = 0
|
|
58
71
|
|
|
59
|
-
paths =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
paths =
|
|
73
|
+
if File.directory?(input_root)
|
|
74
|
+
Dir.glob(input_root.join("**", "*.rb")).sort
|
|
75
|
+
else
|
|
76
|
+
[input_root.to_s]
|
|
77
|
+
end
|
|
64
78
|
|
|
65
79
|
paths.each do |path|
|
|
66
80
|
rel = Pathname(path).relative_path_from(input_root)
|
|
@@ -72,10 +86,11 @@ paths.each do |path|
|
|
|
72
86
|
# needed to tell Opal to wrap the file in an async function and to
|
|
73
87
|
# translate #__await__ calls into real JS await keywords.
|
|
74
88
|
has_magic = source.lines.first(5).any? { |l| l.match?(/#\s*await:/) }
|
|
75
|
-
has_existing_await = source.include?(
|
|
89
|
+
has_existing_await = source.include?(".__await__")
|
|
76
90
|
|
|
77
91
|
begin
|
|
78
|
-
analyzer =
|
|
92
|
+
analyzer =
|
|
93
|
+
HomuraRuntime::AutoAwait::Analyzer.new(registry, debug: options[:debug])
|
|
79
94
|
buffer, nodes = analyzer.process(source, path)
|
|
80
95
|
needs_magic_only_output = has_existing_await && !has_magic
|
|
81
96
|
|
|
@@ -91,13 +106,13 @@ paths.each do |path|
|
|
|
91
106
|
else
|
|
92
107
|
HomuraRuntime::AutoAwait::Transformer.transform(source, nodes, buffer)
|
|
93
108
|
end
|
|
94
|
-
unless has_magic
|
|
95
|
-
transformed = "# await: true\n" + transformed
|
|
96
|
-
end
|
|
109
|
+
transformed = "# await: true\n" + transformed unless has_magic
|
|
97
110
|
out_path = output_root.join(rel)
|
|
98
111
|
FileUtils.mkdir_p(out_path.dirname)
|
|
99
112
|
File.write(out_path, transformed)
|
|
100
|
-
|
|
113
|
+
if options[:debug]
|
|
114
|
+
puts "[auto-await] write #{rel} (#{nodes.length} await#{"s" if nodes.length > 1})"
|
|
115
|
+
end
|
|
101
116
|
changed += 1
|
|
102
117
|
rescue => e
|
|
103
118
|
$stderr.puts "[auto-await] ERROR #{rel}: #{e.message}"
|
data/exe/compile-assets
CHANGED
|
@@ -12,39 +12,39 @@
|
|
|
12
12
|
# Usage:
|
|
13
13
|
# ruby bin/compile-assets --input public --output build/homura_assets.rb --namespace HomuraAssets
|
|
14
14
|
|
|
15
|
-
require
|
|
16
|
-
require
|
|
17
|
-
require
|
|
15
|
+
require "base64"
|
|
16
|
+
require "fileutils"
|
|
17
|
+
require "optparse"
|
|
18
18
|
|
|
19
19
|
MIME_TYPES = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
".css" => "text/css; charset=utf-8",
|
|
21
|
+
".js" => "application/javascript; charset=utf-8",
|
|
22
|
+
".json" => "application/json; charset=utf-8",
|
|
23
|
+
".html" => "text/html; charset=utf-8",
|
|
24
|
+
".svg" => "image/svg+xml",
|
|
25
|
+
".png" => "image/png",
|
|
26
|
+
".jpg" => "image/jpeg",
|
|
27
|
+
".jpeg" => "image/jpeg",
|
|
28
|
+
".gif" => "image/gif",
|
|
29
|
+
".ico" => "image/x-icon",
|
|
30
|
+
".woff" => "font/woff",
|
|
31
|
+
".woff2" => "font/woff2",
|
|
32
|
+
".txt" => "text/plain; charset=utf-8",
|
|
33
|
+
".xml" => "application/xml; charset=utf-8",
|
|
34
|
+
".webp" => "image/webp",
|
|
35
|
+
".map" => "application/json"
|
|
36
36
|
}.freeze
|
|
37
37
|
|
|
38
38
|
def mime_for(path)
|
|
39
39
|
ext = File.extname(path).downcase
|
|
40
|
-
MIME_TYPES[ext] ||
|
|
40
|
+
MIME_TYPES[ext] || "application/octet-stream"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def binary_content_type?(content_type)
|
|
44
|
-
!(
|
|
45
|
-
content_type.include?(
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
!(
|
|
45
|
+
content_type.start_with?("text/") || content_type.include?("javascript") ||
|
|
46
|
+
content_type.include?("json") || content_type.include?("xml")
|
|
47
|
+
)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
HELP = <<~USAGE
|
|
@@ -55,23 +55,31 @@ HELP = <<~USAGE
|
|
|
55
55
|
--input public --output build/homura_assets.rb --namespace HomuraAssets
|
|
56
56
|
USAGE
|
|
57
57
|
|
|
58
|
-
if ARGV.include?(
|
|
58
|
+
if ARGV.include?("-h") || ARGV.include?("--help")
|
|
59
59
|
puts HELP
|
|
60
60
|
exit 0
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
options = {
|
|
64
|
-
input_dir:
|
|
65
|
-
output:
|
|
66
|
-
namespace:
|
|
64
|
+
input_dir: "public",
|
|
65
|
+
output: "build/homura_assets.rb",
|
|
66
|
+
namespace: "HomuraAssets"
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
OptionParser
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
end
|
|
69
|
+
OptionParser
|
|
70
|
+
.new do |op|
|
|
71
|
+
op.banner = "Usage: bin/compile-assets [options]"
|
|
72
|
+
op.on("--input DIR", "Root directory to embed (recursive)") do |d|
|
|
73
|
+
options[:input_dir] = d
|
|
74
|
+
end
|
|
75
|
+
op.on("--output PATH", "Generated Ruby output path") do |p|
|
|
76
|
+
options[:output] = p
|
|
77
|
+
end
|
|
78
|
+
op.on("--namespace NAME", "Ruby module name") do |n|
|
|
79
|
+
options[:namespace] = n
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
.parse!
|
|
75
83
|
|
|
76
84
|
public_dir = File.join(Dir.pwd, options[:input_dir])
|
|
77
85
|
unless File.directory?(public_dir)
|
|
@@ -81,12 +89,13 @@ end
|
|
|
81
89
|
|
|
82
90
|
ns = options[:namespace]
|
|
83
91
|
|
|
84
|
-
files =
|
|
92
|
+
files =
|
|
93
|
+
Dir.glob(File.join(public_dir, "**", "*")).select { |f| File.file?(f) }.sort
|
|
85
94
|
|
|
86
95
|
out_path = options[:output]
|
|
87
96
|
FileUtils.mkdir_p(File.dirname(out_path))
|
|
88
97
|
|
|
89
|
-
File.open(out_path,
|
|
98
|
+
File.open(out_path, "w") do |io|
|
|
90
99
|
io.puts <<~RUBY
|
|
91
100
|
# frozen_string_literal: true
|
|
92
101
|
# Auto-generated by bin/compile-assets — DO NOT EDIT BY HAND.
|
|
@@ -131,7 +140,7 @@ File.open(out_path, 'w') do |io|
|
|
|
131
140
|
RUBY
|
|
132
141
|
|
|
133
142
|
files.each do |full_path|
|
|
134
|
-
rel = full_path.sub(public_dir,
|
|
143
|
+
rel = full_path.sub(public_dir, "") # e.g. "/style.css"
|
|
135
144
|
content = File.binread(full_path)
|
|
136
145
|
ct = mime_for(full_path)
|
|
137
146
|
binary = binary_content_type?(ct)
|
data/exe/compile-erb
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
# The output registers each template with `HomuraTemplates` and
|
|
23
23
|
# monkey-patches `Sinatra::Templates#erb` to dispatch there.
|
|
24
24
|
|
|
25
|
-
require
|
|
26
|
-
require
|
|
25
|
+
require "fileutils"
|
|
26
|
+
require "optparse"
|
|
27
27
|
|
|
28
28
|
HELP = <<~USAGE
|
|
29
29
|
Usage:
|
|
@@ -53,7 +53,7 @@ module HomuraERB
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def normalize_fragment(fragment)
|
|
56
|
-
return
|
|
56
|
+
return "__homura_template_yield__" if supported_yield_fragment?(fragment)
|
|
57
57
|
rewrite_class_variables(fragment)
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -75,13 +75,15 @@ module HomuraERB
|
|
|
75
75
|
# comments are vanishingly rare in real templates. Anything more
|
|
76
76
|
# ambitious would need a Ruby parser, which is overkill for the
|
|
77
77
|
# `@@ivar` shape we actually have to handle here.
|
|
78
|
-
CVAR_OP_ASSIGN_RE =
|
|
78
|
+
CVAR_OP_ASSIGN_RE =
|
|
79
|
+
%r{(?<![@\w])@@([A-Za-z_]\w*)\s*(\|\||&&|\+|-|\*|/|%|\*\*|<<|>>|\||&|\^)=\s*([^;\n]+)}.freeze
|
|
79
80
|
CVAR_ASSIGN_RE = /(?<![@\w])@@([A-Za-z_]\w*)\s*=(?!=)\s*([^;\n]+)/.freeze
|
|
80
81
|
# Read pattern excludes `@@name` that appears on the left-hand side
|
|
81
82
|
# of an assignment (`@@name =`, `@@name +=`, `@@name ||=`, etc.) so
|
|
82
83
|
# the assignment passes still see the raw token to rewrite. The
|
|
83
84
|
# `=(?!=)` guard keeps `==` (comparison) treated as a read.
|
|
84
|
-
CVAR_READ_RE =
|
|
85
|
+
CVAR_READ_RE =
|
|
86
|
+
%r{
|
|
85
87
|
(?<![@\w])@@([A-Za-z_]\w*)\b
|
|
86
88
|
(?!
|
|
87
89
|
\s*
|
|
@@ -94,13 +96,14 @@ module HomuraERB
|
|
|
94
96
|
CVAR_PLACEHOLDER_SUFFIX = "\x01".freeze
|
|
95
97
|
|
|
96
98
|
def rewrite_class_variables(fragment)
|
|
97
|
-
return fragment unless fragment.include?(
|
|
99
|
+
return fragment unless fragment.include?("@@")
|
|
98
100
|
|
|
99
101
|
placeholders = []
|
|
100
|
-
record =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
record =
|
|
103
|
+
lambda do |replacement|
|
|
104
|
+
placeholders << replacement
|
|
105
|
+
"#{CVAR_PLACEHOLDER_PREFIX}#{placeholders.length - 1}#{CVAR_PLACEHOLDER_SUFFIX}"
|
|
106
|
+
end
|
|
104
107
|
|
|
105
108
|
# Reads first: rewrite every `@@name` that is *not* the target of
|
|
106
109
|
# an assignment, including occurrences on the right-hand side of
|
|
@@ -108,36 +111,41 @@ module HomuraERB
|
|
|
108
111
|
# later assignment passes see the original `@@name = ...` shape
|
|
109
112
|
# (without their RHS reads being clobbered) and so the final
|
|
110
113
|
# output never re-enters this same rewrite loop.
|
|
111
|
-
work =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
work =
|
|
115
|
+
fragment.gsub(CVAR_READ_RE) do
|
|
116
|
+
name = Regexp.last_match(1)
|
|
117
|
+
record.call("self.class.class_variable_get(:@@#{name})")
|
|
118
|
+
end
|
|
115
119
|
|
|
116
|
-
work =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
"
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
120
|
+
work =
|
|
121
|
+
work.gsub(CVAR_OP_ASSIGN_RE) do
|
|
122
|
+
name = Regexp.last_match(1)
|
|
123
|
+
op = Regexp.last_match(2)
|
|
124
|
+
value = Regexp.last_match(3)
|
|
125
|
+
replacement =
|
|
126
|
+
case op
|
|
127
|
+
when "||"
|
|
128
|
+
"self.class.class_variable_set(:@@#{name}, (self.class.class_variable_defined?(:@@#{name}) ? self.class.class_variable_get(:@@#{name}) : nil) || (#{value}))"
|
|
129
|
+
when "&&"
|
|
130
|
+
"self.class.class_variable_set(:@@#{name}, (self.class.class_variable_defined?(:@@#{name}) ? self.class.class_variable_get(:@@#{name}) : nil) && (#{value}))"
|
|
131
|
+
else
|
|
132
|
+
"self.class.class_variable_set(:@@#{name}, self.class.class_variable_get(:@@#{name}) #{op} (#{value}))"
|
|
133
|
+
end
|
|
134
|
+
record.call(replacement)
|
|
135
|
+
end
|
|
131
136
|
|
|
132
|
-
work =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
work =
|
|
138
|
+
work.gsub(CVAR_ASSIGN_RE) do
|
|
139
|
+
name = Regexp.last_match(1)
|
|
140
|
+
value = Regexp.last_match(2)
|
|
141
|
+
record.call("self.class.class_variable_set(:@@#{name}, (#{value}))")
|
|
142
|
+
end
|
|
137
143
|
|
|
138
|
-
placeholder_re =
|
|
144
|
+
placeholder_re =
|
|
145
|
+
/#{Regexp.escape(CVAR_PLACEHOLDER_PREFIX)}(\d+)#{Regexp.escape(CVAR_PLACEHOLDER_SUFFIX)}/
|
|
139
146
|
loop do
|
|
140
|
-
replaced =
|
|
147
|
+
replaced =
|
|
148
|
+
work.gsub(placeholder_re) { placeholders[Regexp.last_match(1).to_i] }
|
|
141
149
|
break if replaced == work
|
|
142
150
|
work = replaced
|
|
143
151
|
end
|
|
@@ -146,7 +154,10 @@ module HomuraERB
|
|
|
146
154
|
|
|
147
155
|
def validate_code_fragment!(fragment)
|
|
148
156
|
stripped = fragment.strip
|
|
149
|
-
|
|
157
|
+
unless supported_yield_fragment?(stripped) ||
|
|
158
|
+
yield_with_args_fragment?(stripped)
|
|
159
|
+
return
|
|
160
|
+
end
|
|
150
161
|
|
|
151
162
|
raise <<~MSG.strip
|
|
152
163
|
Unsupported ERB yield form `<% #{stripped} %>`.
|
|
@@ -176,7 +187,7 @@ module HomuraERB
|
|
|
176
187
|
ruby = +"_out = ''\n"
|
|
177
188
|
cursor = 0
|
|
178
189
|
while cursor < source.length
|
|
179
|
-
open_idx = source.index(
|
|
190
|
+
open_idx = source.index("<%", cursor)
|
|
180
191
|
if open_idx.nil?
|
|
181
192
|
static = source[cursor..]
|
|
182
193
|
ruby << "_out = _out + #{static.inspect}\n" unless static.empty?
|
|
@@ -189,21 +200,23 @@ module HomuraERB
|
|
|
189
200
|
ruby << "_out = _out + #{static.inspect}\n"
|
|
190
201
|
end
|
|
191
202
|
|
|
192
|
-
close_idx = source.index(
|
|
193
|
-
|
|
203
|
+
close_idx = source.index("%>", open_idx + 2)
|
|
204
|
+
if close_idx.nil?
|
|
205
|
+
raise "Unmatched `<%` in ERB source (starting at #{open_idx})"
|
|
206
|
+
end
|
|
194
207
|
|
|
195
208
|
inner = source[(open_idx + 2)...close_idx]
|
|
196
209
|
|
|
197
|
-
if inner.start_with?(
|
|
210
|
+
if inner.start_with?("#")
|
|
198
211
|
# `<%# comment %>` — drop
|
|
199
|
-
elsif inner.start_with?(
|
|
212
|
+
elsif inner.start_with?("==")
|
|
200
213
|
# `<%== expression %>` — identical to `<%= %>` in this minimal
|
|
201
214
|
# dialect (no HTML escaping yet; the author is responsible).
|
|
202
215
|
expr = inner[2..].strip
|
|
203
216
|
validate_expression_fragment!(expr)
|
|
204
217
|
expr = normalize_fragment(expr)
|
|
205
218
|
ruby << "_out = _out + ((#{expr})).to_s\n"
|
|
206
|
-
elsif inner.start_with?(
|
|
219
|
+
elsif inner.start_with?("=")
|
|
207
220
|
# `<%= expression %>`
|
|
208
221
|
expr = inner[1..].strip
|
|
209
222
|
validate_expression_fragment!(expr)
|
|
@@ -243,17 +256,17 @@ end
|
|
|
243
256
|
def default_inputs_for(dir)
|
|
244
257
|
# Recursive so `views/posts/index.erb` becomes `:'posts/index'`,
|
|
245
258
|
# matching upstream Sinatra's subdirectory convention.
|
|
246
|
-
Dir.glob(File.join(dir,
|
|
259
|
+
Dir.glob(File.join(dir, "**", "*.erb")).sort
|
|
247
260
|
end
|
|
248
261
|
|
|
249
262
|
def template_name_for(path, root)
|
|
250
263
|
rel = path.dup
|
|
251
264
|
if root
|
|
252
|
-
root_with_sep = root.end_with?(
|
|
265
|
+
root_with_sep = root.end_with?("/") ? root : "#{root}/"
|
|
253
266
|
rel = rel[root_with_sep.length..] if rel.start_with?(root_with_sep)
|
|
254
267
|
end
|
|
255
|
-
rel = File.basename(rel) unless rel.include?(
|
|
256
|
-
rel.delete_suffix(
|
|
268
|
+
rel = File.basename(rel) unless rel.include?("/")
|
|
269
|
+
rel.delete_suffix(".erb").to_sym
|
|
257
270
|
end
|
|
258
271
|
|
|
259
272
|
def emit_header(io, namespace)
|
|
@@ -479,25 +492,34 @@ end
|
|
|
479
492
|
# Entry point
|
|
480
493
|
# ---------------------------------------------------------------------------
|
|
481
494
|
|
|
482
|
-
if ARGV.include?(
|
|
495
|
+
if ARGV.include?("-h") || ARGV.include?("--help")
|
|
483
496
|
puts HELP
|
|
484
497
|
exit 0
|
|
485
498
|
end
|
|
486
499
|
|
|
487
500
|
options = {
|
|
488
|
-
input_dir:
|
|
489
|
-
output:
|
|
490
|
-
namespace:
|
|
501
|
+
input_dir: "views",
|
|
502
|
+
output: "build/homura_templates.rb",
|
|
503
|
+
namespace: "HomuraTemplates",
|
|
491
504
|
stdout: false
|
|
492
505
|
}
|
|
493
506
|
|
|
494
|
-
parser =
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
507
|
+
parser =
|
|
508
|
+
OptionParser.new do |op|
|
|
509
|
+
op.banner = "Usage: bin/compile-erb [options] [--] [files...]"
|
|
510
|
+
op.on("--input DIR", "Directory containing *.erb templates") do |d|
|
|
511
|
+
options[:input_dir] = d
|
|
512
|
+
end
|
|
513
|
+
op.on("--output PATH", "Write generated Ruby to PATH") do |p|
|
|
514
|
+
options[:output] = p
|
|
515
|
+
end
|
|
516
|
+
op.on("--namespace NAME", "Ruby module name for the registry") do |n|
|
|
517
|
+
options[:namespace] = n
|
|
518
|
+
end
|
|
519
|
+
op.on("--stdout", "Write generated Ruby to stdout") do
|
|
520
|
+
options[:stdout] = true
|
|
521
|
+
end
|
|
522
|
+
end
|
|
501
523
|
parser.parse!
|
|
502
524
|
|
|
503
525
|
positional = ARGV.dup
|
|
@@ -505,12 +527,15 @@ namespace = options[:namespace]
|
|
|
505
527
|
|
|
506
528
|
inputs =
|
|
507
529
|
if positional.any?
|
|
508
|
-
positional.flat_map
|
|
530
|
+
positional.flat_map do |a|
|
|
531
|
+
File.directory?(a) ? Dir.glob(File.join(a, "**", "*.erb")).sort : [a]
|
|
532
|
+
end
|
|
509
533
|
else
|
|
510
534
|
default_inputs_for(options[:input_dir])
|
|
511
535
|
end
|
|
512
536
|
|
|
513
|
-
template_root =
|
|
537
|
+
template_root =
|
|
538
|
+
positional.find { |a| File.directory?(a) } || options[:input_dir]
|
|
514
539
|
|
|
515
540
|
write_file = !options[:stdout] && (positional.empty? || options[:output])
|
|
516
541
|
out_path = options[:output]
|
|
@@ -524,7 +549,7 @@ end
|
|
|
524
549
|
|
|
525
550
|
if write_file
|
|
526
551
|
FileUtils.mkdir_p(File.dirname(out_path))
|
|
527
|
-
File.open(out_path,
|
|
552
|
+
File.open(out_path, "w") do |io|
|
|
528
553
|
emit_header(io, namespace)
|
|
529
554
|
emit_templates(io, inputs, namespace, template_root)
|
|
530
555
|
emit_sinatra_patch(io, namespace) unless inputs.empty?
|