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:
Dan Ungureanu 2020-12-14 17:49:37 +02:00 committed by GitHub
parent a7cc17cff7
commit 2d51833ca9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 117 additions and 72 deletions

View File

@ -99,32 +99,57 @@ module Oneboxer
each_onebox_link(doc, extra_paths: extra_paths) do |url, element| each_onebox_link(doc, extra_paths: extra_paths) do |url, element|
onebox, _ = yield(url, element) onebox, _ = yield(url, element)
next if onebox.blank?
if onebox
parsed_onebox = Nokogiri::HTML5::fragment(onebox) parsed_onebox = Nokogiri::HTML5::fragment(onebox)
next unless parsed_onebox.children.count > 0 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
end
changed = true changed = true
element.swap parsed_onebox.to_html
end
end
# strip empty <p> elements parent = element.parent
doc.css("p").each do |p| if parent&.node_name&.downcase == "p" &&
if p.children.empty? && doc.children.count > 1 parsed_onebox.children.any? { |child| HTML5_BLOCK_ELEMENTS.include?(child.node_name.downcase) }
p.remove
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
else
element.replace parsed_onebox
end end
end end
Result.new(doc, changed) Result.new(doc, changed)
end 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) def self.is_previewing?(user_id)
Discourse.redis.get(preview_key(user_id)) == "1" Discourse.redis.get(preview_key(user_id)) == "1"
end end

View File

