From d2aa19579e5b092e64e0f8508e8fa94f32ce1b58 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Wed, 14 Jun 2023 15:39:00 +0530 Subject: [PATCH] feat: Adds support for superscript in help center articles (#7279) - Adds support for superscript when rendering article markdown - Chatwoot Markdown Render to render markdown everywhere Co-authored-by: Sojan --- .../api/v1/portals/articles_controller.rb | 4 +- app/helpers/message_format_helper.rb | 4 +- .../conversation_transcript.html.erb | 2 +- .../email_reply.html.erb | 2 +- .../reply_with_summary.html.erb | 2 +- .../reply_without_summary.html.erb | 2 +- lib/chatwoot_markdown_renderer.rb | 26 ++++++++++ lib/superscript_renderer.rb | 28 +++++++++++ .../v1/portals/articles_controller_spec.rb | 6 ++- spec/lib/chatwoot_markdown_renderer_spec.rb | 44 ++++++++++++++++ spec/lib/superscript_renderer_spec.rb | 50 +++++++++++++++++++ 11 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 lib/chatwoot_markdown_renderer.rb create mode 100644 lib/superscript_renderer.rb create mode 100644 spec/lib/chatwoot_markdown_renderer_spec.rb create mode 100644 spec/lib/superscript_renderer_spec.rb diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index b4739e0d1..1f8be3ad7 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -43,8 +43,6 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B end def render_article_content(content) - # rubocop:disable Rails/OutputSafety - CommonMarker.render_html(content).html_safe - # rubocop:enable Rails/OutputSafety + ChatwootMarkdownRenderer.new(content).render_article end end diff --git a/app/helpers/message_format_helper.rb b/app/helpers/message_format_helper.rb index 1e89c56c1..2c50fd609 100644 --- a/app/helpers/message_format_helper.rb +++ b/app/helpers/message_format_helper.rb @@ -7,8 +7,6 @@ module MessageFormatHelper end def render_message_content(message_content) - # rubocop:disable Rails/OutputSafety - CommonMarker.render_html(message_content).html_safe - # rubocop:enable Rails/OutputSafety + ChatwootMarkdownRenderer.new(message_content).render_message end end diff --git a/app/views/mailers/conversation_reply_mailer/conversation_transcript.html.erb b/app/views/mailers/conversation_reply_mailer/conversation_transcript.html.erb index a372a1c43..9bcd231f9 100644 --- a/app/views/mailers/conversation_reply_mailer/conversation_transcript.html.erb +++ b/app/views/mailers/conversation_reply_mailer/conversation_transcript.html.erb @@ -7,7 +7,7 @@ <% if message.content %> - <%= CommonMarker.render_html(message.content).html_safe %> + <%= ChatwootMarkdownRenderer.new(message_content).render_message %> <% end %> <% if message.attachments %> <% message.attachments.each do |attachment| %> diff --git a/app/views/mailers/conversation_reply_mailer/email_reply.html.erb b/app/views/mailers/conversation_reply_mailer/email_reply.html.erb index 197c6ce60..1d943c27e 100644 --- a/app/views/mailers/conversation_reply_mailer/email_reply.html.erb +++ b/app/views/mailers/conversation_reply_mailer/email_reply.html.erb @@ -1,5 +1,5 @@ <% if @message.content %> - <%= CommonMarker.render_html(@message.content).html_safe %> + <%= ChatwootMarkdownRenderer.new(@message.content).render_message %> <% end %> <% if @message.attachments %> <% @message.attachments.each do |attachment| %> diff --git a/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb b/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb index c6cb77ad2..6c498d793 100644 --- a/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb +++ b/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb @@ -13,7 +13,7 @@ <% if (message.content_type == 'input_csat' && message.message_type == 'template') %>

Click here to rate the conversation.

<% elsif message.content.present? %> - <%= CommonMarker.render_html(message.content).html_safe %> + <%= ChatwootMarkdownRenderer.new(message.content).render_message %> <% end %> <% if message.attachments.count.positive? %>

diff --git a/app/views/mailers/conversation_reply_mailer/reply_without_summary.html.erb b/app/views/mailers/conversation_reply_mailer/reply_without_summary.html.erb index 42c7ed6ab..3c9b8034a 100644 --- a/app/views/mailers/conversation_reply_mailer/reply_without_summary.html.erb +++ b/app/views/mailers/conversation_reply_mailer/reply_without_summary.html.erb @@ -1,7 +1,7 @@ <% @messages.each do |message| %>

<% if message.content %> - <%= CommonMarker.render_html(message.content).html_safe %> + <%= ChatwootMarkdownRenderer.new(message.content).render_message %> <% end %> <% if message.attachments %> <% message.attachments.each do |attachment| %> diff --git a/lib/chatwoot_markdown_renderer.rb b/lib/chatwoot_markdown_renderer.rb new file mode 100644 index 000000000..5fa8998b3 --- /dev/null +++ b/lib/chatwoot_markdown_renderer.rb @@ -0,0 +1,26 @@ +class ChatwootMarkdownRenderer + def initialize(content) + @content = content + end + + def render_message + html = CommonMarker.render_html(@content) + render_as_html_safe(html) + end + + def render_article + superscript_renderer = SuperscriptRenderer.new + doc = CommonMarker.render_doc(@content, :DEFAULT) + html = superscript_renderer.render(doc) + + render_as_html_safe(html) + end + + private + + def render_as_html_safe(html) + # rubocop:disable Rails/OutputSafety + html.html_safe + # rubocop:enable Rails/OutputSafety + end +end diff --git a/lib/superscript_renderer.rb b/lib/superscript_renderer.rb new file mode 100644 index 000000000..485ffd1f0 --- /dev/null +++ b/lib/superscript_renderer.rb @@ -0,0 +1,28 @@ +class SuperscriptRenderer < CommonMarker::HtmlRenderer + def text(node) + content = node.string_content + + # Check for presence of '^' in the content + if content.include?('^') + # Split the text and insert tags where necessary + split_content = parse_sup(content) + # Output the transformed content + out(split_content.join) + else + # Output the original content + out(escape_html(content)) + end + end + + private + + def parse_sup(content) + content.split(/(\^[^\^]+\^)/).map do |segment| + if segment.start_with?('^') && segment.end_with?('^') + "#{escape_html(segment[1..-2])}" + else + escape_html(segment) + end + end + end +end diff --git a/spec/controllers/public/api/v1/portals/articles_controller_spec.rb b/spec/controllers/public/api/v1/portals/articles_controller_spec.rb index 079a85bc1..86651fee1 100644 --- a/spec/controllers/public/api/v1/portals/articles_controller_spec.rb +++ b/spec/controllers/public/api/v1/portals/articles_controller_spec.rb @@ -6,7 +6,10 @@ RSpec.describe 'Public Articles API', type: :request do let!(:portal) { create(:portal, slug: 'test-portal', config: { allowed_locales: %w[en es] }, custom_domain: 'www.example.com') } let!(:category) { create(:category, name: 'category', portal: portal, account_id: account.id, locale: 'en', slug: 'category_slug') } let!(:category_2) { create(:category, name: 'category', portal: portal, account_id: account.id, locale: 'es', slug: 'category_slug') } - let!(:article) { create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id) } + let!(:article) do + create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id, + content: 'This is a *test* content with ^markdown^') + end before do ENV['HELPCENTER_URL'] = ENV.fetch('FRONTEND_URL', nil) @@ -43,6 +46,7 @@ RSpec.describe 'Public Articles API', type: :request do it 'Fetch article with the id' do get "/hc/#{portal.slug}/articles/#{article.slug}" expect(response).to have_http_status(:success) + expect(response.body).to include(ChatwootMarkdownRenderer.new(article.content).render_article) expect(article.reload.views).to eq 1 end diff --git a/spec/lib/chatwoot_markdown_renderer_spec.rb b/spec/lib/chatwoot_markdown_renderer_spec.rb new file mode 100644 index 000000000..74e6777c2 --- /dev/null +++ b/spec/lib/chatwoot_markdown_renderer_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +RSpec.describe ChatwootMarkdownRenderer do + let(:markdown_content) { 'This is a *test* content with ^markdown^' } + let(:doc) { instance_double(CommonMarker::Node) } + let(:renderer) { described_class.new(markdown_content) } + let(:superscript_renderer) { instance_double(SuperscriptRenderer) } + let(:html_content) { '

This is a test content with markdown

' } + + before do + allow(CommonMarker).to receive(:render_doc).with(markdown_content, :DEFAULT).and_return(doc) + allow(SuperscriptRenderer).to receive(:new).and_return(superscript_renderer) + allow(superscript_renderer).to receive(:render).with(doc).and_return(html_content) + end + + describe '#render_article' do + let(:rendered_content) { renderer.render_article } + + it 'renders the markdown content to html' do + expect(rendered_content.to_s).to eq(html_content) + end + + it 'returns an html safe string' do + expect(rendered_content).to be_html_safe + end + end + + describe '#render_message' do + let(:message_html_content) { '

This is a test content with ^markdown^

' } + let(:rendered_message) { renderer.render_message } + + before do + allow(CommonMarker).to receive(:render_html).with(markdown_content).and_return(message_html_content) + end + + it 'renders the markdown message to html' do + expect(rendered_message.to_s).to eq(message_html_content) + end + + it 'returns an html safe string' do + expect(rendered_message).to be_html_safe + end + end +end diff --git a/spec/lib/superscript_renderer_spec.rb b/spec/lib/superscript_renderer_spec.rb new file mode 100644 index 000000000..ac69e4ef1 --- /dev/null +++ b/spec/lib/superscript_renderer_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +describe SuperscriptRenderer do + let(:renderer) { described_class.new } + + def render_markdown(markdown) + doc = CommonMarker.render_doc(markdown, :DEFAULT) + renderer.render(doc) + end + + describe '#text' do + it 'converts text wrapped in ^ to superscript' do + markdown = 'This is an example of a superscript: ^superscript^.' + expect(render_markdown(markdown)).to include('superscript') + end + + it 'does not convert text not wrapped in ^' do + markdown = 'This is an example without superscript.' + expect(render_markdown(markdown)).not_to include('') + end + + it 'converts multiple superscripts in the same text' do + markdown = 'This is an example with ^multiple^ ^superscripts^.' + rendered_html = render_markdown(markdown) + expect(rendered_html.scan('').length).to eq(2) + expect(rendered_html).to include('multiple') + expect(rendered_html).to include('superscripts') + end + end + + describe 'broken ^ usage' do + it 'does not convert text that only starts with ^' do + markdown = 'This is an example with ^broken superscript.' + expected_output = '

This is an example with ^broken superscript.

' + expect(render_markdown(markdown)).to include(expected_output) + end + + it 'does not convert text that only ends with ^' do + markdown = 'This is an example with broken^ superscript.' + expected_output = '

This is an example with broken^ superscript.

' + expect(render_markdown(markdown)).to include(expected_output) + end + + it 'does not convert text with uneven numbers of ^' do + markdown = 'This is an example with ^broken^ superscript^.' + expected_output = '

This is an example with broken superscript^.

' + expect(render_markdown(markdown)).to include(expected_output) + end + end +end