Render inline code in card titles (#2518)
* Remove unused marked JS dependency * Remove unused redcarpet dependency * Render inline code in card titles Add card_html_title helper that HTML-escapes input then converts backtick-wrapped text to <code> elements. Apply to card titles in board preview, card detail, public views, and notification emails. Style inline code elements in titles to match description styling. Co-authored-by: Andy Smith <andy@37signals.com> --------- Co-authored-by: Andy Smith <andy@37signals.com>
This commit is contained in:
@@ -25,7 +25,6 @@ gem "trilogy", "~> 2.9"
|
||||
gem "bcrypt", "~> 3.1.7"
|
||||
gem "geared_pagination", "~> 1.2"
|
||||
gem "rqrcode"
|
||||
gem "redcarpet"
|
||||
gem "rouge"
|
||||
gem "jbuilder"
|
||||
gem "lexxy", bc: "lexxy"
|
||||
|
||||
@@ -355,7 +355,6 @@ GEM
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
tsort
|
||||
redcarpet (3.6.1)
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
@@ -521,7 +520,6 @@ DEPENDENCIES
|
||||
puma (>= 5.0)
|
||||
rack-mini-profiler
|
||||
rails!
|
||||
redcarpet
|
||||
rouge
|
||||
rqrcode
|
||||
rubocop-rails-omakase
|
||||
|
||||
@@ -466,7 +466,6 @@ GEM
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
tsort
|
||||
redcarpet (3.6.1)
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
@@ -676,7 +675,6 @@ DEPENDENCIES
|
||||
rack-mini-profiler
|
||||
rails!
|
||||
rails_structured_logging!
|
||||
redcarpet
|
||||
rouge
|
||||
rqrcode
|
||||
rubocop-rails-omakase
|
||||
|
||||
@@ -220,6 +220,15 @@
|
||||
.card__title-link {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--color-canvas);
|
||||
border: 1px solid var(--color-ink-lighter);
|
||||
border-radius: 0.25ch;
|
||||
font-family: var(--font-mono);
|
||||
font-size: smaller;
|
||||
padding: 0.1ch 0.25ch;
|
||||
}
|
||||
}
|
||||
|
||||
.card__description {
|
||||
|
||||
@@ -2,4 +2,10 @@ module HtmlHelper
|
||||
def format_html(html)
|
||||
Loofah::HTML5::DocumentFragment.parse(html).scrub!(AutoLinkScrubber.new).to_html.html_safe
|
||||
end
|
||||
|
||||
def card_html_title(card)
|
||||
return card.title if card.title.blank?
|
||||
|
||||
ERB::Util.html_escape(card.title).gsub(/`([^`]+)`/, '<code>\1</code>').html_safe
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<h1 class="card__title flex align-start gap-half">
|
||||
<%= link_to card.title, edit_card_path(card), class: "card__title-link" %>
|
||||
<%= link_to card_html_title(card), edit_card_path(card), class: "card__title-link" %>
|
||||
</h1>
|
||||
|
||||
<% unless card.description.blank? %>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="card__body justify-space-between">
|
||||
<div class="card__content">
|
||||
<h3 class="card__title overflow-line-clamp">
|
||||
<%= card.title %>
|
||||
<%= card_html_title(card) %>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="card__body justify-space-between">
|
||||
<div class="card__content">
|
||||
<h1 class="card__title overflow-line-clamp">
|
||||
<%= card.title %>
|
||||
<%= card_html_title(card) %>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<hr class="notification__board-separator">
|
||||
<h2 class="notification__board"><%= link_to board.name, board %></h2>
|
||||
<% board_notifications.group_by(&:card).each do |card, notifications| %>
|
||||
<%= link_to "##{ card.number } #{ card.title }", card, class: "card__title" %>
|
||||
<%= link_to card, class: "card__title" do %>#<%= card.number %> <%= card_html_title(card) %><% end %>
|
||||
<%= render partial: "notification/bundle_mailer/notification", collection: notifications, as: :notification %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="card__content">
|
||||
<h1 class="card__title flex align-start gap-half">
|
||||
<%= tag.span card.title, class: "card__title-link" %>
|
||||
<%= tag.span card_html_title(card), class: "card__title-link" %>
|
||||
</h1>
|
||||
<div class="card__description lexxy-content" data-controller="syntax-highlight">
|
||||
<%= card.description %>
|
||||
|
||||
@@ -13,7 +13,6 @@ pin_all_from "app/javascript/initializers", under: "initializers"
|
||||
pin_all_from "app/javascript/bridge/initializers", under: "bridge/initializers"
|
||||
pin_all_from "app/javascript/bridge/helpers", under: "bridge/helpers"
|
||||
pin_all_from "app/javascript/bridge/controllers/bridge", under: "controllers/bridge", to: "bridge/controllers/bridge"
|
||||
pin "marked" # @15.0.11
|
||||
pin "lexxy"
|
||||
pin "@rails/activestorage", to: "activestorage.esm.js"
|
||||
pin "@rails/actiontext", to: "actiontext.esm.js"
|
||||
|
||||
@@ -42,9 +42,14 @@ class CardsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_redirected_to card_draft_path(card)
|
||||
end
|
||||
|
||||
test "show" do
|
||||
get card_path(cards(:logo))
|
||||
assert_response :success
|
||||
test "show renders inline code in title" do
|
||||
card = cards(:logo)
|
||||
card.update_column :title, "Fix the `bug` in production"
|
||||
|
||||
get card_path(card)
|
||||
assert_select ".card__title-link" do |element|
|
||||
assert_equal "Fix the <code>bug</code> in production", element.inner_html
|
||||
end
|
||||
end
|
||||
|
||||
test "edit" do
|
||||
|
||||
@@ -119,4 +119,29 @@ class HtmlHelperTest < ActionView::TestCase
|
||||
assert_no_match(/<img/, output, "should not create an img element")
|
||||
assert_includes output, "<img"
|
||||
end
|
||||
|
||||
test "card_html_title renders backticks as code elements" do
|
||||
assert_equal "Fix the <code>bug</code> in production", card_html_title(cards(:logo).tap { _1.title = "Fix the `bug` in production" })
|
||||
end
|
||||
|
||||
test "card_html_title renders multiple code spans" do
|
||||
assert_equal "<code>foo</code> and <code>bar</code>", card_html_title(cards(:logo).tap { _1.title = "`foo` and `bar`" })
|
||||
end
|
||||
|
||||
test "card_html_title renders code spans without surrounding spaces" do
|
||||
assert_equal "what<code>about</code>this", card_html_title(cards(:logo).tap { _1.title = "what`about`this" })
|
||||
end
|
||||
|
||||
test "card_html_title escapes HTML tags" do
|
||||
assert_equal "<script>alert(1)</script>", card_html_title(cards(:logo).tap { _1.title = "<script>alert(1)</script>" })
|
||||
end
|
||||
|
||||
test "card_html_title escapes HTML inside backticks" do
|
||||
assert_equal "<code><script></code>", card_html_title(cards(:logo).tap { _1.title = "`<script>`" })
|
||||
end
|
||||
|
||||
test "card_html_title returns blank title as-is" do
|
||||
assert_nil card_html_title(cards(:logo).tap { _1.title = nil })
|
||||
assert_equal "", card_html_title(cards(:logo).tap { _1.title = "" })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -87,6 +87,16 @@ class Notification::BundleMailerTest < ActionMailer::TestCase
|
||||
assert_includes card_titles, "#2 Layout is broken"
|
||||
end
|
||||
|
||||
test "renders inline code in card title" do
|
||||
cards(:logo).update_column :title, "Fix the `bug` in production"
|
||||
create_notification(@user, source: events(:logo_published))
|
||||
|
||||
html = Nokogiri::HTML5(Notification::BundleMailer.notification(@bundle).html_part.body.to_s)
|
||||
|
||||
title_link = html.at_css(".card__title")
|
||||
assert_equal "#1 Fix the <code>bug</code> in production", title_link.inner_html
|
||||
end
|
||||
|
||||
private
|
||||
def create_notification(user, source: events(:logo_published))
|
||||
Notification.create!(user: user, creator: user, source: source, created_at: 30.minutes.ago)
|
||||
|
||||
Vendored
-2583
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user