First Few Unified Embed implementations (Liquid Tags) (#15550)
* Adding {% embed %} for liquid tags
This commit introduces the crease for us to unify our embed experience.
The main concept is introducing a lookup function for the given "link".
And with that lookup function find the underlying `LiquidTagBase` that
can handle the specific URL.
This is part proof of concept and part laying the foundation for a
plugin architecture of different LiquidTags.
We have a lot more work ahead of us to normalize this:
- Update and migrate existing data (not required but hopefully helps us
deprecate existing liquid tags)
- Move these "embeddable" tags into a module space that eases lookup.
- Document how someone might make a Railstie gem to inject new
embeddable formats into their application.
Related to #15099
* start adding UnifiedEmbeds to LiqTag models
* add specs for asciinema and codepen liqtags
* new tags and their specs
* complete first pass of liqtag models
* complete implementation for JsFiddleTag
* fixing twittertimeline tag
* almost done with YoutubeTag embed
* remove Medium UnifiedEmbed due to broken image in prod
* remove semi-working unified embed
* update specs
* Address PR review feedback
* add missing spec
* implement PR feedback
* last of PR review-related changes
* fix failing spec
Co-authored-by: Jeremy Friesen <jeremy.n.friesen@gmail.com>
This commit is contained in:
parent
61b94b5176
commit
abec2e58e9
14 changed files with 220 additions and 16 deletions
|
|
@ -1,6 +1,6 @@
|
|||
class AsciinemaTag < LiquidTagBase
|
||||
PARTIAL = "liquids/asciinema".freeze
|
||||
ASCIINEMA_URL_REGEX = %r{https://asciinema.org/a/(?<id>\d+)}
|
||||
REGISTRY_REGEXP = %r{https://asciinema.org/a/(?<id>\d+)}
|
||||
|
||||
def initialize(_tag_name, id, _parse_context)
|
||||
super
|
||||
|
|
@ -20,7 +20,7 @@ class AsciinemaTag < LiquidTagBase
|
|||
|
||||
def parse_id(input)
|
||||
sanitized_input = input.strip
|
||||
match_data = sanitized_input.match(ASCIINEMA_URL_REGEX)
|
||||
match_data = sanitized_input.match(REGISTRY_REGEXP)
|
||||
match_data ? match_data["id"] : validate(sanitized_input)
|
||||
end
|
||||
|
||||
|
|
@ -32,3 +32,5 @@ class AsciinemaTag < LiquidTagBase
|
|||
end
|
||||
|
||||
Liquid::Template.register_tag("asciinema", AsciinemaTag)
|
||||
|
||||
UnifiedEmbed.register(AsciinemaTag, regexp: AsciinemaTag::REGISTRY_REGEXP)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
class CodepenTag < LiquidTagBase
|
||||
PARTIAL = "liquids/codepen".freeze
|
||||
URL_REGEXP =
|
||||
REGISTRY_REGEXP =
|
||||
%r{\A(http|https)://(codepen\.io|codepen\.io/team)/[a-zA-Z0-9_\-]{1,30}/pen/([a-zA-Z0-9]{5,32})/{0,1}\z}
|
||||
|
||||
def initialize(_tag_name, link, _parse_context)
|
||||
|
|
@ -48,7 +48,7 @@ class CodepenTag < LiquidTagBase
|
|||
|
||||
def valid_link?(link)
|
||||
link_no_space = link.delete(" ")
|
||||
(link_no_space =~ URL_REGEXP)&.zero?
|
||||
(link_no_space =~ REGISTRY_REGEXP)&.zero?
|
||||
end
|
||||
|
||||
def raise_error
|
||||
|
|
@ -57,3 +57,5 @@ class CodepenTag < LiquidTagBase
|
|||
end
|
||||
|
||||
Liquid::Template.register_tag("codepen", CodepenTag)
|
||||
|
||||
UnifiedEmbed.register(CodepenTag, regexp: CodepenTag::REGISTRY_REGEXP)
|
||||
|
|
|
|||
|
|
@ -60,3 +60,5 @@ class GistTag < LiquidTagBase
|
|||
end
|
||||
|
||||
Liquid::Template.register_tag("gist", GistTag)
|
||||
|
||||
UnifiedEmbed.register(GistTag, regexp: %r{https?://gist.github.com})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
class JsFiddleTag < LiquidTagBase
|
||||
PARTIAL = "liquids/jsfiddle".freeze
|
||||
OPTION_REGEXP = /\A(js|html|css|result|,)*\z/
|
||||
LINK_REGEXP = %r{\A(http|https)://(jsfiddle\.net)/[a-zA-Z0-9\-/]*\z}
|
||||
REGISTRY_REGEXP = %r{\A(http|https)://(jsfiddle\.net)/[a-zA-Z0-9\-/]*\z}
|
||||
|
||||
def initialize(_tag_name, link, _parse_context)
|
||||
super
|
||||
|
|
@ -47,8 +47,10 @@ class JsFiddleTag < LiquidTagBase
|
|||
|
||||
def valid_link?(link)
|
||||
link_no_space = link.delete(" ")
|
||||
(link_no_space =~ LINK_REGEXP).zero?
|
||||
(link_no_space =~ REGISTRY_REGEXP).zero?
|
||||
end
|
||||
end
|
||||
|
||||
Liquid::Template.register_tag("jsfiddle", JsFiddleTag)
|
||||
|
||||
UnifiedEmbed.register(JsFiddleTag, regexp: JsFiddleTag::REGISTRY_REGEXP)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
class NextTechTag < LiquidTagBase
|
||||
PARTIAL = "liquids/nexttech".freeze
|
||||
REGISTRY_REGEXP = %r{https?://nt.dev/s/}
|
||||
|
||||
def initialize(_tag_name, share_url, _parse_context)
|
||||
super
|
||||
|
|
@ -35,3 +36,5 @@ class NextTechTag < LiquidTagBase
|
|||
end
|
||||
|
||||
Liquid::Template.register_tag("nexttech", NextTechTag)
|
||||
|
||||
UnifiedEmbed.register(NextTechTag, regexp: NextTechTag::REGISTRY_REGEXP)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ class RedditTag < LiquidTagBase
|
|||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
PARTIAL = "liquids/reddit".freeze
|
||||
URL_REGEXP = %r{\Ahttps://(www.)?reddit.com}
|
||||
REGISTRY_REGEXP = %r{\Ahttps://(www.)?reddit.com}
|
||||
|
||||
def initialize(_tag_name, url, _parse_context)
|
||||
super
|
||||
|
|
@ -61,7 +61,7 @@ class RedditTag < LiquidTagBase
|
|||
end
|
||||
|
||||
def validate_url
|
||||
return true if valid_url?(@url.delete(" ")) && (@url =~ URL_REGEXP)&.zero?
|
||||
return true if valid_url?(@url.delete(" ")) && (@url =~ REGISTRY_REGEXP)&.zero?
|
||||
|
||||
raise StandardError, "Invalid Reddit link: #{@url}"
|
||||
end
|
||||
|
|
@ -73,3 +73,5 @@ class RedditTag < LiquidTagBase
|
|||
end
|
||||
|
||||
Liquid::Template.register_tag("reddit", RedditTag)
|
||||
|
||||
UnifiedEmbed.register(RedditTag, regexp: RedditTag::REGISTRY_REGEXP)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
class SoundcloudTag < LiquidTagBase
|
||||
PARTIAL = "liquids/soundcloud".freeze
|
||||
REGISTRY_REGEXP = %r{https?://soundcloud.com}
|
||||
|
||||
def initialize(_tag_name, link, _parse_context)
|
||||
super
|
||||
|
|
@ -41,3 +42,5 @@ class SoundcloudTag < LiquidTagBase
|
|||
end
|
||||
|
||||
Liquid::Template.register_tag("soundcloud", SoundcloudTag)
|
||||
|
||||
UnifiedEmbed.register(SoundcloudTag, regexp: SoundcloudTag::REGISTRY_REGEXP)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ class TwitterTimelineTag < LiquidTagBase
|
|||
|
||||
URL_REGEXP = %r{\Ahttps://twitter\.com/[a-zA-Z0-9]+/timelines/\d+\Z}
|
||||
|
||||
REGISTRY_REGEXP = %r{https://twitter\.com/[a-zA-Z0-9]+/timelines/\d+}
|
||||
|
||||
SCRIPT = <<~JAVASCRIPT.freeze
|
||||
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
JAVASCRIPT
|
||||
|
|
@ -44,3 +46,5 @@ class TwitterTimelineTag < LiquidTagBase
|
|||
end
|
||||
|
||||
Liquid::Template.register_tag("twitter_timeline", TwitterTimelineTag)
|
||||
|
||||
UnifiedEmbed.register(TwitterTimelineTag, regexp: TwitterTimelineTag::REGISTRY_REGEXP)
|
||||
|
|
|
|||
46
app/liquid_tags/unified_embed.rb
Normal file
46
app/liquid_tags/unified_embed.rb
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# The purpose of this singleton class is to provide a registry for the
|
||||
# numerous "link/embedded" type liquid tags.
|
||||
#
|
||||
# Each of those liquid tags must self-register their lookup regular
|
||||
# expression.
|
||||
class UnifiedEmbed
|
||||
include Singleton
|
||||
|
||||
def self.current
|
||||
instance
|
||||
end
|
||||
|
||||
# @api public
|
||||
#
|
||||
# @param klass [Class] the LiquidTag class that we use when we match
|
||||
# the given :regexp
|
||||
# @param regexp [Regexp] the regular expression that when matched
|
||||
# means we use the associated :klass
|
||||
def self.register(klass, regexp:)
|
||||
instance.register(klass, regexp: regexp)
|
||||
end
|
||||
|
||||
# @api public
|
||||
#
|
||||
# @param link [String] the string that includes the URI for the
|
||||
# embed and possibly additional attributes, depending on how
|
||||
# the registered liquid tag parses this string.
|
||||
# @return [Class] a descendant class of LiquidTagBase
|
||||
def self.find_liquid_tag_for(link:)
|
||||
instance.find_liquid_tag_for(link: link)
|
||||
end
|
||||
|
||||
def initialize
|
||||
@registry = []
|
||||
end
|
||||
|
||||
def register(klass, regexp:)
|
||||
@registry << [regexp, klass]
|
||||
end
|
||||
|
||||
def find_liquid_tag_for(link:)
|
||||
link = ActionController::Base.helpers.strip_tags(link).strip if link.include?("href=")
|
||||
_regexp, klass = @registry.detect { |regexp, _tag_class| regexp.match?(link) }
|
||||
klass
|
||||
end
|
||||
end
|
||||
42
app/liquid_tags/unified_embed_tag.rb
Normal file
42
app/liquid_tags/unified_embed_tag.rb
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# This liquid tag is present to facilitate a unified user experience
|
||||
# for declaring that they want a URL to have "embedded" behavior.
|
||||
#
|
||||
# What do we mean by embedded behavior? A more contextually rich
|
||||
# rendering of the URL, instead of a simple "A-tag".
|
||||
#
|
||||
# @see https://github.com/forem/forem/issues/15099 for details on the
|
||||
# purpose of this class.
|
||||
class UnifiedEmbedTag < LiquidTagBase
|
||||
# You will not get a UnifiedEmbedTag instance, as we are instead
|
||||
# using this class as a lookup (e.g., Factory pattern?) for the
|
||||
# LiquidTagBase instance that is applicable for the given :link.
|
||||
#
|
||||
# @param tag_name [String] in the UI, this was liquid tag name
|
||||
# (e.g., `{% tag_name link %}`)
|
||||
# @param link [String] the URL and additional options for that
|
||||
# particular service.
|
||||
# @param parse_context [Liquid::ParseContext]
|
||||
#
|
||||
# @return [LiquidTagBase]
|
||||
def self.new(tag_name, link, parse_context)
|
||||
klass = UnifiedEmbed.find_liquid_tag_for(link: link)
|
||||
|
||||
if klass
|
||||
# Why the __send__? Because a LiquidTagBase class "privatizes"
|
||||
# the `.new` method. And we want to instantiate the specific
|
||||
# liquid tag for the given link.
|
||||
klass.__send__(:new, tag_name, link, parse_context)
|
||||
else
|
||||
# If we don't know how to handle the embed, let's just give the
|
||||
# user an A-tag.
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def render(_context)
|
||||
link, _options = ActionController::Base.helpers.strip_tags(@markup).split
|
||||
%(<a href="#{link}">#{link}</a>)
|
||||
end
|
||||
end
|
||||
|
||||
Liquid::Template.register_tag("embed", UnifiedEmbedTag)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
class WikipediaTag < LiquidTagBase
|
||||
PARTIAL = "liquids/wikipedia".freeze
|
||||
WIKI_REGEXP = %r{\Ahttps?://([a-z-]+)\.wikipedia.org/wiki/(\S+)\z}
|
||||
REGISTRY_REGEXP = %r{\Ahttps?://([a-z-]+)\.wikipedia.org/wiki/(\S+)\z}
|
||||
TEXT_CLEANUP_XPATH = "//div[contains(@class, 'noprint') or contains(@class, 'hatnote')] | " \
|
||||
"//span[@class='mw-ref'] | //figure | //sup".freeze
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ class WikipediaTag < LiquidTagBase
|
|||
private
|
||||
|
||||
def valid_url?(input)
|
||||
input.match?(WIKI_REGEXP)
|
||||
input.match?(REGISTRY_REGEXP)
|
||||
end
|
||||
|
||||
def get_data(input)
|
||||
|
|
@ -100,3 +100,5 @@ class WikipediaTag < LiquidTagBase
|
|||
end
|
||||
|
||||
Liquid::Template.register_tag("wikipedia", WikipediaTag)
|
||||
|
||||
UnifiedEmbed.register(WikipediaTag, regexp: WikipediaTag::REGISTRY_REGEXP)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
class YoutubeTag < LiquidTagBase
|
||||
PARTIAL = "liquids/youtube".freeze
|
||||
REGISTRY_REGEXP = %r{https?://(www\.)?youtube\.(com|be)/(embed|watch)?(\?v=)?(/)?[a-zA-Z0-9_-]{11}((\?t=)?(\d{1,})?)?}
|
||||
MARKER_TO_SECONDS_MAP = {
|
||||
"h" => 60 * 60,
|
||||
"m" => 60,
|
||||
|
|
@ -8,7 +9,11 @@ class YoutubeTag < LiquidTagBase
|
|||
|
||||
def initialize(_tag_name, id, _parse_context)
|
||||
super
|
||||
@id = parse_id(id)
|
||||
|
||||
# for if id is an unstripped URL; doesn't appear to affect bare youtube ids
|
||||
input = ActionController::Base.helpers.strip_tags(id).strip
|
||||
|
||||
@id = parse_id_or_url(input)
|
||||
@width = 710
|
||||
@height = 399
|
||||
end
|
||||
|
|
@ -26,12 +31,24 @@ class YoutubeTag < LiquidTagBase
|
|||
|
||||
private
|
||||
|
||||
def parse_id(input)
|
||||
input_no_space = input.delete(" ")
|
||||
raise StandardError, "Invalid YouTube ID" unless valid_id?(input_no_space)
|
||||
return translate_start_time(input_no_space) if input_no_space.include?("?t=")
|
||||
def parse_id_or_url(input)
|
||||
if (input =~ REGISTRY_REGEXP)&.zero?
|
||||
extract_youtube_id(input)
|
||||
else
|
||||
input_no_space = input.delete(" ")
|
||||
raise StandardError, "Invalid YouTube ID" unless valid_id?(input_no_space)
|
||||
return translate_start_time(input_no_space) if input_no_space.include?("?t=")
|
||||
|
||||
input_no_space
|
||||
input_no_space
|
||||
end
|
||||
end
|
||||
|
||||
def extract_youtube_id(url)
|
||||
url = url.gsub(/(>|<)/, "").split(%r{(vi/|v=|/v/|youtu\.be/|/embed/)})
|
||||
raise StandardError, "Invalid YouTube URL" if url[2].nil?
|
||||
|
||||
id = url[2].split(/[^a-zA-Z0-9_-]/i) # tweak this to allow for time, fix youtube_tag_spec
|
||||
id[0]
|
||||
end
|
||||
|
||||
def valid_id?(id)
|
||||
|
|
@ -55,3 +72,5 @@ class YoutubeTag < LiquidTagBase
|
|||
end
|
||||
|
||||
Liquid::Template.register_tag("youtube", YoutubeTag)
|
||||
|
||||
UnifiedEmbed.register(YoutubeTag, regexp: YoutubeTag::REGISTRY_REGEXP)
|
||||
|
|
|
|||
57
spec/liquid_tags/unified_embed_spec.rb
Normal file
57
spec/liquid_tags/unified_embed_spec.rb
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe UnifiedEmbed do
|
||||
subject(:unified_embed) { described_class }
|
||||
|
||||
describe ".find_liquid_tag_for" do
|
||||
it "returns GistTag for a gist url" do
|
||||
expect(described_class.find_liquid_tag_for(link: "https://gist.github.com/jeremyf/662585f5c4d22184a6ae133a71bf891a"))
|
||||
.to eq(GistTag)
|
||||
end
|
||||
|
||||
it "returns AsciinemaTag for an asciinema url" do
|
||||
expect(described_class.find_liquid_tag_for(link: "https://asciinema.org/a/330532"))
|
||||
.to eq(AsciinemaTag)
|
||||
end
|
||||
|
||||
it "returns CodepenTag for a codepen url" do
|
||||
expect(described_class.find_liquid_tag_for(link: "https://codepen.io/elisavetTriant/pen/KKvRRyE"))
|
||||
.to eq(CodepenTag)
|
||||
end
|
||||
|
||||
it "returns JsFiddle for a jsfiddle url" do
|
||||
expect(described_class.find_liquid_tag_for(link: "http://jsfiddle.net/link2twenty/v2kx9jcd"))
|
||||
.to eq(JsFiddleTag)
|
||||
end
|
||||
|
||||
it "returns NextTechTag for a nexttech url" do
|
||||
expect(described_class.find_liquid_tag_for(link: "https://nt.dev/s/6ba1fffbd09e"))
|
||||
.to eq(NextTechTag)
|
||||
end
|
||||
|
||||
it "returns RedditTag for a reddit url" do
|
||||
expect(described_class.find_liquid_tag_for(link: "https://www.reddit.com/r/Cricket/comments/qrkwol/match_thread_2nd_semifinal_australia_vs_pakistan/"))
|
||||
.to eq(RedditTag)
|
||||
end
|
||||
|
||||
it "returns SoundcloudTag for a soundcloud url" do
|
||||
expect(described_class.find_liquid_tag_for(link: "https://soundcloud.com/before-30-tv/stranger-moni-lati-lo-1"))
|
||||
.to eq(SoundcloudTag)
|
||||
end
|
||||
|
||||
it "returns TwitterTimelineTag for a twitter timeline url" do
|
||||
expect(described_class.find_liquid_tag_for(link: "https://twitter.com/FreyaHolmer/timelines/1215413954505297922"))
|
||||
.to eq(TwitterTimelineTag)
|
||||
end
|
||||
|
||||
it "returns WikipediaTag for a twitter timeline url" do
|
||||
expect(described_class.find_liquid_tag_for(link: "https://en.wikipedia.org/wiki/Steve_Jobs"))
|
||||
.to eq(WikipediaTag)
|
||||
end
|
||||
|
||||
it "returns YoutubeTag for a youtube url" do
|
||||
expect(described_class.find_liquid_tag_for(link: "https://www.youtube.com/embed/dQw4w9WgXcQ"))
|
||||
.to eq(YoutubeTag)
|
||||
end
|
||||
end
|
||||
end
|
||||
18
spec/liquid_tags/unified_embed_tag_spec.rb
Normal file
18
spec/liquid_tags/unified_embed_tag_spec.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe UnifiedEmbedTag, type: :liquid_tag do
|
||||
it "delegates parsing to the link-matching class" do
|
||||
link = "https://gist.github.com/jeremyf/662585f5c4d22184a6ae133a71bf891a"
|
||||
|
||||
allow(GistTag).to receive(:new).and_call_original
|
||||
parsed_tag = Liquid::Template.parse("{% embed #{link} %}")
|
||||
expect { parsed_tag.render }.not_to raise_error
|
||||
expect(GistTag).to have_received(:new)
|
||||
end
|
||||
|
||||
it "renders an A-tag when no link-matching class is found" do
|
||||
link = "https://takeonrules.com/about"
|
||||
parsed_tag = Liquid::Template.parse("{% embed #{link} %}")
|
||||
expect(parsed_tag.render).to eq(%(<a href="#{link}">#{link}</a>))
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue