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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.github/renovate.json5 +52 -0
  3. data/.github/workflows/ci.yml +123 -0
  4. data/.github/workflows/claude-code-review.yml +69 -0
  5. data/.github/workflows/claude.yml +49 -0
  6. data/.github/workflows/code-smells.yml +146 -0
  7. data/.github/workflows/ruby-lint.yml +78 -0
  8. data/.github/workflows/yardoc.yml +126 -0
  9. data/.gitignore +55 -0
  10. data/.pr_agent.toml +63 -0
  11. data/.pre-commit-config.yaml +89 -0
  12. data/.prettierignore +8 -0
  13. data/.prettierrc +38 -0
  14. data/.reek.yml +98 -0
  15. data/.rubocop.yml +428 -0
  16. data/.serena/.gitignore +3 -0
  17. data/.yardopts +56 -0
  18. data/CHANGELOG.md +44 -0
  19. data/CLAUDE.md +1 -2
  20. data/Gemfile +29 -0
  21. data/Gemfile.lock +189 -0
  22. data/README.md +706 -589
  23. data/Rakefile +46 -0
  24. data/debug_context.rb +25 -0
  25. data/demo/rhales-roda-demo/.gitignore +7 -0
  26. data/demo/rhales-roda-demo/Gemfile +32 -0
  27. data/demo/rhales-roda-demo/Gemfile.lock +151 -0
  28. data/demo/rhales-roda-demo/MAIL.md +405 -0
  29. data/demo/rhales-roda-demo/README.md +376 -0
  30. data/demo/rhales-roda-demo/RODA-TEMPLATE-ENGINES.md +192 -0
  31. data/demo/rhales-roda-demo/Rakefile +49 -0
  32. data/demo/rhales-roda-demo/app.rb +325 -0
  33. data/demo/rhales-roda-demo/bin/rackup +26 -0
  34. data/demo/rhales-roda-demo/config.ru +13 -0
  35. data/demo/rhales-roda-demo/db/migrate/001_create_rodauth_tables.rb +266 -0
  36. data/demo/rhales-roda-demo/db/migrate/002_create_rodauth_password_tables.rb +79 -0
  37. data/demo/rhales-roda-demo/db/migrate/003_add_admin_account.rb +68 -0
  38. data/demo/rhales-roda-demo/templates/change_login.rue +31 -0
  39. data/demo/rhales-roda-demo/templates/change_password.rue +36 -0
  40. data/demo/rhales-roda-demo/templates/close_account.rue +31 -0
  41. data/demo/rhales-roda-demo/templates/create_account.rue +40 -0
  42. data/demo/rhales-roda-demo/templates/dashboard.rue +150 -0
  43. data/demo/rhales-roda-demo/templates/home.rue +78 -0
  44. data/demo/rhales-roda-demo/templates/layouts/main.rue +168 -0
  45. data/demo/rhales-roda-demo/templates/login.rue +65 -0
  46. data/demo/rhales-roda-demo/templates/logout.rue +25 -0
  47. data/demo/rhales-roda-demo/templates/reset_password.rue +26 -0
  48. data/demo/rhales-roda-demo/templates/verify_account.rue +27 -0
  49. data/demo/rhales-roda-demo/test_full_output.rb +27 -0
  50. data/demo/rhales-roda-demo/test_simple.rb +24 -0
  51. data/docs/.gitignore +9 -0
  52. data/docs/architecture/data-flow.md +499 -0
  53. data/examples/dashboard-with-charts.rue +271 -0
  54. data/examples/form-with-validation.rue +180 -0
  55. data/examples/simple-page.rue +61 -0
  56. data/examples/vue.rue +136 -0
  57. data/generate-json-schemas.ts +158 -0
  58. data/json_schemer_migration_summary.md +172 -0
  59. data/lib/rhales/adapters/base_auth.rb +2 -0
  60. data/lib/rhales/adapters/base_request.rb +2 -0
  61. data/lib/rhales/adapters/base_session.rb +2 -0
  62. data/lib/rhales/adapters.rb +7 -0
  63. data/lib/rhales/configuration.rb +161 -1
  64. data/lib/rhales/core/context.rb +354 -0
  65. data/lib/rhales/{rue_document.rb → core/rue_document.rb} +59 -43
  66. data/lib/rhales/{template_engine.rb → core/template_engine.rb} +80 -33
  67. data/lib/rhales/core/view.rb +529 -0
  68. data/lib/rhales/{view_composition.rb → core/view_composition.rb} +81 -9
  69. data/lib/rhales/core.rb +9 -0
  70. data/lib/rhales/errors/hydration_collision_error.rb +2 -0
  71. data/lib/rhales/errors.rb +2 -0
  72. data/lib/rhales/hydration/earliest_injection_detector.rb +153 -0
  73. data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
  74. data/lib/rhales/hydration/hydration_endpoint.rb +215 -0
  75. data/lib/rhales/hydration/hydration_injector.rb +175 -0
  76. data/lib/rhales/{hydration_registry.rb → hydration/hydration_registry.rb} +2 -0
  77. data/lib/rhales/hydration/hydrator.rb +102 -0
  78. data/lib/rhales/hydration/link_based_injection_detector.rb +195 -0
  79. data/lib/rhales/hydration/mount_point_detector.rb +109 -0
  80. data/lib/rhales/hydration/safe_injection_validator.rb +103 -0
  81. data/lib/rhales/hydration.rb +13 -0
  82. data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +7 -13
  83. data/lib/rhales/{tilt.rb → integrations/tilt.rb} +26 -18
  84. data/lib/rhales/integrations.rb +6 -0
  85. data/lib/rhales/middleware/json_responder.rb +191 -0
  86. data/lib/rhales/middleware/schema_validator.rb +300 -0
  87. data/lib/rhales/middleware.rb +6 -0
  88. data/lib/rhales/parsers/handlebars_parser.rb +2 -0
  89. data/lib/rhales/parsers/rue_format_parser.rb +55 -36
  90. data/lib/rhales/parsers.rb +9 -0
  91. data/lib/rhales/{csp.rb → security/csp.rb} +27 -3
  92. data/lib/rhales/utils/json_serializer.rb +114 -0
  93. data/lib/rhales/utils/logging_helpers.rb +75 -0
  94. data/lib/rhales/utils/schema_extractor.rb +132 -0
  95. data/lib/rhales/utils/schema_generator.rb +194 -0
  96. data/lib/rhales/utils.rb +40 -0
  97. data/lib/rhales/version.rb +5 -1
  98. data/lib/rhales.rb +47 -19
  99. data/lib/tasks/rhales_schema.rake +197 -0
  100. data/package.json +10 -0
  101. data/pnpm-lock.yaml +345 -0
  102. data/pnpm-workspace.yaml +2 -0
  103. data/proofs/error_handling.rb +79 -0
  104. data/proofs/expanded_object_inheritance.rb +82 -0
  105. data/proofs/partial_context_scoping_fix.rb +168 -0
  106. data/proofs/ui_context_partial_inheritance.rb +236 -0
  107. data/rhales.gemspec +14 -6
  108. data/schema_vs_data_comparison.md +254 -0
  109. data/test_direct_access.rb +36 -0
  110. metadata +142 -18
  111. data/CLAUDE.locale.txt +0 -7
  112. data/lib/rhales/context.rb +0 -240
  113. data/lib/rhales/hydration_data_aggregator.rb +0 -220
  114. data/lib/rhales/hydrator.rb +0 -141
  115. data/lib/rhales/parsers/handlebars-grammar-review.txt +0 -39
  116. data/lib/rhales/view.rb +0 -412
