hyraft 0.1.0.alpha1

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 (84) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +231 -0
  6. data/exe/hyraft +5 -0
  7. data/lib/hyraft/boot/asset_preloader.rb +185 -0
  8. data/lib/hyraft/boot/preloaded_static.rb +46 -0
  9. data/lib/hyraft/boot/preloader.rb +206 -0
  10. data/lib/hyraft/cli.rb +187 -0
  11. data/lib/hyraft/compiler/compiler.rb +34 -0
  12. data/lib/hyraft/compiler/html_purifier.rb +181 -0
  13. data/lib/hyraft/compiler/javascript_library.rb +281 -0
  14. data/lib/hyraft/compiler/javascript_obfuscator.rb +141 -0
  15. data/lib/hyraft/compiler/parser.rb +27 -0
  16. data/lib/hyraft/compiler/renderer.rb +217 -0
  17. data/lib/hyraft/engine/circuit.rb +35 -0
  18. data/lib/hyraft/engine/port.rb +17 -0
  19. data/lib/hyraft/engine/source.rb +19 -0
  20. data/lib/hyraft/engine.rb +11 -0
  21. data/lib/hyraft/router/api_router.rb +65 -0
  22. data/lib/hyraft/router/web_router.rb +136 -0
  23. data/lib/hyraft/system_info.rb +26 -0
  24. data/lib/hyraft/version.rb +5 -0
  25. data/lib/hyraft.rb +48 -0
  26. data/templates/do_app/Gemfile +50 -0
  27. data/templates/do_app/Rakefile +88 -0
  28. data/templates/do_app/adapter-intake/web-app/display/pages/home/home.hyr +174 -0
  29. data/templates/do_app/adapter-intake/web-app/request/home_web_adapter.rb +19 -0
  30. data/templates/do_app/boot.rb +41 -0
  31. data/templates/do_app/framework/adapters/server/server_api_adapter.rb +51 -0
  32. data/templates/do_app/framework/adapters/server/server_web_adapter.rb +178 -0
  33. data/templates/do_app/framework/compiler/style_resolver.rb +33 -0
  34. data/templates/do_app/framework/errors/error_handler.rb +75 -0
  35. data/templates/do_app/framework/errors/templates/304.html +22 -0
  36. data/templates/do_app/framework/errors/templates/400.html +22 -0
  37. data/templates/do_app/framework/errors/templates/401.html +22 -0
  38. data/templates/do_app/framework/errors/templates/403.html +22 -0
  39. data/templates/do_app/framework/errors/templates/404.html +62 -0
  40. data/templates/do_app/framework/errors/templates/500.html +73 -0
  41. data/templates/do_app/framework/middleware/cors_middleware.rb +37 -0
  42. data/templates/do_app/infra/config/environment.rb +86 -0
  43. data/templates/do_app/infra/config/error_config.rb +80 -0
  44. data/templates/do_app/infra/config/routes/api_routes.rb +2 -0
  45. data/templates/do_app/infra/config/routes/web_routes.rb +10 -0
  46. data/templates/do_app/infra/database/sequel_connection.rb +62 -0
  47. data/templates/do_app/infra/gems/database.rb +7 -0
  48. data/templates/do_app/infra/gems/load_all.rb +4 -0
  49. data/templates/do_app/infra/gems/utilities.rb +1 -0
  50. data/templates/do_app/infra/gems/web.rb +3 -0
  51. data/templates/do_app/infra/server/api-server.ru +13 -0
  52. data/templates/do_app/infra/server/web-server.ru +32 -0
  53. data/templates/do_app/package.json +9 -0
  54. data/templates/do_app/public/favicon.ico +0 -0
  55. data/templates/do_app/public/icons/docs.svg +10 -0
  56. data/templates/do_app/public/icons/expli.svg +13 -0
  57. data/templates/do_app/public/icons/git-repo.svg +13 -0
  58. data/templates/do_app/public/icons/hexagonal-arch.svg +15 -0
  59. data/templates/do_app/public/icons/template-engine.svg +26 -0
  60. data/templates/do_app/public/images/hyr-logo.png +0 -0
  61. data/templates/do_app/public/images/hyr-logo.webp +0 -0
  62. data/templates/do_app/public/index.html +22 -0
  63. data/templates/do_app/public/styles/css/main.css +418 -0
  64. data/templates/do_app/public/styles/css/spa.css +171 -0
  65. data/templates/do_app/shared/helpers/pagination_helper.rb +44 -0
  66. data/templates/do_app/shared/helpers/response_formatter.rb +25 -0
  67. data/templates/do_app/test/acceptance/api/articles_api_acceptance_test.rb +43 -0
  68. data/templates/do_app/test/acceptance/web/articles_acceptance_test.rb +31 -0
  69. data/templates/do_app/test/acceptance/web/home_acceptance_test.rb +17 -0
  70. data/templates/do_app/test/db.rb +106 -0
  71. data/templates/do_app/test/integration/adapter-exhaust/data-gateway/sequel_articles_gateway_test.rb +79 -0
  72. data/templates/do_app/test/integration/adapter-intake/api-app/request/articles_api_adapter_test.rb +61 -0
  73. data/templates/do_app/test/integration/adapter-intake/web-app/request/articles_web_adapter_test.rb +20 -0
  74. data/templates/do_app/test/integration/adapter-intake/web-app/request/home_web_adapter_test.rb +17 -0
  75. data/templates/do_app/test/integration/database/migration_test.rb +35 -0
  76. data/templates/do_app/test/support/mock_api_adapter.rb +82 -0
  77. data/templates/do_app/test/support/mock_articles_gateway.rb +41 -0
  78. data/templates/do_app/test/support/mock_web_adapter.rb +85 -0
  79. data/templates/do_app/test/support/test_patches.rb +33 -0
  80. data/templates/do_app/test/test_helper.rb +526 -0
  81. data/templates/do_app/test/unit/engine/circuit/articles_circuit_test.rb +167 -0
  82. data/templates/do_app/test/unit/engine/port/articles_gateway_port_test.rb +12 -0
  83. data/templates/do_app/test/unit/engine/source/article_test.rb +37 -0
  84. metadata +291 -0
