diff --git a/shard.lock b/shard.lock index 42a8367..124a443 100644 --- a/shard.lock +++ b/shard.lock @@ -60,6 +60,10 @@ shards: git: https://github.com/luckyframework/lucky_task.git version: 0.1.0 + monads: + git: https://github.com/alex-lairan/monads.git + version: 1.0.0 + pg: git: https://github.com/will/crystal-pg.git version: 0.23.2 diff --git a/shard.yml b/shard.yml index 64132a9..54fdc4a 100644 --- a/shard.yml +++ b/shard.yml @@ -26,6 +26,8 @@ dependencies: lucky_task: github: luckyframework/lucky_task version: ~> 0.1.0 + monads: + github: alex-lairan/monads development_dependencies: lucky_flow: github: luckyframework/lucky_flow diff --git a/spec/classes/embedded_converter_spec.cr b/spec/classes/embedded_converter_spec.cr new file mode 100644 index 0000000..4b7ec07 --- /dev/null +++ b/spec/classes/embedded_converter_spec.cr @@ -0,0 +1,99 @@ +require "../spec_helper" + +include Nodes + +describe EmbeddedConverter do + context "when the mediaResource has an iframeSrc value" do + it "returns an EmbeddedContent node" do + paragraph = PostResponse::Paragraph.from_json <<-JSON + { + "text": "", + "type": "IFRAME", + "href": null, + "layout": "INSET_CENTER", + "markups": [], + "iframe": { + "mediaResource": { + "id": "abc123", + "href": "https://twitter.com/user/status/1", + "iframeSrc": "https://cdn.embedly.com/widgets/...", + "iframeWidth": 500, + "iframeHeight": 281 + } + }, + "metadata": null + } + JSON + + result = EmbeddedConverter.convert(paragraph) + + result.should eq( + EmbeddedContent.new( + src: "https://cdn.embedly.com/widgets/...", + originalWidth: 500, + originalHeight: 281, + ) + ) + end + end + + context "when the mediaResource has a blank iframeSrc value" do + context "and the href is unknown" do + it "returns an EmbeddedLink node" do + paragraph = PostResponse::Paragraph.from_json <<-JSON + { + "text": "", + "type": "IFRAME", + "href": null, + "layout": "INSET_CENTER", + "markups": [], + "iframe": { + "mediaResource": { + "id": "abc123", + "href": "https://example.com", + "iframeSrc": "", + "iframeWidth": 0, + "iframeHeight": 0 + } + }, + "metadata": null + } + JSON + + result = EmbeddedConverter.convert(paragraph) + + result.should eq(EmbeddedLink.new(href: "https://example.com")) + end + end + + context "and the href is gist.github.com" do + it "returns an GithubGist node" do + paragraph = PostResponse::Paragraph.from_json <<-JSON + { + "text": "", + "type": "IFRAME", + "href": null, + "layout": "INSET_CENTER", + "markups": [], + "iframe": { + "mediaResource": { + "id": "abc123", + "href": "https://gist.github.com/user/someid", + "iframeSrc": "", + "iframeWidth": 0, + "iframeHeight": 0 + } + }, + "metadata": null + } + JSON + + result = EmbeddedConverter.convert(paragraph) + + result.should eq( + GithubGist.new(href: "https://gist.github.com/user/someid") + ) + end + end + end +end diff --git a/spec/classes/paragraph_converter_spec.cr b/spec/classes/paragraph_converter_spec.cr index 1d31bbb..bac37f5 100644 --- a/spec/classes/paragraph_converter_spec.cr +++ b/spec/classes/paragraph_converter_spec.cr @@ -272,7 +272,10 @@ describe ParagraphConverter do "markups": [], "iframe": { "mediaResource": { - "href": "https://example.com" + "href": "https://example.com", + "iframeSrc": "", + "iframeWidth": 0, + "iframeHeight": 0 } }, "layout": null, @@ -312,7 +315,7 @@ describe ParagraphConverter do Image.new(src: "1*miroimage.png", originalWidth: 618, originalHeight: 682), FigureCaption.new(children: [Text.new("text")] of Child), ] of Child), - IFrame.new(href: "https://example.com"), + EmbeddedLink.new(href: "https://example.com"), MixtapeEmbed.new(children: [ Anchor.new( children: [Text.new("Mixtape")] of Child, diff --git a/spec/components/page_content_spec.cr b/spec/components/page_content_spec.cr index bbb5877..6272a43 100644 --- a/spec/components/page_content_spec.cr +++ b/spec/components/page_content_spec.cr @@ -152,6 +152,24 @@ describe PageContent do HTML end + it "renders a GitHub Gist" do + page = Page.new( + title: "Title", + subtitle: nil, + author: "Author", + created_at: Time.local, + nodes: [ + GithubGist.new(href: "https://gist.github.com/user/some_id"), + ] of Child + ) + + html = PageContent.new(page: page).render_to_string + + html.should eq stripped_html <<-HTML + + HTML + end + it "renders an H3" do page = Page.new( title: "Title", @@ -210,7 +228,32 @@ describe PageContent do HTML end - it "renders an iframe container" do + it "renders embedded content" do + page = Page.new( + title: "Title", + subtitle: nil, + author: "Author", + created_at: Time.local, + nodes: [ + EmbeddedContent.new( + src: "https://example.com", + originalWidth: 1000, + originalHeight: 600, + ), + ] of Child + ) + + html = PageContent.new(page: page).render_to_string + + html.should eq stripped_html <<-HTML +
+ +
+ HTML + end + + it "renders an embedded link container" do page = Page.new( title: "Title", subtitle: nil, @@ -218,7 +261,7 @@ describe PageContent do created_at: Time.local, nodes: [ Paragraph.new(children: [ - IFrame.new(href: "https://example.com"), + EmbeddedLink.new(href: "https://example.com"), ] of Child), ] of Child ) diff --git a/spec/models/nodes_spec.cr b/spec/models/nodes_spec.cr index f9a43ac..1eb20b1 100644 --- a/spec/models/nodes_spec.cr +++ b/spec/models/nodes_spec.cr @@ -1,9 +1,9 @@ require "../spec_helper" module Nodes - describe IFrame do + describe EmbeddedLink do it "returns embedded url with subdomains" do - iframe = IFrame.new(href: "https://dev.example.com/page") + iframe = EmbeddedLink.new(href: "https://dev.example.com/page") iframe.domain.should eq("dev.example.com") end @@ -23,4 +23,17 @@ module Nodes image.src.should eq("https://cdn-images-1.medium.com/fit/c/800/482/image.png") end end + + describe EmbeddedContent do + it "adjusts the width and height proportionally" do + content = EmbeddedContent.new( + src: "https://example.com", + originalWidth: 1000, + originalHeight: 600, + ) + + content.width.should eq("800") + content.height.should eq("480") + end + end end diff --git a/src/classes/embedded_converter.cr b/src/classes/embedded_converter.cr new file mode 100644 index 0000000..6848bfc --- /dev/null +++ b/src/classes/embedded_converter.cr @@ -0,0 +1,42 @@ +class EmbeddedConverter + include Nodes + + GIST_HOST = "https://gist.github.com" + + getter paragraph : PostResponse::Paragraph + + def self.convert(paragraph : PostResponse::Paragraph) : Embedded | Empty + new(paragraph).convert + end + + def initialize(@paragraph : PostResponse::Paragraph) + end + + def convert : Embedded | Empty + Monads::Try(PostResponse::IFrame).new(->{ paragraph.iframe }) + .to_maybe + .fmap(->(iframe : PostResponse::IFrame) { iframe.mediaResource }) + .fmap(->media_to_embedded(PostResponse::MediaResource)) + .value_or(Empty.new) + end + + private def media_to_embedded(media : PostResponse::MediaResource) : Embedded + if media.iframeSrc.blank? + custom_embed(media) + else + EmbeddedContent.new( + src: media.iframeSrc, + originalWidth: media.iframeWidth, + originalHeight: media.iframeHeight + ) + end + end + + private def custom_embed(media : PostResponse::MediaResource) : Embedded + if media.href.starts_with?(GIST_HOST) + GithubGist.new(href: media.href) + else + EmbeddedLink.new(href: media.href) + end + end +end diff --git a/src/classes/paragraph_converter.cr b/src/classes/paragraph_converter.cr index 0596aff..6070cf6 100644 --- a/src/classes/paragraph_converter.cr +++ b/src/classes/paragraph_converter.cr @@ -20,11 +20,7 @@ class ParagraphConverter node = Heading3.new(children: children) when PostResponse::ParagraphType::IFRAME paragraph = paragraphs.shift - if iframe = paragraph.iframe - node = IFrame.new(href: iframe.mediaResource.href) - else - node = Empty.new - end + node = EmbeddedConverter.convert(paragraph) when PostResponse::ParagraphType::IMG paragraph = paragraphs.shift node = convert_img(paragraph) diff --git a/src/clients/medium_client.cr b/src/clients/medium_client.cr index 60fe41a..1898497 100644 --- a/src/clients/medium_client.cr +++ b/src/clients/medium_client.cr @@ -43,6 +43,9 @@ class MediumClient iframe { mediaResource { href + iframeSrc + iframeWidth + iframeHeight } } metadata { diff --git a/src/components/page_content.cr b/src/components/page_content.cr index 248e544..980b424 100644 --- a/src/components/page_content.cr +++ b/src/components/page_content.cr @@ -29,6 +29,26 @@ class PageContent < BaseComponent raw "" end + def render_child(child : EmbeddedContent) + div class: "iframe-wrapper" do + iframe( + src: child.src, + width: child.width, + height: child.height, + frameborder: "0", + allowfullscreen: true, + ) + end + end + + def render_child(child : EmbeddedLink) + div class: "embedded" do + a href: child.href do + text "Embedded content at #{child.domain}" + end + end + end + def render_child(node : Emphasis) em { render_children(node.children) } end @@ -55,6 +75,10 @@ class PageContent < BaseComponent end end + def render_child(child : GithubGist) + script src: child.src + end + def render_child(node : Heading2) h2 { render_children(node.children) } end @@ -63,14 +87,6 @@ class PageContent < BaseComponent h3 { render_children(node.children) } end - def render_child(child : IFrame) - div class: "embedded" do - a href: child.href do - text "Embedded content at #{child.domain}" - end - end - end - def render_child(child : Image) img src: child.src, width: child.width end diff --git a/src/models/nodes.cr b/src/models/nodes.cr index 3c141d8..4721e33 100644 --- a/src/models/nodes.cr +++ b/src/models/nodes.cr @@ -1,5 +1,6 @@ module Nodes - alias Leaf = Text | Image | IFrame + alias Embedded = EmbeddedLink | EmbeddedContent | GithubGist + alias Leaf = Text | Image | Embedded alias Child = Container | Leaf | Empty alias Children = Array(Child) @@ -120,7 +121,40 @@ module Nodes end end - class IFrame + class EmbeddedContent + MAX_WIDTH = 800 + + getter src : String + + def initialize(@src : String, @originalWidth : Int32, @originalHeight : Int32) + end + + def width + [@originalWidth, MAX_WIDTH].min.to_s + end + + def height + if @originalWidth > MAX_WIDTH + (@originalHeight * ratio).round.to_i.to_s + else + @originalHeight.to_s + end + end + + private def ratio + MAX_WIDTH / @originalWidth + end + + def ==(other : EmbeddedContent) + other.src == src && other.width == width && other.height == height + end + + def empty? + false + end + end + + class EmbeddedLink getter href : String def initialize(@href : String) @@ -130,7 +164,7 @@ module Nodes URI.parse(href).host end - def ==(other : IFrame) + def ==(other : EmbeddedLink) other.href == href end @@ -171,4 +205,21 @@ module Nodes false end end + + class GithubGist + def initialize(@href : String) + end + + def src + "#{@href}.js" + end + + def ==(other : GithubGist) + other.src == src + end + + def empty? + false + end + end end diff --git a/src/models/post_response.cr b/src/models/post_response.cr index d1411de..28c6768 100644 --- a/src/models/post_response.cr +++ b/src/models/post_response.cr @@ -82,6 +82,9 @@ class PostResponse class MediaResource < Base property href : String + property iframeSrc : String + property iframeWidth : Int32 + property iframeHeight : Int32 end class Metadata < Base diff --git a/src/shards.cr b/src/shards.cr index ac3bf5c..30fb81f 100644 --- a/src/shards.cr +++ b/src/shards.cr @@ -7,3 +7,4 @@ require "avram" require "lucky" require "carbon" require "authentic" +require "monads"