@ -413,7 +413,7 @@ describe CookedPostProcessor do
it "generates overlay information" do it "generates overlay information" do
cpp.post_process 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> <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 HTML
@ -433,7 +433,7 @@ describe CookedPostProcessor do
cpp.post_process 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> <p><img class="onebox" src="//test.localhost/#{upload_path}/original/1X/1234567890123456.jpg" width="690" height="788"></p>
HTML HTML
end end
@ -449,7 +449,7 @@ describe CookedPostProcessor do
cpp.post_process 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> <p><img src="//test.localhost/#{upload_path}/original/1X/1234567890123456.svg" width="690" height="788"></p>
HTML HTML
end end
@ -576,7 +576,7 @@ describe CookedPostProcessor do
it "crops the image" do it "crops the image" do
cpp.post_process 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> <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 HTML
@ -607,7 +607,7 @@ describe CookedPostProcessor do
it "generates overlay information" do it "generates overlay information" do
cpp.post_process 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> <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 HTML
@ -618,7 +618,7 @@ describe CookedPostProcessor do
upload.update!(original_filename: "><img src=x onerror=alert('haha')>.png") upload.update!(original_filename: "><img src=x onerror=alert('haha')>.png")
cpp.post_process 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="&amp;gt;&amp;lt;img src=x onerror=alert(&amp;#39;haha&amp;#39;)&amp;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">&amp;gt;&amp;lt;img src=x onerror=alert(&amp;#39;haha&amp;#39;)&amp;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> <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="&amp;gt;&amp;lt;img src=x onerror=alert(&amp;#39;haha&amp;#39;)&amp;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">&amp;gt;&amp;lt;img src=x onerror=alert(&amp;#39;haha&amp;#39;)&amp;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 HTML
end end
@ -644,7 +644,7 @@ describe CookedPostProcessor do
it "generates overlay information using image title and ignores alt" do it "generates overlay information using image title and ignores alt" do
cpp.post_process 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> <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 HTML
@ -672,7 +672,7 @@ describe CookedPostProcessor do
it "generates overlay information using image title" do it "generates overlay information using image title" do
cpp.post_process 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> <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 HTML
@ -700,7 +700,7 @@ describe CookedPostProcessor do
it "generates overlay information using image alt" do it "generates overlay information using image alt" do
cpp.post_process 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> <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 HTML
@ -1202,7 +1202,7 @@ describe CookedPostProcessor do
it "uses schemaless url for uploads" do it "uses schemaless url for uploads" do
cpp.optimize_urls 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> <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> <img src="//test.localhost/#{upload_path}/original/1X/1234567890123456.jpg"><br>
<a href="http://www.google.com" rel="noopener nofollow ugc">Google</a><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 it "uses schemaless CDN url for http uploads" do
Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com") Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com")
cpp.optimize_urls 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> <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> <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> <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 it "doesn't use schemaless CDN url for https uploads" do
Rails.configuration.action_controller.stubs(:asset_host).returns("https://my.cdn.com") Rails.configuration.action_controller.stubs(:asset_host).returns("https://my.cdn.com")
cpp.optimize_urls 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> <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> <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> <a href="http://www.google.com" rel="noopener nofollow ugc">Google</a><br>
@ -1244,7 +1244,7 @@ describe CookedPostProcessor do
SiteSetting.login_required = true SiteSetting.login_required = true
Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com") Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com")
cpp.optimize_urls 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> <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> <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> <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 SiteSetting.prevent_anons_from_downloading_files = true
Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com") Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com")
cpp.optimize_urls 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> <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> <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> <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 = CookedPostProcessor.new(the_post)
cpp.optimize_urls 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>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> <p><img src="https://s3.cdn.com/#{stored_path}" alt="smallest.png" data-base62-sha1="#{upload.base62_sha1}" width="10" height="20"></p>
HTML HTML
@ -1315,7 +1315,7 @@ describe CookedPostProcessor do
cpp = CookedPostProcessor.new(the_post) cpp = CookedPostProcessor.new(the_post)
cpp.optimize_urls 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>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> <p><img src="/secure-media-uploads/#{stored_path}" alt="smallest.png" data-base62-sha1="#{upload.base62_sha1}" width="10" height="20"></p>
HTML HTML
@ -1339,12 +1339,9 @@ describe CookedPostProcessor do
cpp = CookedPostProcessor.new(the_post.reload) cpp = CookedPostProcessor.new(the_post.reload)
cpp.post_process_oneboxes cpp.post_process_oneboxes
cpp = CookedPostProcessor.new(the_post.reload)
cpp.post_process_oneboxes
expect(cpp.html).to match_html <<~HTML expect(cpp.html).to match_html <<~HTML
<p>This post has an S3 video onebox:<br> <p>This post has an S3 video onebox:</p>
</p><div class="onebox video-onebox"> <div class="onebox video-onebox">
<video width="100%" height="100%" controls=""> <video width="100%" height="100%" controls="">
<source src="#{video_upload.url}"> <source src="#{video_upload.url}">
<a href="#{video_upload.url}" rel="nofollow ugc noopener">#{video_upload.url}</a> <a href="#{video_upload.url}" rel="nofollow ugc noopener">#{video_upload.url}</a>
@ -1365,15 +1362,13 @@ describe CookedPostProcessor do
secure_url = video_upload.url.sub(SiteSetting.s3_cdn_url, "#{Discourse.base_url}/secure-media-uploads") secure_url = video_upload.url.sub(SiteSetting.s3_cdn_url, "#{Discourse.base_url}/secure-media-uploads")
expect(cpp.html).to match_html <<~HTML.rstrip expect(cpp.html).to match_html <<~HTML
<p>This post has an S3 video onebox:<br> <p>This post has an S3 video onebox:</p><div class="onebox video-onebox">
<div class="onebox video-onebox">
<video width="100%" height="100%" controls=""> <video width="100%" height="100%" controls="">
<source src="#{secure_url}"> <source src="#{secure_url}">
<a href="#{secure_url}">#{secure_url}</a> <a href="#{secure_url}">#{secure_url}</a>
</video> </video>
</div> </div>
</p>
HTML HTML
end 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_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") 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 expect(cpp.html).to match_html <<~HTML
<p>This post has a video upload.<br> <p>This post has a video upload.</p>
<div class="onebox video-onebox"> <div class="onebox video-onebox">
<video width="100%" height="100%" controls=""> <video width="100%" height="100%" controls="">
<source src="#{secure_video_url}"> <source src="#{secure_video_url}">
<a href="#{secure_video_url}">#{secure_video_url}</a> <a href="#{secure_video_url}">#{secure_video_url}</a>
</video> </video>
</div> </div>
</p>
<p>This post has an audio upload.<br> <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> <audio controls=""><source src="#{secure_audio_url}"><a href="#{secure_audio_url}">#{secure_audio_url}</a></audio></p>
<p>And an image upload.<br> <p>And an image upload.<br>
<img src="#{image_upload.url}" alt="#{image_upload.original_filename}" data-base62-sha1="#{image_upload.base62_sha1}"></p> <img src="#{image_upload.url}" alt="#{image_upload.original_filename}" data-base62-sha1="#{image_upload.base62_sha1}"></p>
HTML HTML
end end

View File

@ -18,7 +18,7 @@ describe ExcerptParser do
</details> </details>
HTML 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 <details><summary>FOO</summary>BAR
Lorem ipsum dolor sit amet, consectetur adi&hellip;</details> Lorem ipsum dolor sit amet, consectetur adi&hellip;</details>
HTML HTML

