diff --git a/Gemfile b/Gemfile index 5923ad25..60500f6d 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,7 @@ gem 'actionpack-page_caching' gem 'active_link_to' gem 'bootstrap', '~> 5.3.2' gem 'carrierwave', '~> 3.0' +gem 'commonmarker', '~>1.0.0.pre11' gem 'draper' gem 'friendly_id', '~> 5.5' gem 'globalize' @@ -27,7 +28,6 @@ gem 'oj' # Fast JSON parser and object serializer gem 'pg' # Ruby interface to PostgreSQL RDBMS gem 'puma' # Ruby web server built for concurrency gem 'rails-i18n' -gem 'redcarpet' gem 'route_translator' # Manage translations of routes gem 'sass-rails', '~> 6.0' gem 'simple_form', '~> 5.3' diff --git a/Gemfile.lock b/Gemfile.lock index 3f93646c..221023e1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -160,6 +160,7 @@ GEM descendants_tracker (~> 0.0.1) colored (1.2) colorize (0.8.1) + commonmarker (1.0.0.pre11-x86_64-linux) concurrent-ruby (1.2.2) connection_pool (2.4.1) crass (1.0.6) @@ -281,7 +282,7 @@ GEM language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) - libv8-node (18.16.0.0) + libv8-node (18.16.0.0-x86_64-linux) listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -310,7 +311,6 @@ GEM mini_histogram (0.3.1) mini_magick (4.12.0) mini_mime (1.1.5) - mini_portile2 (2.8.5) mini_racer (0.8.0) libv8-node (~> 18.16.0.0) minitest (5.20.0) @@ -329,8 +329,7 @@ GEM net-protocol net-ssh (7.2.0) nio4r (2.5.9) - nokogiri (1.15.4) - mini_portile2 (~> 2.8.2) + nokogiri (1.15.4-x86_64-linux) racc (~> 1.4) normalize-rails (8.0.1) oj (3.16.1) @@ -579,6 +578,7 @@ GEM PLATFORMS ruby + x86_64-linux DEPENDENCIES actionpack-action_caching @@ -596,6 +596,7 @@ DEPENDENCIES capybara carrierwave (~> 3.0) colored + commonmarker (~> 1.0.0.pre11) cuprite database_cleaner (~> 2.0.1) derailed_benchmarks @@ -627,7 +628,6 @@ DEPENDENCIES rails-controller-testing rails-i18n rails_best_practices - redcarpet reek route_translator rspec-rails (~> 6.0.3) diff --git a/config/initializers/markdown.rb b/config/initializers/markdown.rb index e75d53c6..eb72c29c 100644 --- a/config/initializers/markdown.rb +++ b/config/initializers/markdown.rb @@ -1,40 +1,6 @@ -require 'redcarpet' - -module ActionView - module Template::Handlers - # Rails template handler for Markdown - class Markdown - class_attribute :default_format - self.default_format = Mime[:html] - - # @param template [ActionView::Template] - # @return [String] Ruby code that when evaluated will return the rendered - # content - def call(_template, body) - @markdown ||= Redcarpet::Markdown.new(renderer, params) - "#{@markdown.render(body).inspect}.html_safe" - end - - private - - def params - { - autolink: true, fenced_code_blocks: true, - space_after_headers: true, tables: true - } - end - - def renderer - options = { - link_attributes: { rel: 'nofollow' } - } - Redcarpet::Render::HTML.new(options) - end - end - end -end +require 'handlers/markdown_handler' ActionView::Template.register_template_handler( :md, - ActionView::Template::Handlers::Markdown.new + Handlers::MarkdownHandler.new ) diff --git a/lib/handlers/markdown_handler.rb b/lib/handlers/markdown_handler.rb new file mode 100644 index 00000000..fba4bbae --- /dev/null +++ b/lib/handlers/markdown_handler.rb @@ -0,0 +1,12 @@ +require 'markdown' + +module Handlers + class MarkdownHandler + class_attribute :default_format + self.default_format = Mime[:html] + + def call(_template, body) + Markdown.new(body).to_html.inspect + end + end +end diff --git a/lib/markdown.rb b/lib/markdown.rb new file mode 100644 index 00000000..ac74922a --- /dev/null +++ b/lib/markdown.rb @@ -0,0 +1,25 @@ +require 'commonmarker' + +class Markdown + RENDER = { + hardbreaks: false + }.freeze + + def initialize(input) + @input = input + end + + def to_html + Commonmarker.to_html(input, options:) + end + + private + + attr_reader :input + + def options + { + render: RENDER + } + end +end diff --git a/spec/lib/markdown_spec.rb b/spec/lib/markdown_spec.rb new file mode 100644 index 00000000..7e771ea8 --- /dev/null +++ b/spec/lib/markdown_spec.rb @@ -0,0 +1,214 @@ +require 'spec_helper' +require 'markdown' + +RSpec.describe Markdown do + describe '#to_html' do + it 'returns the HTML code that corresponds to the important text in Markdown' do + important_text_in_markdown = '**Hello** world' + expected_result_in_html = "
Hello world
\n" + markdown = described_class.new(important_text_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the emphasized text in Markdown' do + emphasized_text_in_markdown = 'Hello *world*' + expected_result_in_html = "Hello world
\n" + markdown = described_class.new(emphasized_text_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the striked text in Markdown' do + strikethrough_in_markdown = 'Hello ~~World~~' + expected_result_in_html = "Hello World
My name is 'Bond, James Bond'
\n" + markdown = described_class.new(input) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the inline code in Markdown' do + inline_code_in_markdown = 'This is `inline code`' + expected_result_in_html = "This is inline code
' \
+ '' \
+ 'puts ' \
+ ''' \
+ 'Hello world' \
+ "'\n
\n"
+ markdown = described_class.new(block_code_in_markdown)
+
+ expect(markdown.to_html).to eq(expected_result_in_html)
+ end
+
+ it 'returns the HTML code that corresponds to the header in Markdown' do
+ header_in_markdown = '# The most important heading'
+ expected_result_in_html = 'This is the FractalSoft
+ HTML + markdown = described_class.new(link_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the link in Markdown with www recognition' do + markdown_with_www_link = 'Visit www.fractalsoft.org' + expected_result_in_html = <<~HTML +Visit www.fractalsoft.org
+ HTML + markdown = described_class.new(markdown_with_www_link) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the image as link in Markdown' do + image_as_link_in_markdown = '[![My image](/path/to/image)](https://fractalsoft.org/)' + expected_result_in_html = <<~HTML + + HTML + markdown = described_class.new(image_as_link_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the link in header in Markdown' do + header_link_in_markdown = '# My Header [link](https://fractalsoft.org/)' + expected_result_in_html = '\n\n" + markdown = described_class.new(quote_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the ordered list in Markdown' do + ordered_list_in_markdown = <<~ORDERED_LIST + 1. First item + 2. Second item + 3. Third item + ORDERED_LIST + expected_result_in_html = <<~HTML +Be or not to be
\n
Syntax | +Description | +
---|---|
Header | +Title | +
Paragraph | +Text | +