@@ -0,0 +1,281 @@
1
+ # lib/hyraft/compiler/javascript_library.rb
2
+ require_relative 'javascript_obfuscator'
3
+
4
+ module Hyraft
5
+ module Compiler
6
+ class JavaScriptLibrary
7
+ # Store the original clean version
8
+ CLEAN_LIBRARIES = {
9
+ 'lib/neonpulse' => <<~JAVASCRIPT
10
+ /* NeonPulse Library */
11
+ (() => {
12
+ 'use strict';
13
+
14
+ class NeonPulse {
15
+ #signals = new Map();
16
+ #outputs = new Map();
17
+ #processors = new Map();
18
+ #forms = new Map();
19
+
20
+ constructor() {
21
+ this.#initialize();
22
+ }
23
+
24
+ #initialize = () => {
25
+ const readyHandler = () => {
26
+ this.#connectOutputs();
27
+ this.#connectForms();
28
+ };
29
+
30
+ document.readyState === 'loading'
31
+ ? document.addEventListener('DOMContentLoaded', readyHandler)
32
+ : readyHandler();
33
+ };
34
+
35
+ #connectOutputs = () => {
36
+ const signalElements = [...document.querySelectorAll('[data-neon]')];
37
+ const actionElements = [...document.querySelectorAll('[data-pulse]')];
38
+
39
+ signalElements.forEach(element => {
40
+ const signalName = element.dataset.neon;
41
+ const property = element.dataset.property || 'textContent';
42
+
43
+ if (!this.#outputs.has(signalName)) {
44
+ this.#outputs.set(signalName, []);
45
+ }
46
+ this.#outputs.get(signalName).push({ element, property });
47
+
48
+ const signal = this.#signals.get(signalName);
49
+ signal && this.#updateElement(element, signal.value, property);
50
+ });
51
+
52
+ actionElements.forEach(element => {
53
+ const [processor, action] = element.dataset.pulse?.split('.') ?? [];
54
+ const eventType = element.dataset.event || 'click';
55
+
56
+ element.addEventListener(eventType, event => {
57
+ window[processor]?.[action]?.(event);
58
+ });
59
+ });
60
+ };
61
+
62
+ #connectForms = () => {
63
+ const formElements = [...document.querySelectorAll('form[data-neon-form]')];
64
+
65
+ formElements.forEach(form => {
66
+ const formName = form.dataset.neonForm;
67
+ const [processor, action] = form.dataset.submit?.split('.') ?? [];
68
+
69
+ if (processor && action) {
70
+ form.addEventListener('submit', event => {
71
+ event.preventDefault();
72
+ event.stopPropagation();
73
+ window[processor]?.[action]?.(this.#getFormData(form), event);
74
+ });
75
+
76
+ this.#forms.set(formName, form);
77
+ }
78
+
79
+ this.#bindFormInputs(form);
80
+ });
81
+ };
82
+
83
+ #bindFormInputs = form => {
84
+ const inputs = form.querySelectorAll('input, textarea, select');
85
+
86
+ inputs.forEach(input => {
87
+ const signalName = input.dataset.neon;
88
+ if (!signalName || !this.#signals.has(signalName)) return;
89
+
90
+ const signal = this.#signals.get(signalName);
91
+
92
+ // Set initial value from signal
93
+ if (!['checkbox', 'radio'].includes(input.type)) {
94
+ input.value = signal.value ?? '';
95
+ }
96
+
97
+ // Bind input events to update signals
98
+ input.addEventListener('input', () => {
99
+ if (input.type === 'checkbox') {
100
+ signal.value = input.checked;
101
+ } else if (input.type === 'radio') {
102
+ if (input.checked) signal.value = input.value;
103
+ } else {
104
+ signal.value = input.value;
105
+ }
106
+ });
107
+
108
+ // Bind signal changes to update input
109
+ const unwatch = this.watch(signalName, newValue => {
110
+ if (input.type === 'checkbox') {
111
+ input.checked = !!newValue;
112
+ } else if (input.type === 'radio') {
113
+ input.checked = (input.value === newValue);
114
+ } else {
115
+ input.value = newValue ?? '';
116
+ }
117
+ });
118
+
119
+ // Store unwatch function for cleanup
120
+ if (!this.#forms.has('_watchers')) {
121
+ this.#forms.set('_watchers', new Map());
122
+ }
123
+ this.#forms.get('_watchers').set(input, unwatch);
124
+ });
125
+ };
126
+
127
+ #getFormData = form => {
128
+ const formData = {};
129
+ const inputs = form.querySelectorAll('input, textarea, select');
130
+
131
+ inputs.forEach(input => {
132
+ const name = input.name || input.id;
133
+ if (!name) return;
134
+
135
+ if (input.type === 'checkbox') {
136
+ formData[name] = input.checked;
137
+ } else if (input.type === 'radio') {
138
+ if (input.checked) formData[name] = input.value;
139
+ } else if (input.type === 'select-multiple') {
140
+ formData[name] = [...input.selectedOptions].map(opt => opt.value);
141
+ } else {
142
+ formData[name] = input.value;
143
+ }
144
+ });
145
+
146
+ return formData;
147
+ };
148
+
149
+ #emitSignal = signalName => {
150
+ const connectedElements = this.#outputs.get(signalName);
151
+ const signal = this.#signals.get(signalName);
152
+
153
+ connectedElements?.forEach(({ element, property }) => {
154
+ this.#updateElement(element, signal.value, property);
155
+ });
156
+ };
157
+
158
+ #updateElement = (element, value, property) => {
159
+ if (!element) return;
160
+
161
+ const updates = {
162
+ class: () => element.className = value,
163
+ style: () => element.style.cssText = value,
164
+ value: () => {
165
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
166
+ element.value = value;
167
+ }
168
+ }
169
+ };
170
+
171
+ updates[property]?.() ?? (element[property] = value);
172
+ };
173
+
174
+ // Public API
175
+ neon = (signalName, initialValue) => {
176
+ const proxy = new Proxy({ value: initialValue }, {
177
+ set: (target, property, value) => {
178
+ const success = Reflect.set(target, property, value);
179
+ if (success && property === 'value') {
180
+ this.#emitSignal(signalName);
181
+ }
182
+ return success;
183
+ }
184
+ });
185
+
186
+ this.#signals.set(signalName, proxy);
187
+ return proxy;
188
+ };
189
+
190
+ pulse = (name, actions) => {
191
+ const boundActions = Object.fromEntries(
192
+ Object.entries(actions).map(([key, value]) => [
193
+ key,
194
+ typeof value === 'function' ? value.bind(actions) : value
195
+ ])
196
+ );
197
+
198
+ window[name] = boundActions;
199
+ return boundActions;
200
+ };
201
+
202
+ neonBatch = signalsObj => {
203
+ const results = {};
204
+ Object.keys(signalsObj).forEach(key => {
205
+ results[key] = this.neon(key, signalsObj[key]);
206
+ });
207
+ return results;
208
+ };
209
+
210
+ watch = (signalName, callback) => {
211
+ const signal = this.#signals.get(signalName);
212
+ if (!signal) return;
213
+
214
+ let previousValue = signal.value;
215
+ const watcher = setInterval(() => {
216
+ if (signal.value !== previousValue) {
217
+ callback(signal.value, previousValue);
218
+ previousValue = signal.value;
219
+ }
220
+ }, 100);
221
+
222
+ return () => clearInterval(watcher);
223
+ };
224
+
225
+ form = (formName, initialData) => {
226
+ const formData = this.neonBatch(initialData ?? {});
227
+ return formData;
228
+ };
229
+
230
+ submit = (formName, handler) => {
231
+ const form = this.#forms.get(formName);
232
+ if (form && handler) {
233
+ form.addEventListener('submit', event => {
234
+ event.preventDefault();
235
+ const formData = this.#getFormData(form);
236
+ handler(formData, event);
237
+ });
238
+ }
239
+ };
240
+
241
+ cleanup = () => {
242
+ if (this.#forms.has('_watchers')) {
243
+ this.#forms.get('_watchers').forEach(unwatch => {
244
+ typeof unwatch === 'function' && unwatch();
245
+ });
246
+ }
247
+ };
248
+ }
249
+
250
+ window.neonPulse = new NeonPulse();
251
+ // console.log('🚀 NeonPulse activated);
252
+ })();
253
+ JAVASCRIPT
254
+ }
255
+
256
+ def self.get(library_name, obfuscation_method: :multi_layer)
257
+ clean_code = CLEAN_LIBRARIES[library_name]
258
+ return nil unless clean_code
259
+
260
+ case obfuscation_method
261
+ when :split_and_reassemble
262
+ JavaScriptObfuscator.split_and_reassemble(clean_code)
263
+ when :multi_layer
264
+ JavaScriptObfuscator.multi_layer_obfuscation(clean_code)
265
+ when :none
266
+ clean_code
267
+ else
268
+ JavaScriptObfuscator.multi_layer_obfuscation(clean_code)
269
+ end
270
+ end
271
+
272
+ def self.available_libraries
273
+ CLEAN_LIBRARIES.keys
274
+ end
275
+
276
+ def self.obfuscation_methods
277
+ [:multi_layer, :split_and_reassemble, :none]
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,141 @@
1
+ # lib/hyraft/compiler/javascript_obfuscator.rb
2
+ require 'json'
3
+
4
+ module Hyraft
5
+ module Compiler
6
+ class JavaScriptObfuscator
7
+ class << self
8
+ # Method 4: Split and Reassemble
9
+ def split_and_reassemble(js_code, parts: 8)
10
+ return '' unless js_code && !js_code.strip.empty?
11
+
12
+ # Calculate chunk size
13
+ chunk_size = (js_code.length.to_f / parts).ceil
14
+ # Split into chunks
15
+ chunks = js_code.chars.each_slice(chunk_size).map(&:join)
16
+
17
+ # Convert to JSON for JavaScript array
18
+ chunks_js = chunks.to_json
19
+
20
+ <<~JAVASCRIPT
21
+ (function(){
22
+ try {
23
+ var c=#{chunks_js};
24
+ var s=c.join('');
25
+ var e=document.createElement('script');
26
+ e.textContent=s;
27
+ document.head.appendChild(e);
28
+ } catch(err) {
29
+ console.error('Script load failed:', err);
30
+ }
31
+ })();
32
+ JAVASCRIPT
33
+ end
34
+
35
+ # Method 10: Multi-Layer Obfuscation
36
+ def multi_layer_obfuscation(js_code)
37
+ return '' unless js_code && !js_code.strip.empty?
38
+
39
+ obfuscated = js_code.dup
40
+
41
+ # Layer 1: Remove comments and extra whitespace
42
+ obfuscated = remove_comments_and_whitespace(obfuscated)
43
+
44
+ # Layer 2: Safe variable renaming (EXCLUDE neonPulse)
45
+ obfuscated = rename_variables(obfuscated)
46
+
47
+ # Layer 3: String obfuscation
48
+ obfuscated = obfuscate_strings(obfuscated)
49
+
50
+ # Layer 4: Number obfuscation
51
+ obfuscated = obfuscate_numbers(obfuscated)
52
+
53
+ # Layer 5: Split into chunks and reassemble
54
+ final_js = split_and_reassemble(obfuscated, parts: 6)
55
+
56
+ final_js
57
+ end
58
+
59
+ private
60
+
61
+ def remove_comments_and_whitespace(code)
62
+ # Remove block comments
63
+ code.gsub!(/\/\*[\s\S]*?\*\//, '')
64
+ # Remove line comments
65
+ code.gsub!(/\/\/[^\n\r]*/, '')
66
+ # Collapse multiple whitespace
67
+ code.gsub!(/\s+/, ' ')
68
+ # Remove spaces around operators
69
+ code.gsub!(/\s*([=+\-*\/&|^!<>?{}();:,])\s*/, '\1')
70
+ # Remove multiple semicolons
71
+ code.gsub!(/;\s*;/, ';')
72
+ code.strip
73
+ end
74
+
75
+ def rename_variables(code)
76
+ # Safe renaming - EXCLUDE neonPulse from renaming
77
+ renames = {
78
+ 'NeonPulse' => 'NP', # Rename class internally
79
+ # 'neonPulse' => 'np', # DON'T rename the global instance
80
+ 'signalName' => 'sN',
81
+ 'initialValue' => 'iV',
82
+ 'element' => 'el',
83
+ 'property' => 'pr',
84
+ 'formData' => 'fD',
85
+ 'callback' => 'cb',
86
+ 'handler' => 'hd',
87
+ 'processor' => 'pc',
88
+ 'action' => 'ac',
89
+ 'event' => 'ev'
90
+ }
91
+
92
+ renames.each do |original, replacement|
93
+ code.gsub!(/\b#{original}\b/, replacement)
94
+ end
95
+
96
+ code
97
+ end
98
+
99
+ def obfuscate_strings(code)
100
+ # Obfuscate double-quoted strings
101
+ code.gsub!(/"([^"\\]*(\\.[^"\\]*)*)"/) do |match|
102
+ content = $1
103
+ # Mix of hex and unicode escapes
104
+ obfuscated_content = content.chars.map.with_index { |c, i|
105
+ if i % 3 == 0
106
+ "\\x#{c.ord.to_s(16)}"
107
+ elsif i % 3 == 1
108
+ "\\u#{c.ord.to_s(16).rjust(4, '0')}"
109
+ else
110
+ c
111
+ end
112
+ }.join
113
+ "\"#{obfuscated_content}\""
114
+ end
115
+
116
+ code
117
+ end
118
+
119
+ def obfuscate_numbers(code)
120
+ # Convert some numbers to expressions
121
+ code.gsub!(/\b(\d+)\b/) do |match|
122
+ num = $1.to_i
123
+ if num > 5 && num < 100
124
+ case rand(4)
125
+ when 0 then "(#{num - 1} + 1)"
126
+ when 1 then "(#{num * 2} / 2)"
127
+ when 2 then "(#{num} * 1)"
128
+ when 3 then "0x#{num.to_s(16)}"
129
+ else match
130
+ end
131
+ else
132
+ match
133
+ end
134
+ end
135
+
136
+ code
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,27 @@
1
+ # lib/hyraft/compiler/parser.rb
2
+ module Hyraft
3
+ module Compiler
4
+ class HyraftParser
5
+ def initialize(template)
6
+ @template = template
7
+ end
8
+
9
+ def parse
10
+ {
11
+ metadata: extract('metadata','html'),
12
+ metas: extract('metas','html'),
13
+ displayer: extract('displayer','html'),
14
+ transmuter: extract('transmuter','rb'),
15
+ manifestor: extract('manifestor','js'),
16
+ styles: @template.scan(/<style\s+src="([^"]+)"\s*\/?>/).flatten
17
+ }
18
+ end
19
+
20
+ private
21
+
22
+ def extract(tag, lang)
23
+ @template[/<#{tag}\s+#{lang}>(.*?)<\/#{tag}>/m, 1]&.strip
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,217 @@
1
+ # lib/hyraft/compiler/renderer.rb
2
+ require_relative 'javascript_library'
3
+ require_relative 'html_purifier'
4
+
5
+ module Hyraft
6
+ module Compiler
7
+ class HyraftRenderer
8
+ include HtmlPurifier
9
+
10
+
11
+ def initialize(obfuscation_method: :multi_layer)
12
+ @obfuscation_method = obfuscation_method
13
+ end
14
+
15
+ def render(layout, parsed, locals = {})
16
+ locals&.each { |k, v| instance_variable_set("@#{k}", v) }
17
+
18
+ # Initialize required JS storage
19
+ @required_js = []
20
+
21
+ # Process requires from metadata FIRST
22
+ if parsed[:metadata]
23
+ metadata_content = parsed[:metadata].to_s
24
+
25
+ # Extract and process requires
26
+ requires = extract_requires(metadata_content)
27
+ requires.each { |file_path| load_required_file(file_path) }
28
+ # Remove requires from metadata for final rendering
29
+ parsed[:metadata] = remove_requires(metadata_content)
30
+ end
31
+
32
+ if parsed[:transmuter]
33
+ transmuter_code = parsed[:transmuter]
34
+ processed_transmuter = convert_html_tags(transmuter_code)
35
+ instance_eval(processed_transmuter)
36
+ end
37
+
38
+ content = render_displayer(parsed[:displayer].to_s)
39
+ styles = (parsed[:styles] || []).map { |s| "<link rel='stylesheet' href='#{s}'>" }.join
40
+
41
+ # Combine required JS with main manifestor - IN CORRECT ORDER
42
+ all_js = []
43
+ all_js += @required_js if @required_js # Required libraries first
44
+
45
+ # Add main manifestor code (no obfuscation for app code by default)
46
+ if parsed[:manifestor] && !parsed[:manifestor].strip.empty?
47
+ all_js << parsed[:manifestor]
48
+ end
49
+
50
+ # Combine all JavaScript
51
+ js_content = all_js.join("\n")
52
+ js = js_content.strip.empty? ? '' : "<script>#{js_content}</script>"
53
+
54
+ metas = render_displayer(parsed[:metadata].to_s)
55
+
56
+ result = layout.dup
57
+ inject_title(result, find_title(metas))
58
+ inject_metas(result, metas)
59
+ result.gsub!('<hyraft styles="css">', styles)
60
+ .gsub!('<hyraft content="hyraft">', content)
61
+ .gsub!('<hyraft script="javascript">', js)
62
+ result
63
+ end
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+ private
72
+
73
+ def extract_requires(metadata_content)
74
+ requires = []
75
+ metadata_content.scan(/<require\s+file="([^"]+)"\s*\/>/) do |match|
76
+ requires << match.first
77
+ end
78
+ requires
79
+ end
80
+
81
+ def remove_requires(metadata_content)
82
+ metadata_content.gsub(/<require\s+file="[^"]+"\s*\/>/, '')
83
+ end
84
+
85
+ def load_required_file(relative_path)
86
+ # Clean the library name (remove .hyr extension if present)
87
+ clean_name = relative_path.gsub(/\.hyr$/, '')
88
+
89
+ # Get the obfuscated library code
90
+ js_code = JavaScriptLibrary.get(clean_name, obfuscation_method: @obfuscation_method)
91
+
92
+ if js_code
93
+ @required_js ||= []
94
+ @required_js << js_code
95
+ else
96
+ # If not found in JavaScriptLibrary, fall back to file system search
97
+ full_path = find_required_file_anywhere(relative_path)
98
+
99
+ if full_path && File.exist?(full_path)
100
+ content = File.read(full_path)
101
+ parsed_required = parse_content(content)
102
+
103
+ # Process transmuter from required files (for Ruby code)
104
+ if parsed_required[:transmuter] && !parsed_required[:transmuter].strip.empty?
105
+ required_transmuter = convert_html_tags(parsed_required[:transmuter])
106
+ instance_eval(required_transmuter)
107
+ end
108
+
109
+ # Store manifestor content from required files (for JavaScript)
110
+ if parsed_required[:manifestor] && !parsed_required[:manifestor].strip.empty?
111
+ @required_js ||= []
112
+ # Apply obfuscation to file-based libraries too
113
+ file_js = parsed_required[:manifestor]
114
+ obfuscated_file_js = case @obfuscation_method
115
+ when :split_and_reassemble
116
+ JavaScriptObfuscator.split_and_reassemble(file_js)
117
+ when :multi_layer
118
+ JavaScriptObfuscator.multi_layer_obfuscation(file_js)
119
+ else
120
+ file_js
121
+ end
122
+ @required_js << obfuscated_file_js
123
+ end
124
+ else
125
+ puts "WARNING: Required file not found: #{relative_path}"
126
+ puts "Available built-in libraries: #{JavaScriptLibrary.available_libraries.join(', ')}"
127
+ end
128
+ end
129
+ end
130
+
131
+ def find_required_file_anywhere(relative_path)
132
+ # Remove .hyr extension if present
133
+ clean_path = relative_path.gsub(/\.hyr$/, '')
134
+
135
+ # Search pattern: adapter-intake/*/display/**/{relative_path}.hyr
136
+ search_pattern = File.join(ROOT, 'adapter-intake', '*', 'display', '**', "#{clean_path}.hyr")
137
+ matching_files = Dir.glob(search_pattern)
138
+
139
+ matching_files.first # Return first match
140
+ end
141
+
142
+ def parse_content(content)
143
+ sections = {
144
+ metadata: content[/<metadata[^>]*>(.*?)<\/metadata>/m, 1],
145
+ displayer: content[/<displayer[^>]*>(.*?)<\/displayer>/m, 1],
146
+ transmuter: content[/<transmuter[^>]*>(.*?)<\/transmuter>/m, 1],
147
+ manifestor: content[/<manifestor[^>]*>(.*?)<\/manifestor>/m, 1],
148
+ styles: content.scan(/<style\s+src="([^"]+)"\s*\/?>/).flatten
149
+ }
150
+ sections
151
+ end
152
+
153
+
154
+
155
+
156
+ def convert_html_tags(code)
157
+ code.gsub(/<html>([\s\S]*?)<\/html>/) do |match|
158
+ content = $1.strip
159
+
160
+ lines = content.split("\n")
161
+ indented_content = lines.map { |line| " #{line}" }.join("\n")
162
+ "<<~HTML\n#{indented_content}\nHTML"
163
+ end
164
+ end
165
+
166
+ def render_displayer(html)
167
+ html.gsub(/\[\.\s*(\w+)\s*\.\]/) { respond_to?($1) ? send($1).to_s : "[.#{$1}.]" }
168
+ .gsub(/<([\w-]+)(\s+[^>]*)?\s*\/>/) do |match|
169
+ tag = $1
170
+ attr_str = $2
171
+ render_component(tag, attr_str)
172
+ end
173
+ end
174
+
175
+ def render_component(tag, attr_str)
176
+ method_name = "display_#{tag.tr('-', '_')}"
177
+ attrs = parse_attributes(attr_str.to_s)
178
+
179
+ if respond_to?(method_name)
180
+ return send(method_name, **attrs)
181
+ end
182
+
183
+ attr_html = attrs.map { |k,v| "#{k}='#{v}'" }.join(' ')
184
+ "<div class='#{tag}' #{attr_html}>#{attrs[:name] || tag}</div>"
185
+ end
186
+
187
+ def parse_attributes(str)
188
+ return {} if str.nil? || str.empty?
189
+ str.scan(/(\w+)="(.*?)"/).to_h { |k,v| [k.to_sym, v] }
190
+ end
191
+
192
+ def find_title(metas)
193
+ metas[/\<title\>(.*?)\<\/title\>/m, 1] ||
194
+ (instance_variable_defined?(:@page_title) && @page_title) ||
195
+ (page_title if respond_to?(:page_title))
196
+ end
197
+
198
+ def inject_title(result, title)
199
+ return unless title
200
+ result.sub!(/<title>.*<\/title>/m, "<title>#{title}</title>") ||
201
+ result.sub!('</head>', " <title>#{title}</title>\n</head>")
202
+ end
203
+
204
+ def inject_metas(result, metas)
205
+ return if metas.empty?
206
+
207
+ if result.include?('<hyraft meta="tags">')
208
+ result.gsub!('<hyraft meta="tags">', metas)
209
+ elsif result.include?('<hyraft styles="css">')
210
+ result.sub!('<hyraft styles="css">', "#{metas}\n<hyraft styles=\"css\">")
211
+ else
212
+ result.sub!('</head>', "#{metas}\n</head>")
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hyraft
4
+ class Circuit
5
+ def initialize(ports = {})
6
+ @ports = ports
7
+ end
8
+
9
+ # Magic: Automatically route custom methods to execute
10
+ def method_missing(method, *args, &block)
11
+ if respond_to_missing?(method)
12
+ execute(operation: method, params: args.first || {})
13
+ else
14
+ super
15
+ end
16
+ end
17
+
18
+ def respond_to_missing?(method, include_private = false)
19
+ # Check if it's a public method defined in the subclass OR
20
+ # if the execute method can handle it
21
+ self.class.public_method_defined?(method) || can_handle_operation?(method)
22
+ end
23
+
24
+ def execute(input = {})
25
+ raise NotImplementedError, "Circuits must implement execute method"
26
+ end
27
+
28
+ private
29
+
30
+ def can_handle_operation?(operation)
31
+ # Subclasses should override this to return true for operations they handle
32
+ false
33
+ end
34
+ end
35
+ end