From 4d8b4e078ba3bce5fbd591c4f2661341e23bca92 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 22 Feb 2023 05:00:42 +0100 Subject: [PATCH] amelioration(a11y): extrait un nouveau composant pour rendre du texte saisi par un humain accessible --- Gemfile | 1 + Gemfile.lock | 2 + app/components/simple_format_component.rb | 51 +++++++++++++ .../simple_format_component.html.haml | 1 + app/lib/redcarpet/bare_renderer.rb | 21 ++++++ .../simple_format_component_spec.rb | 73 +++++++++++++++++++ 6 files changed, 149 insertions(+) create mode 100644 app/components/simple_format_component.rb create mode 100644 app/components/simple_format_component/simple_format_component.html.haml create mode 100644 app/lib/redcarpet/bare_renderer.rb create mode 100644 spec/components/simple_format_component_spec.rb diff --git a/Gemfile b/Gemfile index 3fcb4c749..214c8c5b0 100644 --- a/Gemfile +++ b/Gemfile @@ -70,6 +70,7 @@ gem 'rack-attack' gem 'rails' gem 'rails-i18n' # Locales par défaut gem 'rake-progressbar', require: false +gem 'redcarpet' gem 'rexml' # add missing gem due to ruby3 (https://github.com/Shopify/bootsnap/issues/325) gem 'rgeo-geojson' gem 'rqrcode' diff --git a/Gemfile.lock b/Gemfile.lock index 02f1dc484..2f1796a89 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -573,6 +573,7 @@ GEM rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) + redcarpet (3.6.0) regexp_parser (2.6.0) request_store (1.5.0) rack (>= 1.4) @@ -909,6 +910,7 @@ DEPENDENCIES rails-erd rails-i18n rake-progressbar + redcarpet rexml rgeo-geojson rqrcode diff --git a/app/components/simple_format_component.rb b/app/components/simple_format_component.rb new file mode 100644 index 000000000..19a2d0e35 --- /dev/null +++ b/app/components/simple_format_component.rb @@ -0,0 +1,51 @@ +class SimpleFormatComponent < ApplicationComponent + # see: https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use + REDCARPET_EXTENSIONS = { + no_intra_emphasis: false, + tables: false, + fenced_code_blocks: false, + autolink: false, + disable_indented_code_blocks: false, + strikethrough: false, + lax_spacing: false, + space_after_headers: false, + superscript: false, + underline: false, + highlight: false, + quote: false, + footnotes: false + } + + # see: https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch + REDCARPET_RENDERER_OPTS = { + no_images: true + } + + def initialize(text, allow_a: true, class_names_map: {}) + @text = (text || "").gsub(/\R/, "\n\n") # force double \n otherwise a single one won't split paragraph + .split("\n\n") # + .map(&:lstrip) # this block prevent redcarpet to consider " text" as block code by lstriping + .join("\n\n") # + @allow_a = allow_a + @renderer = Redcarpet::Markdown.new( + Redcarpet::BareRenderer.new(link_attributes: external_link_attributes, class_names_map: class_names_map), + REDCARPET_EXTENSIONS.merge(autolink: @allow_a) + ) + end + + def external_link_attributes + { target: '_blank', rel: 'noopener noreferrer' } + end + + def tags + if @allow_a + Rails.configuration.action_view.sanitized_allowed_tags + ['a'] + else + Rails.configuration.action_view.sanitized_allowed_tags + end + end + + def attributes + ['target', 'rel', 'href', 'class'] + end +end diff --git a/app/components/simple_format_component/simple_format_component.html.haml b/app/components/simple_format_component/simple_format_component.html.haml new file mode 100644 index 000000000..61913e949 --- /dev/null +++ b/app/components/simple_format_component/simple_format_component.html.haml @@ -0,0 +1 @@ += sanitize(@renderer.render(@text), tags:, attributes:) diff --git a/app/lib/redcarpet/bare_renderer.rb b/app/lib/redcarpet/bare_renderer.rb new file mode 100644 index 000000000..645570662 --- /dev/null +++ b/app/lib/redcarpet/bare_renderer.rb @@ -0,0 +1,21 @@ +module Redcarpet + class BareRenderer < Redcarpet::Render::HTML + include ActionView::Helpers::TagHelper + + # won't use rubocop tag method because it is missing output buffer + # rubocop:disable Rails/ContentTag + def list(content, list_type) + tag = list_type == :ordered ? :ol : :ul + content_tag(tag, content, { class: @options[:class_names_map].fetch(:list) {} }, false) + end + + def list_item(content, list_type) + content_tag(:li, content.strip.gsub(/<\/?p>/, ''), {}, false) + end + + def paragraph(text) + content_tag(:p, text, { class: @options[:class_names_map].fetch(:paragraph) {} }, false) + end + # rubocop:enable Rails/ContentTag + end +end diff --git a/spec/components/simple_format_component_spec.rb b/spec/components/simple_format_component_spec.rb new file mode 100644 index 000000000..3cc691718 --- /dev/null +++ b/spec/components/simple_format_component_spec.rb @@ -0,0 +1,73 @@ +describe SimpleFormatComponent, type: :component do + let(:allow_a) { false } + before { render_inline(described_class.new(text, allow_a: allow_a)) } + + context 'one line' do + let(:text) do + "1er paragraphe" + end + it { expect(page).to have_selector("p", count: 1, text: text) } + end + + context 'one with leading spaces' do + let(:text) do + <<-TEXT + 1er paragraphe +TEXT + end + it { expect(page).to have_selector("p", count: 1, text: text.strip) } + end + + context 'two lines' do + let(:text) do + <<~TEXT + 1er paragraphe + 2eme paragraphe + TEXT + end + + it { expect(page).to have_selector("p", count: 2) } + it { text.split("\n").map(&:strip).map { expect(page).to have_text(_1) } } + end + + context 'unordered list items' do + let(:text) do + <<~TEXT + - 1er paragraphe + - paragraphe + TEXT + end + + it { expect(page).to have_selector("ul", count: 1) } + it { expect(page).to have_selector("li", count: 2) } + end + + context 'ordered list items' do + let(:text) do + <<~TEXT + 1. 1er paragraphe + 2. paragraphe + TEXT + end + + it { expect(page).to have_selector("ol", count: 1) } + it { expect(page).to have_selector("li", count: 2) } + end + + context 'auto-link' do + let(:text) do + <<~TEXT + bonjour https://www.demarches-simplifiees.fr + TEXT + end + + context 'enabled' do + let(:allow_a) { true } + it { expect(page).to have_selector("a") } + end + + context 'disabled' do + it { expect(page).not_to have_selector("a") } + end + end +end