@@ -1,240 +0,0 @@
1
- # lib/rhales/context.rb
2
-
3
- require_relative 'configuration'
4
- require_relative 'adapters/base_auth'
5
- require_relative 'adapters/base_session'
6
- require_relative 'adapters/base_request'
7
- require_relative 'csp'
8
-
9
- module Rhales
10
- # RSFCContext provides a clean interface for RSFC templates to access
11
- # server-side data. Follows the established pattern from InitScriptContext
12
- # and EnvironmentContext for focused, single-responsibility context objects.
13
- #
14
- # The context provides two layers of data:
15
- # 1. App: Framework-provided data (CSRF tokens, authentication, config)
16
- # 2. Props: Application data passed to the view (user, content, features)
17
- #
18
- # App data is accessible as both direct variables and through the app.* namespace.
19
- # Props take precedence over app data for variable resolution.
20
- #
21
- # One RSFCContext instance is created per page render and shared across
22
- # the main template and all partials to maintain security boundaries.
23
- class Context
24
- attr_reader :req, :sess, :cust, :locale, :props, :config, :app_data
25
-
26
- def initialize(req, sess = nil, cust = nil, locale_override = nil, props: {}, config: nil)
27
- @req = req
28
- @sess = sess || default_session
29
- @cust = cust || default_customer
30
- @config = config || Rhales.configuration
31
- @locale = locale_override || @config.default_locale
32
-
33
- # Normalize props keys to strings for consistent access
34
- @props = normalize_keys(props).freeze
35
-
36
- # Build context layers (two-layer model: app + props)
37
- @app_data = build_app_data.freeze
38
-
39
- # Pre-compute all_data before freezing
40
- # Props take precedence over app data, and add app namespace
41
- @all_data = @app_data.merge(@props).merge({ 'app' => @app_data }).freeze
42
-
43
- # Make context immutable after creation
44
- freeze
45
- end
46
-
47
- # Get variable value with dot notation support (e.g., "user.id", "features.account_creation")
48
- def get(variable_path)
49
- path_parts = variable_path.split('.')
50
- current_value = all_data
51
-
52
- path_parts.each do |part|
53
- case current_value
54
- when Hash
55
- if current_value.key?(part)
56
- current_value = current_value[part]
57
- elsif current_value.key?(part.to_sym)
58
- current_value = current_value[part.to_sym]
59
- else
60
- return nil
61
- end
62
- when Object
63
- if current_value.respond_to?(part)
64
- current_value = current_value.public_send(part)
65
- elsif current_value.respond_to?("#{part}?")
66
- current_value = current_value.public_send("#{part}?")
67
- else
68
- return nil
69
- end
70
- else
71
- return nil
72
- end
73
-
74
- return nil if current_value.nil?
75
- end
76
-
77
- current_value
78
- end
79
-
80
- # Get all available data (runtime + business + computed)
81
- attr_reader :all_data
82
-
83
- # Check if variable exists
84
- def variable?(variable_path)
85
- !get(variable_path).nil?
86
- end
87
-
88
- # Get list of all available variable paths (for validation)
89
- def available_variables
90
- collect_variable_paths(all_data)
91
- end
92
-
93
- # Resolve variable (alias for get method for hydrator compatibility)
94
- def resolve_variable(variable_path)
95
- get(variable_path)
96
- end
97
-
98
- private
99
-
100
-
101
- # Build consolidated app data (replaces runtime_data + computed_data)
102
- def build_app_data
103
- app = {}
104
-
105
- # Request context (from current runtime_data)
106
- if req && req.respond_to?(:env) && req.env
107
- app['csrf_token'] = req.env.fetch(@config.csrf_token_name, nil)
108
- app['nonce'] = get_or_generate_nonce
109
- app['request_id'] = req.env.fetch('request_id', nil)
110
- app['domain_strategy'] = req.env.fetch('domain_strategy', :default)
111
- app['display_domain'] = req.env.fetch('display_domain', nil)
112
- else
113
- # Generate nonce even without request if CSP is enabled
114
- app['nonce'] = get_or_generate_nonce
115
- end
116
-
117
- # Configuration (from both layers)
118
- app['environment'] = @config.app_environment
119
- app['api_base_url'] = @config.api_base_url
120
- app['features'] = @config.features
121
- app['development'] = @config.development?
122
-
123
- # Authentication & UI (from current computed_data)
124
- app['authenticated'] = authenticated?
125
- app['theme_class'] = determine_theme_class
126
-
127
- app
128
- end
129
-
130
- # Build API base URL from configuration (deprecated - moved to config)
131
- def build_api_base_url
132
- @config.api_base_url
133
- end
134
-
135
- # Determine theme class for CSS
136
- def determine_theme_class
137
- # Default theme logic - can be overridden by business data
138
- if props['theme']
139
- "theme-#{props['theme']}"
140
- elsif cust && cust.respond_to?(:theme_preference)
141
- "theme-#{cust.theme_preference}"
142
- else
143
- 'theme-light'
144
- end
145
- end
146
-
147
- # Check if user is authenticated
148
- def authenticated?
149
- sess && sess.authenticated? && cust && !cust.anonymous?
150
- end
151
-
152
- # Get default session instance
153
- def default_session
154
- Rhales::Adapters::AnonymousSession.new
155
- end
156
-
157
- # Get default customer instance
158
- def default_customer
159
- Rhales::Adapters::AnonymousAuth.new
160
- end
161
-
162
- # Normalize hash keys to strings recursively
163
- def normalize_keys(data)
164
- case data
165
- when Hash
166
- data.each_with_object({}) do |(key, value), result|
167
- result[key.to_s] = normalize_keys(value)
168
- end
169
- when Array
170
- data.map { |item| normalize_keys(item) }
171
- else
172
- data
173
- end
174
- end
175
-
176
- # Recursively collect all variable paths from nested data
177
- def collect_variable_paths(data, prefix = '')
178
- paths = []
179
-
180
- case data
181
- when Hash
182
- data.each do |key, value|
183
- current_path = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
184
- paths << current_path
185
-
186
- if value.is_a?(Hash) || value.is_a?(Object)
187
- paths.concat(collect_variable_paths(value, current_path))
188
- end
189
- end
190
- when Object
191
- # For objects, collect method names that look like attributes
192
- data.public_methods(false).each do |method|
193
- method_name = method.to_s
194
- next if method_name.end_with?('=') # Skip setters
195
- next if method_name.start_with?('_') # Skip private-ish methods
196
-
197
- current_path = prefix.empty? ? method_name : "#{prefix}.#{method_name}"
198
- paths << current_path
199
- end
200
- end
201
-
202
- paths
203
- end
204
-
205
- # Get or generate nonce for CSP
206
- def get_or_generate_nonce
207
- # Try to get existing nonce from request env
208
- if req && req.respond_to?(:env) && req.env
209
- existing_nonce = req.env.fetch(@config.nonce_header_name, nil)
210
- return existing_nonce if existing_nonce
211
- end
212
-
213
- # Generate new nonce if auto_nonce is enabled or CSP is enabled
214
- return CSP.generate_nonce if @config.auto_nonce || (@config.csp_enabled && csp_nonce_required?)
215
-
216
- # Return nil if nonce is not needed
217
- nil
218
- end
219
-
220
- # Check if CSP policy requires nonce
221
- def csp_nonce_required?
222
- return false unless @config.csp_enabled
223
-
224
- csp = CSP.new(@config)
225
- csp.nonce_required?
226
- end
227
-
228
- class << self
229
- # Create context with business data for a specific view
230
- def for_view(req, sess, cust, locale, config: nil, **props)
231
- new(req, sess, cust, locale, props: props, config: config)
232
- end
233
-
234
- # Create minimal context for testing
235
- def minimal(props: {}, config: nil)
236
- new(nil, nil, nil, 'en', props: props, config: config)
237
- end
238
- end
239
- end
240
- end
@@ -1,220 +0,0 @@
1
- # lib/rhales/hydration_data_aggregator.rb
2
-
3
- require 'json'
4
- require_relative 'template_engine'
5
- require_relative 'errors'
6
-
7
- module Rhales
8
- # HydrationDataAggregator traverses the ViewComposition and executes
9
- # all <data> sections to produce a single, merged JSON structure.
10
- #
11
- # This class implements the server-side data aggregation phase of the
12
- # two-pass rendering model, handling:
13
- # - Traversal of the template dependency tree
14
- # - Execution of <data> sections with full server context
15
- # - Merge strategies (deep, shallow, strict)
16
- # - Collision detection and error reporting
17
- #
18
- # The aggregator replaces the HydrationRegistry by performing all
19
- # data merging in a single, coordinated pass.
20
- class HydrationDataAggregator
21
- class JSONSerializationError < StandardError; end
22
-
23
- def initialize(context)
24
- @context = context
25
- @window_attributes = {}
26
- @merged_data = {}
27
- end
28
-
29
- # Aggregate all hydration data from the view composition
30
- def aggregate(composition)
31
- composition.each_document_in_render_order do |template_name, parser|
32
- process_template(template_name, parser)
33
- end
34
-
35
- @merged_data
36
- end
37
-
38
- private
39
-
40
- def process_template(template_name, parser)
41
- data_content = parser.section('data')
42
- return unless data_content
43
-
44
- window_attr = parser.window_attribute || 'data'
45
- merge_strategy = parser.merge_strategy
46
-
47
- # Build template path for error reporting
48
- template_path = build_template_path(parser)
49
-
50
- # Process the data section first to check if it's empty
51
- processed_data = process_data_section(data_content, parser)
52
-
53
- # Check for collisions only if the data is not empty
54
- if @window_attributes.key?(window_attr) && merge_strategy.nil? && !empty_data?(processed_data)
55
- existing = @window_attributes[window_attr]
56
- existing_data = @merged_data[window_attr]
57
-
58
- # Only raise collision error if existing data is also not empty
59
- unless empty_data?(existing_data)
60
- raise ::Rhales::HydrationCollisionError.new(window_attr, existing[:path], template_path)
61
- end
62
- end
63
-
64
- # Merge or set the data
65
- if @merged_data.key?(window_attr)
66
- @merged_data[window_attr] = merge_data(
67
- @merged_data[window_attr],
68
- processed_data,
69
- merge_strategy || 'deep',
70
- window_attr,
71
- template_path
72
- )
73
- else
74
- @merged_data[window_attr] = processed_data
75
- end
76
-
77
- # Track the window attribute
78
- @window_attributes[window_attr] = {
79
- path: template_path,
80
- merge_strategy: merge_strategy
81
- }
82
- end
83
-
84
- def process_data_section(data_content, parser)
85
- # Create a JSON-aware context wrapper for data sections
86
- json_context = JsonAwareContext.new(@context)
87
-
88
- # Process template variables in the data section
89
- processed_content = TemplateEngine.render(data_content, json_context)
90
-
91
- # Parse as JSON
92
- begin
93
- JSON.parse(processed_content)
94
- rescue JSON::ParserError => ex
95
- template_path = build_template_path(parser)
96
- raise JSONSerializationError,
97
- "Invalid JSON in data section at #{template_path}: #{ex.message}\n" \
98
- "Processed content: #{processed_content[0..200]}..."
99
- end
100
- end
101
-
102
- def merge_data(target, source, strategy, window_attr, template_path)
103
- case strategy
104
- when 'deep'
105
- deep_merge(target, source)
106
- when 'shallow'
107
- shallow_merge(target, source, window_attr, template_path)
108
- when 'strict'
109
- strict_merge(target, source, window_attr, template_path)
110
- else
111
- raise ArgumentError, "Unknown merge strategy: #{strategy}"
112
- end
113
- end
114
-
115
- def deep_merge(target, source)
116
- result = target.dup
117
-
118
- source.each do |key, value|
119
- if result.key?(key) && result[key].is_a?(Hash) && value.is_a?(Hash)
120
- result[key] = deep_merge(result[key], value)
121
- else
122
- result[key] = value
123
- end
124
- end
125
-
126
- result
127
- end
128
-
129
- def shallow_merge(target, source, window_attr, template_path)
130
- result = target.dup
131
-
132
- source.each do |key, value|
133
- if result.key?(key)
134
- raise ::Rhales::HydrationCollisionError.new(
135
- "#{window_attr}.#{key}",
136
- @window_attributes[window_attr][:path],
137
- template_path
138
- )
139
- end
140
- result[key] = value
141
- end
142
-
143
- result
144
- end
145
-
146
- def strict_merge(target, source, window_attr, template_path)
147
- # In strict mode, any collision is an error
148
- target.each_key do |key|
149
- if source.key?(key)
150
- raise ::Rhales::HydrationCollisionError.new(
151
- "#{window_attr}.#{key}",
152
- @window_attributes[window_attr][:path],
153
- template_path
154
- )
155
- end
156
- end
157
-
158
- target.merge(source)
159
- end
160
-
161
- def build_template_path(parser)
162
- data_node = parser.section_node('data')
163
- line_number = data_node ? data_node.location.start_line : 1
164
-
165
- if parser.file_path
166
- "#{parser.file_path}:#{line_number}"
167
- else
168
- "<inline>:#{line_number}"
169
- end
170
- end
171
-
172
- # Check if data is considered empty for collision detection
173
- def empty_data?(data)
174
- return true if data.nil?
175
- return true if data == {}
176
- return true if data == []
177
- return true if data.respond_to?(:empty?) && data.empty?
178
- false
179
- end
180
- end
181
-
182
- # Context wrapper that automatically converts Ruby objects to JSON in data sections
183
- class JsonAwareContext
184
- def initialize(context)
185
- @context = context
186
- end
187
-
188
- # Delegate all methods to the wrapped context
189
- def method_missing(method, *args, &block)
190
- @context.send(method, *args, &block)
191
- end
192
-
193
- def respond_to_missing?(method, include_private = false)
194
- @context.respond_to?(method, include_private)
195
- end
196
-
197
- # Override get method to return JSON-serialized objects
198
- def get(variable_path)
199
- value = @context.get(variable_path)
200
-
201
- # Convert Ruby objects to JSON for data sections
202
- case value
203
- when Hash, Array
204
- begin
205
- value.to_json
206
- rescue JSON::GeneratorError, SystemStackError => ex
207
- # Handle serialization errors (circular references, unsupported types, etc.)
208
- raise JSONSerializationError,
209
- "Failed to serialize Ruby object to JSON: #{ex.message}. " \
210
- "Object type: #{value.class}, var path: #{variable_path}..."
211
- end
212
- else
213
- value
214
- end
215
- end
216
-
217
- # Alias for compatibility with template engine
218
- alias_method :resolve_variable, :get
219
- end
220
- end
@@ -1,141 +0,0 @@
1
- # lib/rhales/hydrator.rb
2
-
3
- require 'json'
4
- require 'securerandom'
5
-
6
- module Rhales
7
- # Data Hydrator for RSFC client-side data injection
8
- #
9
- # ## RSFC Security Model: Server-to-Client Security Boundary
10
- #
11
- # The Hydrator enforces a critical security boundary between server and client:
12
- #
13
- # ### Server Side (Template Rendering)
14
- # - Templates have FULL server context access (like ERB/HAML)
15
- # - Can access user objects, database connections, internal APIs
16
- # - Can access secrets, configuration, authentication state
17
- # - Can process sensitive business logic
18
- #
19
- # ### Client Side (Data Hydration)
20
- # - Only data declared in <data> section reaches the browser
21
- # - Creates explicit allowlist like designing a REST API
22
- # - Server-side variable interpolation processes secrets safely
23
- # - JSON serialization validates data structure
24
- #
25
- # ### Process Flow
26
- # 1. Server processes <data> section with full context access
27
- # 2. Variables like {{user.name}} are interpolated server-side
28
- # 3. Result is serialized as JSON and sent to client
29
- # 4. Client receives only the processed, safe data
30
- #
31
- # ### Example
32
- # ```rue
33
- # <data>
34
- # {
35
- # "user_name": "{{user.name}}", // Safe: just the name
36
- # "theme": "{{user.theme_preference}}" // Safe: just the theme
37
- # }
38
- # </data>
39
- # ```
40
- #
41
- # Server template can access {{user.admin?}} and {{internal_config}},
42
- # but client only gets the declared user_name and theme values.
43
- #
44
- # This creates an API-like boundary where data is serialized once and
45
- # parsed once, enforcing the same security model as REST endpoints.
46
- #
47
- # Note: With the new two-pass architecture, the Hydrator's role is
48
- # greatly simplified. All data merging happens server-side in the
49
- # HydrationDataAggregator, so this class only handles JSON generation
50
- # for individual templates (used during the aggregation phase).
51
- class Hydrator
52
- class HydrationError < StandardError; end
53
- class JSONSerializationError < HydrationError; end
54
-
55
- attr_reader :parser, :context, :window_attribute
56
-
57
- def initialize(parser, context)
58
- @parser = parser
59
- @context = context
60
- @window_attribute = parser.window_attribute || 'data'
61
- end
62
-
63
- # This method is now deprecated in favor of the two-pass architecture
64
- # It's kept for backward compatibility but will be removed in future versions
65
- def generate_hydration_html
66
- warn "[DEPRECATION] Hydrator#generate_hydration_html is deprecated. Use the two-pass rendering architecture instead."
67
- ""
68
- end
69
-
70
- # Process <data> section and return JSON string
71
- def process_data_section
72
- data_content = @parser.section('data')
73
- return '{}' unless data_content
74
-
75
- # Process variable interpolations in the data section
76
- processed_content = process_data_variables(data_content)
77
-
78
- # Validate and return JSON
79
- validate_json(processed_content)
80
- processed_content
81
- rescue JSON::ParserError => ex
82
- raise JSONSerializationError, "Invalid JSON in data section: #{ex.message}"
83
- end
84
-
85
- # Get processed data as Ruby hash (for internal use)
86
- def processed_data_hash
87
- json_string = process_data_section
88
- JSON.parse(json_string)
89
- rescue JSON::ParserError => ex
90
- raise JSONSerializationError, "Cannot parse processed data as JSON: #{ex.message}"
91
- end
92
-
93
- private
94
-
95
- # Process variable interpolations in data section
96
- # Uses Rhales consistently for all template processing
97
- def process_data_variables(data_content)
98
- rhales = TemplateEngine.new(data_content, @context)
99
- rhales.render
100
- end
101
-
102
- # Validate that processed content is valid JSON
103
- def validate_json(json_string)
104
- JSON.parse(json_string)
105
- rescue JSON::ParserError => ex
106
- raise JSONSerializationError, "Processed data section is not valid JSON: #{ex.message}"
107
- end
108
-
109
- # Build template path with line number for error reporting
110
- # (Used by HydrationDataAggregator)
111
- def build_template_path
112
- data_node = @parser.section_node('data')
113
- line_number = data_node ? data_node.location.start_line : 1
114
-
115
- if @parser.file_path
116
- "#{@parser.file_path}:#{line_number}"
117
- else
118
- "<inline>:#{line_number}"
119
- end
120
- end
121
-
122
- class << self
123
- # Convenience method to generate hydration HTML
124
- # DEPRECATED: Use the two-pass rendering architecture instead
125
- def generate(parser, context)
126
- warn "[DEPRECATION] Hydrator.generate is deprecated. Use the two-pass rendering architecture instead."
127
- new(parser, context).generate_hydration_html
128
- end
129
-
130
- # Generate only JSON data (for testing or API endpoints)
131
- def generate_json(parser, context)
132
- new(parser, context).process_data_section
133
- end
134
-
135
- # Generate data hash (for internal processing)
136
- def generate_data_hash(parser, context)
137
- new(parser, context).processed_data_hash
138
- end
139
- end
140
- end
141
- end
@@ -1,39 +0,0 @@
1
- According to Gemini 2.5 Pro.
2
-
3
-
4
- The `HandlebarsGrammar` class is **not a formal grammar definition**.
5
-
6
- Instead, it is a **parser** that **implements** the rules of the Handlebars grammar. This is a crucial distinction:
7
-
8
- 1. **Formal Grammar (The Blueprint):** A formal grammar is a set of abstract, mathematical rules that define a language. It specifies the valid sequences of symbols and their structure, often using a notation like Backus-Naur Form (BNF). It is a *specification*, not code.
9
-
10
- *Example (simplified BNF for a Handlebars variable):*
11
- ```
12
- expression ::= '{{' identifier '}}'
13
- identifier ::= letter ( letter | digit )*
14
- ```
15
-
16
- 2. **Parser (The Implementation):** A parser is a piece of software that takes an input string and determines if it conforms to the rules of a formal grammar. As a byproduct, it usually produces a data structure, like an Abstract Syntax Tree (AST), that represents the input's structure.
17
-
18
- The `HandlebarsGrammar` class is what is known as a **hand-rolled parser**. It was written manually in Ruby to recognize and structure Handlebars syntax, rather than being automatically generated by a tool.
19
-
20
- ### What Makes it a Parser (Not a Grammar)
21
-
22
- You can see the classic components of a hand-rolled parser right in its methods:
23
-
24
- * **Lexical Analysis (Scanning):** It consumes the input character by character and identifies tokens.
25
- * `current_char`, `peek_char`, `advance`: These methods manage the input stream.
26
- * `parse_text_until_handlebars`: This logic finds the boundary between plain text and a Handlebars expression (`{{`).
27
-
28
- * **Syntactic Analysis (Parsing):** It applies the grammar rules (as implemented in Ruby logic) to the token stream to build a structural representation.
29
- * `parse_template`: The main entry point that orchestrates the parsing process.
30
- * `create_if_block`, `create_each_block`: These methods correspond directly to the production rules for Handlebars block helpers. They build `Node` objects.
31
- * `Node` class: This defines the structure of the AST that the parser produces as its output.
32
-
33
- ### Why No Prism Reference?
34
-
35
- Parser generators like Prism, ANTLR, or Treetop work by taking a formal grammar definition as input and *generating* the parser code for you.
36
-
37
- Since `HandlebarsGrammar` is a hand-rolled parser, it doesn't need a generator. The developer has written the parsing logic directly in Ruby. This approach is common for languages with relatively simple, non-ambiguous syntax, as it avoids adding an external dependency and gives the developer full control over the parsing process and error handling.
38
-
39
- In summary: You've correctly identified that the class isn't a formal grammar. It's a **manual implementation of a parser** for that grammar, which is why it contains procedural logic for scanning and structuring text instead of a declarative set of grammar rules.