rhales 0.3.0 → 0.5.3
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 -2
- data/Gemfile +29 -0
- data/Gemfile.lock +189 -0
- data/README.md +706 -589
- 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 +161 -1
- data/lib/rhales/core/context.rb +354 -0
- data/lib/rhales/{rue_document.rb → core/rue_document.rb} +59 -43
- data/lib/rhales/{template_engine.rb → core/template_engine.rb} +80 -33
- data/lib/rhales/core/view.rb +529 -0
- data/lib/rhales/{view_composition.rb → core/view_composition.rb} +81 -9
- 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/hydration/earliest_injection_detector.rb +153 -0
- data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
- data/lib/rhales/hydration/hydration_endpoint.rb +215 -0
- data/lib/rhales/hydration/hydration_injector.rb +175 -0
- data/lib/rhales/{hydration_registry.rb → hydration/hydration_registry.rb} +2 -0
- data/lib/rhales/hydration/hydrator.rb +102 -0
- data/lib/rhales/hydration/link_based_injection_detector.rb +195 -0
- data/lib/rhales/hydration/mount_point_detector.rb +109 -0
- data/lib/rhales/hydration/safe_injection_validator.rb +103 -0
- data/lib/rhales/hydration.rb +13 -0
- data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +7 -13
- data/lib/rhales/{tilt.rb → integrations/tilt.rb} +26 -18
- 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 +55 -36
- 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 +5 -1
- data/lib/rhales.rb +47 -19
- 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 +142 -18
- data/CLAUDE.locale.txt +0 -7
- data/lib/rhales/context.rb +0 -240
- data/lib/rhales/hydration_data_aggregator.rb +0 -220
- data/lib/rhales/hydrator.rb +0 -141
- data/lib/rhales/parsers/handlebars-grammar-review.txt +0 -39
- data/lib/rhales/view.rb +0 -412
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# lib/rhales/template_engine.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require 'erb'
|
|
4
|
-
require_relative 'parsers/rue_format_parser'
|
|
5
|
-
require_relative 'parsers/handlebars_parser'
|
|
6
|
+
require_relative '../parsers/rue_format_parser'
|
|
7
|
+
require_relative '../parsers/handlebars_parser'
|
|
6
8
|
require_relative 'rue_document'
|
|
9
|
+
require_relative '../hydration/hydrator'
|
|
7
10
|
|
|
8
11
|
module Rhales
|
|
9
12
|
# Rhales - Ruby Handlebars-style template engine
|
|
@@ -27,6 +30,8 @@ module Rhales
|
|
|
27
30
|
# - {{#each items}} ... {{/each}} - Iteration with context
|
|
28
31
|
# - {{> partial_name}} - Partial inclusion
|
|
29
32
|
class TemplateEngine
|
|
33
|
+
include Rhales::Utils::LoggingHelpers
|
|
34
|
+
|
|
30
35
|
class RenderError < ::Rhales::RenderError; end
|
|
31
36
|
class PartialNotFoundError < RenderError; end
|
|
32
37
|
class UndefinedVariableError < RenderError; end
|
|
@@ -42,31 +47,46 @@ module Rhales
|
|
|
42
47
|
end
|
|
43
48
|
|
|
44
49
|
def render
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
template_type = simple_template? ? :handlebars : :rue
|
|
51
|
+
|
|
52
|
+
log_timed_operation(Rhales.logger, :debug, 'Template compiled',
|
|
53
|
+
template_type: template_type, cached: false
|
|
54
|
+
) do
|
|
55
|
+
# Check if this is a simple template or a full .rue file
|
|
56
|
+
if simple_template?
|
|
57
|
+
# Use HandlebarsParser for simple templates
|
|
58
|
+
parser = HandlebarsParser.new(@template_content)
|
|
59
|
+
parser.parse!
|
|
60
|
+
render_content_nodes(parser.ast.children)
|
|
61
|
+
else
|
|
62
|
+
# Use RueDocument for .rue files
|
|
63
|
+
@parser = RueDocument.new(@template_content)
|
|
64
|
+
@parser.parse!
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
# Get template section via RueDocument
|
|
67
|
+
template_content = @parser.section('template')
|
|
68
|
+
raise RenderError, 'Missing template section' unless template_content
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
# Render the template section as a simple template
|
|
71
|
+
render_template_string(template_content)
|
|
72
|
+
end
|
|
62
73
|
end
|
|
63
74
|
rescue ::Rhales::ParseError => ex
|
|
64
75
|
# Parse errors already have good error messages with location
|
|
76
|
+
log_with_metadata(Rhales.logger, :error, 'Template parse error',
|
|
77
|
+
error: ex.message, line: ex.line, column: ex.column, section: ex.source_type
|
|
78
|
+
)
|
|
65
79
|
raise RenderError, "Template parsing failed: #{ex.message}"
|
|
66
80
|
rescue ::Rhales::ValidationError => ex
|
|
67
81
|
# Validation errors from RueDocument
|
|
82
|
+
log_with_metadata(Rhales.logger, :error, 'Template validation error',
|
|
83
|
+
error: ex.message, template_type: :rue
|
|
84
|
+
)
|
|
68
85
|
raise RenderError, "Template validation failed: #{ex.message}"
|
|
69
86
|
rescue StandardError => ex
|
|
87
|
+
log_with_metadata(Rhales.logger, :error, 'Template render error',
|
|
88
|
+
error: ex.message, error_class: ex.class.name
|
|
89
|
+
)
|
|
70
90
|
raise RenderError, "Template rendering failed: #{ex.message}"
|
|
71
91
|
end
|
|
72
92
|
|
|
@@ -80,11 +100,6 @@ module Rhales
|
|
|
80
100
|
@parser&.schema_path
|
|
81
101
|
end
|
|
82
102
|
|
|
83
|
-
# Access all data attributes from parsed .rue file
|
|
84
|
-
def data_attributes
|
|
85
|
-
@parser&.data_attributes || {}
|
|
86
|
-
end
|
|
87
|
-
|
|
88
103
|
# Get template variables used in the template
|
|
89
104
|
def template_variables
|
|
90
105
|
@parser&.template_variables || []
|
|
@@ -98,7 +113,7 @@ module Rhales
|
|
|
98
113
|
private
|
|
99
114
|
|
|
100
115
|
def simple_template?
|
|
101
|
-
!@template_content.match?(/^<(
|
|
116
|
+
!@template_content.match?(/^<(schema|template|logic)\b/)
|
|
102
117
|
end
|
|
103
118
|
|
|
104
119
|
def render_template_string(template_string)
|
|
@@ -130,7 +145,7 @@ module Rhales
|
|
|
130
145
|
when :each_block
|
|
131
146
|
result += render_each_block(node)
|
|
132
147
|
when :handlebars_expression
|
|
133
|
-
# Handle
|
|
148
|
+
# Handle handlebars expressions
|
|
134
149
|
result += render_handlebars_expression(node)
|
|
135
150
|
end
|
|
136
151
|
end
|
|
@@ -143,7 +158,13 @@ module Rhales
|
|
|
143
158
|
raw = node.value[:raw]
|
|
144
159
|
|
|
145
160
|
value = get_variable_value(name)
|
|
146
|
-
|
|
161
|
+
|
|
162
|
+
if raw
|
|
163
|
+
warn_if_unescaped(name, value, 'variable_expression')
|
|
164
|
+
value.to_s
|
|
165
|
+
else
|
|
166
|
+
escape_html(value.to_s)
|
|
167
|
+
end
|
|
147
168
|
end
|
|
148
169
|
|
|
149
170
|
def render_partial_expression(node)
|
|
@@ -204,7 +225,12 @@ module Rhales
|
|
|
204
225
|
''
|
|
205
226
|
else # Variables
|
|
206
227
|
value = get_variable_value(content)
|
|
207
|
-
raw
|
|
228
|
+
if raw
|
|
229
|
+
warn_if_unescaped(content, value, 'handlebars_expression')
|
|
230
|
+
value.to_s
|
|
231
|
+
else
|
|
232
|
+
escape_html(value.to_s)
|
|
233
|
+
end
|
|
208
234
|
end
|
|
209
235
|
end
|
|
210
236
|
|
|
@@ -214,9 +240,24 @@ module Rhales
|
|
|
214
240
|
partial_content = @partial_resolver.call(partial_name)
|
|
215
241
|
raise PartialNotFoundError, "Partial '#{partial_name}' not found" unless partial_content
|
|
216
242
|
|
|
217
|
-
#
|
|
218
|
-
|
|
219
|
-
|
|
243
|
+
# Check if this is a .rue document with sections
|
|
244
|
+
if partial_content.match?(/^<(schema|template|logic)\b/)
|
|
245
|
+
# Parse as RueDocument to handle schema sections properly
|
|
246
|
+
partial_doc = RueDocument.new(partial_content)
|
|
247
|
+
partial_doc.parse!
|
|
248
|
+
|
|
249
|
+
# Extract template section
|
|
250
|
+
template_content = partial_doc.section('template')
|
|
251
|
+
raise PartialNotFoundError, "Partial '#{partial_name}' missing template section" unless template_content
|
|
252
|
+
|
|
253
|
+
# Render template with current context
|
|
254
|
+
engine = self.class.new(template_content, @context, partial_resolver: @partial_resolver)
|
|
255
|
+
engine.render
|
|
256
|
+
else
|
|
257
|
+
# Simple template without sections - render as before
|
|
258
|
+
engine = self.class.new(partial_content, @context, partial_resolver: @partial_resolver)
|
|
259
|
+
engine.render
|
|
260
|
+
end
|
|
220
261
|
end
|
|
221
262
|
|
|
222
263
|
# Get variable value from context
|
|
@@ -234,11 +275,19 @@ module Rhales
|
|
|
234
275
|
@context.get(variable_name)
|
|
235
276
|
elsif @context.respond_to?(:[])
|
|
236
277
|
@context[variable_name] || @context[variable_name.to_sym]
|
|
237
|
-
else
|
|
238
|
-
nil
|
|
239
278
|
end
|
|
240
279
|
end
|
|
241
280
|
|
|
281
|
+
# Log warning for unescaped variable usage unless whitelisted
|
|
282
|
+
def warn_if_unescaped(variable_name, value, template_context)
|
|
283
|
+
return if Rhales.config.allowed_unescaped_variables.include?(variable_name.to_s)
|
|
284
|
+
|
|
285
|
+
log_with_metadata(
|
|
286
|
+
Rhales.logger, :warn, 'Unescaped variable usage',
|
|
287
|
+
variable: variable_name, value_type: value.class.name, template_context: template_context
|
|
288
|
+
)
|
|
289
|
+
end
|
|
290
|
+
|
|
242
291
|
# Evaluate condition for if/unless blocks
|
|
243
292
|
def evaluate_condition(condition)
|
|
244
293
|
value = get_variable_value(condition)
|
|
@@ -343,8 +392,6 @@ module Rhales
|
|
|
343
392
|
# Load and parse the partial .rue file
|
|
344
393
|
document = RueDocument.parse_file(partial_path)
|
|
345
394
|
document.section('template')
|
|
346
|
-
else
|
|
347
|
-
nil
|
|
348
395
|
end
|
|
349
396
|
end
|
|
350
397
|
end
|