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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +231 -0
- data/exe/hyraft +5 -0
- data/lib/hyraft/boot/asset_preloader.rb +185 -0
- data/lib/hyraft/boot/preloaded_static.rb +46 -0
- data/lib/hyraft/boot/preloader.rb +206 -0
- data/lib/hyraft/cli.rb +187 -0
- data/lib/hyraft/compiler/compiler.rb +34 -0
- data/lib/hyraft/compiler/html_purifier.rb +181 -0
- data/lib/hyraft/compiler/javascript_library.rb +281 -0
- data/lib/hyraft/compiler/javascript_obfuscator.rb +141 -0
- data/lib/hyraft/compiler/parser.rb +27 -0
- data/lib/hyraft/compiler/renderer.rb +217 -0
- data/lib/hyraft/engine/circuit.rb +35 -0
- data/lib/hyraft/engine/port.rb +17 -0
- data/lib/hyraft/engine/source.rb +19 -0
- data/lib/hyraft/engine.rb +11 -0
- data/lib/hyraft/router/api_router.rb +65 -0
- data/lib/hyraft/router/web_router.rb +136 -0
- data/lib/hyraft/system_info.rb +26 -0
- data/lib/hyraft/version.rb +5 -0
- data/lib/hyraft.rb +48 -0
- data/templates/do_app/Gemfile +50 -0
- data/templates/do_app/Rakefile +88 -0
- data/templates/do_app/adapter-intake/web-app/display/pages/home/home.hyr +174 -0
- data/templates/do_app/adapter-intake/web-app/request/home_web_adapter.rb +19 -0
- data/templates/do_app/boot.rb +41 -0
- data/templates/do_app/framework/adapters/server/server_api_adapter.rb +51 -0
- data/templates/do_app/framework/adapters/server/server_web_adapter.rb +178 -0
- data/templates/do_app/framework/compiler/style_resolver.rb +33 -0
- data/templates/do_app/framework/errors/error_handler.rb +75 -0
- data/templates/do_app/framework/errors/templates/304.html +22 -0
- data/templates/do_app/framework/errors/templates/400.html +22 -0
- data/templates/do_app/framework/errors/templates/401.html +22 -0
- data/templates/do_app/framework/errors/templates/403.html +22 -0
- data/templates/do_app/framework/errors/templates/404.html +62 -0
- data/templates/do_app/framework/errors/templates/500.html +73 -0
- data/templates/do_app/framework/middleware/cors_middleware.rb +37 -0
- data/templates/do_app/infra/config/environment.rb +86 -0
- data/templates/do_app/infra/config/error_config.rb +80 -0
- data/templates/do_app/infra/config/routes/api_routes.rb +2 -0
- data/templates/do_app/infra/config/routes/web_routes.rb +10 -0
- data/templates/do_app/infra/database/sequel_connection.rb +62 -0
- data/templates/do_app/infra/gems/database.rb +7 -0
- data/templates/do_app/infra/gems/load_all.rb +4 -0
- data/templates/do_app/infra/gems/utilities.rb +1 -0
- data/templates/do_app/infra/gems/web.rb +3 -0
- data/templates/do_app/infra/server/api-server.ru +13 -0
- data/templates/do_app/infra/server/web-server.ru +32 -0
- data/templates/do_app/package.json +9 -0
- data/templates/do_app/public/favicon.ico +0 -0
- data/templates/do_app/public/icons/docs.svg +10 -0
- data/templates/do_app/public/icons/expli.svg +13 -0
- data/templates/do_app/public/icons/git-repo.svg +13 -0
- data/templates/do_app/public/icons/hexagonal-arch.svg +15 -0
- data/templates/do_app/public/icons/template-engine.svg +26 -0
- data/templates/do_app/public/images/hyr-logo.png +0 -0
- data/templates/do_app/public/images/hyr-logo.webp +0 -0
- data/templates/do_app/public/index.html +22 -0
- data/templates/do_app/public/styles/css/main.css +418 -0
- data/templates/do_app/public/styles/css/spa.css +171 -0
- data/templates/do_app/shared/helpers/pagination_helper.rb +44 -0
- data/templates/do_app/shared/helpers/response_formatter.rb +25 -0
- data/templates/do_app/test/acceptance/api/articles_api_acceptance_test.rb +43 -0
- data/templates/do_app/test/acceptance/web/articles_acceptance_test.rb +31 -0
- data/templates/do_app/test/acceptance/web/home_acceptance_test.rb +17 -0
- data/templates/do_app/test/db.rb +106 -0
- data/templates/do_app/test/integration/adapter-exhaust/data-gateway/sequel_articles_gateway_test.rb +79 -0
- data/templates/do_app/test/integration/adapter-intake/api-app/request/articles_api_adapter_test.rb +61 -0
- data/templates/do_app/test/integration/adapter-intake/web-app/request/articles_web_adapter_test.rb +20 -0
- data/templates/do_app/test/integration/adapter-intake/web-app/request/home_web_adapter_test.rb +17 -0
- data/templates/do_app/test/integration/database/migration_test.rb +35 -0
- data/templates/do_app/test/support/mock_api_adapter.rb +82 -0
- data/templates/do_app/test/support/mock_articles_gateway.rb +41 -0
- data/templates/do_app/test/support/mock_web_adapter.rb +85 -0
- data/templates/do_app/test/support/test_patches.rb +33 -0
- data/templates/do_app/test/test_helper.rb +526 -0
- data/templates/do_app/test/unit/engine/circuit/articles_circuit_test.rb +167 -0
- data/templates/do_app/test/unit/engine/port/articles_gateway_port_test.rb +12 -0
- data/templates/do_app/test/unit/engine/source/article_test.rb +37 -0
- metadata +291 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# framework/boot/preloader.rb - Complete fixed version
|
|
2
|
+
module Hyraft
|
|
3
|
+
module Preloader
|
|
4
|
+
COLORS = {
|
|
5
|
+
green: "\e[32m",
|
|
6
|
+
cyan: "\e[36m",
|
|
7
|
+
yellow: "\e[33m",
|
|
8
|
+
red: "\e[31m",
|
|
9
|
+
orange: "\e[38;5;214m",
|
|
10
|
+
blue: "\e[34m",
|
|
11
|
+
lightblue: "\e[94m",
|
|
12
|
+
reset: "\e[0m"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@preloaded_templates = {}
|
|
16
|
+
@layout_content = nil
|
|
17
|
+
@stats = {
|
|
18
|
+
total_templates: 0,
|
|
19
|
+
total_bytes: 0,
|
|
20
|
+
compiled_bytes: 0,
|
|
21
|
+
load_time: 0.0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def self.preload_templates
|
|
25
|
+
puts "#{COLORS[:green]} Hyraft Preloader: Scanning for templates...#{COLORS[:reset]}"
|
|
26
|
+
|
|
27
|
+
start_time = Time.now
|
|
28
|
+
|
|
29
|
+
# Load layout first
|
|
30
|
+
layout_file = File.join(ROOT, 'public', 'index.html')
|
|
31
|
+
@layout_content = File.read(layout_file) if File.exist?(layout_file)
|
|
32
|
+
|
|
33
|
+
templates_found = discover_templates
|
|
34
|
+
puts "Found #{templates_found.size} template files"
|
|
35
|
+
|
|
36
|
+
templates_found.each do |file_path|
|
|
37
|
+
preload_template(file_path)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@stats[:load_time] = Time.now - start_time
|
|
41
|
+
print_stats
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.discover_templates
|
|
45
|
+
Dir.glob(File.join('adapter-intake', '**', '*.hyr')).sort
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.preload_template(file_path)
|
|
49
|
+
template_key = file_path
|
|
50
|
+
.delete_prefix('adapter-intake/')
|
|
51
|
+
.sub(/\.hyr$/, '')
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
content = File.read(file_path)
|
|
55
|
+
sections = extract_sections(content)
|
|
56
|
+
|
|
57
|
+
# ONLY pre-compile templates WITHOUT transmuter code (pure HTML templates)
|
|
58
|
+
compiled_html = nil
|
|
59
|
+
compile_success = false
|
|
60
|
+
compile_error = nil
|
|
61
|
+
|
|
62
|
+
if @layout_content && sections[:transmuter].to_s.strip.empty?
|
|
63
|
+
begin
|
|
64
|
+
# Only compile templates that don't have Ruby code in transmuter
|
|
65
|
+
renderer = Hyraft::Compiler::HyraftRenderer.new
|
|
66
|
+
|
|
67
|
+
# For pure HTML templates, we can safely compile with empty locals
|
|
68
|
+
# The [.variable.] placeholders will remain as-is for runtime replacement
|
|
69
|
+
compiled_html = renderer.render(@layout_content.dup, sections, {})
|
|
70
|
+
compile_success = true
|
|
71
|
+
|
|
72
|
+
rescue => e
|
|
73
|
+
compile_error = e.message
|
|
74
|
+
end
|
|
75
|
+
else
|
|
76
|
+
compile_error = "has transmuter code" if sections[:transmuter] && !sections[:transmuter].empty?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
@preloaded_templates[template_key] = {
|
|
80
|
+
sections: sections,
|
|
81
|
+
compiled_html: compiled_html,
|
|
82
|
+
compile_success: compile_success,
|
|
83
|
+
compile_error: compile_error,
|
|
84
|
+
bytesize: content.bytesize,
|
|
85
|
+
compiled_bytes: compiled_html&.bytesize || 0,
|
|
86
|
+
mtime: File.mtime(file_path),
|
|
87
|
+
full_path: file_path
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@stats[:total_templates] += 1
|
|
91
|
+
@stats[:total_bytes] += content.bytesize
|
|
92
|
+
@stats[:compiled_bytes] += compiled_html&.bytesize || 0
|
|
93
|
+
|
|
94
|
+
if compile_success
|
|
95
|
+
puts " #{COLORS[:green]}✓#{COLORS[:reset]} #{COLORS[:lightblue]}#{template_key}#{COLORS[:reset]}#{COLORS[:orange]}.hyr#{COLORS[:reset]} (#{COLORS[:yellow]}#{(compiled_html.bytesize / 1024.0).round(2)} KB#{COLORS[:reset]})"
|
|
96
|
+
elsif compile_error == "has transmuter code"
|
|
97
|
+
puts " #{COLORS[:blue]}ℹ#{COLORS[:reset]} #{COLORS[:lightblue]}#{template_key}#{COLORS[:reset]}#{COLORS[:orange]}.hyr#{COLORS[:reset]} (#{COLORS[:cyan]}dynamic template#{COLORS[:reset]})"
|
|
98
|
+
else
|
|
99
|
+
puts " #{COLORS[:yellow]}⚠#{COLORS[:reset]} #{COLORS[:lightblue]}#{template_key}#{COLORS[:reset]}#{COLORS[:orange]}.hyr#{COLORS[:reset]} (#{COLORS[:red]}sections only#{COLORS[:reset]})"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
rescue => e
|
|
103
|
+
puts " #{COLORS[:red]}✗#{COLORS[:reset]} #{COLORS[:cyan]}#{template_key}#{COLORS[:reset]}#{COLORS[:green]}.hyr#{COLORS[:reset]} #{COLORS[:red]}Error: #{e.message}#{COLORS[:reset]}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.get_template(template_key)
|
|
108
|
+
@preloaded_templates[template_key]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.template_preloaded?(template_key)
|
|
112
|
+
@preloaded_templates.key?(template_key)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.render_template(template_key, locals = {})
|
|
116
|
+
return "<h1>Template not found: #{template_key}</h1>" unless template_preloaded?(template_key)
|
|
117
|
+
|
|
118
|
+
template_data = get_template(template_key)
|
|
119
|
+
|
|
120
|
+
# ULTRA FAST PATH: Use pre-compiled HTML (only for templates without transmuter)
|
|
121
|
+
if template_data[:compiled_html] && template_data[:compile_success]
|
|
122
|
+
return apply_locals_to_compiled(template_data[:compiled_html].dup, locals)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# FAST PATH: Use preloaded sections with HyraftRenderer (for dynamic templates)
|
|
126
|
+
layout_file = File.join(ROOT, 'public', 'index.html')
|
|
127
|
+
layout_content = File.read(layout_file) if File.exist?(layout_file)
|
|
128
|
+
|
|
129
|
+
renderer = Hyraft::Compiler::HyraftRenderer.new
|
|
130
|
+
renderer.render(layout_content, template_data[:sections], locals)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# FAST: Apply locals to pre-compiled HTML (only for placeholders, no Ruby execution)
|
|
134
|
+
def self.apply_locals_to_compiled(compiled_html, locals)
|
|
135
|
+
return compiled_html if locals.empty?
|
|
136
|
+
|
|
137
|
+
# Simple string replacement for [.variable.] placeholders
|
|
138
|
+
locals.each do |key, value|
|
|
139
|
+
placeholder = "[.#{key}.]"
|
|
140
|
+
compiled_html = compiled_html.gsub(placeholder, value.to_s)
|
|
141
|
+
end
|
|
142
|
+
compiled_html
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.stats
|
|
146
|
+
@stats.dup
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def self.extract_sections(content)
|
|
152
|
+
{
|
|
153
|
+
metadata: extract_section(content, 'metadata'),
|
|
154
|
+
displayer: extract_section(content, 'displayer'),
|
|
155
|
+
transmuter: extract_section(content, 'transmuter'),
|
|
156
|
+
manifestor: extract_section(content, 'manifestor'),
|
|
157
|
+
styles: extract_styles(content)
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def self.extract_section(content, section_name)
|
|
162
|
+
match = content.match(/<#{section_name}.*?>(.*?)<\/#{section_name}>/m)
|
|
163
|
+
match ? match[1].strip : ''
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.extract_styles(content)
|
|
167
|
+
content.scan(/<style[^>]*href="([^"]*)"/).flatten
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def self.print_stats
|
|
171
|
+
compiled_count = @preloaded_templates.count { |_, t| t[:compile_success] }
|
|
172
|
+
dynamic_count = @preloaded_templates.count { |_, t| t[:compile_error] == "has transmuter code" }
|
|
173
|
+
sections_count = @preloaded_templates.count - compiled_count - dynamic_count
|
|
174
|
+
|
|
175
|
+
puts "\n#{COLORS[:green]}Preload Statistics:#{COLORS[:reset]}"
|
|
176
|
+
puts " #{COLORS[:cyan]}Templates:#{COLORS[:reset]} #{@stats[:total_templates]} (#{compiled_count} pre-compiled, #{dynamic_count} dynamic, #{sections_count} sections only)"
|
|
177
|
+
puts " #{COLORS[:cyan]}Source Size:#{COLORS[:reset]} #{(@stats[:total_bytes] / 1024.0).round(2)} KB"
|
|
178
|
+
puts " #{COLORS[:cyan]}Compiled Size:#{COLORS[:reset]} #{(@stats[:compiled_bytes] / 1024.0).round(2)} KB"
|
|
179
|
+
puts " #{COLORS[:cyan]}Load Time:#{COLORS[:reset]} #{@stats[:load_time].round(3)}s"
|
|
180
|
+
puts " #{COLORS[:cyan]}Memory:#{COLORS[:reset]} #{memory_usage} MB"
|
|
181
|
+
|
|
182
|
+
if compiled_count > 0
|
|
183
|
+
puts " #{COLORS[:cyan]}Status:#{COLORS[:reset]} #{COLORS[:green]}PARTIALLY PRE-COMPILED#{COLORS[:reset]}\n\n"
|
|
184
|
+
else
|
|
185
|
+
puts " #{COLORS[:cyan]}Status:#{COLORS[:reset]} #{COLORS[:yellow]}SECTIONS ONLY#{COLORS[:reset]}\n\n"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.memory_usage
|
|
190
|
+
if Gem.win_platform?
|
|
191
|
+
# Windows (no `ps -o rss=`)
|
|
192
|
+
get_windows_memory
|
|
193
|
+
else
|
|
194
|
+
# Linux/Mac
|
|
195
|
+
(`ps -o rss= -p #{Process.pid}`.to_i / 1024.0).round(2)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def self.get_windows_memory
|
|
200
|
+
memory_kb = `tasklist /FI "PID eq #{Process.pid}" /FO CSV /NH`
|
|
201
|
+
.split(",")[4].to_s.gsub('"','').gsub(/[^0-9]/, '').to_i
|
|
202
|
+
(memory_kb / 1024.0).round(2)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
end
|
|
206
|
+
end
|
data/lib/hyraft/cli.rb
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# hyraft/lib/hyraft/cli.rb
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'set'
|
|
6
|
+
|
|
7
|
+
module Hyraft
|
|
8
|
+
class CLI
|
|
9
|
+
def self.start(argv)
|
|
10
|
+
new(argv).execute
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(argv)
|
|
14
|
+
@argv = argv
|
|
15
|
+
@command = argv[0]
|
|
16
|
+
@app_name = argv[1]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def execute
|
|
20
|
+
case @command
|
|
21
|
+
when "do"
|
|
22
|
+
do_application
|
|
23
|
+
when "version", "-v", "--version"
|
|
24
|
+
puts "Hyraft version #{VERSION}"
|
|
25
|
+
when nil, "help", "-h", "--help"
|
|
26
|
+
show_help
|
|
27
|
+
else
|
|
28
|
+
puts "Unknown command: #{@command}"
|
|
29
|
+
show_help
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def show_help
|
|
36
|
+
puts "Hyraft - Hexagonal Architecture Framework"
|
|
37
|
+
puts "Commands:"
|
|
38
|
+
puts " set APP_NAME Create a new Hyraft application"
|
|
39
|
+
puts " version Show version information"
|
|
40
|
+
puts " help Show this help message"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def do_application
|
|
44
|
+
return puts "Error: Need app name" unless @app_name
|
|
45
|
+
|
|
46
|
+
template_dir = find_template_dir
|
|
47
|
+
unless template_dir && Dir.exist?(template_dir)
|
|
48
|
+
puts "Error: Could not find template directory"
|
|
49
|
+
exit 1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
target_dir = File.expand_path(@app_name)
|
|
53
|
+
|
|
54
|
+
if Dir.exist?(target_dir)
|
|
55
|
+
puts "Error: Directory '#{@app_name}' already exists"
|
|
56
|
+
exit 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
puts "Creating Hyraft application: #{@app_name}"
|
|
60
|
+
copy_template_structure(template_dir, target_dir)
|
|
61
|
+
ensure_hexagonal_structure(target_dir)
|
|
62
|
+
create_env_file(target_dir)
|
|
63
|
+
puts "✅ Hyraft app '#{@app_name}' created successfully!"
|
|
64
|
+
puts " cd #{@app_name}"
|
|
65
|
+
puts " bundle install && npm install"
|
|
66
|
+
puts " hyraft-server thin"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def find_template_dir
|
|
70
|
+
locations = [
|
|
71
|
+
-> {
|
|
72
|
+
gem_spec = Gem::Specification.find_by_name('hyraft')
|
|
73
|
+
File.join(gem_spec.gem_dir, 'templates', 'do_app')
|
|
74
|
+
},
|
|
75
|
+
-> { File.expand_path('../../templates/do_app', __dir__) },
|
|
76
|
+
-> { File.expand_path('templates/do_app', Dir.pwd) }
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
locations.each do |location_proc|
|
|
80
|
+
begin
|
|
81
|
+
dir = location_proc.call
|
|
82
|
+
return dir if Dir.exist?(dir)
|
|
83
|
+
rescue Gem::LoadError, StandardError
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def copy_template_structure(source, destination)
|
|
92
|
+
FileUtils.mkdir_p(destination)
|
|
93
|
+
copy_entire_structure(source, destination)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def copy_entire_structure(source, destination)
|
|
97
|
+
require 'find'
|
|
98
|
+
copied_paths = Set.new
|
|
99
|
+
|
|
100
|
+
Find.find(source) do |path|
|
|
101
|
+
next if path == source
|
|
102
|
+
relative_path = path.sub("#{source}/", '')
|
|
103
|
+
target_path = File.join(destination, relative_path)
|
|
104
|
+
next if copied_paths.include?(relative_path)
|
|
105
|
+
copied_paths.add(relative_path)
|
|
106
|
+
|
|
107
|
+
if File.directory?(path)
|
|
108
|
+
FileUtils.mkdir_p(target_path)
|
|
109
|
+
puts " create #{target_path}/" unless Dir.empty?(path)
|
|
110
|
+
else
|
|
111
|
+
FileUtils.mkdir_p(File.dirname(target_path))
|
|
112
|
+
FileUtils.cp(path, target_path)
|
|
113
|
+
puts " create #{target_path}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def ensure_hexagonal_structure(app_dir)
|
|
119
|
+
hex_dirs = [
|
|
120
|
+
'engine/circuit',
|
|
121
|
+
'engine/port',
|
|
122
|
+
'engine/source',
|
|
123
|
+
'adapter-intake/api-app/request',
|
|
124
|
+
'adapter-intake/web-app/request',
|
|
125
|
+
'adapter-exhaust/data-gateway/',
|
|
126
|
+
'infra/database/migrations',
|
|
127
|
+
'public/uploads',
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
puts "Ensuring hexagonal architecture structure..."
|
|
131
|
+
|
|
132
|
+
hex_dirs.each do |dir|
|
|
133
|
+
full_path = File.join(app_dir, dir)
|
|
134
|
+
unless Dir.exist?(full_path)
|
|
135
|
+
FileUtils.mkdir_p(full_path)
|
|
136
|
+
puts " create #{full_path}/" if Dir.empty?(full_path)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def create_env_file(app_dir)
|
|
142
|
+
env_path = File.join(app_dir, 'env.yml')
|
|
143
|
+
|
|
144
|
+
env_content = <<~YAML
|
|
145
|
+
# env.yml
|
|
146
|
+
|
|
147
|
+
# Application Settings
|
|
148
|
+
|
|
149
|
+
APP_NAME: myapp
|
|
150
|
+
SERVER_PORT: 1091
|
|
151
|
+
SERVER_PORT_API: 1092
|
|
152
|
+
|
|
153
|
+
development:
|
|
154
|
+
DB_CONNECTION: mysql
|
|
155
|
+
DB_HOST: localhost
|
|
156
|
+
DB_PORT: 3306
|
|
157
|
+
DB_DATABASE: myapp_development
|
|
158
|
+
DB_USERNAME: root
|
|
159
|
+
DB_PASSWORD:
|
|
160
|
+
DB_CHARSET: utf8mb4
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
test:
|
|
164
|
+
DB_CONNECTION: mysql
|
|
165
|
+
DB_HOST: localhost
|
|
166
|
+
DB_PORT: 3306
|
|
167
|
+
DB_DATABASE: myapp_test
|
|
168
|
+
DB_USERNAME: root
|
|
169
|
+
DB_PASSWORD:
|
|
170
|
+
DB_CHARSET: utf8mb4
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
production:
|
|
174
|
+
DB_CONNECTION: mysql
|
|
175
|
+
DB_HOST: localhost
|
|
176
|
+
DB_PORT: 3306
|
|
177
|
+
DB_DATABASE: myapp_production
|
|
178
|
+
DB_USERNAME:
|
|
179
|
+
DB_PASSWORD:
|
|
180
|
+
DB_CHARSET: utf8mb4
|
|
181
|
+
YAML
|
|
182
|
+
|
|
183
|
+
File.write(env_path, env_content)
|
|
184
|
+
puts " create #{env_path}"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# lib/hyraft/compiler/compiler.rb
|
|
2
|
+
require_relative 'renderer'
|
|
3
|
+
|
|
4
|
+
module Hyraft
|
|
5
|
+
module Compiler
|
|
6
|
+
class HyraftCompiler
|
|
7
|
+
def initialize(layout_file)
|
|
8
|
+
@layout_file = layout_file
|
|
9
|
+
@renderer = HyraftRenderer.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# compile(view_file, data = {}) -> string
|
|
13
|
+
def compile(view_file, data = {})
|
|
14
|
+
layout_content = File.read(@layout_file)
|
|
15
|
+
parsed = parse_hyraft(view_file)
|
|
16
|
+
@renderer.render(layout_content, parsed, data)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def parse_hyraft(file)
|
|
22
|
+
content = File.read(file)
|
|
23
|
+
{
|
|
24
|
+
displayer: content[/\<displayer html\>(.*?)\<\/displayer\>/m, 1],
|
|
25
|
+
transmuter: content[/\<transmuter rb\>(.*?)\<\/transmuter\>/m, 1],
|
|
26
|
+
styles: content.scan(/<style\s+src="([^"]+)"\s*\/?>/).flatten,
|
|
27
|
+
manifestor: content[/\<manifestor js\>(.*?)\<\/manifestor\>/m, 1],
|
|
28
|
+
metadata: content[/\<metadata html\>(.*?)\<\/metadata\>/m, 1],
|
|
29
|
+
metas: content[/\<metas html\>(.*?)\<\/metas\>/m, 1]
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# lib/hyraft/compiler/html_purifier.rb
|
|
2
|
+
module Hyraft
|
|
3
|
+
module Compiler
|
|
4
|
+
module HtmlPurifier
|
|
5
|
+
# Purifies HTML content by escaping dangerous characters to prevent XSS attacks
|
|
6
|
+
# Usage: purify_html(user_input) - makes any string safe for HTML output
|
|
7
|
+
def purify_html(unsafe)
|
|
8
|
+
return '' unless unsafe
|
|
9
|
+
unsafe.to_s
|
|
10
|
+
.gsub(/&/, "&")
|
|
11
|
+
.gsub(/</, "<")
|
|
12
|
+
.gsub(/>/, ">")
|
|
13
|
+
.gsub(/"/, """)
|
|
14
|
+
.gsub(/'/, "'")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Alias for purify_html for different use cases
|
|
18
|
+
def escape_html(unsafe)
|
|
19
|
+
purify_html(unsafe)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Additional HTML safety methods
|
|
23
|
+
def purify_html_attribute(unsafe)
|
|
24
|
+
return '' unless unsafe
|
|
25
|
+
unsafe.to_s
|
|
26
|
+
.gsub(/&/, "&")
|
|
27
|
+
.gsub(/</, "<")
|
|
28
|
+
.gsub(/>/, ">")
|
|
29
|
+
.gsub(/"/, """)
|
|
30
|
+
.gsub(/'/, "'")
|
|
31
|
+
.gsub(/\//, "/")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def purify_javascript(unsafe)
|
|
35
|
+
return '' unless unsafe
|
|
36
|
+
unsafe.to_s
|
|
37
|
+
.gsub(/\\/, "\\\\")
|
|
38
|
+
.gsub(/'/, "\\'")
|
|
39
|
+
.gsub(/"/, "\\\"")
|
|
40
|
+
.gsub(/\n/, "\\n")
|
|
41
|
+
.gsub(/\r/, "\\r")
|
|
42
|
+
.gsub(/</, "\\u003C")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Highlight text with search term while maintaining HTML safety
|
|
46
|
+
# @param text [String] The text to search within
|
|
47
|
+
# @param term [String] The search term to highlight
|
|
48
|
+
# @param highlight_tag [String] HTML tag to use for highlighting (default: 'mark')
|
|
49
|
+
# @param css_class [String] CSS class to add to highlight tag (optional)
|
|
50
|
+
# @return [String] Text with highlighted terms
|
|
51
|
+
def highlight_text(text, term, highlight_tag: 'mark', css_class: nil)
|
|
52
|
+
return text if term.empty? || text.empty?
|
|
53
|
+
|
|
54
|
+
# First purify the text to make it safe
|
|
55
|
+
safe_text = purify_html(text)
|
|
56
|
+
safe_term = purify_html(term)
|
|
57
|
+
|
|
58
|
+
# Escape the term for regex
|
|
59
|
+
escaped_term = Regexp.escape(safe_term)
|
|
60
|
+
|
|
61
|
+
# Build the opening tag with optional CSS class
|
|
62
|
+
opening_tag = if css_class
|
|
63
|
+
"<#{highlight_tag} class=\"#{purify_html_attribute(css_class)}\">"
|
|
64
|
+
else
|
|
65
|
+
"<#{highlight_tag}>"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
closing_tag = "</#{highlight_tag}>"
|
|
69
|
+
|
|
70
|
+
# Highlight the term
|
|
71
|
+
safe_text.gsub(/(#{escaped_term})/i) do |match|
|
|
72
|
+
"#{opening_tag}#{match}#{closing_tag}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Alternative highlighting with span and default Bootstrap classes
|
|
77
|
+
# @param text [String] The text to search within
|
|
78
|
+
# @param term [String] The search term to highlight
|
|
79
|
+
# @param highlight_class [String] CSS class for highlighting (default: 'bg-warning text-dark px-1 rounded')
|
|
80
|
+
# @return [String] Text with highlighted terms
|
|
81
|
+
def highlight_with_class(text, term, highlight_class: 'bg-warning text-dark px-1 rounded')
|
|
82
|
+
highlight_text(text, term, highlight_tag: 'span', css_class: highlight_class)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ========== HTML HELPER METHODS ==========
|
|
86
|
+
|
|
87
|
+
# Truncate text to specified length
|
|
88
|
+
# @param text [String] The text to truncate
|
|
89
|
+
# @param length [Integer] Maximum length before truncation (default: 100)
|
|
90
|
+
# @param suffix [String] Suffix to add when truncated (default: '...')
|
|
91
|
+
# @param preserve_words [Boolean] Whether to preserve whole words (default: false)
|
|
92
|
+
# @return [String] Truncated text
|
|
93
|
+
def truncate_text(text, length: 100, suffix: '...', preserve_words: false)
|
|
94
|
+
return '' unless text
|
|
95
|
+
text = text.to_s
|
|
96
|
+
|
|
97
|
+
if text.length <= length
|
|
98
|
+
return text
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if preserve_words
|
|
102
|
+
# Try to break at a word boundary
|
|
103
|
+
truncated = text[0, length]
|
|
104
|
+
last_space = truncated.rindex(/\s/)
|
|
105
|
+
|
|
106
|
+
if last_space && last_space > length * 0.8 # Only break if we have a reasonable word boundary
|
|
107
|
+
truncated = text[0, last_space]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
truncated + suffix
|
|
111
|
+
else
|
|
112
|
+
text[0, length] + suffix
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Create a safe HTML link
|
|
117
|
+
# @param text [String] The link text
|
|
118
|
+
# @param url [String] The link URL
|
|
119
|
+
# @param attrs [Hash] Additional HTML attributes (e.g., {class: 'btn', target: '_blank'})
|
|
120
|
+
# @return [String] Safe HTML <a> tag
|
|
121
|
+
def link_to(text, url, **attrs)
|
|
122
|
+
# Build attributes string
|
|
123
|
+
attr_string = attrs.map do |key, value|
|
|
124
|
+
"#{key}=\"#{purify_html_attribute(value)}\""
|
|
125
|
+
end.join(' ')
|
|
126
|
+
|
|
127
|
+
attr_string = " #{attr_string}" unless attr_string.empty?
|
|
128
|
+
|
|
129
|
+
"<a href=\"#{purify_html_attribute(url)}\"#{attr_string}>#{purify_html(text)}</a>"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Format datetime object
|
|
133
|
+
# @param datetime [Time, DateTime, String] The datetime to format
|
|
134
|
+
# @param format [String] strftime format string (default: '%Y-%m-%d %H:%M')
|
|
135
|
+
# @param default [String] Default value if datetime is nil (default: '')
|
|
136
|
+
# @return [String] Formatted datetime
|
|
137
|
+
def format_datetime(datetime, format: '%Y-%m-%d %H:%M', default: '')
|
|
138
|
+
return default unless datetime
|
|
139
|
+
|
|
140
|
+
if datetime.is_a?(String)
|
|
141
|
+
# Try to parse the string
|
|
142
|
+
begin
|
|
143
|
+
datetime = Time.parse(datetime)
|
|
144
|
+
rescue
|
|
145
|
+
return default
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
if datetime.respond_to?(:strftime)
|
|
150
|
+
datetime.strftime(format)
|
|
151
|
+
else
|
|
152
|
+
default
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Format a number with commas (e.g., 1000 -> "1,000")
|
|
157
|
+
# @param number [Numeric, String] The number to format
|
|
158
|
+
# @param delimiter [String] Thousands delimiter (default: ',')
|
|
159
|
+
# @param separator [String] Decimal separator (default: '.')
|
|
160
|
+
# @return [String] Formatted number
|
|
161
|
+
def number_with_delimiter(number, delimiter: ',', separator: '.')
|
|
162
|
+
return '' unless number
|
|
163
|
+
|
|
164
|
+
parts = number.to_s.split('.')
|
|
165
|
+
parts[0] = parts[0].reverse.scan(/\d{1,3}/).join(delimiter).reverse
|
|
166
|
+
|
|
167
|
+
parts.join(separator)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Pluralize a word based on count
|
|
171
|
+
# @param count [Integer] The count
|
|
172
|
+
# @param singular [String] Singular form of the word
|
|
173
|
+
# @param plural [String] Plural form of the word (optional, will add 's' by default)
|
|
174
|
+
# @return [String] Pluralized phrase
|
|
175
|
+
def pluralize(count, singular, plural = nil)
|
|
176
|
+
plural ||= singular + 's'
|
|
177
|
+
count == 1 ? "1 #{singular}" : "#{count} #{plural}"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|