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

\n" + markdown = described_class.new(strikethrough_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code the corresponds to the text containing quotation marks in Markdown' do + input = "My name is 'Bond, James Bond'" + expected_result_in_html = "

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

\n" + markdown = described_class.new(inline_code_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the block code in Markdown' do + block_code_in_markdown = <<~BLOCK_CODE + ```ruby + puts 'Hello world' + ``` + BLOCK_CODE + expected_result_in_html = '
' \
+                                '' \
+                                '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 = '

' \ + "The most important heading

\n" + markdown = described_class.new(header_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' do + link_in_markdown = 'This is the [FractalSoft](https://fractalsoft.org/)' + expected_result_in_html = <<~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 +

My image

+ 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 = '

' \ + "My Header link

\n" + markdown = described_class.new(header_link_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the quote in Markdown' do + quote_in_markdown = '> Be or not to be' + expected_result_in_html = "
\n

Be or not to be

\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 +
    +
  1. First item
  2. +
  3. Second item
  4. +
  5. Third item
  6. +
+ HTML + markdown = described_class.new(ordered_list_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the unordered list in Markdown' do + unordered_list_in_markdown = <<~UNORDERED_LIST + - First item + - Second item + - Third item + UNORDERED_LIST + expected_result_in_html = <<~HTML + + HTML + markdown = described_class.new(unordered_list_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the image in Markdown' do + image_in_markdown = '![My image](/path/to/image)' + expected_result_in_html = <<~HTML +

My image

+ HTML + markdown = described_class.new(image_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the table in Markdown' do + table_in_markdown = <<~TABLE + | Syntax | Description | + |-------------|-------------| + | Header | Title | + | Paragraph | Text | + TABLE + expected_result_in_html = <<~HTML + + + + + + + + + + + + + + + + + +
SyntaxDescription
HeaderTitle
ParagraphText
+ HTML + markdown = described_class.new(table_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + + it 'returns the HTML code that corresponds to the task list in Markdown' do + task_list_in_markdown = <<~TASK_LIST + - [ ] first to do + - [x] second to do + TASK_LIST + expected_result_in_html = <<~HTML + + HTML + markdown = described_class.new(task_list_in_markdown) + + expect(markdown.to_html).to eq(expected_result_in_html) + end + end +end