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 "bcrypt", "~> 3.1.7"
|
||||||
gem "geared_pagination", "~> 1.2"
|
gem "geared_pagination", "~> 1.2"
|
||||||
gem "rqrcode"
|
gem "rqrcode"
|
||||||
gem "redcarpet"
|
|
||||||
gem "rouge"
|
gem "rouge"
|
||||||
gem "jbuilder"
|
gem "jbuilder"
|
||||||
gem "lexxy", bc: "lexxy"
|
gem "lexxy", bc: "lexxy"
|
||||||
|
|||||||
@@ -355,7 +355,6 @@ GEM
|
|||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
tsort
|
tsort
|
||||||
redcarpet (3.6.1)
|
|
||||||
regexp_parser (2.11.3)
|
regexp_parser (2.11.3)
|
||||||
reline (0.6.3)
|
reline (0.6.3)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
@@ -521,7 +520,6 @@ DEPENDENCIES
|
|||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
rack-mini-profiler
|
rack-mini-profiler
|
||||||
rails!
|
rails!
|
||||||
redcarpet
|
|
||||||
rouge
|
rouge
|
||||||
rqrcode
|
rqrcode
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
|
|||||||
@@ -466,7 +466,6 @@ GEM
|
|||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
tsort
|
tsort
|
||||||
redcarpet (3.6.1)
|
|
||||||
regexp_parser (2.11.3)
|
regexp_parser (2.11.3)
|
||||||
reline (0.6.3)
|
reline (0.6.3)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
@@ -676,7 +675,6 @@ DEPENDENCIES
|
|||||||
rack-mini-profiler
|
rack-mini-profiler
|
||||||
rails!
|
rails!
|
||||||
rails_structured_logging!
|
rails_structured_logging!
|
||||||
redcarpet
|
|
||||||
rouge
|
rouge
|
||||||
rqrcode
|
rqrcode
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
|
|||||||
@@ -220,6 +220,15 @@
|
|||||||
.card__title-link {
|
.card__title-link {
|
||||||
color: inherit;
|
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 {
|
.card__description {
|
||||||
|
|||||||
@@ -2,4 +2,10 @@ module HtmlHelper
|
|||||||
def format_html(html)
|
def format_html(html)
|
||||||
Loofah::HTML5::DocumentFragment.parse(html).scrub!(AutoLinkScrubber.new).to_html.html_safe
|
Loofah::HTML5::DocumentFragment.parse(html).scrub!(AutoLinkScrubber.new).to_html.html_safe
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<h1 class="card__title flex align-start gap-half">
|
<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>
|
</h1>
|
||||||
|
|
||||||
<% unless card.description.blank? %>
|
<% unless card.description.blank? %>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<div class="card__body justify-space-between">
|
<div class="card__body justify-space-between">
|
||||||
<div class="card__content">
|
<div class="card__content">
|
||||||
<h3 class="card__title overflow-line-clamp">
|
<h3 class="card__title overflow-line-clamp">
|
||||||
<%= card.title %>
|
<%= card_html_title(card) %>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="card__body justify-space-between">
|
<div class="card__body justify-space-between">
|
||||||
<div class="card__content">
|
<div class="card__content">
|
||||||
<h1 class="card__title overflow-line-clamp">
|
<h1 class="card__title overflow-line-clamp">
|
||||||
<%= card.title %>
|
<%= card_html_title(card) %>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<hr class="notification__board-separator">
|
<hr class="notification__board-separator">
|
||||||
<h2 class="notification__board"><%= link_to board.name, board %></h2>
|
<h2 class="notification__board"><%= link_to board.name, board %></h2>
|
||||||
<% board_notifications.group_by(&:card).each do |card, notifications| %>
|
<% 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 %>
|
<%= render partial: "notification/bundle_mailer/notification", collection: notifications, as: :notification %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="card__content">
|
<div class="card__content">
|
||||||
<h1 class="card__title flex align-start gap-half">
|
<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>
|
</h1>
|
||||||
<div class="card__description lexxy-content" data-controller="syntax-highlight">
|
<div class="card__description lexxy-content" data-controller="syntax-highlight">
|
||||||
<%= card.description %>
|
<%= 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/initializers", under: "bridge/initializers"
|
||||||
pin_all_from "app/javascript/bridge/helpers", under: "bridge/helpers"
|
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_all_from "app/javascript/bridge/controllers/bridge", under: "controllers/bridge", to: "bridge/controllers/bridge"
|
||||||
pin "marked" # @15.0.11
|
|
||||||
pin "lexxy"
|
pin "lexxy"
|
||||||
pin "@rails/activestorage", to: "activestorage.esm.js"
|
pin "@rails/activestorage", to: "activestorage.esm.js"
|
||||||
pin "@rails/actiontext", to: "actiontext.esm.js"
|
pin "@rails/actiontext", to: "actiontext.esm.js"
|
||||||
|
|||||||
@@ -42,9 +42,14 @@ class CardsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_redirected_to card_draft_path(card)
|
assert_redirected_to card_draft_path(card)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "show" do
|
test "show renders inline code in title" do
|
||||||
get card_path(cards(:logo))
|
card = cards(:logo)
|
||||||
assert_response :success
|
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
|
end
|
||||||
|
|
||||||
test "edit" do
|
test "edit" do
|
||||||
|
|||||||
@@ -119,4 +119,29 @@ class HtmlHelperTest < ActionView::TestCase
|
|||||||
assert_no_match(/<img/, output, "should not create an img element")
|
assert_no_match(/<img/, output, "should not create an img element")
|
||||||
assert_includes output, "<img"
|
assert_includes output, "<img"
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -87,6 +87,16 @@ class Notification::BundleMailerTest < ActionMailer::TestCase
|
|||||||
assert_includes card_titles, "#2 Layout is broken"
|
assert_includes card_titles, "#2 Layout is broken"
|
||||||
end
|
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
|
private
|
||||||
def create_notification(user, source: events(:logo_published))
|
def create_notification(user, source: events(:logo_published))
|
||||||
Notification.create!(user: user, creator: user, source: source, created_at: 30.minutes.ago)
|
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