rhales 0.4.0 → 0.5.4
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/.github/renovate.json5 +52 -0
- data/.github/workflows/ci.yml +123 -0
- data/.github/workflows/claude-code-review.yml +69 -0
- data/.github/workflows/claude.yml +49 -0
- data/.github/workflows/code-smells.yml +146 -0
- data/.github/workflows/ruby-lint.yml +78 -0
- data/.github/workflows/yardoc.yml +126 -0
- data/.gitignore +55 -0
- data/.pr_agent.toml +63 -0
- data/.pre-commit-config.yaml +89 -0
- data/.prettierignore +8 -0
- data/.prettierrc +38 -0
- data/.reek.yml +98 -0
- data/.rubocop.yml +428 -0
- data/.serena/.gitignore +3 -0
- data/.yardopts +56 -0
- data/CHANGELOG.md +44 -0
- data/CLAUDE.md +1 -1
- data/Gemfile +29 -0
- data/Gemfile.lock +189 -0
- data/README.md +686 -868
- data/Rakefile +46 -0
- data/debug_context.rb +25 -0
- data/demo/rhales-roda-demo/.gitignore +7 -0
- data/demo/rhales-roda-demo/Gemfile +32 -0
- data/demo/rhales-roda-demo/Gemfile.lock +151 -0
- data/demo/rhales-roda-demo/MAIL.md +405 -0
- data/demo/rhales-roda-demo/README.md +376 -0
- data/demo/rhales-roda-demo/RODA-TEMPLATE-ENGINES.md +192 -0
- data/demo/rhales-roda-demo/Rakefile +49 -0
- data/demo/rhales-roda-demo/app.rb +325 -0
- data/demo/rhales-roda-demo/bin/rackup +26 -0
- data/demo/rhales-roda-demo/config.ru +13 -0
- data/demo/rhales-roda-demo/db/migrate/001_create_rodauth_tables.rb +266 -0
- data/demo/rhales-roda-demo/db/migrate/002_create_rodauth_password_tables.rb +79 -0
- data/demo/rhales-roda-demo/db/migrate/003_add_admin_account.rb +68 -0
- data/demo/rhales-roda-demo/templates/change_login.rue +31 -0
- data/demo/rhales-roda-demo/templates/change_password.rue +36 -0
- data/demo/rhales-roda-demo/templates/close_account.rue +31 -0
- data/demo/rhales-roda-demo/templates/create_account.rue +40 -0
- data/demo/rhales-roda-demo/templates/dashboard.rue +150 -0
- data/demo/rhales-roda-demo/templates/home.rue +78 -0
- data/demo/rhales-roda-demo/templates/layouts/main.rue +168 -0
- data/demo/rhales-roda-demo/templates/login.rue +65 -0
- data/demo/rhales-roda-demo/templates/logout.rue +25 -0
- data/demo/rhales-roda-demo/templates/reset_password.rue +26 -0
- data/demo/rhales-roda-demo/templates/verify_account.rue +27 -0
- data/demo/rhales-roda-demo/test_full_output.rb +27 -0
- data/demo/rhales-roda-demo/test_simple.rb +24 -0
- data/docs/.gitignore +9 -0
- data/docs/architecture/data-flow.md +499 -0
- data/examples/dashboard-with-charts.rue +271 -0
- data/examples/form-with-validation.rue +180 -0
- data/examples/simple-page.rue +61 -0
- data/examples/vue.rue +136 -0
- data/generate-json-schemas.ts +158 -0
- data/json_schemer_migration_summary.md +172 -0
- data/lib/rhales/adapters/base_auth.rb +2 -0
- data/lib/rhales/adapters/base_request.rb +2 -0
- data/lib/rhales/adapters/base_session.rb +2 -0
- data/lib/rhales/adapters.rb +7 -0
- data/lib/rhales/configuration.rb +47 -0
- data/lib/rhales/core/context.rb +354 -0
- data/lib/rhales/{rue_document.rb → core/rue_document.rb} +56 -38
- data/lib/rhales/{template_engine.rb → core/template_engine.rb} +66 -59
- data/lib/rhales/{view.rb → core/view.rb} +112 -135
- data/lib/rhales/{view_composition.rb → core/view_composition.rb} +78 -8
- data/lib/rhales/core.rb +9 -0
- data/lib/rhales/errors/hydration_collision_error.rb +2 -0
- data/lib/rhales/errors.rb +2 -0
- data/lib/rhales/{earliest_injection_detector.rb → hydration/earliest_injection_detector.rb} +69 -7
- data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
- data/lib/rhales/{hydration_endpoint.rb → hydration/hydration_endpoint.rb} +16 -12
- data/lib/rhales/{hydration_injector.rb → hydration/hydration_injector.rb} +4 -0
- data/lib/rhales/{hydration_registry.rb → hydration/hydration_registry.rb} +2 -0
- data/lib/rhales/hydration/hydrator.rb +102 -0
- data/lib/rhales/{link_based_injection_detector.rb → hydration/link_based_injection_detector.rb} +4 -0
- data/lib/rhales/{mount_point_detector.rb → hydration/mount_point_detector.rb} +4 -0
- data/lib/rhales/{safe_injection_validator.rb → hydration/safe_injection_validator.rb} +49 -2
- data/lib/rhales/hydration.rb +13 -0
- data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +3 -1
- data/lib/rhales/{tilt.rb → integrations/tilt.rb} +22 -15
- data/lib/rhales/integrations.rb +6 -0
- data/lib/rhales/middleware/json_responder.rb +191 -0
- data/lib/rhales/middleware/schema_validator.rb +300 -0
- data/lib/rhales/middleware.rb +6 -0
- data/lib/rhales/parsers/handlebars_parser.rb +2 -0
- data/lib/rhales/parsers/rue_format_parser.rb +9 -7
- data/lib/rhales/parsers.rb +9 -0
- data/lib/rhales/{csp.rb → security/csp.rb} +27 -3
- data/lib/rhales/utils/json_serializer.rb +114 -0
- data/lib/rhales/utils/logging_helpers.rb +75 -0
- data/lib/rhales/utils/schema_extractor.rb +132 -0
- data/lib/rhales/utils/schema_generator.rb +194 -0
- data/lib/rhales/utils.rb +40 -0
- data/lib/rhales/version.rb +3 -1
- data/lib/rhales.rb +41 -24
- data/lib/tasks/rhales_schema.rake +197 -0
- data/package.json +10 -0
- data/pnpm-lock.yaml +345 -0
- data/pnpm-workspace.yaml +2 -0
- data/proofs/error_handling.rb +79 -0
- data/proofs/expanded_object_inheritance.rb +82 -0
- data/proofs/partial_context_scoping_fix.rb +168 -0
- data/proofs/ui_context_partial_inheritance.rb +236 -0
- data/rhales.gemspec +14 -6
- data/schema_vs_data_comparison.md +254 -0
- data/test_direct_access.rb +36 -0
- metadata +141 -23
- data/CLAUDE.locale.txt +0 -7
- data/lib/rhales/context.rb +0 -239
- data/lib/rhales/hydration_data_aggregator.rb +0 -221
- data/lib/rhales/hydrator.rb +0 -141
- data/lib/rhales/parsers/handlebars-grammar-review.txt +0 -39
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# lib/rhales/view_composition.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require_relative 'rue_document'
|
|
4
|
-
require_relative 'refinements/require_refinements'
|
|
6
|
+
require_relative '../integrations/refinements/require_refinements'
|
|
5
7
|
|
|
6
8
|
using Rhales::Ruequire
|
|
7
9
|
|
|
@@ -23,6 +25,8 @@ module Rhales
|
|
|
23
25
|
# - Immutable: Once created, the composition is read-only
|
|
24
26
|
# - Cacheable: Can be cached in production for performance
|
|
25
27
|
class ViewComposition
|
|
28
|
+
include Rhales::Utils::LoggingHelpers
|
|
29
|
+
|
|
26
30
|
class TemplateNotFoundError < StandardError; end
|
|
27
31
|
class CircularDependencyError < StandardError; end
|
|
28
32
|
|
|
@@ -39,9 +43,23 @@ module Rhales
|
|
|
39
43
|
|
|
40
44
|
# Resolve all template dependencies
|
|
41
45
|
def resolve!
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
log_timed_operation(Rhales.logger, :debug, 'Template dependency resolution',
|
|
47
|
+
root_template: @root_template_name
|
|
48
|
+
) do
|
|
49
|
+
load_template_recursive(@root_template_name)
|
|
50
|
+
freeze_composition
|
|
51
|
+
|
|
52
|
+
# Log resolution results
|
|
53
|
+
log_with_metadata(Rhales.logger, :debug, 'Template composition resolved',
|
|
54
|
+
root_template: @root_template_name,
|
|
55
|
+
total_templates: @templates.size,
|
|
56
|
+
total_dependencies: @dependencies.values.sum(&:size),
|
|
57
|
+
partials: @dependencies.values.flatten.uniq,
|
|
58
|
+
layout: layout
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self
|
|
62
|
+
end
|
|
45
63
|
end
|
|
46
64
|
|
|
47
65
|
# Iterate through all documents in render order
|
|
@@ -84,25 +102,50 @@ module Rhales
|
|
|
84
102
|
@dependencies[template_name] || []
|
|
85
103
|
end
|
|
86
104
|
|
|
105
|
+
# Get the layout for the root template (if any)
|
|
106
|
+
def layout
|
|
107
|
+
root_template = @templates[@root_template_name]
|
|
108
|
+
return nil unless root_template
|
|
109
|
+
|
|
110
|
+
root_template.layout
|
|
111
|
+
end
|
|
87
112
|
|
|
88
113
|
private
|
|
89
114
|
|
|
90
|
-
def load_template_recursive(template_name,
|
|
115
|
+
def load_template_recursive(template_name, parent_path = nil)
|
|
116
|
+
depth = @loading.size
|
|
117
|
+
|
|
91
118
|
# Check for circular dependencies
|
|
92
119
|
if @loading.include?(template_name)
|
|
120
|
+
log_with_metadata(Rhales.logger, :error, 'Circular dependency detected',
|
|
121
|
+
template: template_name,
|
|
122
|
+
dependency_chain: @loading.to_a,
|
|
123
|
+
depth: depth
|
|
124
|
+
)
|
|
93
125
|
raise CircularDependencyError, "Circular dependency detected: #{template_name} -> #{@loading.to_a.join(' -> ')}"
|
|
94
126
|
end
|
|
95
127
|
|
|
96
|
-
# Skip if already loaded
|
|
97
|
-
|
|
128
|
+
# Skip if already loaded (cache hit)
|
|
129
|
+
if @templates.key?(template_name)
|
|
130
|
+
Rhales.logger.debug("Template cache hit: template=#{template_name}")
|
|
131
|
+
return
|
|
132
|
+
end
|
|
98
133
|
|
|
99
134
|
@loading.add(template_name)
|
|
100
135
|
|
|
101
136
|
begin
|
|
102
137
|
# Load template using the provided loader
|
|
138
|
+
start_time = now_in_μs
|
|
103
139
|
parser = @loader.call(template_name)
|
|
140
|
+
load_duration = now_in_μs - start_time
|
|
104
141
|
|
|
105
142
|
unless parser
|
|
143
|
+
log_with_metadata(Rhales.logger, :error, 'Template not found',
|
|
144
|
+
template: template_name,
|
|
145
|
+
parent: parent_path,
|
|
146
|
+
depth: depth,
|
|
147
|
+
search_duration: load_duration
|
|
148
|
+
)
|
|
106
149
|
raise TemplateNotFoundError, "Template not found: #{template_name}"
|
|
107
150
|
end
|
|
108
151
|
|
|
@@ -111,15 +154,42 @@ module Rhales
|
|
|
111
154
|
@dependencies[template_name] = []
|
|
112
155
|
|
|
113
156
|
# Extract and load partials
|
|
114
|
-
extract_partials(parser)
|
|
157
|
+
partials = extract_partials(parser)
|
|
158
|
+
|
|
159
|
+
if partials.any?
|
|
160
|
+
log_with_metadata(Rhales.logger, :debug, 'Resolved partial',
|
|
161
|
+
template: template_name,
|
|
162
|
+
partials_found: partials,
|
|
163
|
+
partial_count: partials.size,
|
|
164
|
+
depth: depth,
|
|
165
|
+
load_duration: load_duration
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
partials.each do |partial_name|
|
|
115
170
|
@dependencies[template_name] << partial_name
|
|
116
171
|
load_template_recursive(partial_name, template_name)
|
|
117
172
|
end
|
|
118
173
|
|
|
119
174
|
# Load layout if specified and not already loaded
|
|
120
175
|
if parser.layout && !@templates.key?(parser.layout)
|
|
176
|
+
log_with_metadata(Rhales.logger, :debug, 'Layout resolution',
|
|
177
|
+
template: template_name,
|
|
178
|
+
layout: parser.layout,
|
|
179
|
+
depth: depth
|
|
180
|
+
)
|
|
121
181
|
load_template_recursive(parser.layout, template_name)
|
|
122
182
|
end
|
|
183
|
+
|
|
184
|
+
# Log successful template load
|
|
185
|
+
log_with_metadata(Rhales.logger, :debug, 'Template loaded',
|
|
186
|
+
template: template_name,
|
|
187
|
+
parent: parent_path,
|
|
188
|
+
depth: depth,
|
|
189
|
+
has_partials: partials.any?,
|
|
190
|
+
has_layout: !parser.layout.nil?,
|
|
191
|
+
load_duration: load_duration
|
|
192
|
+
)
|
|
123
193
|
ensure
|
|
124
194
|
@loading.delete(template_name)
|
|
125
195
|
end
|
data/lib/rhales/core.rb
ADDED
data/lib/rhales/errors.rb
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# lib/rhales/hydration/earliest_injection_detector.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
1
5
|
require 'strscan'
|
|
2
6
|
require_relative 'safe_injection_validator'
|
|
3
7
|
|
|
@@ -22,6 +26,8 @@ module Rhales
|
|
|
22
26
|
def detect(template_html)
|
|
23
27
|
scanner = StringScanner.new(template_html)
|
|
24
28
|
validator = SafeInjectionValidator.new(template_html)
|
|
29
|
+
# Build byte-to-char map once for the entire template
|
|
30
|
+
@byte_to_char_map = build_byte_to_char_map(template_html)
|
|
25
31
|
|
|
26
32
|
# Try head section injection points first
|
|
27
33
|
head_injection_point = detect_head_injection_point(scanner, validator, template_html)
|
|
@@ -64,7 +70,9 @@ module Rhales
|
|
|
64
70
|
|
|
65
71
|
# Find opening <body> tag
|
|
66
72
|
if scanner.scan_until(/<body\b[^>]*>/i)
|
|
67
|
-
|
|
73
|
+
# Convert byte position to character position using pre-built map
|
|
74
|
+
byte_body_start = scanner.pos - scanner.matched.length
|
|
75
|
+
body_start = @byte_to_char_map[byte_body_start]
|
|
68
76
|
safe_position = find_safe_injection_position(validator, body_start)
|
|
69
77
|
return safe_position if safe_position
|
|
70
78
|
end
|
|
@@ -77,11 +85,15 @@ module Rhales
|
|
|
77
85
|
|
|
78
86
|
# Find opening <head> tag
|
|
79
87
|
return nil unless scanner.scan_until(/<head\b[^>]*>/i)
|
|
80
|
-
|
|
88
|
+
# Convert byte position to character position using pre-built map
|
|
89
|
+
byte_head_start = scanner.pos
|
|
90
|
+
head_start = @byte_to_char_map[byte_head_start]
|
|
81
91
|
|
|
82
92
|
# Find closing </head> tag
|
|
83
93
|
return nil unless scanner.scan_until(/<\/head>/i)
|
|
84
|
-
|
|
94
|
+
# Convert byte position to character position using pre-built map
|
|
95
|
+
byte_head_end = scanner.pos - scanner.matched.length
|
|
96
|
+
head_end = @byte_to_char_map[byte_head_end]
|
|
85
97
|
|
|
86
98
|
[head_start, head_end]
|
|
87
99
|
end
|
|
@@ -90,9 +102,13 @@ module Rhales
|
|
|
90
102
|
head_content = template_html[head_start...head_end]
|
|
91
103
|
scanner = StringScanner.new(head_content)
|
|
92
104
|
last_link_end = nil
|
|
105
|
+
byte_to_char_map = build_byte_to_char_map(head_content)
|
|
93
106
|
|
|
94
107
|
while scanner.scan_until(/<link\b[^>]*\/?>/i)
|
|
95
|
-
|
|
108
|
+
# scanner.pos is byte position within head_content
|
|
109
|
+
byte_pos = scanner.pos
|
|
110
|
+
# Convert to character position using pre-built map
|
|
111
|
+
last_link_end = byte_to_char_map[byte_pos]
|
|
96
112
|
end
|
|
97
113
|
|
|
98
114
|
last_link_end ? head_start + last_link_end : nil
|
|
@@ -102,9 +118,13 @@ module Rhales
|
|
|
102
118
|
head_content = template_html[head_start...head_end]
|
|
103
119
|
scanner = StringScanner.new(head_content)
|
|
104
120
|
last_meta_end = nil
|
|
121
|
+
byte_to_char_map = build_byte_to_char_map(head_content)
|
|
105
122
|
|
|
106
123
|
while scanner.scan_until(/<meta\b[^>]*\/?>/i)
|
|
107
|
-
|
|
124
|
+
# scanner.pos is byte position within head_content
|
|
125
|
+
byte_pos = scanner.pos
|
|
126
|
+
# Convert to character position using pre-built map
|
|
127
|
+
last_meta_end = byte_to_char_map[byte_pos]
|
|
108
128
|
end
|
|
109
129
|
|
|
110
130
|
last_meta_end ? head_start + last_meta_end : nil
|
|
@@ -113,14 +133,18 @@ module Rhales
|
|
|
113
133
|
def find_after_first_script(template_html, head_start, head_end)
|
|
114
134
|
head_content = template_html[head_start...head_end]
|
|
115
135
|
scanner = StringScanner.new(head_content)
|
|
136
|
+
byte_to_char_map = build_byte_to_char_map(head_content)
|
|
116
137
|
|
|
117
138
|
# Find first script opening tag
|
|
118
139
|
if scanner.scan_until(/<script\b[^>]*>/i)
|
|
119
|
-
|
|
140
|
+
# Only the script end position is needed for this method, not the start position
|
|
120
141
|
|
|
121
142
|
# Find corresponding closing tag
|
|
122
143
|
if scanner.scan_until(/<\/script>/i)
|
|
123
|
-
|
|
144
|
+
# scanner.pos is byte position within head_content
|
|
145
|
+
byte_script_end = scanner.pos
|
|
146
|
+
# Convert to character position using pre-built map
|
|
147
|
+
first_script_end = byte_to_char_map[byte_script_end]
|
|
124
148
|
return head_start + first_script_end
|
|
125
149
|
end
|
|
126
150
|
end
|
|
@@ -145,5 +169,43 @@ module Rhales
|
|
|
145
169
|
# No safe position found
|
|
146
170
|
nil
|
|
147
171
|
end
|
|
172
|
+
|
|
173
|
+
# Builds a mapping from byte positions to character positions for efficient
|
|
174
|
+
# conversion when processing UTF-8 strings with StringScanner.
|
|
175
|
+
#
|
|
176
|
+
# This method creates a hash where keys are byte positions and values are
|
|
177
|
+
# the corresponding character positions. For multibyte UTF-8 characters,
|
|
178
|
+
# only the starting byte position has an entry in the map.
|
|
179
|
+
#
|
|
180
|
+
# @param str [String] The UTF-8 encoded string to map
|
|
181
|
+
# @return [Hash<Integer, Integer>] A hash mapping byte positions to character positions
|
|
182
|
+
#
|
|
183
|
+
# @example ASCII string
|
|
184
|
+
# build_byte_to_char_map("Hello")
|
|
185
|
+
# # => {0=>0, 1=>1, 2=>2, 3=>3, 4=>4, 5=>5}
|
|
186
|
+
#
|
|
187
|
+
# @example UTF-8 with multibyte characters
|
|
188
|
+
# build_byte_to_char_map("café") # é is 2 bytes
|
|
189
|
+
# # => {0=>0, 1=>1, 2=>2, 3=>3, 5=>4} # Note: byte 4 is continuation byte
|
|
190
|
+
#
|
|
191
|
+
def build_byte_to_char_map(str)
|
|
192
|
+
map = {}
|
|
193
|
+
char_pos = 0
|
|
194
|
+
byte_pos = 0
|
|
195
|
+
|
|
196
|
+
# Iterate through each character (not byte) in the string
|
|
197
|
+
str.each_char do |char|
|
|
198
|
+
# Map the starting byte position of this character
|
|
199
|
+
map[byte_pos] = char_pos
|
|
200
|
+
|
|
201
|
+
# Advance byte position by the byte size of this character
|
|
202
|
+
byte_pos += char.bytesize
|
|
203
|
+
char_pos += 1
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Add final mapping for the end of the string
|
|
207
|
+
map[byte_pos] = char_pos
|
|
208
|
+
map
|
|
209
|
+
end
|
|
148
210
|
end
|
|
149
211
|
end
|