From f7a72fd2b5f54540c4c40c0b372041201d35a42d Mon Sep 17 00:00:00 2001
From: Edward Loveall <edward@edwardloveall.com>
Date: Mon, 5 Jul 2021 14:10:40 -0400
Subject: [PATCH] Render image inside a figure with a caption

Most images have a caption (or at least have the option of being
captioned). Instead of displaying the raw image, it's not rendered
inside a <figure> tag with a <figcaption> (possibly blank) as a
sibling. The <figcaption> can be marked up with links.
---
 spec/classes/paragraph_converter_spec.cr | 47 +++++++++++++++++++++++-
 spec/components/page_content_spec.cr     | 24 ++++++++++++
 src/classes/paragraph_converter.cr       | 18 ++++++---
 src/components/page_content.cr           |  8 ++++
 src/models/nodes.cr                      |  6 +++
 5 files changed, 97 insertions(+), 6 deletions(-)

diff --git a/spec/classes/paragraph_converter_spec.cr b/spec/classes/paragraph_converter_spec.cr
index 14faef2..fcf37b6 100644
--- a/spec/classes/paragraph_converter_spec.cr
+++ b/spec/classes/paragraph_converter_spec.cr
@@ -158,6 +158,48 @@ describe ParagraphConverter do
     result.should eq expected
   end
 
+  it "converts an IMG to a Figure" do
+    paragraph = PostResponse::Paragraph.from_json <<-JSON
+      {
+        "text": "Image by someuser",
+        "type": "IMG",
+        "markups": [
+          {
+            "title": "",
+            "type": "A",
+            "href": "https://unsplash.com/@someuser",
+            "userId": null,
+            "start": 9,
+            "end": 17,
+            "rel": "photo-creator",
+            "anchorType": "LINK"
+          }
+        ],
+        "href": null,
+        "iframe": null,
+        "layout": "INSET_CENTER",
+        "metadata": {
+          "id": "image.png",
+          "originalWidth": 618,
+          "originalHeight": 682
+        }
+      }
+    JSON
+    expected = [
+      Figure.new(children: [
+        Image.new(src: "image.png"),
+        FigureCaption.new(children: [
+          Text.new("Image by "),
+          Anchor.new(href: "https://unsplash.com/@someuser", text: "someuser"),
+        ] of Child),
+      ] of Child),
+    ]
+
+    result = ParagraphConverter.new.convert([paragraph])
+
+    result.should eq expected
+  end
+
   it "converts all the tags" do
     paragraphs = Array(PostResponse::Paragraph).from_json <<-JSON
       [
@@ -260,7 +302,10 @@ describe ParagraphConverter do
       BlockQuote.new([Text.new("text")] of Child),
       UnorderedList.new([ListItem.new([Text.new("text")] of Child)] of Child),
       OrderedList.new([ListItem.new([Text.new("text")] of Child)] of Child),
-      Image.new(src: "1*miroimage.png"),
+      Figure.new(children: [
+        Image.new(src: "1*miroimage.png"),
+        FigureCaption.new(children: [Text.new("text")] of Child),
+      ] of Child),
       IFrame.new(href: "https://example.com"),
     ]
 
diff --git a/spec/components/page_content_spec.cr b/spec/components/page_content_spec.cr
index ff25dac..a3c1f2b 100644
--- a/spec/components/page_content_spec.cr
+++ b/spec/components/page_content_spec.cr
@@ -87,6 +87,26 @@ describe PageContent do
     html.should eq %(<p>This is <em>neat!</em></p>)
   end
 
+  it "renders a figure and figure caption" do
+    page = Page.new(nodes: [
+      Figure.new(children: [
+        Image.new(src: "image.png"),
+        FigureCaption.new(children: [
+          Text.new("A caption")
+        ] of Child),
+      ] of Child),
+    ] of Child)
+
+    html = PageContent.new(page: page).render_to_string
+
+    html.should eq stripped_html <<-HTML
+      <figure>
+        <img src="https://cdn-images-1.medium.com/image.png">
+        <figcaption>A caption</figcaption>
+      </figure>
+    HTML
+  end
+
   it "renders an H3" do
     page = Page.new(nodes: [
       Heading3.new(children: [
@@ -207,3 +227,7 @@ describe PageContent do
     html.should eq %(<a href="https://medium.com/u/abc123">Some User</a>)
   end
 end
+
+def stripped_html(html : String)
+  html.gsub(/\n\s*/, "").strip
+end
diff --git a/src/classes/paragraph_converter.cr b/src/classes/paragraph_converter.cr
index 1288a04..071a742 100644
--- a/src/classes/paragraph_converter.cr
+++ b/src/classes/paragraph_converter.cr
@@ -29,11 +29,7 @@ class ParagraphConverter
         end
       when PostResponse::ParagraphType::IMG
         paragraph = paragraphs.shift
-        if metadata = paragraph.metadata
-          node = Image.new(src: metadata.id)
-        else
-          node = Empty.new
-        end
+        node = convert_img(paragraph)
       when PostResponse::ParagraphType::OLI
         list_items = convert_oli(paragraphs)
         node = OrderedList.new(children: list_items)
@@ -76,4 +72,16 @@ class ParagraphConverter
       [] of Child
     end
   end
+
+  private def convert_img(paragraph : PostResponse::Paragraph) : Child
+    if metadata = paragraph.metadata
+      caption_markup = MarkupConverter.convert(paragraph.text, paragraph.markups)
+      Figure.new(children: [
+        Image.new(src: metadata.id),
+        FigureCaption.new(children: caption_markup)
+      ] of Child)
+    else
+      Empty.new
+    end
+  end
 end
diff --git a/src/components/page_content.cr b/src/components/page_content.cr
index 52c4eb3..1a50d29 100644
--- a/src/components/page_content.cr
+++ b/src/components/page_content.cr
@@ -38,6 +38,14 @@ class PageContent < BaseComponent
     raw "<!-- an Empty was rendered -->"
   end
 
+  def render_child(node : Figure)
+    figure { render_children(node.children) }
+  end
+
+  def render_child(node : FigureCaption)
+    figcaption { render_children(node.children) }
+  end
+
   def render_child(node : Heading3)
     h3 { render_children(node.children) }
   end
diff --git a/src/models/nodes.cr b/src/models/nodes.cr
index b533099..cb1b873 100644
--- a/src/models/nodes.cr
+++ b/src/models/nodes.cr
@@ -33,6 +33,12 @@ module Nodes
   class Emphasis < Container
   end
 
+  class Figure < Container
+  end
+
+  class FigureCaption < Container
+  end
+
   class Heading3 < Container
   end