From 6b39b6e8704da03ee79515f4601b7f5fd94b5963 Mon Sep 17 00:00:00 2001 From: Iain Beeston Date: Wed, 20 Sep 2023 09:18:40 +0100 Subject: [PATCH 01/14] Print the filename if set If you run `ErubiImplementation#validate!` (as described in the readme) and there's a validation error it will print the line number and column but not the filename. You can pass the filename to erubi (like this `BetterHtml::BetterErb::ErubiImplementation.new(data, config: BetterHtml.config, filename:).validate!`) but `ErubiImplementation` doesn't use that information in it's error message. I've updated runtime checks so that if the filename is passed to `ErubiImplementation` it will be used in error messages, which can really help when fixing errors. --- README.md | 6 ++++-- lib/better_html/better_erb/runtime_checks.rb | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8868fc7..d5558e6 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ class ErbImplementationTest < ActiveSupport::TestCase pathname = Pathname.new(filename).relative_path_from(Rails.root) test "html errors in #{pathname}" do data = File.read(filename) - BetterHtml::BetterErb::ErubiImplementation.new(data).validate! + BetterHtml::BetterErb::ErubiImplementation.new(data, config: BetterHtml.config, filename:).validate! end end end @@ -261,7 +261,9 @@ RSpec.describe "BetterHtml" do Dir[erb_glob].each do |filename| data = File.read(filename) - expect { BetterHtml::BetterErb::ErubiImplementation.new(data).validate! }.not_to raise_exception + expect { + BetterHtml::BetterErb::ErubiImplementation.new(data, config: BetterHtml.config, filename:).validate! + }.not_to raise_exception end end end diff --git a/lib/better_html/better_erb/runtime_checks.rb b/lib/better_html/better_erb/runtime_checks.rb index 1f2dccd..27b1b81 100644 --- a/lib/better_html/better_erb/runtime_checks.rb +++ b/lib/better_html/better_erb/runtime_checks.rb @@ -155,7 +155,8 @@ def check_unquoted_value(type, start, stop, line, column) end def build_location(line, column, length) - s = +"On line #{line} column #{column}:\n" + s = filename ? "In #{filename}\n" : "" + s << "On line #{line} column #{column}:\n" s << "#{extract_line(line)}\n" s << "#{" " * column}#{"^" * length}" end From 9502d76aa5357b8fb70371e1e0cab612e6f9b31e Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 14 Dec 2023 01:03:22 -0800 Subject: [PATCH 02/14] Use ruby_strdup with xfree ruby_strdup is like strdup, but using xmalloc which informs Ruby's GC about the allocation (confusingly by including ruby/util.h strdup is defined into ruby_strdup, but let's use ruby_strdup for clarity). --- ext/better_html_ext/parser.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/better_html_ext/parser.c b/ext/better_html_ext/parser.c index f10d4f0..1bd7ef1 100644 --- a/ext/better_html_ext/parser.c +++ b/ext/better_html_ext/parser.c @@ -1,4 +1,5 @@ #include +#include #include #include "html_tokenizer.h" #include "parser.h" @@ -81,7 +82,7 @@ static inline void parser_append_ref(struct token_reference_t *dest, struct toke static void parser_add_error(struct parser_t *parser, const char *message) { REALLOC_N(parser->errors, struct parser_document_error_t, parser->errors_count + 1); - parser->errors[parser->errors_count].message = strdup(message); + parser->errors[parser->errors_count].message = ruby_strdup(message); parser->errors[parser->errors_count].pos = parser->tk.scan.cursor; parser->errors[parser->errors_count].mb_pos = parser->tk.scan.mb_cursor; parser->errors[parser->errors_count].line_number = parser->doc.line_number; From 370f77acc40cfbb48d19f9b3c4644e23bba49b98 Mon Sep 17 00:00:00 2001 From: Eugene Kenny Date: Tue, 9 Jan 2024 00:58:02 +0000 Subject: [PATCH 03/14] Accept = as expression right trim Erubi supports this, so it should be parsed as trim and not code. --- lib/better_html/tokenizer/base_erb.rb | 2 +- test/better_html/tokenizer/html_erb_test.rb | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/better_html/tokenizer/base_erb.rb b/lib/better_html/tokenizer/base_erb.rb index ec356d1..13c3eec 100644 --- a/lib/better_html/tokenizer/base_erb.rb +++ b/lib/better_html/tokenizer/base_erb.rb @@ -10,7 +10,7 @@ module Tokenizer class BaseErb < ::Erubi::Engine REGEXP_WITHOUT_TRIM = /<%(={1,2})?(.*?)()?%>([ \t]*\r?\n)?/m STMT_TRIM_MATCHER = /\A(-|#)?(.*?)([-=])?\z/m - EXPR_TRIM_MATCHER = /\A(.*?)(-)?\z/m + EXPR_TRIM_MATCHER = /\A(.*?)([-=])?\z/m attr_reader :tokens attr_reader :current_position diff --git a/test/better_html/tokenizer/html_erb_test.rb b/test/better_html/tokenizer/html_erb_test.rb index 6cfb501..8cf22d5 100644 --- a/test/better_html/tokenizer/html_erb_test.rb +++ b/test/better_html/tokenizer/html_erb_test.rb @@ -101,6 +101,17 @@ class HtmlErbTest < ActiveSupport::TestCase assert_attributes ({ type: :erb_end, loc: { begin_pos: 13, end_pos: 15, source: "%>" } }), scanner.tokens[3] end + test "expression right trim with =%>" do + scanner = HtmlErb.new(buffer("<%= literal =%>")) + assert_equal 5, scanner.tokens.size + + assert_attributes ({ type: :erb_begin, loc: { begin_pos: 0, end_pos: 2, source: "<%" } }), scanner.tokens[0] + assert_attributes ({ type: :indicator, loc: { begin_pos: 2, end_pos: 3, source: "=" } }), scanner.tokens[1] + assert_attributes ({ type: :code, loc: { begin_pos: 3, end_pos: 12, source: " literal " } }), scanner.tokens[2] + assert_attributes ({ type: :trim, loc: { begin_pos: 12, source: "=" } }), scanner.tokens[3] + assert_attributes ({ type: :erb_end, loc: { begin_pos: 13, end_pos: 15, source: "%>" } }), scanner.tokens[4] + end + test "line number for multi-line statements" do scanner = HtmlErb.new(buffer("before <% multi\nline %> after")) assert_equal 5, scanner.tokens.size From 59660e81a5d4df68829a8657f9cae72cad8b1788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Thu, 14 Mar 2024 01:40:00 +0000 Subject: [PATCH 04/14] Test with Ruby 3.3 --- .github/workflows/ruby.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 44999ad..fe4351a 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [3.2, 3.1, '3.0', 2.7] + ruby: [3.3, 3.2, 3.1, '3.0', 2.7] gemfile: - Gemfile - gemfiles/Gemfile-rails-6-0 @@ -40,7 +40,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.2 + ruby-version: 3.3 bundler-cache: true - name: Install Valgrind run: sudo apt-get install valgrind @@ -62,8 +62,8 @@ jobs: if: ${{ always() }} runs-on: ubuntu-latest name: Build (matrix) - needs: [lint, test] + needs: [lint, test, memcheck] steps: - name: Check build matrix status - if: ${{ needs.test.result != 'success' || needs.lint.result != 'success' }} + if: ${{ needs.test.result != 'success' || needs.lint.result != 'success' || needs.memcheck.result != 'success' }} run: exit 1 From bcc49872d16f26f4470686b97ead127f0a2b1e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Wed, 13 Mar 2024 22:16:37 -0400 Subject: [PATCH 05/14] Don't try to append to a frozen string --- lib/better_html/better_erb/runtime_checks.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/better_html/better_erb/runtime_checks.rb b/lib/better_html/better_erb/runtime_checks.rb index 27b1b81..1463d6e 100644 --- a/lib/better_html/better_erb/runtime_checks.rb +++ b/lib/better_html/better_erb/runtime_checks.rb @@ -155,7 +155,7 @@ def check_unquoted_value(type, start, stop, line, column) end def build_location(line, column, length) - s = filename ? "In #{filename}\n" : "" + s = filename ? +"In #{filename}\n" : +"" s << "On line #{line} column #{column}:\n" s << "#{extract_line(line)}\n" s << "#{" " * column}#{"^" * length}" From 2b2553360c2f5695a496164ca2c057f09561c988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Wed, 13 Mar 2024 22:16:56 -0400 Subject: [PATCH 06/14] Don't set default options --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d5558e6..40e0aa3 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ class ErbImplementationTest < ActiveSupport::TestCase pathname = Pathname.new(filename).relative_path_from(Rails.root) test "html errors in #{pathname}" do data = File.read(filename) - BetterHtml::BetterErb::ErubiImplementation.new(data, config: BetterHtml.config, filename:).validate! + BetterHtml::BetterErb::ErubiImplementation.new(data, filename:).validate! end end end @@ -262,7 +262,7 @@ RSpec.describe "BetterHtml" do Dir[erb_glob].each do |filename| data = File.read(filename) expect { - BetterHtml::BetterErb::ErubiImplementation.new(data, config: BetterHtml.config, filename:).validate! + BetterHtml::BetterErb::ErubiImplementation.new(data, filename:).validate! }.not_to raise_exception end end From 03c317c172fcc15b5459144c6df6bdf729c48d15 Mon Sep 17 00:00:00 2001 From: Iain Beeston Date: Wed, 20 Sep 2023 08:53:12 +0100 Subject: [PATCH 07/14] Only pass template language to parser `assert_erb_safety` accepts a `filename` option which prints the filename in the error message, which is invaluable for finding which file the error comes from (without this the error message just reports that there is an error in "(buffer)"). However, passing the filename right now throws an error: `ArgumentError: unknown keyword: :filename`, which comes about because `assert_erb_safety` passes all of it's own options to straight to `BetterHtml::Parser` and that doesn't support a `filename` option (in fact it only supports `template_language`). I've changed `assert_erb_safety` so that it only passes the `template_language` option through to `Parser`, which fixes the error and allows `assert_erb_safety` to correctly include the filename in the assertion message. --- README.md | 2 +- lib/better_html/test_helper/safe_erb_tester.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8868fc7..dbe04b7 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ class ErbSafetyTest < ActiveSupport::TestCase Dir[ERB_GLOB].each do |filename| pathname = Pathname.new(filename).relative_path_from(Rails.root) test "missing javascript escapes in #{pathname}" do - assert_erb_safety File.read(filename) + assert_erb_safety(File.read(filename), filename:) end end end diff --git a/lib/better_html/test_helper/safe_erb_tester.rb b/lib/better_html/test_helper/safe_erb_tester.rb index 557ced7..9a3cac2 100644 --- a/lib/better_html/test_helper/safe_erb_tester.rb +++ b/lib/better_html/test_helper/safe_erb_tester.rb @@ -41,7 +41,7 @@ def assert_erb_safety(data, **options) options[:template_language] ||= :html buffer = ::Parser::Source::Buffer.new(options[:filename] || "(buffer)") buffer.source = data - parser = BetterHtml::Parser.new(buffer, **options) + parser = BetterHtml::Parser.new(buffer, template_language: options[:template_language]) tester_classes = [ SafeErb::NoStatements, From e79538756254d48553a33522f77fd472cf4570f3 Mon Sep 17 00:00:00 2001 From: Iain Beeston Date: Thu, 26 Oct 2023 13:26:58 +0100 Subject: [PATCH 08/14] Added support for annotate_rendered_view_with_filenames ActionView's erb template handler can annotate html with comments to indicate which file any given snippet of html comes from. This is very useful in development, but it isn't supported by better-html. This adds equivalent support to better-html, using the same approach that ActionView takes, so it should work automatically for anyone switching from rails' default erb handler. --- lib/better_html/better_erb.rb | 14 ++++++++++++-- lib/better_html/config.rb | 1 + lib/better_html/railtie.rb | 6 ++++++ .../better_erb/implementation_test.rb | 18 ++++++++++++++++-- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/better_html/better_erb.rb b/lib/better_html/better_erb.rb index 9ff935b..5fc8ecb 100644 --- a/lib/better_html/better_erb.rb +++ b/lib/better_html/better_erb.rb @@ -52,10 +52,20 @@ def generate(template, source) klass ||= self.class.erb_implementation escape = self.class.escape_ignore_list.include?(template.type) + + options = { + escape: escape, + trim: (self.class.erb_trim_mode == "-"), + } + if BetterHtml.config.annotate_rendered_view_with_filenames && template.format == :html + options[:preamble] = "@output_buffer.safe_append='';" + options[:postamble] = "@output_buffer.safe_append='';" \ + "@output_buffer.to_s" + end + generator = klass.new( erb, - escape: escape, - trim: (self.class.erb_trim_mode == "-") + **options ) generator.validate! if generator.respond_to?(:validate!) generator.src diff --git a/lib/better_html/config.rb b/lib/better_html/config.rb index 1582536..eb74401 100644 --- a/lib/better_html/config.rb +++ b/lib/better_html/config.rb @@ -15,6 +15,7 @@ class Config property :template_exclusion_filter property :lodash_safe_javascript_expression, default: -> { [/\AJSON\.stringify\(/] } property :disable_parser_validation, default: false + property :annotate_rendered_view_with_filenames, default: false def javascript_attribute_name?(name) javascript_attribute_names.any? { |other| other === name.to_s } # rubocop:disable Style/CaseEquality diff --git a/lib/better_html/railtie.rb b/lib/better_html/railtie.rb index ebd583a..328a07a 100644 --- a/lib/better_html/railtie.rb +++ b/lib/better_html/railtie.rb @@ -7,5 +7,11 @@ class Railtie < Rails::Railtie initializer "better_html.better_erb.initialization" do BetterHtml::BetterErb.prepend! end + + config.after_initialize do + ActiveSupport.on_load(:action_view) do + BetterHtml.config.annotate_rendered_view_with_filenames = ActionView::Base.annotate_rendered_view_with_filenames + end + end end end diff --git a/test/better_html/better_erb/implementation_test.rb b/test/better_html/better_erb/implementation_test.rb index 8c356b9..2b75bd5 100644 --- a/test/better_html/better_erb/implementation_test.rb +++ b/test/better_html/better_erb/implementation_test.rb @@ -366,6 +366,20 @@ class ImplementationTest < ActiveSupport::TestCase end end + if ActionView.version >= Gem::Version.new("6.1") + test "with ActionView 6.1 comments are added to show the filename when annotate_rendered_view_with_filenames=true" do + config = build_config(annotate_rendered_view_with_filenames: true) + assert_equal "bar", + render("bar", config: config, filename: "_better_test.html.erb") + end + else + test "with ActionView 6.0 annotate_rendered_view_with_filenames=true does not change the output" do + config = build_config(annotate_rendered_view_with_filenames: true) + assert_equal "bar", + render("bar", config: config, filename: "_better_test.html.erb") + end + end + test "capture works as intended" do output = render(<<-HTML) <%- foo = capture do -%> @@ -420,13 +434,13 @@ def build_config(**options) BetterHtml::Config.new(**options) end - def render(source, config: build_config, locals: {}) + def render(source, config: build_config, locals: {}, filename: "test.html.erb") old_config = BetterHtml.config BetterHtml.config = config ActionView::Template.new( source, - "test.html.erb", + filename, ActionView::Template::Handlers::ERB.new, virtual_path: "partial", format: :html, From 694d3ed966094eedeefb37524c3eef24bd423536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Thu, 14 Mar 2024 22:42:17 +0000 Subject: [PATCH 09/14] Merge initialize to configure better_html --- lib/better_html/railtie.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/better_html/railtie.rb b/lib/better_html/railtie.rb index 328a07a..8e25172 100644 --- a/lib/better_html/railtie.rb +++ b/lib/better_html/railtie.rb @@ -6,9 +6,7 @@ module BetterHtml class Railtie < Rails::Railtie initializer "better_html.better_erb.initialization" do BetterHtml::BetterErb.prepend! - end - config.after_initialize do ActiveSupport.on_load(:action_view) do BetterHtml.config.annotate_rendered_view_with_filenames = ActionView::Base.annotate_rendered_view_with_filenames end From 97e133b73566fff7ac268b010907fca33a95ed06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Thu, 14 Mar 2024 22:44:02 +0000 Subject: [PATCH 10/14] Upgrade all development dependencies to the latest versions --- Gemfile.lock | 102 +++++++++++++++++++++++++++++---------------------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5089419..29cfda7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,80 +12,96 @@ PATH GEM remote: https://rubygems.org/ specs: - actionview (7.0.5) - activesupport (= 7.0.5) + actionview (7.1.3.2) + activesupport (= 7.1.3.2) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activesupport (7.0.5) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activesupport (7.1.3.2) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.7) builder (3.2.4) - byebug (9.1.0) - coderay (1.1.2) - concurrent-ruby (1.2.2) + byebug (11.1.3) + coderay (1.1.3) + concurrent-ruby (1.2.3) + connection_pool (2.4.1) crass (1.0.6) + drb (2.2.1) erubi (1.12.0) - i18n (1.14.1) + i18n (1.14.4) concurrent-ruby (~> 1.0) - json (2.6.2) - loofah (2.21.3) + json (2.7.1) + language_server-protocol (3.17.0.3) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - method_source (0.9.0) - mini_portile2 (2.8.2) - minitest (5.16.2) - mocha (1.14.0) - nokogiri (1.15.2) + method_source (1.0.0) + mini_portile2 (2.8.5) + minitest (5.22.3) + mocha (2.1.0) + ruby2_keywords (>= 0.0.5) + mutex_m (0.2.0) + nokogiri (1.16.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) - parallel (1.22.1) - parser (3.1.2.1) + parallel (1.24.0) + parser (3.3.0.5) ast (~> 2.4.1) - pry (0.11.2) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry-byebug (3.5.0) - byebug (~> 9.1) - pry (~> 0.10) - racc (1.7.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + racc + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.10.1) + byebug (~> 11.0) + pry (>= 0.13, < 0.15) + racc (1.7.3) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) rainbow (3.1.1) - rake (13.0.6) - rake-compiler (1.2.0) + rake (13.1.0) + rake-compiler (1.2.7) rake - regexp_parser (2.5.0) - rexml (3.2.5) - rubocop (1.35.0) + regexp_parser (2.9.0) + rexml (3.2.6) + rubocop (1.62.1) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.1.2.1) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.20.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.21.0) - parser (>= 3.1.1.0) - rubocop-shopify (2.9.0) - rubocop (~> 1.33) - ruby-progressbar (1.11.0) - ruby_memcheck (1.3.2) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.31.2) + parser (>= 3.3.0.4) + rubocop-shopify (2.15.1) + rubocop (~> 1.51) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + ruby_memcheck (2.3.0) nokogiri smart_properties (1.17.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.2.0) + unicode-display_width (2.5.0) PLATFORMS ruby From e1a14db65f16f45f5a06fe3c24f4c191909a5b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Thu, 14 Mar 2024 22:44:14 +0000 Subject: [PATCH 11/14] Avoid warning in the C extension --- ext/better_html_ext/html_tokenizer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/better_html_ext/html_tokenizer.c b/ext/better_html_ext/html_tokenizer.c index 348d1c2..5cb5ddd 100644 --- a/ext/better_html_ext/html_tokenizer.c +++ b/ext/better_html_ext/html_tokenizer.c @@ -4,7 +4,7 @@ static VALUE mHtmlTokenizer = Qnil; -void Init_better_html_ext() +void Init_better_html_ext(void) { mHtmlTokenizer = rb_define_module("HtmlTokenizer"); Init_html_tokenizer_tokenizer(mHtmlTokenizer); From 4a1cbc022dbfa54994886d1535b2d87037878631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Thu, 14 Mar 2024 22:52:57 +0000 Subject: [PATCH 12/14] Match company style guide --- lib/better_html/better_erb.rb | 2 +- lib/better_html/better_erb/runtime_checks.rb | 4 +- .../better_erb/validated_output_buffer.rb | 34 +- lib/better_html/html_attributes.rb | 2 +- lib/better_html/parser.rb | 23 +- .../safe_erb/allowed_script_type.rb | 2 +- .../test_helper/safe_erb/tag_interpolation.rb | 32 +- .../test_helper/safe_lodash_tester.rb | 10 +- lib/better_html/tokenizer/base_erb.rb | 2 +- lib/better_html/tokenizer/html_lodash.rb | 2 +- lib/better_html/tokenizer/location.rb | 2 +- lib/tasks/better_html_tasks.rake | 1 + .../better_erb/implementation_test.rb | 163 +++-- test/better_html/helpers_test.rb | 5 +- test/better_html/parser_test.rb | 266 +++++-- .../safe_erb/tag_interpolation_test.rb | 26 +- test/better_html/tokenizer/html_erb_test.rb | 5 +- .../better_html/tokenizer/html_lodash_test.rb | 19 +- test/dummy/config.ru | 2 +- test/dummy/config/environments/production.rb | 2 +- .../initializers/backtrace_silencers.rb | 1 + test/dummy/config/initializers/inflections.rb | 1 + test/dummy/config/initializers/mime_types.rb | 1 + test/html_tokenizer/parser_test.rb | 25 +- test/html_tokenizer/tokenizer_test.rb | 681 +++++++++++------- 25 files changed, 830 insertions(+), 483 deletions(-) diff --git a/lib/better_html/better_erb.rb b/lib/better_html/better_erb.rb index 5fc8ecb..83b49ed 100644 --- a/lib/better_html/better_erb.rb +++ b/lib/better_html/better_erb.rb @@ -65,7 +65,7 @@ def generate(template, source) generator = klass.new( erb, - **options + **options, ) generator.validate! if generator.respond_to?(:validate!) generator.src diff --git a/lib/better_html/better_erb/runtime_checks.rb b/lib/better_html/better_erb/runtime_checks.rb index 1463d6e..5426bc9 100644 --- a/lib/better_html/better_erb/runtime_checks.rb +++ b/lib/better_html/better_erb/runtime_checks.rb @@ -119,7 +119,7 @@ def check_tag_name(type, start, stop, line, column) return if text.upcase == "!DOCTYPE" return if @config.partial_tag_name_pattern.match?(text) - s = +"Invalid tag name #{text.inspect} does not match "\ + s = +"Invalid tag name #{text.inspect} does not match " \ "regular expression #{@config.partial_tag_name_pattern.inspect}\n" s << build_location(line, column, text.size) raise BetterHtml::HtmlError, s @@ -129,7 +129,7 @@ def check_attribute_name(type, start, stop, line, column) text = @parser.document[start...stop] return if @config.partial_attribute_name_pattern.match?(text) - s = +"Invalid attribute name #{text.inspect} does not match "\ + s = +"Invalid attribute name #{text.inspect} does not match " \ "regular expression #{@config.partial_attribute_name_pattern.inspect}\n" s << build_location(line, column, text.size) raise BetterHtml::HtmlError, s diff --git a/lib/better_html/better_erb/validated_output_buffer.rb b/lib/better_html/better_erb/validated_output_buffer.rb index 3544b46..9b536ec 100644 --- a/lib/better_html/better_erb/validated_output_buffer.rb +++ b/lib/better_html/better_erb/validated_output_buffer.rb @@ -17,7 +17,7 @@ def safe_quoted_value_append=(value) value = properly_escaped(value) if value.include?(@context[:quote_character]) - raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\ + raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation " \ "into a quoted attribute value. The value cannot contain the character #{@context[:quote_character]}." end @@ -25,15 +25,15 @@ def safe_quoted_value_append=(value) end def safe_unquoted_value_append=(value) - raise DontInterpolateHere, "Do not interpolate without quotes around this "\ - "attribute value. Instead of "\ - "<#{@context[:tag_name]} #{@context[:attribute_name]}=#{@context[:attribute_value]}<%=#{@code}%>> "\ + raise DontInterpolateHere, "Do not interpolate without quotes around this " \ + "attribute value. Instead of " \ + "<#{@context[:tag_name]} #{@context[:attribute_name]}=#{@context[:attribute_value]}<%=#{@code}%>> " \ "try <#{@context[:tag_name]} #{@context[:attribute_name]}=\"#{@context[:attribute_value]}<%=#{@code}%>\">." end def safe_space_after_attribute_append=(value) - raise DontInterpolateHere, "Add a space after this attribute value. Instead of "\ - "<#{@context[:tag_name]} #{@context[:attribute_name]}=\"#{@context[:attribute_value]}\"<%=#{@code}%>> "\ + raise DontInterpolateHere, "Add a space after this attribute value. Instead of " \ + "<#{@context[:tag_name]} #{@context[:attribute_name]}=\"#{@context[:attribute_value]}\"<%=#{@code}%>> " \ "try <#{@context[:tag_name]} #{@context[:attribute_name]}=\"#{@context[:attribute_value]}\" <%=#{@code}%>>." end @@ -43,7 +43,7 @@ def safe_attribute_name_append=(value) value = value.to_s unless value =~ /\A[a-z0-9\-]*\z/ - raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\ + raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation " \ "into a attribute name around '#{@context[:attribute_name]}<%=#{@code}%>'." end @@ -54,8 +54,8 @@ def safe_after_attribute_name_append=(value) return if value.nil? unless value.is_a?(BetterHtml::HtmlAttributes) - raise DontInterpolateHere, "Do not interpolate #{value.class} in a tag. "\ - "Instead of <#{@context[:tag_name]} <%=#{@code}%>> please "\ + raise DontInterpolateHere, "Do not interpolate #{value.class} in a tag. " \ + "Instead of <#{@context[:tag_name]} <%=#{@code}%>> please " \ "try <#{@context[:tag_name]} <%= html_attributes(attr: value) %>>." end @@ -63,7 +63,7 @@ def safe_after_attribute_name_append=(value) end def safe_after_equal_append=(value) - raise DontInterpolateHere, "Do not interpolate without quotes after "\ + raise DontInterpolateHere, "Do not interpolate without quotes after " \ "attribute around '#{@context[:attribute_name]}=<%=#{@code}%>'." end @@ -71,8 +71,8 @@ def safe_tag_append=(value) return if value.nil? unless value.is_a?(BetterHtml::HtmlAttributes) - raise DontInterpolateHere, "Do not interpolate #{value.class} in a tag. "\ - "Instead of <#{@context[:tag_name]} <%=#{@code}%>> please "\ + raise DontInterpolateHere, "Do not interpolate #{value.class} in a tag. " \ + "Instead of <#{@context[:tag_name]} <%=#{@code}%>> please " \ "try <#{@context[:tag_name]} <%= html_attributes(attr: value) %>>." end @@ -85,7 +85,7 @@ def safe_tag_name_append=(value) value = value.to_s unless value =~ /\A[a-z0-9\:\-]*\z/ - raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\ + raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation " \ "into a tag name around: <#{@context[:tag_name]}<%=#{@code}%>>." end @@ -100,12 +100,12 @@ def safe_rawtext_append=(value) if @context[:tag_name].downcase == "script" && (value =~ /#{@context[:rawtext_text]}<%=#{@code}%>." end @@ -120,7 +120,7 @@ def safe_comment_append=(value) # in a we disallow --> if value =~ /-->/ - raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\ + raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation " \ "into a html comment around: ", locals: { value: "-->".html_safe }) end assert_kind_of BetterHtml::UnsafeHtmlError, e.cause - assert_equal "Detected invalid characters as part of the interpolation "\ - "into a html comment around: ")) - assert_equal s(:document, - s(:comment, + assert_equal s( + :document, + s( + :comment, " foo ", s(:erb, s(:indicator, "="), nil, s(:code, " bar "), nil), - " baz ")), + " baz ", + ), + ), tree.ast end @@ -85,12 +93,21 @@ class ParserTest < ActiveSupport::TestCase test "tag without name" do tree = Parser.new(buffer("foo < bar")) - assert_equal s(:document, + assert_equal s( + :document, s(:text, "foo "), - s(:tag, nil, nil, - s(:tag_attributes, - s(:attribute, s(:attribute_name, "bar"), nil, nil)), - nil)), tree.ast + s( + :tag, + nil, + nil, + s( + :tag_attributes, + s(:attribute, s(:attribute_name, "bar"), nil, nil), + ), + nil, + ), + ), + tree.ast end test "consume tag nodes with solidus" do @@ -113,136 +130,226 @@ class ParserTest < ActiveSupport::TestCase test "consume tag nodes with interpolation" do tree = Parser.new(buffer("-thing>")) - assert_equal s(:document, - s(:tag, + assert_equal s( + :document, + s( + :tag, nil, s(:tag_name, "ns:", s(:erb, s(:indicator, "="), nil, s(:code, " name "), nil), "-thing"), nil, - nil)), tree.ast + nil, + ), + ), + tree.ast end test "consume tag attributes with erb" do tree = Parser.new(buffer("
name=bar>")) - assert_equal s(:document, - s(:tag, nil, + assert_equal s( + :document, + s( + :tag, + nil, s(:tag_name, "div"), - s(:tag_attributes, - s(:attribute, + s( + :tag_attributes, + s( + :attribute, s(:attribute_name, "class"), s(:equal), - s(:attribute_value, "foo")), - s(:erb, s(:indicator, "="), nil, - s(:code, " erb "), nil), - s(:attribute, + s(:attribute_value, "foo"), + ), + s( + :erb, + s(:indicator, "="), + nil, + s(:code, " erb "), + nil, + ), + s( + :attribute, s(:attribute_name, "name"), s(:equal), - s(:attribute_value, "bar")),), - nil)), tree.ast + s(:attribute_value, "bar"), + ), + ), + nil, + ), + ), + tree.ast end test "consume tag attributes nodes unquoted value" do tree = Parser.new(buffer("
")) - assert_equal s(:document, - s(:tag, nil, + assert_equal s( + :document, + s( + :tag, + nil, s(:tag_name, "div"), - s(:tag_attributes, - s(:attribute, + s( + :tag_attributes, + s( + :attribute, s(:attribute_name, "foo"), s(:equal), - s(:attribute_value, "bar"))), - nil)), tree.ast + s(:attribute_value, "bar"), + ), + ), + nil, + ), + ), + tree.ast end test "consume attributes without name" do tree = Parser.new(buffer("
")) - assert_equal s(:document, - s(:tag, nil, + assert_equal s( + :document, + s( + :tag, + nil, s(:tag_name, "div"), - s(:tag_attributes, - s(:attribute, + s( + :tag_attributes, + s( + :attribute, nil, nil, - s(:attribute_value, s(:quote, "'"), "thing", s(:quote, "'")))), - nil)), tree.ast + s(:attribute_value, s(:quote, "'"), "thing", s(:quote, "'")), + ), + ), + nil, + ), + ), + tree.ast end test "consume tag attributes nodes quoted value" do tree = Parser.new(buffer("
")) - assert_equal s(:document, - s(:tag, nil, + assert_equal s( + :document, + s( + :tag, + nil, s(:tag_name, "div"), - s(:tag_attributes, - s(:attribute, + s( + :tag_attributes, + s( + :attribute, s(:attribute_name, "foo"), s(:equal), - s(:attribute_value, s(:quote, "\""), "bar", s(:quote, "\"")))), - nil)), tree.ast + s(:attribute_value, s(:quote, "\""), "bar", s(:quote, "\"")), + ), + ), + nil, + ), + ), + tree.ast end test "consume tag attributes nodes interpolation in name and value" do tree = Parser.new(buffer("
=\"some <%= value %> foo\">")) - assert_equal s(:document, - s(:tag, nil, + assert_equal s( + :document, + s( + :tag, + nil, s(:tag_name, "div"), - s(:tag_attributes, - s(:attribute, + s( + :tag_attributes, + s( + :attribute, s(:attribute_name, "data-", s(:erb, s(:indicator, "="), nil, s(:code, " foo "), nil)), s(:equal), - s(:attribute_value, + s( + :attribute_value, s(:quote, "\""), "some ", s(:erb, s(:indicator, "="), nil, s(:code, " value "), nil), " foo", - s(:quote, "\""),),)), - nil)), tree.ast + s(:quote, "\""), + ), + ), + ), + nil, + ), + ), + tree.ast end test "consume text nodes" do tree = Parser.new(buffer("here is <%= some %> text")) - assert_equal s(:document, - s(:text, + assert_equal s( + :document, + s( + :text, "here is ", s(:erb, s(:indicator, "="), nil, s(:code, " some "), nil), - " text")), tree.ast + " text", + ), + ), + tree.ast end test "javascript template parsing works" do tree = Parser.new(buffer("here is <%= some %> text"), template_language: :javascript) - assert_equal s(:document, - s(:text, + assert_equal s( + :document, + s( + :text, "here is ", s(:erb, s(:indicator, "="), nil, s(:code, " some "), nil), - " text")), tree.ast + " text", + ), + ), + tree.ast end test "javascript template does not consume html tags" do tree = Parser.new(buffer("
/>"), template_language: :javascript) - assert_equal s(:document, - s(:text, + assert_equal s( + :document, + s( + :text, "
")), tree.ast + " />", + ), + ), + tree.ast end test "lodash template parsing works" do tree = Parser.new(buffer('
'), template_language: :lodash) - assert_equal s(:document, - s(:tag, + assert_equal s( + :document, + s( + :tag, nil, s(:tag_name, "div"), - s(:tag_attributes, - s(:attribute, + s( + :tag_attributes, + s( + :attribute, s(:attribute_name, "class"), s(:equal), - s(:attribute_value, + s( + :attribute_value, s(:quote, "\""), s(:lodash, s(:indicator, "="), s(:code, " foo ")), - s(:quote, "\"")))), - nil)), tree.ast + s(:quote, "\""), + ), + ), + ), + nil, + ), + ), + tree.ast end test "nodes are all nested under document" do @@ -256,25 +363,36 @@ class ParserTest < ActiveSupport::TestCase
HTML - assert_equal s(:document, + assert_equal s( + :document, s(:text, "some text\n"), s(:comment, " a comment "), - s(:text, + s( + :text, "\nsome more text\n", s(:erb, s(:indicator, "="), nil, s(:code, " an erb tag "), s(:trim)), - "\n"), - s(:tag, + "\n", + ), + s( + :tag, nil, s(:tag_name, "div"), - s(:tag_attributes, - s(:attribute, + s( + :tag_attributes, + s( + :attribute, s(:attribute_name, "class"), s(:equal), - s(:attribute_value, s(:quote, "\""), "foo", s(:quote, "\"")))), - nil), + s(:attribute_value, s(:quote, "\""), "foo", s(:quote, "\"")), + ), + ), + nil, + ), s(:text, "\n content\n"), s(:tag, s(:solidus), s(:tag_name, "div"), nil, nil), - s(:text, "\n"),), tree.ast + s(:text, "\n"), + ), + tree.ast end end end diff --git a/test/better_html/test_helper/safe_erb/tag_interpolation_test.rb b/test/better_html/test_helper/safe_erb/tag_interpolation_test.rb index 2036ac1..43edff3 100644 --- a/test/better_html/test_helper/safe_erb/tag_interpolation_test.rb +++ b/test/better_html/test_helper/safe_erb/tag_interpolation_test.rb @@ -63,7 +63,7 @@ class TagInterpolationTest < ActiveSupport::TestCase assert_equal "name", errors.first.location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors.first.message + errors.first.message, ) end @@ -77,13 +77,13 @@ class TagInterpolationTest < ActiveSupport::TestCase assert_equal "bar", errors[0].location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors[0].message + errors[0].message, ) assert_equal "baz", errors[1].location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors[1].message + errors[1].message, ) end @@ -96,7 +96,7 @@ class TagInterpolationTest < ActiveSupport::TestCase assert_equal "unsafe", errors.first.location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors.first.message + errors.first.message, ) end @@ -123,7 +123,7 @@ class TagInterpolationTest < ActiveSupport::TestCase assert_equal "bar", errors.first.location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors.first.message + errors.first.message, ) end @@ -178,7 +178,7 @@ class TagInterpolationTest < ActiveSupport::TestCase assert_equal "unsafe.to_json.html_safe", errors[1].location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors[1].message + errors[1].message, ) end @@ -223,7 +223,7 @@ class TagInterpolationTest < ActiveSupport::TestCase assert_equal "raw unsafe.to_json", errors[1].location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors[1].message + errors[1].message, ) end @@ -236,7 +236,7 @@ class TagInterpolationTest < ActiveSupport::TestCase assert_equal "unsafe", errors[0].location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors[0].message + errors[0].message, ) end @@ -249,7 +249,7 @@ class TagInterpolationTest < ActiveSupport::TestCase assert_equal "unsafe", errors.first.location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors.first.message + errors.first.message, ) end @@ -262,7 +262,7 @@ class TagInterpolationTest < ActiveSupport::TestCase assert_equal "unsafe", errors.first.location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors.first.message + errors.first.message, ) end @@ -283,7 +283,7 @@ class TagInterpolationTest < ActiveSupport::TestCase assert_equal "unsafe", errors.first.location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors.first.message + errors.first.message, ) end @@ -296,7 +296,7 @@ class TagInterpolationTest < ActiveSupport::TestCase assert_equal "unsafe", errors.first.location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors.first.message + errors.first.message, ) end @@ -309,7 +309,7 @@ class TagInterpolationTest < ActiveSupport::TestCase assert_equal "unsafe", errors.first.location.source assert_equal( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", - errors.first.message + errors.first.message, ) end diff --git a/test/better_html/tokenizer/html_erb_test.rb b/test/better_html/tokenizer/html_erb_test.rb index 8cf22d5..f3fb6a2 100644 --- a/test/better_html/tokenizer/html_erb_test.rb +++ b/test/better_html/tokenizer/html_erb_test.rb @@ -13,7 +13,8 @@ class HtmlErbTest < ActiveSupport::TestCase assert_attributes ({ type: :text, loc: { begin_pos: 0, end_pos: 14, source: "just some text" }, - }), scanner.tokens[0] + }), + scanner.tokens[0] end test "statement" do @@ -74,7 +75,7 @@ class HtmlErbTest < ActiveSupport::TestCase scanner.tokens[5] assert_attributes( { type: :attribute_quoted_value, loc: { begin_pos: 12, end_pos: 24, source: "your store’s" } }, - scanner.tokens[6] + scanner.tokens[6], ) assert_attributes ({ type: :attribute_quoted_value_end, loc: { begin_pos: 24, end_pos: 25, source: "'" } }), scanner.tokens[7] diff --git a/test/better_html/tokenizer/html_lodash_test.rb b/test/better_html/tokenizer/html_lodash_test.rb index 3e07e74..ce25256 100644 --- a/test/better_html/tokenizer/html_lodash_test.rb +++ b/test/better_html/tokenizer/html_lodash_test.rb @@ -76,10 +76,21 @@ class HtmlLodashTest < ActiveSupport::TestCase test "parses out html correctly" do scanner = HtmlLodash.new(buffer('
')) assert_equal 12, scanner.tokens.size - assert_equal [:tag_start, :tag_name, :whitespace, :attribute_name, - :equal, :attribute_quoted_value_start, - :lodash_begin, :indicator, :code, :lodash_end, - :attribute_quoted_value_end, :tag_end,], scanner.tokens.map(&:type) + assert_equal [ + :tag_start, + :tag_name, + :whitespace, + :attribute_name, + :equal, + :attribute_quoted_value_start, + :lodash_begin, + :indicator, + :code, + :lodash_end, + :attribute_quoted_value_end, + :tag_end, + ], + scanner.tokens.map(&:type) assert_equal ["<", "div", " ", "class", "=", "\"", "[%", "=", " foo ", "%]", "\"", ">"], scanner.tokens.map(&:loc).map(&:source) end diff --git a/test/dummy/config.ru b/test/dummy/config.ru index 097cb7b..89bfd0b 100644 --- a/test/dummy/config.ru +++ b/test/dummy/config.ru @@ -2,5 +2,5 @@ # This file is used by Rack-based servers to start the application. -require ::File.expand_path("../config/environment", __FILE__) +require File.expand_path("../config/environment", __FILE__) run Rails.application diff --git a/test/dummy/config/environments/production.rb b/test/dummy/config/environments/production.rb index bdd9a47..f7012f3 100644 --- a/test/dummy/config/environments/production.rb +++ b/test/dummy/config/environments/production.rb @@ -74,7 +74,7 @@ config.active_support.deprecation = :notify # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new + config.log_formatter = Logger::Formatter.new # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false diff --git a/test/dummy/config/initializers/backtrace_silencers.rb b/test/dummy/config/initializers/backtrace_silencers.rb index d0f0d3b..4b63f28 100644 --- a/test/dummy/config/initializers/backtrace_silencers.rb +++ b/test/dummy/config/initializers/backtrace_silencers.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. diff --git a/test/dummy/config/initializers/inflections.rb b/test/dummy/config/initializers/inflections.rb index aa7435f..dc84742 100644 --- a/test/dummy/config/initializers/inflections.rb +++ b/test/dummy/config/initializers/inflections.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections diff --git a/test/dummy/config/initializers/mime_types.rb b/test/dummy/config/initializers/mime_types.rb index 6e1d16f..be6fedc 100644 --- a/test/dummy/config/initializers/mime_types.rb +++ b/test/dummy/config/initializers/mime_types.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: diff --git a/test/html_tokenizer/parser_test.rb b/test/html_tokenizer/parser_test.rb index 7c1aeb0..bce1f88 100644 --- a/test/html_tokenizer/parser_test.rb +++ b/test/html_tokenizer/parser_test.rb @@ -533,17 +533,20 @@ def test_attribute_with_mutlibyte_characters assert_equal(data.join, @parser.document) assert_equal(data.join.size, @parser.document_length) assert_equal(data.join.size, @parser.column_number) - assert_equal([ - [:tag_start, 0, 1, "<"], - [:tag_name, 1, 4, "div"], - [:whitespace, 4, 5, " "], - [:attribute_name, 5, 10, "title"], - [:equal, 10, 11, "="], - [:attribute_quoted_value_start, 11, 12, "'"], - [:attribute_quoted_value, 12, 24, "your store’s"], - [:attribute_quoted_value_end, 24, 25, "'"], - [:tag_end, 25, 26, ">"], - ], tokens) + assert_equal( + [ + [:tag_start, 0, 1, "<"], + [:tag_name, 1, 4, "div"], + [:whitespace, 4, 5, " "], + [:attribute_name, 5, 10, "title"], + [:equal, 10, 11, "="], + [:attribute_quoted_value_start, 11, 12, "'"], + [:attribute_quoted_value, 12, 24, "your store’s"], + [:attribute_quoted_value_end, 24, 25, "'"], + [:tag_end, 25, 26, ">"], + ], + tokens, + ) end def test_valid_syntaxes diff --git a/test/html_tokenizer/tokenizer_test.rb b/test/html_tokenizer/tokenizer_test.rb index f50c25b..1bfded8 100644 --- a/test/html_tokenizer/tokenizer_test.rb +++ b/test/html_tokenizer/tokenizer_test.rb @@ -5,12 +5,18 @@ module HtmlTokenizer class TokenizerTest < Minitest::Test def test_closing_tag_without_start_is_text - assert_equal([ - [:text, ">"], - ], tokenize(">")) - assert_equal([ - [:tag_start, "<"], [:tag_name, "foo"], [:tag_end, ">"], [:text, ">"], - ], tokenize(">")) + assert_equal( + [ + [:text, ">"], + ], + tokenize(">"), + ) + assert_equal( + [ + [:tag_start, "<"], [:tag_name, "foo"], [:tag_end, ">"], [:text, ">"], + ], + tokenize(">"), + ) end def test_tokenize_text @@ -19,45 +25,64 @@ def test_tokenize_text end def test_namespace_tag_name_multipart - assert_equal([ - [:tag_start, "<"], [:tag_name, "foo:"], [:tag_name, "bar"], - ], tokenize(""], - ], tokenize("")) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "!DOCTYPE"], + [:whitespace, " "], + [:attribute_name, "html"], + [:tag_end, ">"], + ], + tokenize(""), + ) end def test_tokenize_multiple_elements - assert_equal([ - [:tag_start, "<"], [:tag_name, "div"], [:tag_end, ">"], - [:text, " bla "], - [:tag_start, "<"], [:tag_name, "strong"], [:tag_end, ">"], - ], tokenize("
bla ")) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:tag_end, ">"], + [:text, " bla "], + [:tag_start, "<"], + [:tag_name, "strong"], + [:tag_end, ">"], + ], + tokenize("
bla "), + ) end def test_tokenize_complex_doctype text = '' - assert_equal([ - [:tag_start, "<"], - [:tag_name, "!DOCTYPE"], - [:whitespace, " "], - [:attribute_name, "html"], - [:whitespace, " "], - [:attribute_name, "PUBLIC"], - [:whitespace, " "], - [:attribute_quoted_value_start, "\""], - [:attribute_quoted_value, "-//W3C//DTD XHTML 1.0 Transitional//EN"], - [:attribute_quoted_value_end, "\""], - [:whitespace, " "], - [:attribute_quoted_value_start, "\""], - [:attribute_quoted_value, "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"], - [:attribute_quoted_value_end, "\""], - [:tag_end, ">"], - ], tokenize(text)) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "!DOCTYPE"], + [:whitespace, " "], + [:attribute_name, "html"], + [:whitespace, " "], + [:attribute_name, "PUBLIC"], + [:whitespace, " "], + [:attribute_quoted_value_start, "\""], + [:attribute_quoted_value, "-//W3C//DTD XHTML 1.0 Transitional//EN"], + [:attribute_quoted_value_end, "\""], + [:whitespace, " "], + [:attribute_quoted_value_start, "\""], + [:attribute_quoted_value, "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"], + [:attribute_quoted_value_end, "\""], + [:tag_end, ">"], + ], + tokenize(text), + ) end def test_tokenize_html_comment @@ -70,11 +95,16 @@ def test_tokenize_comment_with_newlines EOF - assert_equal([ - [:text, " "], [:comment_start, ""], [:text, "\n"], - ], result) + assert_equal( + [ + [:text, " "], + [:comment_start, ""], + [:text, "\n"], + ], + result, + ) end def test_tokenize_cdata_section @@ -84,26 +114,57 @@ def test_tokenize_cdata_section def test_tokenizer_cdata_regression result = tokenize("") - assert_equal([[:cdata_start, ""],], result) + assert_equal( + [ + [:cdata_start, ""], + ], + result, + ) end def test_tokenizer_cdata_context result = tokenize("<") - assert_equal([[:cdata_start, ""], [:tag_start, "<"],], result) + assert_equal( + [ + [:cdata_start, ""], + [:tag_start, "<"], + ], + result, + ) end def test_tokenizer_comment_regression result = tokenize("") - assert_equal([[:comment_start, ""],], result) + assert_equal( + [ + [:comment_start, ""], + ], + result, + ) end def test_tokenizer_parse_tag_after_comment_regression result = tokenize("
  • ") - assert_equal([[:comment_start, ""], - [:text, " "], [:tag_start, "<"], [:tag_name, "li"], [:tag_end, ">"],], result) + assert_equal( + [ + [:comment_start, ""], + [:text, " "], + [:tag_start, "<"], + [:tag_name, "li"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_basic_tag @@ -128,8 +189,10 @@ def test_tokenize_tag_multipart_name def test_tokenize_tag_name_ending_with_slash result = tokenize("
    ") - assert_equal([[:tag_start, "<"], [:tag_name, "div"], [:solidus, "/"], [:attribute_name, "1"], [:tag_end, ">"]], - result) + assert_equal( + [[:tag_start, "<"], [:tag_name, "div"], [:solidus, "/"], [:attribute_name, "1"], [:tag_end, ">"]], + result, + ) end def test_tokenize_empty_tag @@ -149,262 +212,353 @@ def test_tokenize_end_tag def test_tokenize_tag_attribute_with_double_quote result = tokenize('
    ') - assert_equal([ - [:tag_start, "<"], - [:tag_name, "div"], - [:whitespace, " "], - [:attribute_name, "foo"], - [:equal, "="], - [:attribute_quoted_value_start, "\""], - [:attribute_quoted_value, "bar"], - [:attribute_quoted_value_end, "\""], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "foo"], + [:equal, "="], + [:attribute_quoted_value_start, "\""], + [:attribute_quoted_value, "bar"], + [:attribute_quoted_value_end, "\""], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_unquoted_attributes_separated_with_solidus result = tokenize("
    ") - assert_equal([ - [:tag_start, "<"], - [:tag_name, "div"], - [:whitespace, " "], - [:attribute_name, "foo"], - [:equal, "="], - [:attribute_unquoted_value, "1/bar=2"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "foo"], + [:equal, "="], + [:attribute_unquoted_value, "1/bar=2"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_quoted_attributes_separated_with_solidus result = tokenize('
    ') - assert_equal([ - [:tag_start, "<"], - [:tag_name, "div"], - [:whitespace, " "], - [:attribute_name, "foo"], - [:equal, "="], - [:attribute_quoted_value_start, "\""], - [:attribute_quoted_value, "1"], - [:attribute_quoted_value_end, "\""], - [:solidus, "/"], - [:attribute_name, "bar"], - [:equal, "="], - [:attribute_quoted_value_start, "\""], - [:attribute_quoted_value, "2"], - [:attribute_quoted_value_end, "\""], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "foo"], + [:equal, "="], + [:attribute_quoted_value_start, "\""], + [:attribute_quoted_value, "1"], + [:attribute_quoted_value_end, "\""], + [:solidus, "/"], + [:attribute_name, "bar"], + [:equal, "="], + [:attribute_quoted_value_start, "\""], + [:attribute_quoted_value, "2"], + [:attribute_quoted_value_end, "\""], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_tag_attribute_without_space result = tokenize('
    ') - assert_equal([ - [:tag_start, "<"], - [:tag_name, "div"], - [:whitespace, " "], - [:attribute_name, "foo"], - [:equal, "="], - [:attribute_quoted_value_start, "\""], - [:attribute_quoted_value, "bar"], - [:attribute_quoted_value_end, "\""], - [:attribute_name, "baz"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "foo"], + [:equal, "="], + [:attribute_quoted_value_start, "\""], + [:attribute_quoted_value, "bar"], + [:attribute_quoted_value_end, "\""], + [:attribute_name, "baz"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_multipart_unquoted_attribute result = tokenize("
    ") - assert_equal([ - [:tag_start, "<"], - [:tag_name, "div"], - [:whitespace, " "], - [:attribute_name, "foo"], - [:equal, "="], - [:attribute_unquoted_value, "bar"], - [:attribute_unquoted_value, "baz"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "foo"], + [:equal, "="], + [:attribute_unquoted_value, "bar"], + [:attribute_unquoted_value, "baz"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_quoted_attribute_separately result = tokenize("
    ") - assert_equal([ - [:tag_start, "<"], - [:tag_name, "div"], - [:whitespace, " "], - [:attribute_name, "foo"], - [:equal, "="], - [:attribute_quoted_value_start, "'"], - [:attribute_quoted_value, "bar"], - [:attribute_quoted_value_end, "'"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "foo"], + [:equal, "="], + [:attribute_quoted_value_start, "'"], + [:attribute_quoted_value, "bar"], + [:attribute_quoted_value_end, "'"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_quoted_attribute_in_multiple_parts result = tokenize("
    ") - assert_equal([ - [:tag_start, "<"], - [:tag_name, "div"], - [:whitespace, " "], - [:attribute_name, "foo"], - [:equal, "="], - [:attribute_quoted_value_start, "'"], - [:attribute_quoted_value, "bar"], - [:attribute_quoted_value, "baz"], - [:attribute_quoted_value_end, "'"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "foo"], + [:equal, "="], + [:attribute_quoted_value_start, "'"], + [:attribute_quoted_value, "bar"], + [:attribute_quoted_value, "baz"], + [:attribute_quoted_value_end, "'"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_tag_attribute_with_single_quote result = tokenize("
    ") - assert_equal([ - [:tag_start, "<"], - [:tag_name, "div"], - [:whitespace, " "], - [:attribute_name, "foo"], - [:equal, "="], - [:attribute_quoted_value_start, "'"], - [:attribute_quoted_value, "bar"], - [:attribute_quoted_value_end, "'"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "foo"], + [:equal, "="], + [:attribute_quoted_value_start, "'"], + [:attribute_quoted_value, "bar"], + [:attribute_quoted_value_end, "'"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_tag_attribute_with_no_quotes result = tokenize("
    ") - assert_equal([ - [:tag_start, "<"], [:tag_name, "div"], [:whitespace, " "], - [:attribute_name, "foo"], [:equal, "="], [:attribute_unquoted_value, "bla"], [:whitespace, " "], - [:attribute_name, "bar"], [:equal, "="], [:attribute_unquoted_value, "blo"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "foo"], + [:equal, "="], + [:attribute_unquoted_value, "bla"], + [:whitespace, " "], + [:attribute_name, "bar"], + [:equal, "="], + [:attribute_unquoted_value, "blo"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_double_equals result = tokenize("
    ") - assert_equal([ - [:tag_start, "<"], [:tag_name, "div"], [:whitespace, " "], - [:attribute_name, "foo"], [:equal, "="], [:attribute_unquoted_value, "blabar=blo"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "foo"], + [:equal, "="], + [:attribute_unquoted_value, "blabar=blo"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_closing_tag result = tokenize('
    ') - assert_equal([ - [:tag_start, "<"], - [:tag_name, "div"], - [:whitespace, " "], - [:attribute_name, "foo"], - [:equal, "="], - [:attribute_quoted_value_start, "\""], - [:attribute_quoted_value, "bar"], - [:attribute_quoted_value_end, "\""], - [:whitespace, " "], - [:solidus, "/"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "foo"], + [:equal, "="], + [:attribute_quoted_value_start, "\""], + [:attribute_quoted_value, "bar"], + [:attribute_quoted_value_end, "\""], + [:whitespace, " "], + [:solidus, "/"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_script_tag result = tokenize("") - assert_equal([ - [:tag_start, "<"], [:tag_name, "script"], [:tag_end, ">"], - [:text, "foo "], [:text, " bar"], - [:tag_start, "<"], [:solidus, "/"], [:tag_name, "script"], [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "script"], + [:tag_end, ">"], + [:text, "foo "], + [:text, " bar"], + [:tag_start, "<"], + [:solidus, "/"], + [:tag_name, "script"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_textarea_tag result = tokenize("") - assert_equal([ - [:tag_start, "<"], [:tag_name, "textarea"], [:tag_end, ">"], - [:text, "hello"], - [:tag_start, "<"], [:solidus, "/"], [:tag_name, "textarea"], [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "textarea"], + [:tag_end, ">"], + [:text, "hello"], + [:tag_start, "<"], + [:solidus, "/"], + [:tag_name, "textarea"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_style_tag result = tokenize("") - assert_equal([ - [:tag_start, "<"], [:tag_name, "style"], [:tag_end, ">"], - [:text, ""], - [:tag_start, "<"], [:solidus, "/"], [:tag_name, "style"], [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "style"], + [:tag_end, ">"], + [:text, ""], + [:tag_start, "<"], + [:solidus, "/"], + [:tag_name, "style"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_script_containing_html result = tokenize('') - assert_equal([ - [:tag_start, "<"], - [:tag_name, "script"], - [:whitespace, " "], - [:attribute_name, "type"], - [:equal, "="], - [:attribute_quoted_value_start, "\""], - [:attribute_quoted_value, "text/html"], - [:attribute_quoted_value_end, "\""], - [:tag_end, ">"], - [:text, "foo "], - [:text, " bar"], - [:tag_start, "<"], - [:solidus, "/"], - [:tag_name, "script"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "script"], + [:whitespace, " "], + [:attribute_name, "type"], + [:equal, "="], + [:attribute_quoted_value_start, "\""], + [:attribute_quoted_value, "text/html"], + [:attribute_quoted_value_end, "\""], + [:tag_end, ">"], + [:text, "foo "], + [:text, " bar"], + [:tag_start, "<"], + [:solidus, "/"], + [:tag_name, "script"], + [:tag_end, ">"], + ], + result, + ) end def test_end_of_tag_on_newline - data = ["\ + data = [ + "\
    ",] + ", + "", + ">", + ] result = tokenize(*data) - assert_equal([ - [:text, " "], - [:tag_start, "<"], - [:tag_name, "div"], - [:whitespace, " "], - [:attribute_name, "define"], - [:equal, "="], - [:attribute_quoted_value_start, "\""], - [:attribute_quoted_value, "{credential_96_credential1: new Shopify.ProviderCredentials()}"], - [:attribute_quoted_value_end, "\""], - [:whitespace, "\n "], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:text, " "], + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "define"], + [:equal, "="], + [:attribute_quoted_value_start, "\""], + [:attribute_quoted_value, "{credential_96_credential1: new Shopify.ProviderCredentials()}"], + [:attribute_quoted_value_end, "\""], + [:whitespace, "\n "], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_multi_part_attribute_name result = tokenize("
    ") - assert_equal([ - [:tag_start, "<"], [:tag_name, "div"], [:whitespace, " "], - [:attribute_name, "data-"], [:attribute_name, "shipping"], [:attribute_name, "-type"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "data-"], + [:attribute_name, "shipping"], + [:attribute_name, "-type"], + [:tag_end, ">"], + ], + result, + ) end def test_tokenize_attribute_name_with_space_before_equal result = tokenize('GST/HST') - assert_equal([ - [:tag_start, "<"], - [:tag_name, "a"], - [:whitespace, " "], - [:attribute_name, "href"], - [:whitespace, " "], - [:equal, "="], - [:attribute_quoted_value_start, "\""], - [:attribute_quoted_value, "http://www.cra-arc.gc.ca/tx/bsnss/tpcs/gst-tps/menu-eng.html"], - [:attribute_quoted_value_end, "\""], - [:tag_end, ">"], - [:text, "GST/HST"], - [:tag_start, "<"], - [:solidus, "/"], - [:tag_name, "a"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "a"], + [:whitespace, " "], + [:attribute_name, "href"], + [:whitespace, " "], + [:equal, "="], + [:attribute_quoted_value_start, "\""], + [:attribute_quoted_value, "http://www.cra-arc.gc.ca/tx/bsnss/tpcs/gst-tps/menu-eng.html"], + [:attribute_quoted_value_end, "\""], + [:tag_end, ">"], + [:text, "GST/HST"], + [:tag_start, "<"], + [:solidus, "/"], + [:tag_name, "a"], + [:tag_end, ">"], + ], + result, + ) end def test_raise_in_block @@ -421,32 +575,43 @@ def test_raise_in_block def test_tokenize_end_of_script_regression result = tokenize("") - assert_equal([ - [:tag_start, "<"], [:tag_name, "script"], [:tag_end, ">"], - [:text, "<"], - [:tag_start, "<"], [:solidus, "/"], [:tag_name, "script"], [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "script"], + [:tag_end, ">"], + [:text, "<"], + [:tag_start, "<"], + [:solidus, "/"], + [:tag_name, "script"], + [:tag_end, ">"], + ], + result, + ) end def test_html_with_mutlibyte_characters data = "
    foo
    " result = tokenize(data) - assert_equal([ - [:tag_start, "<"], - [:tag_name, "div"], - [:whitespace, " "], - [:attribute_name, "title"], - [:equal, "="], - [:attribute_quoted_value_start, "'"], - [:attribute_quoted_value, "your store’s"], - [:attribute_quoted_value_end, "'"], - [:tag_end, ">"], - [:text, "foo"], - [:tag_start, "<"], - [:solidus, "/"], - [:tag_name, "div"], - [:tag_end, ">"], - ], result) + assert_equal( + [ + [:tag_start, "<"], + [:tag_name, "div"], + [:whitespace, " "], + [:attribute_name, "title"], + [:equal, "="], + [:attribute_quoted_value_start, "'"], + [:attribute_quoted_value, "your store’s"], + [:attribute_quoted_value_end, "'"], + [:tag_end, ">"], + [:text, "foo"], + [:tag_start, "<"], + [:solidus, "/"], + [:tag_name, "div"], + [:tag_end, ">"], + ], + result, + ) end private From c2cdd6091d9eefaac31899fb351f938b9ed4d0b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Thu, 14 Mar 2024 22:57:36 +0000 Subject: [PATCH 13/14] Drop support for Ruby 2.7 It is EOL. --- .github/workflows/ruby.yml | 2 +- .rubocop.yml | 2 +- better_html.gemspec | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index fe4351a..35f74da 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [3.3, 3.2, 3.1, '3.0', 2.7] + ruby: [3.3, 3.2, 3.1, '3.0'] gemfile: - Gemfile - gemfiles/Gemfile-rails-6-0 diff --git a/.rubocop.yml b/.rubocop.yml index 23b1ed2..e1c3f16 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,6 +4,6 @@ inherit_gem: rubocop-shopify: rubocop.yml AllCops: - TargetRubyVersion: 2.7 + TargetRubyVersion: 3.0 NewCops: disable SuggestExtensions: false diff --git a/better_html.gemspec b/better_html.gemspec index c7564be..1337f33 100644 --- a/better_html.gemspec +++ b/better_html.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |s| s.description = "Better HTML for Rails. Provides sane html helpers that make it easier to do the right thing." s.license = "MIT" - s.required_ruby_version = ">= 2.7.0" + s.required_ruby_version = ">= 3.0.0" s.metadata = { "bug_tracker_uri" => "https://github.com/Shopify/better-html/issues", From 8f77d960998659d3399a478a0187d63e6bd4afc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Thu, 14 Mar 2024 23:02:58 +0000 Subject: [PATCH 14/14] Prepare for 2.1.0 --- Gemfile.lock | 2 +- lib/better_html/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 29cfda7..f13cf4f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - better_html (2.0.2) + better_html (2.1.0) actionview (>= 6.0) activesupport (>= 6.0) ast (~> 2.0) diff --git a/lib/better_html/version.rb b/lib/better_html/version.rb index d668fa8..34fce26 100644 --- a/lib/better_html/version.rb +++ b/lib/better_html/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module BetterHtml - VERSION = "2.0.2" + VERSION = "2.1.0" end