From 3b939f6b9f11a9c8f5e67756b98db59034a89225 Mon Sep 17 00:00:00 2001 From: Francisco Quintero Date: Mon, 1 Jun 2020 17:52:07 -0500 Subject: [PATCH] [deploy] Add Reddit Liquid Tag (#8111) * Implement Reddit liquid tag * Set reddit content in locals * Change Reddit liquid tag content layout I decided a UI similar to the Stack Exchange tag would be better. Now the Reddit tag looks a bit similar. * Add tests for RedditTag * Add reddit liquid tag example to docs Also emphasize notice about compilation of article when using any liquid tag * Parse markdown text reddit content for RedditTag * Display date RedditTag post was published * Remove file location comments * Change order of conditional for Reddit liquid tag Sometimes the attribute `post_hint` might be null and neither condition would apply. * Add example to use Reddit liquid tag * Change reddit svg icon for Reddit liquid tag * handle and verify status of Reddit links Here we make sure the passed link is a valid one in terms of URI composition (e.g. protocol) but also that the URL is one that points to existing content in Reddit. If the URL isn't returning anything but a successful response code (200) then we treat it as an invalid one. * Do not use VCR for RedditTag specs The generated cassette for this tag was really huge (6000+ lines). Using mocks (as in the MediumTag spec) still does the job and without that huge VCR file. * Remove index line from schema.db * Improve RedditJsonFromUrlService - Change User-Agent in requests to one read from app configuration - Remove Reddit URL check by making a GET request - Add regexp to validate URL looks like a Reddit URL * Move RedditJsonFromUrlService methods to RedditTag Also changed the way the request was being mocked. Previously, it was by mocking the service class now I'm mocking HTTParty directly and returning the JSON structure Reddit uses. --- app/assets/images/reddit-icon.svg | 1 + app/assets/stylesheets/ltags/LiquidTags.scss | 1 + app/assets/stylesheets/ltags/RedditTag.scss | 98 ++++++++++++++++++++ app/liquid_tags/reddit_tag.rb | 73 +++++++++++++++ app/views/liquids/_reddit.html.erb | 32 +++++++ app/views/pages/_editor_guide_text.html.erb | 7 ++ docs/frontend/liquid-tags.md | 4 +- spec/liquid_tags/reddit_tag_spec.rb | 88 ++++++++++++++++++ 8 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 app/assets/images/reddit-icon.svg create mode 100644 app/assets/stylesheets/ltags/RedditTag.scss create mode 100644 app/liquid_tags/reddit_tag.rb create mode 100644 app/views/liquids/_reddit.html.erb create mode 100644 spec/liquid_tags/reddit_tag_spec.rb diff --git a/app/assets/images/reddit-icon.svg b/app/assets/images/reddit-icon.svg new file mode 100644 index 000000000..524c15d52 --- /dev/null +++ b/app/assets/images/reddit-icon.svg @@ -0,0 +1 @@ + diff --git a/app/assets/stylesheets/ltags/LiquidTags.scss b/app/assets/stylesheets/ltags/LiquidTags.scss index 1cb633022..f8ef04748 100644 --- a/app/assets/stylesheets/ltags/LiquidTags.scss +++ b/app/assets/stylesheets/ltags/LiquidTags.scss @@ -14,3 +14,4 @@ @import 'ListingTag'; @import 'StackexchangeTag'; @import 'WikipediaTag'; +@import 'RedditTag'; diff --git a/app/assets/stylesheets/ltags/RedditTag.scss b/app/assets/stylesheets/ltags/RedditTag.scss new file mode 100644 index 000000000..91684fd57 --- /dev/null +++ b/app/assets/stylesheets/ltags/RedditTag.scss @@ -0,0 +1,98 @@ +@import 'variables'; + +.ltag__reddit--container { + max-width: 620px; + border: 1px solid $light-medium-gray; + border-radius: 3px; + box-shadow: $shadow; + margin:1.1em auto 1.30em; + padding: 10px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important; + overflow: hidden; + + .ltag__reddit--title-container { + border-bottom: 1px solid #eaecef; + display: flex; + flex-direction: column; + + header { + display: flex; + justify-content: space-between; + + .ltag__reddit--title { + h1 { + margin-left: 5px; + margin-bottom: 0; + margin-top: 8px; + font-size: 1.4em; + padding-bottom: 0; + + img { + height: 40px; + width: 40px; + display: inline-block; + vertical-align: -9px; + } + } + } + + .ltag__reddit--post-metadata { + font-size: 0.6em; + + span { + display: inline-block; + padding: 7px 6px; + margin-left: 6px; + } + } + } + } + + .ltag__reddit--body { + padding: 1em 0.5em; + max-height: calc(21vw + 165px); + overflow: hidden; + + @media screen and (min-width: 430px) { + max-height: calc(20vw + 153px); + } + @media screen and (min-width: 800px) { + max-height: 310px; + } + + h1,h2,h3,h4,h5,h6 { + font-weight: 500 !important; + } + + p { + margin-top: 0px; + padding: 0px; + margin-bottom: 15px; + font-size: 1.1rem; + } + } + + .ltag__reddit--btn--container { + padding: 0.1em 0 1.15em; + z-index: 100; + text-align: center; + position: relative; + box-shadow: 0px 0px 60px 42px #fff; + } + + .ltag__reddit--btn { + color: #0366d6; + background-color: #f1f8ff; + border-radius: 3px; + line-height: 20px; + padding: 0.25em 1.2em; + opacity: 0.9; + font-weight: bold; + border: 1px solid #0366d6; + font-size: 0.75em; + + &:hover { + opacity: 1; + } + } +} diff --git a/app/liquid_tags/reddit_tag.rb b/app/liquid_tags/reddit_tag.rb new file mode 100644 index 000000000..b385b686c --- /dev/null +++ b/app/liquid_tags/reddit_tag.rb @@ -0,0 +1,73 @@ +class RedditTag < LiquidTagBase + include ActionView::Helpers::SanitizeHelper + + PARTIAL = "liquids/reddit".freeze + URL_REGEXP = /\Ahttps\:\/\/(www.)?reddit.com/.freeze + + def initialize(_tag_name, url, _tokens) + @url = ActionController::Base.helpers.strip_tags(url).strip + @reddit_content = parse_url + end + + def render(_context) + ActionController::Base.new.render_to_string( + partial: PARTIAL, + locals: { + author: @reddit_content[:author], + title: @reddit_content[:title], + post_url: @reddit_content[:post_url], + created_at: @reddit_content[:created_at], + post_hint: @reddit_content[:post_hint], + image_url: @reddit_content[:image_url], + thumbnail: @reddit_content[:thumbnail], + html_text: @reddit_content[:selftext_html], + markdown_text: @reddit_content[:selftext] + }, + ) + end + + private + + def parse_url + validate_url + + # Requests to Reddit require a custom `User-Agent` header to prevent 429 errors + json = HTTParty.get("#{@url}.json", headers: { "User-Agent" => "#{ApplicationConfig['COMMUNITY_NAME']} (#{URL.url})" }) + + # The JSON response is an array with two items. + # The first one is the post itself, the second one are the comments + data = json.first["data"]["children"][0]["data"] + + { + author: data["author"], + title: data["title"], + post_url: @url, + created_at: Time.zone.at(data["created_utc"]).strftime("%b %e '%y"), + post_hint: data["post_hint"], + image_url: data["url"], + thumbnail: data["thumbnail"], + selftext: parse_markdown_content(data["selftext"]), + selftext_html: data["selftext_html"] + } + end + + def parse_markdown_content(content) + markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTMLRouge, REDCARPET_CONFIG) + text = markdown.render(content) + + sanitize(HTML_Truncator.truncate(text, 60)) + end + + def validate_url + return true if valid_url?(@url.delete(" ")) && (@url =~ URL_REGEXP)&.zero? + + raise StandardError, "Invalid Reddit link: #{@url}" + end + + def valid_url?(url) + url = URI.parse(url) + url.is_a?(URI::HTTP) + end +end + +Liquid::Template.register_tag("reddit", RedditTag) diff --git a/app/views/liquids/_reddit.html.erb b/app/views/liquids/_reddit.html.erb new file mode 100644 index 000000000..7c78f5b20 --- /dev/null +++ b/app/views/liquids/_reddit.html.erb @@ -0,0 +1,32 @@ +
+
+
+
+

+ " alt="Reddit Logo" /> + + <%= title %> + +

+ +
+
+
+
+

+ <% if post_hint == 'image' %> + <%= title %> + <% else %> + <%= markdown_text %> + <% end %> +

+
+
+ +
+
diff --git a/app/views/pages/_editor_guide_text.html.erb b/app/views/pages/_editor_guide_text.html.erb index 887b2e548..67fa3ae9e 100644 --- a/app/views/pages/_editor_guide_text.html.erb +++ b/app/views/pages/_editor_guide_text.html.erb @@ -342,6 +342,13 @@

Asciinema Embed

All you need is the Asciinema id:

{% asciinema 239367 %} + +

Reddit Tag

+

Enter the full URL of the post you want to embed

+
+{% reddit https://www.reddit.com/r/aww/comments/ag3s4b/ive_waited_28_years_to_finally_havr_my_first_pet %}
+        
+

Parsing Liquid Tags as a Code Example

To parse Liquid tags as code, simply wrap it with a single backtick or triple backticks.

`{% mytag %}{{ site.SOMETHING }}{% endmytag %}`

diff --git a/docs/frontend/liquid-tags.md b/docs/frontend/liquid-tags.md index fd0d46ed5..d4cc26efc 100644 --- a/docs/frontend/liquid-tags.md +++ b/docs/frontend/liquid-tags.md @@ -24,8 +24,7 @@ Develop them with that mindset in terms of naming things. They should be documented but also intuitive. They should also be fairly flexible in the arguments they take. Currently, this could use improvements. -Liquid tags are "compiled" when an article is saved. So you will need to re-save -articles to see HTML changes. +_Note: Liquid tags are "compiled" when an article is saved. So you will need to re-save articles to see HTML changes._ Here is a bunch of liquid tags supported on DEV: @@ -54,6 +53,7 @@ Here is a bunch of liquid tags supported on DEV: {% blogcast 1234 %} {% kotlin https://pl.kotl.in/owreUFFUG %} {% wikipedia https://en.wikipedia.org/wiki/Wikipedia %} +{% reddit https://www.reddit.com/r/aww/comments/ag3s4b/ive_waited_28_years_to_finally_havr_my_first_pet %} ``` ## How liquid tags are developed diff --git a/spec/liquid_tags/reddit_tag_spec.rb b/spec/liquid_tags/reddit_tag_spec.rb new file mode 100644 index 000000000..d96dfd45e --- /dev/null +++ b/spec/liquid_tags/reddit_tag_spec.rb @@ -0,0 +1,88 @@ +require "rails_helper" + +RSpec.describe RedditTag, type: :liquid_tag do + context "when it is an image post" do + describe "#render" do + let(:image_post) { "https://www.reddit.com/r/aww/comments/ag3s4b/ive_waited_28_years_to_finally_havr_my_first_pet" } + let(:response) do + { + "author" => "Miaogua007", + "title" => "I've waited 28 years to …Everyone, meet Mycroft.", + "post_url" => "https://www.reddit.com/r/aww/comments/ag3s4b/ive_waited_28_years_to_finally_havr_my_first_pet", + "created_utc" => 1_547_520_425, + "post_hint" => "image", + "image_url" => "https://i.redd.it/jpqodmc83ia21.jpg", + "thumbnail" => "https://b.thumbs.redditm…ge_mn1MHd6uirdou8H3o.jpg", + "selftext" => "", + "selftext_html" => nil + } + end + + def generate_reddit_liquid(url) + Liquid::Template.register_tag("reddit", RedditTag) + Liquid::Template.parse("{% reddit #{url} %}") + end + + before do + allow(HTTParty).to receive(:get).and_return([ + { + "data" => { + "children" => [ + { "data" => response }, + ] + } + }, + ]) + end + + it "renders reddit content" do + reddit_liquid = generate_reddit_liquid(image_post) + expect(reddit_liquid.render).to include("ltag__reddit") + expect(reddit_liquid.render).to include(response["author"]) + expect(reddit_liquid.render).to include(response["thumbnail"]) + end + end + end + + context "when it is a text post" do + describe "#render" do + let(:text_post) { "https://www.reddit.com/r/IAmA/comments/afvl2w/im_scott_from_scotts_cheap_flights_my_profession" } + let(:response) do + { + "author" => "scottkeyes", + "title" => "I'm Scott from Scott's C…r the next 8 hours. AMA", + "post_url" => "https://www.reddit.com/r/IAmA/comments/afvl2w/im_scott_from_scotts_cheap_flights_my_profession", + "created_utc" => 1_547_470_871, + "post_hint" => "self", + "image_url" => "", + "thumbnail" => "self", + "selftext" => "I may have the world’s b…year for cheap flights!!", + "selftext_html" => "" + } + end + + def generate_reddit_liquid(url) + Liquid::Template.register_tag("reddit", RedditTag) + Liquid::Template.parse("{% reddit #{url} %}") + end + + before do + allow(HTTParty).to receive(:get).and_return([ + { + "data" => { + "children" => [ + { "data" => response }, + ] + } + }, + ]) + end + + it "renders reddit content" do + reddit_liquid = generate_reddit_liquid(text_post) + expect(reddit_liquid.render).to include("ltag__reddit") + expect(reddit_liquid.render).to include(response["author"]) + end + end + end +end