From adc2c88c79a7d4cc42f1ac7caa7d26fe4d1665bb Mon Sep 17 00:00:00 2001 From: Jared White Date: Tue, 16 Apr 2024 15:28:11 -0700 Subject: [PATCH] Add more functionality to Foundation - translation safety - module nesting - also clean up outdated subclass tracking for route class files --- .../lib/bridgetown-core/helpers.rb | 9 +++- .../lib/bridgetown-core/rack/boot.rb | 2 - .../lib/bridgetown-core/rack/routes.rb | 26 +--------- .../test/source/src/_locales/en.yml | 1 + bridgetown-core/test/test_ruby_helpers.rb | 5 ++ bridgetown-foundation/.rubocop.yml | 1 + .../bridgetown/foundation/core_ext/module.rb | 32 +++++++++++++ .../foundation/safe_translations.rb | 47 +++++++++++++++++++ bridgetown-foundation/script/console | 12 +++++ bridgetown-website/Gemfile | 1 + bridgetown-website/Gemfile.lock | 8 ++++ 11 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 bridgetown-foundation/lib/bridgetown/foundation/core_ext/module.rb create mode 100644 bridgetown-foundation/lib/bridgetown/foundation/safe_translations.rb create mode 100755 bridgetown-foundation/script/console diff --git a/bridgetown-core/lib/bridgetown-core/helpers.rb b/bridgetown-core/lib/bridgetown-core/helpers.rb index 9f181dad8..4d9be29e2 100644 --- a/bridgetown-core/lib/bridgetown-core/helpers.rb +++ b/bridgetown-core/lib/bridgetown-core/helpers.rb @@ -155,10 +155,17 @@ def translate(key, **options) # rubocop:disable Metrics/CyclomaticComplexity, Me key = "#{view_path.tr("/", ".")}#{key}" if view_path.present? end - ActiveSupport::HtmlSafeTranslation.translate(key, **options) + return I18n.translate(key, **options) unless %r{(?:_|\b)html\z}.match?(key) + + translate_with_html(key, **options) end alias_method :t, :translate + def translate_with_html(key, **options) + escaper = ->(input) { input.to_s.encode(xml: :attr).gsub(%r{\A"|"\Z}, "") } + Bridgetown::Foundation::SafeTranslations.translate(key, escaper, **options) + end + # Delegates to I18n.localize with no additional functionality. # # @return [String] the localized string diff --git a/bridgetown-core/lib/bridgetown-core/rack/boot.rb b/bridgetown-core/lib/bridgetown-core/rack/boot.rb index f99ad186b..126acd124 100644 --- a/bridgetown-core/lib/bridgetown-core/rack/boot.rb +++ b/bridgetown-core/lib/bridgetown-core/rack/boot.rb @@ -65,7 +65,6 @@ def self.autoload_server_folder( # rubocop:todo Metrics loader.reload loader.eager_load - Bridgetown::Rack::Routes.reload_subclasses rescue SyntaxError => e Bridgetown::Errors.print_build_error(e) end.start @@ -78,7 +77,6 @@ def self.autoload_server_folder( # rubocop:todo Metrics next unless load_path == server_folder loader.eager_load - Bridgetown::Rack::Routes.reload_subclasses end loaders_manager.setup_loaders([server_folder]) diff --git a/bridgetown-core/lib/bridgetown-core/rack/routes.rb b/bridgetown-core/lib/bridgetown-core/rack/routes.rb index 8002248ef..d656d4ea0 100644 --- a/bridgetown-core/lib/bridgetown-core/rack/routes.rb +++ b/bridgetown-core/lib/bridgetown-core/rack/routes.rb @@ -45,9 +45,6 @@ def print_routes end # rubocop:enable Bridgetown/NoPutsAllowed, Metrics/MethodLength - # @return [Hash] - attr_accessor :tracked_subclasses - # @return [Proc] attr_accessor :router_block @@ -59,30 +56,9 @@ def <=>(other) "#{priorities[priority]}#{self}" <=> "#{priorities[other.priority]}#{other}" end - # @param base [Class(Routes)] - def inherited(base) - Bridgetown::Rack::Routes.track_subclass base - super - end - - # @param klass [Class(Routes)] - def track_subclass(klass) - Bridgetown::Rack::Routes.tracked_subclasses ||= {} - Bridgetown::Rack::Routes.tracked_subclasses[klass.name] = klass - end - # @return [Array] def sorted_subclasses - Bridgetown::Rack::Routes.tracked_subclasses&.values&.sort - end - - # @return [void] - def reload_subclasses - Bridgetown::Rack::Routes.tracked_subclasses&.each_key do |klassname| - Kernel.const_get(klassname) - rescue NameError - Bridgetown::Rack::Routes.tracked_subclasses.delete klassname - end + Bridgetown::Rack::Routes.descendants.sort end # Add a router block via the current Routes class diff --git a/bridgetown-core/test/source/src/_locales/en.yml b/bridgetown-core/test/source/src/_locales/en.yml index 9b00003ec..2cc483fe1 100644 --- a/bridgetown-core/test/source/src/_locales/en.yml +++ b/bridgetown-core/test/source/src/_locales/en.yml @@ -3,6 +3,7 @@ en: foo: foo bar: bar foo_html: foo + dangerous_html: contacts: bar: foo: foo diff --git a/bridgetown-core/test/test_ruby_helpers.rb b/bridgetown-core/test/test_ruby_helpers.rb index 395178b12..88505158c 100644 --- a/bridgetown-core/test/test_ruby_helpers.rb +++ b/bridgetown-core/test/test_ruby_helpers.rb @@ -97,6 +97,11 @@ def setup assert @helpers.translate("about.foo_html").html_safe? end + should "return escaped interpolated values within html safe translation" do + assert_equal "", + @helpers.translate("about.dangerous_html", me: "Me") + end + should "not return html safe string when key does not end with _html" do refute @helpers.translate("about.foo").html_safe? end diff --git a/bridgetown-foundation/.rubocop.yml b/bridgetown-foundation/.rubocop.yml index 7e1ca7a38..862e28a88 100644 --- a/bridgetown-foundation/.rubocop.yml +++ b/bridgetown-foundation/.rubocop.yml @@ -4,3 +4,4 @@ inherit_from: ../.rubocop.yml AllCops: Exclude: - "*.gemspec" + - "script/console" diff --git a/bridgetown-foundation/lib/bridgetown/foundation/core_ext/module.rb b/bridgetown-foundation/lib/bridgetown/foundation/core_ext/module.rb new file mode 100644 index 000000000..76f025b8b --- /dev/null +++ b/bridgetown-foundation/lib/bridgetown/foundation/core_ext/module.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Bridgetown::Foundation + module CoreExt + module Module + module Nested + def nested_within?(other) + other.nested_parents.within?(nested_parents[1..]) + end + + def nested_parents + return [] unless name + + nesting_segments = name.split("::")[...-1] + nesting_segments.map.each_with_index do |_nesting_name, index| + Kernel.const_get(nesting_segments[..-(index + 1)].join("::")) + end + end + + def nested_parent + nested_parents.first + end + + def nested_name + name&.split("::")&.last + end + end + + ::Module.include Nested + end + end +end diff --git a/bridgetown-foundation/lib/bridgetown/foundation/safe_translations.rb b/bridgetown-foundation/lib/bridgetown/foundation/safe_translations.rb new file mode 100644 index 000000000..99aeec911 --- /dev/null +++ b/bridgetown-foundation/lib/bridgetown/foundation/safe_translations.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Bridgetown + module Foundation + # NOTE: this is tested by `test/test_ruby_helpers.rb` in bridgetown-core + # + # This is loosely based on the HtmlSafeTranslation module from ActiveSupport, but you can + # actually use it for any kind of safety use case in a translation setting because its + # decoupled from any specific escaping or safety mechanisms. + module SafeTranslations + def self.translate(key, escaper, safety_method = :html_safe, **options) + safe_options = escape_translation_options(options, escaper) + + i18n_error = false + + exception_handler = ->(*args) do + i18n_error = true + I18n.exception_handler.(*args) + end + + I18n.translate(key, **safe_options, exception_handler:).then do |translation| + i18n_error ? translation : safe_translation(translation, safety_method) + end + end + + def self.escape_translation_options(options, escaper) + @reserved_i18n_keys ||= I18n::RESERVED_KEYS.to_set + + options.to_h do |name, value| + unless @reserved_i18n_keys.include?(name) || (name == :count && value.is_a?(Numeric)) + next [name, escaper.(value)] + end + + [name, value] + end + end + + def self.safe_translation(translation, safety_method) + @safe_value ||= -> { _1.respond_to?(safety_method) ? _1.send(safety_method) : _1 } + + return translation.map { @safe_value.(_1) } if translation.respond_to?(:map) + + @safe_value.(translation) + end + end + end +end diff --git a/bridgetown-foundation/script/console b/bridgetown-foundation/script/console new file mode 100755 index 000000000..9d1cba79b --- /dev/null +++ b/bridgetown-foundation/script/console @@ -0,0 +1,12 @@ +#!/usr/bin/env ruby + +require "bundler" +Bundler.setup + +require "bridgetown-foundation" + +module Bridgetown + module Foundation + binding.irb + end +end diff --git a/bridgetown-website/Gemfile b/bridgetown-website/Gemfile index bf8a0d76e..1a97aee70 100644 --- a/bridgetown-website/Gemfile +++ b/bridgetown-website/Gemfile @@ -6,6 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem "bridgetown", path: "../bridgetown" gem "bridgetown-builder", path: "../bridgetown-builder" gem "bridgetown-core", path: "../bridgetown-core" +gem "bridgetown-foundation", path: "../bridgetown-foundation" gem "bridgetown-paginate", path: "../bridgetown-paginate" gem "puma", "< 7" diff --git a/bridgetown-website/Gemfile.lock b/bridgetown-website/Gemfile.lock index 94e21fda7..0a68ee808 100644 --- a/bridgetown-website/Gemfile.lock +++ b/bridgetown-website/Gemfile.lock @@ -12,6 +12,7 @@ PATH activesupport (>= 6.0, < 8.0) addressable (~> 2.4) amazing_print (~> 1.2) + bridgetown-foundation (= 1.3.4) colorator (~> 1.0) csv (~> 3.2) erubi (~> 1.9) @@ -34,6 +35,12 @@ PATH tilt (~> 2.0) zeitwerk (~> 2.5) +PATH + remote: ../bridgetown-foundation + specs: + bridgetown-foundation (1.3.4) + zeitwerk (~> 2.5) + PATH remote: ../bridgetown-paginate specs: @@ -147,6 +154,7 @@ DEPENDENCIES bridgetown-builder! bridgetown-core! bridgetown-feed (~> 3) + bridgetown-foundation! bridgetown-paginate! bridgetown-quick-search (~> 2.0) bridgetown-seo-tag (~> 6.0)