'.html_safe
48 |
49 | render partial: 'shared/navbar'
50 | ```
51 |
52 | ### (Optional) Nicely Format the HTML for source inspection
53 |
54 | File: `config/environments/development.rb` or `config/environments/test.rb`
55 | ```ruby
56 | config.middleware.use Ruby2html::HtmlBeautifierMiddleware
57 | ```
58 |
59 | #### Or use your current .erb views
60 |
61 | ### In your ApplicationController
62 |
63 | File: `app/controllers/application_controller.rb`
64 |
65 | ```ruby
66 | # frozen_string_literal: true
67 |
68 | class ApplicationController < ActionController::Base
69 | include Ruby2html::RailsHelper # to access the <%= html %> helper
70 | end
71 | ```
72 |
73 | File: `app/views/your_view.html.erb`
74 |
75 | Replace your ERB with beautiful Ruby code:
76 |
77 | ```erb
78 | <%=
79 | html(self) do
80 | h1 "Welcome to Ruby2html! 🎉", class: 'main-title', 'data-controller': 'welcome'
81 | div id: 'content', class: 'container' do
82 | link_to 'Home Sweet Home 🏠', root_path, class: 'btn btn-primary', 'data-turbo': false
83 | end
84 |
85 | @items.each do |item|
86 | h2 class: 'item-title', id: "item-#{item[:id]}" do
87 | item.title
88 | end
89 | p class: 'item-description' do
90 | item.description
91 | end
92 | end
93 |
94 | plain "
Inline html
".html_safe
95 |
96 | render partial: 'shared/navbar'
97 | end
98 | %>
99 | ```
100 |
101 | ### Benchmark
102 |
103 | ```bash
104 | ruby 3.3.4 (2024-07-09 revision be1089c8ec) +YJIT [x86_64-linux]
105 | Warming up --------------------------------------
106 | GET /benchmark/html (ERB)
107 | 40.000 i/100ms
108 | GET /benchmark/ruby (Ruby2html templates .html.rb)
109 | 12.000 i/100ms
110 | GET /benchmark/ruby (Ruby2html + view components)
111 | 12.000 i/100ms
112 | GET /benchmark/slim (Slim)
113 | 46.000 i/100ms
114 | GET /benchmark/phlex (Phlex)
115 | 34.000 i/100ms
116 | Calculating -------------------------------------
117 | GET /benchmark/html (ERB)
118 | 414.030 (± 2.4%) i/s - 24.840k in 60.032818s
119 | GET /benchmark/ruby (Ruby2html templates .html.rb)
120 | 124.973 (± 3.2%) i/s - 7.500k in 60.071485s
121 | GET /benchmark/ruby (Ruby2html + view components)
122 | 123.211 (± 4.1%) i/s - 7.380k in 60.000731s
123 | GET /benchmark/slim (Slim)
124 | 431.525 (± 9.0%) i/s - 25.668k in 60.103492s
125 | GET /benchmark/phlex (Phlex)
126 | 328.925 (± 7.0%) i/s - 19.618k in 60.019961s
127 |
128 | Comparison:
129 | GET /benchmark/slim (Slim): 431.5 i/s
130 | GET /benchmark/html (ERB): 414.0 i/s - same-ish: difference falls within error
131 | GET /benchmark/phlex (Phlex): 328.9 i/s - 1.31x slower
132 | GET /benchmark/ruby (Ruby2html templates .html.rb): 125.0 i/s - 3.45x slower
133 | GET /benchmark/ruby (Ruby2html + view components): 123.2 i/s - 3.50x slower
134 | ```
135 |
136 | ### With ViewComponents
137 |
138 | Ruby2html seamlessly integrates with ViewComponents, offering flexibility in how you define your component's HTML structure. You can use the `call` method with Ruby2html syntax, or stick with traditional `.erb` template files.
139 |
140 | File: `app/components/application_component.rb`
141 |
142 | ```ruby
143 | # frozen_string_literal: true
144 |
145 | class ApplicationComponent < ViewComponent::Base
146 | include Ruby2html::ComponentHelper
147 | end
148 | ```
149 |
150 | #### Option 1: Using `call` method with Ruby2html
151 |
152 | File: `app/components/greeting_component.rb`
153 |
154 | ```ruby
155 | # frozen_string_literal: true
156 |
157 | class GreetingComponent < ApplicationComponent
158 | def initialize(name)
159 | @name = name
160 | end
161 |
162 | def call
163 | html do
164 | h1 class: 'greeting', 'data-user': @name do
165 | "Hello, #{@name}! 👋"
166 | end
167 | p class: 'welcome-message' do
168 | 'Welcome to the wonderful world of Ruby2html!'
169 | end
170 | end
171 | end
172 | end
173 | ```
174 |
175 | #### Option 2: Using traditional ERB template
176 |
177 | File: `app/components/farewell_component.rb`
178 |
179 | ```ruby
180 | # frozen_string_literal: true
181 |
182 | class FarewellComponent < ApplicationComponent
183 | def initialize(name)
184 | @name = name
185 | end
186 | end
187 | ```
188 |
189 | File: `app/components/farewell_component.html.rb`
190 |
191 | ```rb
192 | div class: 'farewell' do
193 | h1 class: 'farewell-message' do
194 | "Goodbye, #{@name}! 👋"
195 | end
196 | p class: 'farewell-text' do
197 | 'We hope to see you again soon!'
198 | end
199 | end
200 | ```
201 |
202 | This flexibility allows you to:
203 | - Use Ruby2html syntax for new components or when refactoring existing ones
204 | - Keep using familiar ERB templates where preferred
205 | - Mix and match approaches within your application as needed
206 |
207 | ### More Component Examples
208 |
209 | File: `app/components/first_component.rb`
210 |
211 | ```ruby
212 | # frozen_string_literal: true
213 |
214 | class FirstComponent < ApplicationComponent
215 | def initialize
216 | @item = 'Hello, World!'
217 | end
218 |
219 | def call
220 | html do
221 | h1 id: 'first-component-title' do
222 | 'first component'
223 | end
224 | div class: 'content-wrapper' do
225 | h2 'A subheading'
226 | end
227 | p class: 'greeting-text', 'data-testid': 'greeting' do
228 | @item
229 | end
230 | end
231 | end
232 | end
233 | ```
234 |
235 | File: `app/components/second_component.rb`
236 |
237 | ```ruby
238 | # frozen_string_literal: true
239 |
240 | class SecondComponent < ApplicationComponent
241 | def call
242 | html do
243 | h1 class: 'my-class', id: 'second-component-title', 'data-controller': 'second' do
244 | 'second component'
245 | end
246 | link_to 'Home', root_path, class: 'nav-link', 'data-turbo-frame': false
247 | end
248 | end
249 | end
250 | ```
251 |
252 | ## Without Rails
253 | ```ruby
254 | renderer = Ruby2html::Render.new(nil) do # context by default is nil, you can use self or any other object
255 | html do
256 | head do
257 | title 'Ruby2html Example'
258 | end
259 | body do
260 | h1 'Hello, World!'
261 | end
262 | end
263 | end
264 |
265 | puts renderer.render # => "Ruby2html Example
Hello, World!
"
266 | ```
267 |
268 | ## 🐢 Gradual Adoption
269 |
270 | One of the best features of Ruby2html is that you don't need to rewrite all your views at once! You can adopt it gradually, mixing Ruby2html with your existing ERB templates. This allows for a smooth transition at your own pace.
271 |
272 | ### Mixed usage example
273 |
274 | File: `app/views/your_mixed_view.html.erb`
275 |
276 | ```erb
277 |
Welcome to our gradually evolving page!
278 |
279 | <%= render partial: 'legacy_erb_partial' %>
280 |
281 | <%=
282 | html(self) do
283 | div class: 'ruby2html-section' do
284 | h2 "This section is powered by Ruby2html!"
285 | p "Isn't it beautiful? 😍"
286 | end
287 | end
288 | %>
289 |
290 | <%= render ModernComponent.new %>
291 |
292 |
295 | ```
296 |
297 | In this example, you can see how Ruby2html seamlessly integrates with existing ERB code. This approach allows you to:
298 |
299 | - Keep your existing ERB templates and partials
300 | - Gradually introduce Ruby2html in specific sections
301 | - Use Ruby2html in new components while maintaining older ones
302 | - Refactor your views at your own pace
303 |
304 | Remember, there's no rush! You can keep your `.erb` files and Ruby2html code side by side until you're ready to fully transition. This flexibility ensures that adopting Ruby2html won't disrupt your existing workflow or require a massive rewrite of your application. 🌈
305 |
306 | ## 🛠 Development
307 |
308 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
309 |
310 | ## 🤝 Contributing
311 |
312 | Bug reports and pull requests are welcome on GitHub at https://github.com/sebyx07/ruby2html. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/sebyx07/ruby2html/blob/master/CODE_OF_CONDUCT.md).
313 |
314 | ## 📜 License
315 |
316 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
317 |
318 | ## 🌈 Code of Conduct
319 |
320 | Everyone interacting in the Ruby2html project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/sebyx07/ruby2html/blob/master/CODE_OF_CONDUCT.md).
321 |
322 | ## 🌟 Features
323 |
324 | - Write views in pure Ruby 💎
325 | - Seamless Rails integration 🛤️
326 | - ViewComponent support with flexible template options 🧩
327 | - Automatic HTML beautification 💅
328 | - Easy addition of custom attributes and data attributes 🏷️
329 | - Gradual adoption - mix with existing ERB templates 🐢
330 | - Improved readability and maintainability 📚
331 | - Full access to Ruby's power in your views 💪
332 |
333 | Start writing your views in Ruby today and experience the magic of Ruby2html! ✨🔮
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'config/application'
4 | require 'rake/extensiontask'
5 | require 'rake/testtask'
6 |
7 | # Load Rails tasks
8 | Rails.application.load_tasks
9 |
10 | # Add C extension compilation task
11 | Rake::ExtensionTask.new('ruby2html') do |ext|
12 | ext.lib_dir = 'lib/ruby2html'
13 | ext.ext_dir = 'ext/ruby2html'
14 | end
15 |
16 | task build: :compile
17 |
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebyx07/ruby2html/8b26ce02c33a402c38255067536ebf1e1e7a598c/app/assets/images/.keep
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
6 | * vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Channel < ActionCable::Channel::Base
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Connection < ActionCable::Connection::Base
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/components/application_component.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationComponent < ViewComponent::Base
4 | include Ruby2html::ComponentHelper
5 | end
6 |
--------------------------------------------------------------------------------
/app/components/benchmark_component.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class BenchmarkComponent < ApplicationComponent
4 | def initialize(data)
5 | @complex_data = data
6 | end
7 |
8 | def call
9 | html do
10 | h1 'Benchmark: Ruby2html'
11 |
12 | h2 'User Statistics'
13 | ul do
14 | li { plain "Total Users: #{@complex_data[:stats][:total_users]}" }
15 | li { plain "Average Orders per User: #{@complex_data[:stats][:average_orders_per_user]}" }
16 | li { plain "Most Expensive Item: #{@complex_data[:stats][:most_expensive_item]}" }
17 | li { plain "Most Popular Country: #{@complex_data[:stats][:most_popular_country]}" }
18 | end
19 |
20 | h2 'User List'
21 | @complex_data[:users].each do |user|
22 | div class: 'user-card' do
23 | h3 user[:name]
24 | p { plain "Email: #{user[:email]}" }
25 | p { plain "Address: #{user[:address][:street]}, #{user[:address][:city]}, #{user[:address][:country]}" }
26 |
27 | h4 'Orders'
28 | user[:orders].each do |order|
29 | div class: 'order' do
30 | p { plain "Order ID: #{order[:id]}" }
31 | p { plain "Total: $#{order[:total]}" }
32 | ul do
33 | order[:items].each do |item|
34 | li { plain "#{item[:name]} - $#{item[:price]} (Quantity: #{item[:quantity]})" }
35 | end
36 | end
37 | end
38 | end
39 | end
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/app/components/first_component.html.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | h1 'first component'
4 |
5 | div do
6 | h2 'A subheading'
7 | end
8 |
9 | another_div do
10 | h1 'nested!!!!'
11 | end
12 |
13 | p @item
14 |
--------------------------------------------------------------------------------
/app/components/first_component.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class FirstComponent < ApplicationComponent
4 | def initialize
5 | @item = 'Hello, World!'
6 | end
7 |
8 | def another_div
9 | div do
10 | "Item value: #{@item}"
11 | end
12 |
13 | div class: 'another' do
14 | h2 'Another subheading from component'
15 | end
16 |
17 | h1 'Yet Another heading from component'
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/components/second_component.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class SecondComponent < ApplicationComponent
4 | def call
5 | html do
6 | h1 class: 'my-class' do
7 | plain 'Second Component'
8 | end
9 | link_to 'Home', root_path
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/components/third_component.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ThirdComponent < ApplicationComponent
4 | def initialize
5 | @item = 'Hello, World!'
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/components/third_component/third_component.html.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | h1 'Third component'
4 | div do
5 | "Item value: #{@item}"
6 | end
7 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationController < ActionController::Base
4 | # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
5 | allow_browser versions: :modern
6 | include Ruby2html::RailsHelper
7 | end
8 |
--------------------------------------------------------------------------------
/app/controllers/benchmark_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class BenchmarkController < ApplicationController
4 | def normal_html
5 | @complex_data = COMPLEX_DATA
6 | end
7 |
8 | def ruby_2html
9 | @complex_data = COMPLEX_DATA
10 | end
11 |
12 | def slim_html
13 | @complex_data = COMPLEX_DATA
14 | end
15 |
16 | def phlex
17 | render PhlexView.new(COMPLEX_DATA)
18 | end
19 |
20 | def compruby
21 | render BenchmarkComponent.new(COMPLEX_DATA)
22 | end
23 |
24 | COMPLEX_DATA = {
25 | users: 50.times.map do |i|
26 | {
27 | id: i + 1,
28 | name: Faker::Name.name,
29 | email: Faker::Internet.email,
30 | address: {
31 | street: Faker::Address.street_address,
32 | city: Faker::Address.city,
33 | country: Faker::Address.country
34 | },
35 | orders: rand(1..5).times.map do
36 | {
37 | id: Faker::Alphanumeric.alphanumeric(number: 10),
38 | total: Faker::Commerce.price(range: 10..1000.0),
39 | items: rand(1..10).times.map do
40 | {
41 | name: Faker::Commerce.product_name,
42 | price: Faker::Commerce.price(range: 5..500.0),
43 | quantity: rand(1..5)
44 | }
45 | end
46 | }
47 | end
48 | }
49 | end,
50 | stats: {
51 | total_users: 50,
52 | average_orders_per_user: rand(1.0..5.0).round(2),
53 | most_expensive_item: Faker::Commerce.product_name,
54 | most_popular_country: Faker::Address.country
55 | }
56 | }.freeze
57 | end
58 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebyx07/ruby2html/8b26ce02c33a402c38255067536ebf1e1e7a598c/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/app/controllers/home_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class HomeController < ApplicationController
4 | def index
5 | @items = [
6 | {
7 | title: 'Item 1',
8 | description: 'Description 1'
9 | }
10 | ]
11 | end
12 |
13 | def rb_files
14 | @value = 'value'
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationHelper
4 | end
5 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationJob < ActiveJob::Base
4 | # Automatically retry jobs that encountered a deadlock
5 | # retry_on ActiveRecord::Deadlocked
6 |
7 | # Most jobs are safe to ignore if the underlying records are no longer available
8 | # discard_on ActiveJob::DeserializationError
9 | end
10 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationMailer < ActionMailer::Base
4 | default from: 'from@example.com'
5 | layout 'mailer'
6 | end
7 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationRecord < ActiveRecord::Base
4 | primary_abstract_class
5 | end
6 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebyx07/ruby2html/8b26ce02c33a402c38255067536ebf1e1e7a598c/app/models/concerns/.keep
--------------------------------------------------------------------------------
/app/phlex/phlex_view.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PhlexView < Phlex::HTML
4 | def initialize(complex_data)
5 | @complex_data = complex_data
6 | end
7 |
8 | def view_template
9 | h1 { 'Benchmark: PhlexView' }
10 |
11 | h2 { 'User Statistics' }
12 | ul do
13 | li { "Total Users: #{@complex_data[:stats][:total_users]}" }
14 | li { "Average Orders per User: #{@complex_data[:stats][:average_orders_per_user]}" }
15 | li { "Most Expensive Item: #{@complex_data[:stats][:most_expensive_item]}" }
16 | li { "Most Popular Country: #{@complex_data[:stats][:most_popular_country]}" }
17 | end
18 |
19 | h2 { 'User List' }
20 | @complex_data[:users].each do |user|
21 | div class: 'user-card' do
22 | h3 { user[:name] }
23 | p { "Email: #{user[:email]}" }
24 | p { "Address: #{user[:address][:street]}, #{user[:address][:city]}, #{user[:address][:country]}" }
25 |
26 | h4 { 'Orders' }
27 | user[:orders].each do |order|
28 | div class: 'order' do
29 | p { "Order ID: #{order[:id]}" }
30 | p { "Total: $#{order[:total]}" }
31 | ul do
32 | order[:items].each do |item|
33 | li { "#{item[:name]} - $#{item[:price]} (Quantity: #{item[:quantity]})" }
34 | end
35 | end
36 | end
37 | end
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/app/views/benchmark/normal_html.html.erb:
--------------------------------------------------------------------------------
1 |
Benchmark: Normal HTML (ERB)
2 |
3 |
User Statistics
4 |
5 |
Total Users: <%= @complex_data[:stats][:total_users] %>
6 |
Average Orders per User: <%= @complex_data[:stats][:average_orders_per_user] %>
7 |
Most Expensive Item: <%= @complex_data[:stats][:most_expensive_item] %>
8 |
Most Popular Country: <%= @complex_data[:stats][:most_popular_country] %>
9 |
10 |
11 |
User List
12 | <% @complex_data[:users].each do |user| %>
13 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebyx07/ruby2html/8b26ce02c33a402c38255067536ebf1e1e7a598c/public/icon.png
--------------------------------------------------------------------------------
/public/icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/ruby2html.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'lib/gem/ruby2html/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = 'ruby2html'
7 | spec.version = Ruby2html::VERSION
8 | spec.authors = ['sebi']
9 | spec.email = ['gore.sebyx@yahoo.com']
10 |
11 | spec.summary = 'Transform Ruby code into beautiful, structured HTML with C-optimized performance'
12 | spec.description = 'Ruby2HTML empowers developers to write view logic in pure Ruby, ' \
13 | 'seamlessly converting it into clean, well-formatted HTML. ' \
14 | 'Enhance your templating workflow, improve readability, and ' \
15 | 'leverage the full power of Ruby in your views. ' \
16 | 'Features include Rails integration, custom component support, ' \
17 | 'automatic HTML beautification, and C-optimized rendering performance.'
18 | spec.homepage = 'https://github.com/sebyx07/ruby2html'
19 | spec.license = 'MIT'
20 | spec.required_ruby_version = '>= 3.0.0'
21 |
22 | spec.metadata['homepage_uri'] = spec.homepage
23 | spec.metadata['source_code_uri'] = spec.homepage
24 |
25 | # Include C extension
26 | spec.extensions = ['ext/ruby2html/extconf.rb']
27 |
28 | # Include both lib and ext directories
29 | spec.files = Dir.glob('{lib,ext}/{**/*,*}') +
30 | ['README.md', File.basename(__FILE__)]
31 |
32 | # Set require paths for both the gem and extension
33 | spec.require_paths = %w[lib/gem lib]
34 |
35 | # Runtime dependencies
36 | spec.add_dependency 'htmlbeautifier', '>= 1.4'
37 |
38 | # Development dependencies
39 | spec.add_development_dependency 'rake', '~> 13.0'
40 | spec.add_development_dependency 'rake-compiler', '~> 1.2'
41 | spec.add_development_dependency 'minitest', '~> 5.14'
42 | spec.add_development_dependency 'benchmark-ips', '~> 2.10'
43 | end
44 |
--------------------------------------------------------------------------------
/spec/benchmarks/requests_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe 'HTML vs Ruby2html Benchmark', type: :request do
6 | it 'compares requests per second for normal HTML and Ruby2html', skip: ENV['CI'].present? do
7 | Benchmark.ips do |x|
8 | # Configure the benchmark
9 | x.config(time: 60, warmup: 10)
10 |
11 | x.report('GET /benchmark/html (ERB)') do
12 | get '/benchmark/html'
13 | expect(response).to have_http_status(:success)
14 | end
15 |
16 | x.report('GET /benchmark/ruby (Ruby2html templates .html.rb)') do
17 | get '/benchmark/ruby'
18 | expect(response).to have_http_status(:success)
19 | end
20 |
21 | x.report('GET /benchmark/ruby (Ruby2html + view components)') do
22 | get '/benchmark/compruby'
23 | expect(response).to have_http_status(:success)
24 | end
25 |
26 | x.report('GET /benchmark/slim (Slim)') do
27 | get '/benchmark/slim'
28 | expect(response).to have_http_status(:success)
29 | end
30 |
31 | x.report('GET /benchmark/phlex (Phlex)') do
32 | get '/benchmark/phlex'
33 | expect(response).to have_http_status(:success)
34 | end
35 |
36 | # Compare the results
37 | x.compare!
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/spec/features/home_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.feature 'Homes', type: :feature do
6 | it 'can visit root' do
7 | visit root_path
8 | expect(page).to have_content('Hello')
9 | end
10 |
11 | it 'can visit rb_files' do
12 | visit rb_files_path
13 | expect(page).to have_content('RbFiles')
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/gem/render_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 | require 'active_support/core_ext/string/output_safety'
5 |
6 | RSpec.describe Ruby2html::Render do
7 | let(:context) { double('context') }
8 | let(:render) { described_class.new(context) { div { p 'Hello, World!' } } }
9 |
10 | describe '#render' do
11 | it 'renders basic HTML' do
12 | expect(render.render).to eq('
Hello, World!
')
13 | end
14 |
15 | it 'renders nested elements' do
16 | render = described_class.new(context) do
17 | div do
18 | h1 'Title'
19 | p 'Paragraph'
20 | end
21 | end
22 | expect(render.render).to eq('
Title
Paragraph
')
23 | end
24 |
25 | it 'handles attributes' do
26 | render = described_class.new(context) do
27 | div class: 'container', id: 'main' do
28 | p 'Content'
29 | end
30 | end
31 | expect(render.render).to eq('
Content
')
32 | end
33 |
34 | it 'escapes HTML in content' do
35 | render = described_class.new(context) { p '' }
36 | expect(render.render).to eq('
<script>alert("XSS")</script>
')
37 | end
38 |
39 | it 'handles void elements' do
40 | render = described_class.new(context) { img src: 'image.jpg', alt: 'An image' }
41 | expect(render.render).to eq('')
42 | end
43 | end
44 |
45 | describe '#plain' do
46 | it 'renders unescaped content for ActiveSupport::SafeBuffer' do
47 | safe_buffer = 'Safe HTML'.html_safe
48 | render = described_class.new(context) { plain safe_buffer }
49 | expect(render.render).to eq('Safe HTML')
50 | end
51 |
52 | it 'escapes regular strings' do
53 | render = described_class.new(context) { plain 'Unsafe HTML' }
54 | expect(render.render).to eq('<em>Unsafe HTML</em>')
55 | end
56 | end
57 |
58 | describe '#component' do
59 | it 'renders component output' do
60 | component_output = 'Content'
61 | render = described_class.new(context) { component component_output }
62 | expect(render.render).to eq(component_output)
63 | end
64 | end
65 |
66 | describe 'method_missing' do
67 | it 'delegates unknown methods to context' do
68 | expect(context).to receive(:custom_helper).and_return('Custom Helper Output')
69 | render = described_class.new(context) { plain(custom_helper) }
70 | expect(render.render).to eq('Custom Helper Output')
71 | end
72 |
73 | it 'raises NoMethodError or NameError for undefined methods' do
74 | render = described_class.new(context) { undefined_method }
75 | expect { render.render }.to raise_error(StandardError) # This will catch both NoMethodError and NameError
76 | end
77 | end
78 |
79 | describe 'instance variables' do
80 | it 'sets instance variables from context' do
81 | allow(context).to receive(:instance_variables).and_return([:@test_var])
82 | allow(context).to receive(:instance_variable_get).with(:@test_var).and_return('Test Value')
83 |
84 | render = described_class.new(context) { plain @test_var }
85 | expect(render.render).to eq('Test Value')
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is copied to spec/ when you run 'rails generate rspec:install'
4 | require 'spec_helper'
5 | require 'capybara/rspec'
6 | require 'benchmark/ips'
7 | ENV['RAILS_ENV'] ||= 'test'
8 | require_relative '../config/environment'
9 | # Prevent database truncation if the environment is production
10 | abort('The Rails environment is running in production mode!') if Rails.env.production?
11 | require 'rspec/rails'
12 | # Add additional requires below this line. Rails is not loaded until this point!
13 |
14 | # Requires supporting ruby files with custom matchers and macros, etc, in
15 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
16 | # run as spec files by default. This means that files in spec/support that end
17 | # in _spec.rb will both be required and run as specs, causing the specs to be
18 | # run twice. It is recommended that you do not name files matching this glob to
19 | # end with _spec.rb. You can configure this pattern with the --pattern
20 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
21 | #
22 | # The following line is provided for convenience purposes. It has the downside
23 | # of increasing the boot-up time by auto-requiring all files in the support
24 | # directory. Alternatively, in the individual `*_spec.rb` files, manually
25 | # require only the support files necessary.
26 | #
27 | # Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f }
28 |
29 | # Checks for pending migrations and applies them before tests are run.
30 | # If you are not using ActiveRecord, you can remove these lines.
31 | begin
32 | ActiveRecord::Migration.maintain_test_schema!
33 | rescue ActiveRecord::PendingMigrationError => e
34 | abort e.to_s.strip
35 | end
36 | RSpec.configure do |config|
37 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
38 | config.fixture_paths = [
39 | Rails.root.join('spec/fixtures')
40 | ]
41 |
42 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
43 | # examples within a transaction, remove the following line or assign false
44 | # instead of true.
45 | config.use_transactional_fixtures = true
46 |
47 | # You can uncomment this line to turn off ActiveRecord support entirely.
48 | # config.use_active_record = false
49 |
50 | # RSpec Rails can automatically mix in different behaviours to your tests
51 | # based on their file location, for example enabling you to call `get` and
52 | # `post` in specs under `spec/controllers`.
53 | #
54 | # You can disable this behaviour by removing the line below, and instead
55 | # explicitly tag your specs with their type, e.g.:
56 | #
57 | # RSpec.describe UsersController, type: :controller do
58 | # # ...
59 | # end
60 | #
61 | # The different available types are documented in the features, such as in
62 | # https://rspec.info/features/6-0/rspec-rails
63 | config.infer_spec_type_from_file_location!
64 |
65 | # Filter lines from Rails gems in backtraces.
66 | config.filter_rails_from_backtrace!
67 | # arbitrary gems may also be filtered via:
68 | # config.filter_gems_from_backtrace("gem name")
69 | end
70 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all
4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5 | # The generated `.rspec` file contains `--require spec_helper` which will cause
6 | # this file to always be loaded, without a need to explicitly require it in any
7 | # files.
8 | #
9 | # Given that it is always loaded, you are encouraged to keep this file as
10 | # light-weight as possible. Requiring heavyweight dependencies from this file
11 | # will add to the boot time of your test suite on EVERY test run, even for an
12 | # individual file that may not need all of that loaded. Instead, consider making
13 | # a separate helper file that requires the additional dependencies and performs
14 | # the additional setup, and require it from the spec files that actually need
15 | # it.
16 | #
17 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
18 | RSpec.configure do |config|
19 | # rspec-expectations config goes here. You can use an alternate
20 | # assertion/expectation library such as wrong or the stdlib/minitest
21 | # assertions if you prefer.
22 | config.expect_with :rspec do |expectations|
23 | # This option will default to `true` in RSpec 4. It makes the `description`
24 | # and `failure_message` of custom matchers include text for helper methods
25 | # defined using `chain`, e.g.:
26 | # be_bigger_than(2).and_smaller_than(4).description
27 | # # => "be bigger than 2 and smaller than 4"
28 | # ...rather than:
29 | # # => "be bigger than 2"
30 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
31 | end
32 |
33 | # rspec-mocks config goes here. You can use an alternate test double
34 | # library (such as bogus or mocha) by changing the `mock_with` option here.
35 | config.mock_with :rspec do |mocks|
36 | # Prevents you from mocking or stubbing a method that does not exist on
37 | # a real object. This is generally recommended, and will default to
38 | # `true` in RSpec 4.
39 | mocks.verify_partial_doubles = true
40 | end
41 |
42 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
43 | # have no way to turn it off -- the option exists only for backwards
44 | # compatibility in RSpec 3). It causes shared context metadata to be
45 | # inherited by the metadata hash of host groups and examples, rather than
46 | # triggering implicit auto-inclusion in groups with matching metadata.
47 | config.shared_context_metadata_behavior = :apply_to_host_groups
48 |
49 | # The settings below are suggested to provide a good initial experience
50 | # with RSpec, but feel free to customize to your heart's content.
51 | =begin
52 | # This allows you to limit a spec run to individual examples or groups
53 | # you care about by tagging them with `:focus` metadata. When nothing
54 | # is tagged with `:focus`, all examples get run. RSpec also provides
55 | # aliases for `it`, `describe`, and `context` that include `:focus`
56 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
57 | config.filter_run_when_matching :focus
58 |
59 | # Allows RSpec to persist some state between runs in order to support
60 | # the `--only-failures` and `--next-failure` CLI options. We recommend
61 | # you configure your source control system to ignore this file.
62 | config.example_status_persistence_file_path = "spec/examples.txt"
63 |
64 | # Limits the available syntax to the non-monkey patched syntax that is
65 | # recommended. For more details, see:
66 | # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
67 | config.disable_monkey_patching!
68 |
69 | # Many RSpec users commonly either run the entire suite or an individual
70 | # file, and it's useful to allow more verbose output when running an
71 | # individual spec file.
72 | if config.files_to_run.one?
73 | # Use the documentation formatter for detailed output,
74 | # unless a formatter has already been configured
75 | # (e.g. via a command-line flag).
76 | config.default_formatter = "doc"
77 | end
78 |
79 | # Print the 10 slowest examples and example groups at the
80 | # end of the spec run, to help surface which specs are running
81 | # particularly slow.
82 | config.profile_examples = 10
83 |
84 | # Run specs in random order to surface order dependencies. If you find an
85 | # order dependency and want to debug it, you can fix the order by providing
86 | # the seed, which is printed after each run.
87 | # --seed 1234
88 | config.order = :random
89 |
90 | # Seed global randomization in this process using the `--seed` CLI option.
91 | # Setting this allows you to use `--seed` to deterministically reproduce
92 | # test failures related to randomization by passing the same `--seed` value
93 | # as the one that triggered the failure.
94 | Kernel.srand config.seed
95 | =end
96 | end
97 |
--------------------------------------------------------------------------------
/storage/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebyx07/ruby2html/8b26ce02c33a402c38255067536ebf1e1e7a598c/storage/.keep
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebyx07/ruby2html/8b26ce02c33a402c38255067536ebf1e1e7a598c/tmp/.keep
--------------------------------------------------------------------------------
/tmp/pids/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebyx07/ruby2html/8b26ce02c33a402c38255067536ebf1e1e7a598c/tmp/pids/.keep
--------------------------------------------------------------------------------
/tmp/storage/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebyx07/ruby2html/8b26ce02c33a402c38255067536ebf1e1e7a598c/tmp/storage/.keep
--------------------------------------------------------------------------------
/vendor/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebyx07/ruby2html/8b26ce02c33a402c38255067536ebf1e1e7a598c/vendor/.keep
--------------------------------------------------------------------------------