View File

@ -300,4 +300,27 @@ describe Oneboxer do
end end
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 end

View File

@ -147,7 +147,7 @@ describe UsernameChanger do
post = create_post_and_change_username(raw: ".@foo -@foo %@foo _@foo ,@foo ;@foo @@foo") 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.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> <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>
%<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~~") post = create_post_and_change_username(raw: "**@foo** *@foo* _@foo_ ~~@foo~~")
expect(post.raw).to eq("**@bar** *@bar* _@bar_ ~~@bar~~") 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> <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>
<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-") 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.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>. <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>,
<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") 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.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> <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 HTML
end end
@ -312,7 +312,7 @@ describe UsernameChanger do
post = create_post_and_change_username(raw: "@թռչուն @թռչուն鳥 @թռչուն-鳥 @թռչուն_鳥 @թռչուն٩", target_username: 'птица') post = create_post_and_change_username(raw: "@թռչուն @թռչուն鳥 @թռչուն-鳥 @թռչուն_鳥 @թռչուն٩", target_username: 'птица')
expect(post.raw).to eq("@птица @թռչուն鳥 @թռչուն-鳥 @թռչուն_鳥 @թռչուն٩") 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> <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 HTML
end end
@ -361,7 +361,7 @@ describe UsernameChanger do
dolor sit amet dolor sit amet
RAW RAW
expect(post.cooked).to match_html(<<~HTML.rstrip) expect(post.cooked).to match_html <<~HTML
<p>Lorem ipsum</p> <p>Lorem ipsum</p>
<aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}"> <aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}">
<div class="title"> <div class="title">
@ -455,9 +455,8 @@ describe UsernameChanger do
post = create_post_and_change_username(raw: raw) post = create_post_and_change_username(raw: raw)
expect(post.raw).to eq(raw) expect(post.raw).to eq(raw)
expect(post.cooked).to match_html <<~HTML
expect(post.cooked).to match_html(<<~HTML.rstrip) <aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
<p><aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
<img alt="" width="20" height="20" src="#{avatar_url}" class="avatar"> <img alt="" width="20" height="20" src="#{avatar_url}" class="avatar">
@ -467,7 +466,7 @@ describe UsernameChanger do
quoted post quoted post
</blockquote> </blockquote>
</aside> </aside>
<br>
<aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}"> <aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
@ -478,7 +477,6 @@ describe UsernameChanger do
quoted post quoted post
</blockquote> </blockquote>
</aside> </aside>
</p>
HTML HTML
end end
@ -487,9 +485,8 @@ describe UsernameChanger do
post = create_post_and_change_username(raw: raw) post = create_post_and_change_username(raw: raw)
expect(post.raw).to eq(raw) expect(post.raw).to eq(raw)
expect(post.cooked).to match_html <<~HTML
expect(post.cooked).to match_html(<<~HTML.rstrip) <aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
<p><aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
<img alt="" width="20" height="20" src="#{avatar_url}" class="avatar"> <img alt="" width="20" height="20" src="#{avatar_url}" class="avatar">
@ -499,7 +496,7 @@ describe UsernameChanger do
quoted post quoted post
</blockquote> </blockquote>
</aside> </aside>
<br>
<aside class="quote" data-post="#{another_quoted_post.post_number}" data-topic="#{another_quoted_post.topic.id}"> <aside class="quote" data-post="#{another_quoted_post.post_number}" data-topic="#{another_quoted_post.topic.id}">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
@ -510,7 +507,6 @@ describe UsernameChanger do
evil post evil post
</blockquote> </blockquote>
</aside> </aside>
</p>
HTML HTML
end end
end end

View File

@ -3,9 +3,7 @@
require 'nokogiri/xml/parse_options' require 'nokogiri/xml/parse_options'
RSpec::Matchers.define :match_html do |expected| RSpec::Matchers.define :match_html do |expected|
match do |actual| match do |actual|
a = make_canonical_html(expected).to_html.gsub(/\s+/, " ").strip make_canonical_html(expected).eql? make_canonical_html(actual)
b = make_canonical_html(actual).to_html.gsub(/\s+/, " ").strip
a.eql? b
end end
failure_message do |actual| failure_message do |actual|
@ -17,7 +15,17 @@ RSpec::Matchers.define :match_html do |expected|
end end
def make_canonical_html(html) 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
end end