FIX: Make Oneboxer#apply insert block Oneboxes correctly (#11449)
It used to insert block Oneboxes inside paragraphs which resulted in
invalid HTML. This needed an additional parsing for removal of empty
paragraphs and the resulting HTML could still be invalid.
This commit ensure that block Oneboxes are inserted correctly, by
splitting the paragraph containing the link and putting the block
between the two. Paragraphs left with nothing but whitespaces will
be removed.
Follow up to 7f3a30d79f
.
This commit is contained in:
parent
a7cc17cff7
commit
2d51833ca9
|
@ -99,32 +99,57 @@ module Oneboxer
|
|||
|
||||
each_onebox_link(doc, extra_paths: extra_paths) do |url, element|
|
||||
onebox, _ = yield(url, element)
|
||||
next if onebox.blank?
|
||||
|
||||
if onebox
|
||||
parsed_onebox = Nokogiri::HTML5::fragment(onebox)
|
||||
next unless parsed_onebox.children.count > 0
|
||||
parsed_onebox = Nokogiri::HTML5::fragment(onebox)
|
||||
next if parsed_onebox.children.blank?
|
||||
|
||||
if element&.parent&.node_name&.downcase == "p" &&
|
||||
element.parent.children.count == 1 &&
|
||||
HTML5_BLOCK_ELEMENTS.include?(parsed_onebox.children[0].node_name.downcase)
|
||||
element = element.parent
|
||||
changed = true
|
||||
|
||||
parent = element.parent
|
||||
if parent&.node_name&.downcase == "p" &&
|
||||
parsed_onebox.children.any? { |child| HTML5_BLOCK_ELEMENTS.include?(child.node_name.downcase) }
|
||||
|
||||
siblings = parent.children
|
||||
element_idx = siblings.find_index(element)
|
||||
before_idx = first_significant_element_index(siblings, element_idx - 1, -1)
|
||||
after_idx = first_significant_element_index(siblings, element_idx + 1, +1)
|
||||
|
||||
if before_idx < 0 && after_idx >= siblings.size
|
||||
parent.replace parsed_onebox
|
||||
elsif before_idx < 0
|
||||
parent.children = siblings[after_idx..siblings.size]
|
||||
parent.add_previous_sibling(parsed_onebox)
|
||||
elsif after_idx >= siblings.size
|
||||
parent.children = siblings[0..before_idx]
|
||||
parent.add_next_sibling(parsed_onebox)
|
||||
else
|
||||
parent_rest = parent.dup
|
||||
|
||||
parent.children = siblings[0..before_idx]
|
||||
parent_rest.children = siblings[after_idx..siblings.size]
|
||||
|
||||
parent.add_next_sibling(parent_rest)
|
||||
parent.add_next_sibling(parsed_onebox)
|
||||
end
|
||||
|
||||
changed = true
|
||||
element.swap parsed_onebox.to_html
|
||||
end
|
||||
end
|
||||
|
||||
# strip empty <p> elements
|
||||
doc.css("p").each do |p|
|
||||
if p.children.empty? && doc.children.count > 1
|
||||
p.remove
|
||||
else
|
||||
element.replace parsed_onebox
|
||||
end
|
||||
end
|
||||
|
||||
Result.new(doc, changed)
|
||||
end
|
||||
|
||||
def self.first_significant_element_index(elements, index, step)
|
||||
while index >= 0 && index < elements.size &&
|
||||
(elements[index].node_name.downcase == "br" ||
|
||||
(elements[index].node_name.downcase == "text" && elements[index].to_html.strip.blank?))
|
||||
index = index + step
|
||||
end
|
||||
|
||||
index
|
||||
end
|
||||
|
||||
def self.is_previewing?(user_id)
|
||||
Discourse.redis.get(preview_key(user_id)) == "1"
|
||||
end
|
||||
|
|
|
@ -413,7 +413,7 @@ describe CookedPostProcessor do
|
|||
it "generates overlay information" do
|
||||
cpp.post_process
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost#{upload.url}" data-download-href="//test.localhost/#{upload_path}/#{upload.sha1}" title="logo.png"><img src="//test.localhost/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_690x788.png" width="690" height="788"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">logo.png</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg></div></a></div></p>
|
||||
HTML
|
||||
|
||||
|
@ -433,7 +433,7 @@ describe CookedPostProcessor do
|
|||
|
||||
cpp.post_process
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><img class="onebox" src="//test.localhost/#{upload_path}/original/1X/1234567890123456.jpg" width="690" height="788"></p>
|
||||
HTML
|
||||
end
|
||||
|
@ -449,7 +449,7 @@ describe CookedPostProcessor do
|
|||
|
||||
cpp.post_process
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><img src="//test.localhost/#{upload_path}/original/1X/1234567890123456.svg" width="690" height="788"></p>
|
||||
HTML
|
||||
end
|
||||
|
@ -576,7 +576,7 @@ describe CookedPostProcessor do
|
|||
it "crops the image" do
|
||||
cpp.post_process
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost#{upload.url}" data-download-href="//test.localhost/#{upload_path}/#{upload.sha1}" title="logo.png"><img src="//test.localhost/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_230x500.png" width="230" height="500"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">logo.png</span><span class="informations">1125×2436 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg></div></a></div></p>
|
||||
HTML
|
||||
|
||||
|
@ -607,7 +607,7 @@ describe CookedPostProcessor do
|
|||
it "generates overlay information" do
|
||||
cpp.post_process
|
||||
|
||||
expect(cpp.html). to match_html <<~HTML.rstrip
|
||||
expect(cpp.html). to match_html <<~HTML
|
||||
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost/subfolder#{upload.url}" data-download-href="//test.localhost/subfolder/#{upload_path}/#{upload.sha1}" title="logo.png"><img src="//test.localhost/subfolder/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_690x788.png" width="690" height="788"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">logo.png</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg></div></a></div></p>
|
||||
HTML
|
||||
|
||||
|
@ -618,7 +618,7 @@ describe CookedPostProcessor do
|
|||
upload.update!(original_filename: "><img src=x onerror=alert('haha')>.png")
|
||||
cpp.post_process
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost/subfolder#{upload.url}" data-download-href="//test.localhost/subfolder/#{upload_path}/#{upload.sha1}" title="&gt;&lt;img src=x onerror=alert(&#39;haha&#39;)&gt;.png"><img src="//test.localhost/subfolder/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_690x788.png" width="690" height="788"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">&gt;&lt;img src=x onerror=alert(&#39;haha&#39;)&gt;.png</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg></div></a></div></p>
|
||||
HTML
|
||||
end
|
||||
|
@ -644,7 +644,7 @@ describe CookedPostProcessor do
|
|||
it "generates overlay information using image title and ignores alt" do
|
||||
cpp.post_process
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost#{upload.url}" data-download-href="//test.localhost/#{upload_path}/#{upload.sha1}" title="WAT"><img src="//test.localhost/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_690x788.png" title="WAT" alt="RED" width="690" height="788"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">WAT</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg></div></a></div></p>
|
||||
HTML
|
||||
|
||||
|
@ -672,7 +672,7 @@ describe CookedPostProcessor do
|
|||
it "generates overlay information using image title" do
|
||||
cpp.post_process
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost#{upload.url}" data-download-href="//test.localhost/#{upload_path}/#{upload.sha1}" title="WAT"><img src="//test.localhost/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_690x788.png" title="WAT" width="690" height="788"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">WAT</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg></div></a></div></p>
|
||||
HTML
|
||||
|
||||
|
@ -700,7 +700,7 @@ describe CookedPostProcessor do
|
|||
it "generates overlay information using image alt" do
|
||||
cpp.post_process
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost#{upload.url}" data-download-href="//test.localhost/#{upload_path}/#{upload.sha1}" title="RED"><img src="//test.localhost/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_690x788.png" alt="RED" width="690" height="788"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">RED</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg></div></a></div></p>
|
||||
HTML
|
||||
|
||||
|
@ -1202,7 +1202,7 @@ describe CookedPostProcessor do
|
|||
|
||||
it "uses schemaless url for uploads" do
|
||||
cpp.optimize_urls
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><a href="//test.localhost/#{upload_path}/original/2X/2345678901234567.jpg">Link</a><br>
|
||||
<img src="//test.localhost/#{upload_path}/original/1X/1234567890123456.jpg"><br>
|
||||
<a href="http://www.google.com" rel="noopener nofollow ugc">Google</a><br>
|
||||
|
@ -1217,7 +1217,7 @@ describe CookedPostProcessor do
|
|||
it "uses schemaless CDN url for http uploads" do
|
||||
Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com")
|
||||
cpp.optimize_urls
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><a href="//my.cdn.com/#{upload_path}/original/2X/2345678901234567.jpg">Link</a><br>
|
||||
<img src="//my.cdn.com/#{upload_path}/original/1X/1234567890123456.jpg"><br>
|
||||
<a href="http://www.google.com" rel="noopener nofollow ugc">Google</a><br>
|
||||
|
@ -1230,7 +1230,7 @@ describe CookedPostProcessor do
|
|||
it "doesn't use schemaless CDN url for https uploads" do
|
||||
Rails.configuration.action_controller.stubs(:asset_host).returns("https://my.cdn.com")
|
||||
cpp.optimize_urls
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><a href="https://my.cdn.com/#{upload_path}/original/2X/2345678901234567.jpg">Link</a><br>
|
||||
<img src="https://my.cdn.com/#{upload_path}/original/1X/1234567890123456.jpg"><br>
|
||||
<a href="http://www.google.com" rel="noopener nofollow ugc">Google</a><br>
|
||||
|
@ -1244,7 +1244,7 @@ describe CookedPostProcessor do
|
|||
SiteSetting.login_required = true
|
||||
Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com")
|
||||
cpp.optimize_urls
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><a href="//my.cdn.com/#{upload_path}/original/2X/2345678901234567.jpg">Link</a><br>
|
||||
<img src="//my.cdn.com/#{upload_path}/original/1X/1234567890123456.jpg"><br>
|
||||
<a href="http://www.google.com" rel="noopener nofollow ugc">Google</a><br>
|
||||
|
@ -1258,7 +1258,7 @@ describe CookedPostProcessor do
|
|||
SiteSetting.prevent_anons_from_downloading_files = true
|
||||
Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com")
|
||||
cpp.optimize_urls
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p><a href="//my.cdn.com/#{upload_path}/original/2X/2345678901234567.jpg">Link</a><br>
|
||||
<img src="//my.cdn.com/#{upload_path}/original/1X/1234567890123456.jpg"><br>
|
||||
<a href="http://www.google.com" rel="noopener nofollow ugc">Google</a><br>
|
||||
|
@ -1297,7 +1297,7 @@ describe CookedPostProcessor do
|
|||
cpp = CookedPostProcessor.new(the_post)
|
||||
cpp.optimize_urls
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p>This post has a local emoji <img src="https://local.cdn.com/images/emoji/twitter/+1.png?v=#{Emoji::EMOJI_VERSION}" title=":+1:" class="emoji" alt=":+1:"> and an external upload</p>
|
||||
<p><img src="https://s3.cdn.com/#{stored_path}" alt="smallest.png" data-base62-sha1="#{upload.base62_sha1}" width="10" height="20"></p>
|
||||
HTML
|
||||
|
@ -1315,7 +1315,7 @@ describe CookedPostProcessor do
|
|||
cpp = CookedPostProcessor.new(the_post)
|
||||
cpp.optimize_urls
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p>This post has a local emoji <img src="https://local.cdn.com/images/emoji/twitter/+1.png?v=#{Emoji::EMOJI_VERSION}" title=":+1:" class="emoji" alt=":+1:"> and an external upload</p>
|
||||
<p><img src="/secure-media-uploads/#{stored_path}" alt="smallest.png" data-base62-sha1="#{upload.base62_sha1}" width="10" height="20"></p>
|
||||
HTML
|
||||
|
@ -1339,17 +1339,14 @@ describe CookedPostProcessor do
|
|||
cpp = CookedPostProcessor.new(the_post.reload)
|
||||
cpp.post_process_oneboxes
|
||||
|
||||
cpp = CookedPostProcessor.new(the_post.reload)
|
||||
cpp.post_process_oneboxes
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p>This post has an S3 video onebox:<br>
|
||||
</p><div class="onebox video-onebox">
|
||||
<video width="100%" height="100%" controls="">
|
||||
<source src="#{video_upload.url}">
|
||||
<a href="#{video_upload.url}" rel="nofollow ugc noopener">#{video_upload.url}</a>
|
||||
</video>
|
||||
</div>
|
||||
<p>This post has an S3 video onebox:</p>
|
||||
<div class="onebox video-onebox">
|
||||
<video width="100%" height="100%" controls="">
|
||||
<source src="#{video_upload.url}">
|
||||
<a href="#{video_upload.url}" rel="nofollow ugc noopener">#{video_upload.url}</a>
|
||||
</video>
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
|
||||
|
@ -1365,15 +1362,13 @@ describe CookedPostProcessor do
|
|||
|
||||
secure_url = video_upload.url.sub(SiteSetting.s3_cdn_url, "#{Discourse.base_url}/secure-media-uploads")
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
<p>This post has an S3 video onebox:<br>
|
||||
<div class="onebox video-onebox">
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p>This post has an S3 video onebox:</p><div class="onebox video-onebox">
|
||||
<video width="100%" height="100%" controls="">
|
||||
<source src="#{secure_url}">
|
||||
<a href="#{secure_url}">#{secure_url}</a>
|
||||
</video>
|
||||
</div>
|
||||
</p>
|
||||
HTML
|
||||
end
|
||||
|
||||
|
@ -1415,20 +1410,18 @@ describe CookedPostProcessor do
|
|||
secure_video_url = video_upload.url.sub(SiteSetting.s3_cdn_url, "#{Discourse.base_url}/secure-media-uploads")
|
||||
secure_audio_url = audio_upload.url.sub(SiteSetting.s3_cdn_url, "#{Discourse.base_url}/secure-media-uploads")
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML.rstrip
|
||||
<p>This post has a video upload.<br>
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p>This post has a video upload.</p>
|
||||
<div class="onebox video-onebox">
|
||||
<video width="100%" height="100%" controls="">
|
||||
<source src="#{secure_video_url}">
|
||||
<a href="#{secure_video_url}">#{secure_video_url}</a>
|
||||
</video>
|
||||
</div>
|
||||
</p>
|
||||
<p>This post has an audio upload.<br>
|
||||
<audio controls=""><source src="#{secure_audio_url}"><a href="#{secure_audio_url}">#{secure_audio_url}</a></audio></p>
|
||||
<p>And an image upload.<br>
|
||||
<img src="#{image_upload.url}" alt="#{image_upload.original_filename}" data-base62-sha1="#{image_upload.base62_sha1}"></p>
|
||||
|
||||
HTML
|
||||
end
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ describe ExcerptParser do
|
|||
</details>
|
||||
HTML
|
||||
|
||||
expect(ExcerptParser.get_excerpt(html, 50, {})).to match_html(<<~HTML.rstrip)
|
||||
expect(ExcerptParser.get_excerpt(html, 50, {})).to match_html <<~HTML
|
||||
<details><summary>FOO</summary>BAR
|
||||
Lorem ipsum dolor sit amet, consectetur adi…</details>
|
||||
HTML
|
||||
|
|
|
@ -300,4 +300,27 @@ describe Oneboxer do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#apply' do
|
||||
it 'generates valid HTML' do
|
||||
raw = "Before Onebox\nhttps://example.com\nAfter Onebox"
|
||||
cooked = Oneboxer.apply(PrettyText.cook(raw)) { '<div>onebox</div>' }
|
||||
doc = Nokogiri::HTML5::fragment(cooked.to_html)
|
||||
expect(doc.to_html).to match_html <<~HTML
|
||||
<p>Before Onebox</p>
|
||||
<div>onebox</div>
|
||||
<p>After Onebox</p>
|
||||
HTML
|
||||
|
||||
raw = "Before Onebox\nhttps://example.com\nhttps://example.com\nAfter Onebox"
|
||||
cooked = Oneboxer.apply(PrettyText.cook(raw)) { '<div>onebox</div>' }
|
||||
doc = Nokogiri::HTML5::fragment(cooked.to_html)
|
||||
expect(doc.to_html).to match_html <<~HTML
|
||||
<p>Before Onebox</p>
|
||||
<div>onebox</div>
|
||||
<div>onebox</div>
|
||||
<p>After Onebox</p>
|
||||
HTML
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -147,7 +147,7 @@ describe UsernameChanger do
|
|||
post = create_post_and_change_username(raw: ".@foo -@foo %@foo _@foo ,@foo ;@foo @@foo")
|
||||
|
||||
expect(post.raw).to eq(".@bar -@bar %@bar _@bar ,@bar ;@bar @@bar")
|
||||
expect(post.cooked).to match_html(<<~HTML.rstrip)
|
||||
expect(post.cooked).to match_html <<~HTML
|
||||
<p>.<a class="mention" href="/u/bar">@bar</a>
|
||||
-<a class="mention" href="/u/bar">@bar</a>
|
||||
%<a class="mention" href="/u/bar">@bar</a>
|
||||
|
@ -169,7 +169,7 @@ describe UsernameChanger do
|
|||
post = create_post_and_change_username(raw: "**@foo** *@foo* _@foo_ ~~@foo~~")
|
||||
|
||||
expect(post.raw).to eq("**@bar** *@bar* _@bar_ ~~@bar~~")
|
||||
expect(post.cooked).to match_html(<<~HTML.rstrip)
|
||||
expect(post.cooked).to match_html <<~HTML
|
||||
<p><strong><a class="mention" href="/u/bar">@bar</a></strong>
|
||||
<em><a class="mention" href="/u/bar">@bar</a></em>
|
||||
<em><a class="mention" href="/u/bar">@bar</a></em>
|
||||
|
@ -181,7 +181,7 @@ describe UsernameChanger do
|
|||
post = create_post_and_change_username(raw: "@foo. @foo, @foo: @foo; @foo_ @foo-")
|
||||
|
||||
expect(post.raw).to eq("@bar. @bar, @bar: @bar; @bar_ @bar-")
|
||||
expect(post.cooked).to match_html(<<~HTML.rstrip)
|
||||
expect(post.cooked).to match_html <<~HTML
|
||||
<p><a class="mention" href="/u/bar">@bar</a>.
|
||||
<a class="mention" href="/u/bar">@bar</a>,
|
||||
<a class="mention" href="/u/bar">@bar</a>:
|
||||
|
@ -225,7 +225,7 @@ describe UsernameChanger do
|
|||
post = create_post_and_change_username(raw: "@foo @foobar @foo-bar @foo_bar @foo1")
|
||||
|
||||
expect(post.raw).to eq("@bar @foobar @foo-bar @foo_bar @foo1")
|
||||
expect(post.cooked).to match_html(<<~HTML.rstrip)
|
||||
expect(post.cooked).to match_html <<~HTML
|
||||
<p><a class="mention" href="/u/bar">@bar</a> <a class="mention" href="/u/foobar">@foobar</a> <a class="mention" href="/u/foo-bar">@foo-bar</a> <a class="mention" href="/u/foo_bar">@foo_bar</a> <a class="mention" href="/u/foo1">@foo1</a></p>
|
||||
HTML
|
||||
end
|
||||
|
@ -312,7 +312,7 @@ describe UsernameChanger do
|
|||
post = create_post_and_change_username(raw: "@թռչուն @թռչուն鳥 @թռչուն-鳥 @թռչուն_鳥 @թռչուն٩", target_username: 'птица')
|
||||
|
||||
expect(post.raw).to eq("@птица @թռչուն鳥 @թռչուն-鳥 @թռչուն_鳥 @թռչուն٩")
|
||||
expect(post.cooked).to match_html(<<~HTML.rstrip)
|
||||
expect(post.cooked).to match_html <<~HTML
|
||||
<p><a class="mention" href="/u/%D0%BF%D1%82%D0%B8%D1%86%D0%B0">@птица</a> <a class="mention" href="/u/%D5%A9%D5%BC%D5%B9%D5%B8%D6%82%D5%B6%E9%B3%A5">@թռչուն鳥</a> <a class="mention" href="/u/%D5%A9%D5%BC%D5%B9%D5%B8%D6%82%D5%B6-%E9%B3%A5">@թռչուն-鳥</a> <a class="mention" href="/u/%D5%A9%D5%BC%D5%B9%D5%B8%D6%82%D5%B6_%E9%B3%A5">@թռչուն_鳥</a> <a class="mention" href="/u/%D5%A9%D5%BC%D5%B9%D5%B8%D6%82%D5%B6%D9%A9">@թռչուն٩</a></p>
|
||||
HTML
|
||||
end
|
||||
|
@ -361,7 +361,7 @@ describe UsernameChanger do
|
|||
dolor sit amet
|
||||
RAW
|
||||
|
||||
expect(post.cooked).to match_html(<<~HTML.rstrip)
|
||||
expect(post.cooked).to match_html <<~HTML
|
||||
<p>Lorem ipsum</p>
|
||||
<aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}">
|
||||
<div class="title">
|
||||
|
@ -455,9 +455,8 @@ describe UsernameChanger do
|
|||
post = create_post_and_change_username(raw: raw)
|
||||
|
||||
expect(post.raw).to eq(raw)
|
||||
|
||||
expect(post.cooked).to match_html(<<~HTML.rstrip)
|
||||
<p><aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
|
||||
expect(post.cooked).to match_html <<~HTML
|
||||
<aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
|
||||
<div class="title">
|
||||
<div class="quote-controls"></div>
|
||||
<img alt="" width="20" height="20" src="#{avatar_url}" class="avatar">
|
||||
|
@ -467,7 +466,7 @@ describe UsernameChanger do
|
|||
quoted post
|
||||
</blockquote>
|
||||
</aside>
|
||||
<br>
|
||||
|
||||
<aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
|
||||
<div class="title">
|
||||
<div class="quote-controls"></div>
|
||||
|
@ -478,7 +477,6 @@ describe UsernameChanger do
|
|||
quoted post
|
||||
</blockquote>
|
||||
</aside>
|
||||
</p>
|
||||
HTML
|
||||
end
|
||||
|
||||
|
@ -487,9 +485,8 @@ describe UsernameChanger do
|
|||
post = create_post_and_change_username(raw: raw)
|
||||
|
||||
expect(post.raw).to eq(raw)
|
||||
|
||||
expect(post.cooked).to match_html(<<~HTML.rstrip)
|
||||
<p><aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
|
||||
expect(post.cooked).to match_html <<~HTML
|
||||
<aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
|
||||
<div class="title">
|
||||
<div class="quote-controls"></div>
|
||||
<img alt="" width="20" height="20" src="#{avatar_url}" class="avatar">
|
||||
|
@ -499,7 +496,7 @@ describe UsernameChanger do
|
|||
quoted post
|
||||
</blockquote>
|
||||
</aside>
|
||||
<br>
|
||||
|
||||
<aside class="quote" data-post="#{another_quoted_post.post_number}" data-topic="#{another_quoted_post.topic.id}">
|
||||
<div class="title">
|
||||
<div class="quote-controls"></div>
|
||||
|
@ -510,7 +507,6 @@ describe UsernameChanger do
|
|||
evil post
|
||||
</blockquote>
|
||||
</aside>
|
||||
</p>
|
||||
HTML
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,9 +3,7 @@
|
|||
require 'nokogiri/xml/parse_options'
|
||||
RSpec::Matchers.define :match_html do |expected|
|
||||
match do |actual|
|
||||
a = make_canonical_html(expected).to_html.gsub(/\s+/, " ").strip
|
||||
b = make_canonical_html(actual).to_html.gsub(/\s+/, " ").strip
|
||||
a.eql? b
|
||||
make_canonical_html(expected).eql? make_canonical_html(actual)
|
||||
end
|
||||
|
||||
failure_message do |actual|
|
||||
|
@ -17,7 +15,17 @@ RSpec::Matchers.define :match_html do |expected|
|
|||
end
|
||||
|
||||
def make_canonical_html(html)
|
||||
Nokogiri::HTML5(html) { |config| config[:options] = Nokogiri::XML::ParseOptions::NOBLANKS | Nokogiri::XML::ParseOptions::COMPACT }
|
||||
doc = Nokogiri::HTML5(html) do |config|
|
||||
config[:options] = Nokogiri::XML::ParseOptions::NOBLANKS | Nokogiri::XML::ParseOptions::COMPACT
|
||||
end
|
||||
|
||||
doc.traverse do |node|
|
||||
if node.node_name&.downcase == "text"
|
||||
node.content = node.content.gsub(/\s+/, ' ').strip
|
||||
end
|
||||
end
|
||||
|
||||
doc.to_html
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue