diff --git a/shard.yml b/shard.yml
index c19ca00..64132a9 100644
--- a/shard.yml
+++ b/shard.yml
@@ -29,4 +29,4 @@ dependencies:
development_dependencies:
lucky_flow:
github: luckyframework/lucky_flow
- version: ~> 0.7.3
\ No newline at end of file
+ version: ~> 0.7.3
diff --git a/spec/classes/iframe_media_resolver_spec.cr b/spec/classes/iframe_media_resolver_spec.cr
new file mode 100644
index 0000000..ed167a8
--- /dev/null
+++ b/spec/classes/iframe_media_resolver_spec.cr
@@ -0,0 +1,27 @@
+require "../spec_helper"
+
+include Nodes
+
+describe IFrameMediaResolver do
+ around_each do |example|
+ original_client = IFrameMediaResolver.http_client
+ IFrameMediaResolver.http_client = FakeMediumClient
+ example.run
+ IFrameMediaResolver.http_client = original_client
+ end
+
+ it "returns a url of the embedded page" do
+ iframe = PostResponse::IFrame.from_json <<-JSON
+ {
+ "mediaResource": {
+ "id": "d4515fff7ecd02786e75fc8997c94bbf"
+ }
+ }
+ JSON
+ resolver = IFrameMediaResolver.new(iframe: iframe)
+
+ result = resolver.fetch_href
+
+ result.should eq("https://example.com")
+ end
+end
diff --git a/spec/classes/markup_converter_spec.cr b/spec/classes/markup_converter_spec.cr
new file mode 100644
index 0000000..a87a2b2
--- /dev/null
+++ b/spec/classes/markup_converter_spec.cr
@@ -0,0 +1,99 @@
+require "../spec_helper"
+
+include Nodes
+
+describe MarkupConverter do
+ it "returns just text with no markups" do
+ json = <<-JSON
+ {
+ "text": "Hello, world",
+ "type": "P",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ }
+ JSON
+ paragraph = PostResponse::Paragraph.from_json(json)
+
+ result = MarkupConverter.convert(text: paragraph.text, markups: paragraph.markups)
+
+ result.should eq([Text.new(content: "Hello, world")])
+ end
+
+ it "returns just text with multiple markups" do
+ json = <<-JSON
+ {
+ "text": "strong and emphasized only",
+ "type": "P",
+ "markups": [
+ {
+ "title": null,
+ "type": "STRONG",
+ "href": null,
+ "start": 0,
+ "end": 6,
+ "rel": null,
+ "anchorType": null
+ },
+ {
+ "title": null,
+ "type": "EM",
+ "href": null,
+ "start": 11,
+ "end": 21,
+ "rel": null,
+ "anchorType": null
+ }
+ ],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ }
+ JSON
+ paragraph = PostResponse::Paragraph.from_json(json)
+
+ result = MarkupConverter.convert(text: paragraph.text, markups: paragraph.markups)
+
+ result.should eq([
+ Strong.new(children: [Text.new(content: "strong")] of Child),
+ Text.new(content: " and "),
+ Emphasis.new(children: [Text.new(content: "emphasized")] of Child),
+ Text.new(content: " only"),
+ ])
+ end
+
+ it "returns just text with a code markup" do
+ json = <<-JSON
+ {
+ "text": "inline code",
+ "type": "P",
+ "markups": [
+ {
+ "title": null,
+ "type": "CODE",
+ "href": null,
+ "start": 7,
+ "end": 11,
+ "rel": null,
+ "anchorType": null
+ }
+ ],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ }
+ JSON
+ paragraph = PostResponse::Paragraph.from_json(json)
+
+ result = MarkupConverter.convert(text: paragraph.text, markups: paragraph.markups)
+
+ result.should eq([
+ Text.new(content: "inline "),
+ Code.new(children: [Text.new(content: "code")] of Child),
+ ])
+ end
+end
diff --git a/spec/classes/paragraph_converter_spec.cr b/spec/classes/paragraph_converter_spec.cr
new file mode 100644
index 0000000..14faef2
--- /dev/null
+++ b/spec/classes/paragraph_converter_spec.cr
@@ -0,0 +1,271 @@
+require "../spec_helper"
+
+include Nodes
+
+describe ParagraphConverter do
+ around_each do |example|
+ original_client = IFrameMediaResolver.http_client
+ IFrameMediaResolver.http_client = FakeMediumClient
+ example.run
+ IFrameMediaResolver.http_client = original_client
+ end
+
+ it "converts a simple structure with no markups" do
+ paragraphs = Array(PostResponse::Paragraph).from_json <<-JSON
+ [
+ {
+ "text": "Title",
+ "type": "H3",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ }
+ ]
+ JSON
+ expected = [Heading3.new(children: [Text.new(content: "Title")] of Child)]
+
+ result = ParagraphConverter.new.convert(paragraphs)
+
+ result.should eq expected
+ end
+
+ it "converts a simple structure with a markup" do
+ paragraphs = Array(PostResponse::Paragraph).from_json <<-JSON
+ [
+ {
+ "text": "inline code",
+ "type": "P",
+ "markups": [
+ {
+ "name": null,
+ "title": null,
+ "type": "CODE",
+ "href": null,
+ "start": 7,
+ "end": 11,
+ "rel": null,
+ "anchorType": null
+ }
+ ],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ }
+ ]
+ JSON
+ expected = [
+ Paragraph.new(children: [
+ Text.new(content: "inline "),
+ Code.new(children: [Text.new(content: "code")] of Child),
+ ] of Child)
+ ]
+
+ result = ParagraphConverter.new.convert(paragraphs)
+
+ result.should eq expected
+ end
+
+ it "groups
list items into one list" do
+ paragraphs = Array(PostResponse::Paragraph).from_json <<-JSON
+ [
+ {
+ "text": "One",
+ "type": "ULI",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ },
+ {
+ "text": "Two",
+ "type": "ULI",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ },
+ {
+ "text": "Not a list item",
+ "type": "P",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ }
+ ]
+ JSON
+ expected = [
+ UnorderedList.new(children: [
+ ListItem.new(children: [Text.new(content: "One")] of Child),
+ ListItem.new(children: [Text.new(content: "Two")] of Child),
+ ] of Child),
+ Paragraph.new(children: [Text.new(content: "Not a list item")] of Child),
+ ]
+
+ result = ParagraphConverter.new.convert(paragraphs)
+
+ result.should eq expected
+ end
+
+ it "groups list items into one list" do
+ paragraphs = Array(PostResponse::Paragraph).from_json <<-JSON
+ [
+ {
+ "text": "One",
+ "type": "OLI",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ },
+ {
+ "text": "Two",
+ "type": "OLI",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ },
+ {
+ "text": "Not a list item",
+ "type": "P",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ }
+ ]
+ JSON
+ expected = [
+ OrderedList.new(children: [
+ ListItem.new(children: [Text.new(content: "One")] of Child),
+ ListItem.new(children: [Text.new(content: "Two")] of Child),
+ ] of Child),
+ Paragraph.new(children: [Text.new(content: "Not a list item")] of Child),
+ ]
+
+ result = ParagraphConverter.new.convert(paragraphs)
+
+ result.should eq expected
+ end
+
+ it "converts all the tags" do
+ paragraphs = Array(PostResponse::Paragraph).from_json <<-JSON
+ [
+ {
+ "text": "text",
+ "type": "H3",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ },
+ {
+ "text": "text",
+ "type": "H4",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ },
+ {
+ "text": "text",
+ "type": "P",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ },
+ {
+ "text": "text",
+ "type": "PRE",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ },
+ {
+ "text": "text",
+ "type": "BQ",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ },
+ {
+ "text": "text",
+ "type": "ULI",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ },
+ {
+ "text": "text",
+ "type": "OLI",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": null
+ },
+ {
+ "text": "text",
+ "type": "IMG",
+ "markups": [],
+ "href": null,
+ "iframe": null,
+ "layout": null,
+ "metadata": {
+ "id": "1*miroimage.png",
+ "originalWidth": 618,
+ "originalHeight": 682
+ }
+ },
+ {
+ "text": "",
+ "type": "IFRAME",
+ "markups": [],
+ "href": null,
+ "iframe": {
+ "mediaResource": {
+ "id": "7c6231d165bf9fc1853f259a7b55bd14"
+ }
+ },
+ "layout": null,
+ "metadata": null
+ }
+ ]
+ JSON
+ expected = [
+ Heading3.new([Text.new("text")] of Child),
+ Heading4.new([Text.new("text")] of Child),
+ Paragraph.new([Text.new("text")] of Child),
+ Preformatted.new([Text.new("text")] of Child),
+ 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"),
+ IFrame.new(href: "https://example.com"),
+ ]
+
+ result = ParagraphConverter.new.convert(paragraphs)
+
+ result.should eq expected
+ end
+end
diff --git a/spec/components/page_content_spec.cr b/spec/components/page_content_spec.cr
new file mode 100644
index 0000000..3bab1c1
--- /dev/null
+++ b/spec/components/page_content_spec.cr
@@ -0,0 +1,189 @@
+require "../spec_helper"
+
+include Nodes
+
+describe PageContent do
+ it "renders a single parent/child node structure" do
+ page = Page.new(nodes: [
+ Paragraph.new(children: [
+ Text.new(content: "hi"),
+ ] of Child)
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %(hi
)
+ end
+
+ it "renders multiple childrens" do
+ page = Page.new(nodes: [
+ Paragraph.new(children: [
+ Text.new(content: "Hello, "),
+ Emphasis.new(children: [
+ Text.new(content: "World!")
+ ] of Child)
+ ] of Child),
+ UnorderedList.new(children: [
+ ListItem.new(children: [
+ Text.new(content: "List!")
+ ] of Child),
+ ListItem.new(children: [
+ Text.new(content: "Again!"),
+ ] of Child)
+ ] of Child)
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %(Hello, World!
)
+ end
+
+ it "renders a blockquote" do
+ page = Page.new(nodes: [
+ BlockQuote.new(children: [
+ Text.new("Wayne Gretzky. Michael Scott.")
+ ] of Child)
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %(Wayne Gretzky. Michael Scott.
)
+ end
+
+ it "renders code" do
+ page = Page.new(nodes: [
+ Code.new(children: [
+ Text.new("foo = bar")
+ ] of Child)
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %(foo = bar
)
+ end
+
+ it "renders empasis" do
+ page = Page.new(nodes: [
+ Paragraph.new(children: [
+ Text.new(content: "This is "),
+ Emphasis.new(children: [
+ Text.new(content: "neat!")
+ ] of Child),
+ ] of Child),
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %(This is neat!
)
+ end
+
+ it "renders an H3" do
+ page = Page.new(nodes: [
+ Heading3.new(children: [
+ Text.new(content: "Title!"),
+ ] of Child),
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %(Title!
)
+ end
+
+ it "renders an H4" do
+ page = Page.new(nodes: [
+ Heading4.new(children: [
+ Text.new(content: "In Conclusion..."),
+ ] of Child),
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %(In Conclusion...
)
+ end
+
+ it "renders an image" do
+ page = Page.new(nodes: [
+ Paragraph.new(children: [
+ Image.new(src: "image.png"),
+ ] of Child)
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %()
+ end
+
+ it "renders an iframe container" do
+ page = Page.new(nodes: [
+ Paragraph.new(children: [
+ IFrame.new(href: "https://example.com"),
+ ] of Child)
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %()
+ end
+
+ it "renders an ordered list" do
+ page = Page.new(nodes: [
+ OrderedList.new(children: [
+ ListItem.new(children: [Text.new("One")] of Child),
+ ListItem.new(children: [Text.new("Two")] of Child),
+ ] of Child),
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %(- One
- Two
)
+ end
+
+ it "renders an preformatted text" do
+ page = Page.new(nodes: [
+ Paragraph.new(children: [
+ Text.new("Hello, world!"),
+ ] of Child),
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %(Hello, world!
)
+ end
+
+ it "renders an preformatted text" do
+ page = Page.new(nodes: [
+ Preformatted.new(children: [
+ Text.new("New\nline"),
+ ] of Child),
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %(New\nline
)
+ end
+
+ it "renders strong text" do
+ page = Page.new(nodes: [
+ Strong.new(children: [
+ Text.new("Oh yeah!"),
+ ] of Child),
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %(Oh yeah!)
+ end
+
+ it "renders an unordered list" do
+ page = Page.new(nodes: [
+ UnorderedList.new(children: [
+ ListItem.new(children: [Text.new("Apple")] of Child),
+ ListItem.new(children: [Text.new("Banana")] of Child),
+ ] of Child),
+ ] of Child)
+
+ html = PageContent.new(page: page).render_to_string
+
+ html.should eq %()
+ end
+end
diff --git a/spec/support/fake_medium_client.cr b/spec/support/fake_medium_client.cr
new file mode 100644
index 0000000..9ae65e2
--- /dev/null
+++ b/spec/support/fake_medium_client.cr
@@ -0,0 +1,9 @@
+class FakeMediumClient < MediumClient
+ def self.media_data(media_id : String) : MediaResponse::Root
+ MediaResponse::Root.from_json(
+ <<-JSON
+ {"payload": {"value": {"href": "https://example.com"}}}
+ JSON
+ )
+ end
+end
diff --git a/src/actions/articles/show.cr b/src/actions/articles/show.cr
index efa10d8..842e17f 100644
--- a/src/actions/articles/show.cr
+++ b/src/actions/articles/show.cr
@@ -1,12 +1,16 @@
require "json"
class Articles::Show < BrowserAction
- get "/post/:post_id" do
+ get "/posts/:post_id" do
if Lucky::Env.use_local?
response = LocalClient.post_data(post_id)
else
response = MediumClient.post_data(post_id)
end
- html ShowPage, post_response: response
+ content = ParagraphConverter.new.convert(
+ response.data.post.content.bodyModel.paragraphs
+ )
+ page = Page.new(nodes: content)
+ html ShowPage, page: page
end
end
diff --git a/src/app.cr b/src/app.cr
index 1a2f4d1..b5f93a5 100644
--- a/src/app.cr
+++ b/src/app.cr
@@ -3,6 +3,7 @@ require "./shards"
Lucky::AssetHelpers.load_manifest
require "./app_database"
+require "./constants"
require "./models/base_model"
require "./models/mixins/**"
require "./models/**"
@@ -16,6 +17,8 @@ require "./actions/mixins/**"
require "./actions/**"
require "./components/base_component"
require "./components/**"
+require "./classes/**"
+require "./clients/**"
require "./pages/**"
require "../config/env"
require "../config/**"
diff --git a/src/classes/iframe_media_resolver.cr b/src/classes/iframe_media_resolver.cr
new file mode 100644
index 0000000..06d6e61
--- /dev/null
+++ b/src/classes/iframe_media_resolver.cr
@@ -0,0 +1,13 @@
+class IFrameMediaResolver
+ class_property http_client : MediumClient.class = MediumClient
+
+ getter iframe
+
+ def initialize(@iframe : PostResponse::IFrame)
+ end
+
+ def fetch_href
+ response = @@http_client.media_data(iframe.mediaResource.id)
+ response.payload.value.href
+ end
+end
diff --git a/src/classes/markup_converter.cr b/src/classes/markup_converter.cr
new file mode 100644
index 0000000..0e40f53
--- /dev/null
+++ b/src/classes/markup_converter.cr
@@ -0,0 +1,74 @@
+struct StringSplit
+ getter pre, content, post
+
+ def initialize(@pre : String, @content : String, @post : String)
+ end
+end
+
+class MarkupConverter
+ include Nodes
+
+ getter markups : Array(PostResponse::Markup)
+ getter text : String
+
+ def self.convert(text : String, markups : Array(PostResponse::Markup))
+ new(text, markups).convert
+ end
+
+ def initialize(@text : String, @markups : Array(PostResponse::Markup))
+ end
+
+ def convert : Array(Child)
+ if markups.empty?
+ return [Text.new(content: text)] of Child
+ end
+ offset = 0
+ text_splits = markups.reduce([text]) do |splits, markup|
+ individual_split = split_string(markup.start - offset, markup.end - offset, splits.pop)
+ offset = markup.end
+ splits.push(individual_split.pre)
+ splits.push(individual_split.content)
+ splits.push(individual_split.post)
+ end
+ text_splits.in_groups_of(2, "").map_with_index do |split, index|
+ plain, to_be_marked = split
+ markup = markups.fetch(index, Text.new(""))
+ if markup.is_a?(Text)
+ [Text.new(plain)] of Child
+ else
+ case markup.type
+ when PostResponse::MarkupType::CODE
+ container = construct_markup(to_be_marked, Code)
+ when PostResponse::MarkupType::EM
+ container = construct_markup(to_be_marked, Emphasis)
+ when PostResponse::MarkupType::STRONG
+ container = construct_markup(to_be_marked, Strong)
+ else
+ container = construct_markup(to_be_marked, Code)
+ end
+ [Text.new(plain), container] of Child
+ end
+ end.flatten.reject(&.empty?)
+ end
+
+ private def construct_markup(text : String, container : Container.class) : Child
+ container.new(children: [Text.new(content: text)] of Child)
+ end
+
+ private def split_string(start : Int32, finish : Int32, string : String)
+ if start.zero?
+ pre = ""
+ else
+ pre = string[0...start]
+ end
+
+ if finish == string.size
+ post = ""
+ else
+ post = string[finish...string.size]
+ end
+
+ content = string[start...finish]
+ StringSplit.new(pre, content, post)
+ end
+end
diff --git a/src/classes/paragraph_converter.cr b/src/classes/paragraph_converter.cr
new file mode 100644
index 0000000..1288a04
--- /dev/null
+++ b/src/classes/paragraph_converter.cr
@@ -0,0 +1,79 @@
+class ParagraphConverter
+ include Nodes
+
+ def convert(paragraphs : Array(PostResponse::Paragraph)) : Array(Child)
+ if paragraphs.first?.nil?
+ return [Empty.new] of Child
+ else
+ case paragraphs.first.type
+ when PostResponse::ParagraphType::BQ
+ paragraph = paragraphs.shift
+ children = MarkupConverter.convert(paragraph.text, paragraph.markups)
+ node = BlockQuote.new(children: children)
+ when PostResponse::ParagraphType::H3
+ paragraph = paragraphs.shift
+ children = MarkupConverter.convert(paragraph.text, paragraph.markups)
+ node = Heading3.new(children: children)
+ when PostResponse::ParagraphType::H4
+ paragraph = paragraphs.shift
+ children = MarkupConverter.convert(paragraph.text, paragraph.markups)
+ node = Heading4.new(children: children)
+ when PostResponse::ParagraphType::IFRAME
+ paragraph = paragraphs.shift
+ if iframe = paragraph.iframe
+ resolver = IFrameMediaResolver.new(iframe: iframe)
+ href = resolver.fetch_href
+ node = IFrame.new(href: href)
+ else
+ node = Empty.new
+ end
+ when PostResponse::ParagraphType::IMG
+ paragraph = paragraphs.shift
+ if metadata = paragraph.metadata
+ node = Image.new(src: metadata.id)
+ else
+ node = Empty.new
+ end
+ when PostResponse::ParagraphType::OLI
+ list_items = convert_oli(paragraphs)
+ node = OrderedList.new(children: list_items)
+ when PostResponse::ParagraphType::P
+ paragraph = paragraphs.shift
+ children = MarkupConverter.convert(paragraph.text, paragraph.markups)
+ node = Paragraph.new(children: children)
+ when PostResponse::ParagraphType::PRE
+ paragraph = paragraphs.shift
+ children = MarkupConverter.convert(paragraph.text, paragraph.markups)
+ node = Preformatted.new(children: children)
+ when PostResponse::ParagraphType::ULI
+ list_items = convert_uli(paragraphs)
+ node = UnorderedList.new(children: list_items)
+ else
+ paragraphs.shift # so we don't recurse infinitely
+ node = Empty.new
+ end
+
+ [node, convert(paragraphs)].flatten.reject(&.empty?)
+ end
+ end
+
+ private def convert_uli(paragraphs : Array(PostResponse::Paragraph)) : Array(Child)
+ if paragraphs.first? && paragraphs.first.type.is_a?(PostResponse::ParagraphType::ULI)
+ paragraph = paragraphs.shift
+ children = MarkupConverter.convert(paragraph.text, paragraph.markups)
+ [ListItem.new(children: children)] + convert_uli(paragraphs)
+ else
+ [] of Child
+ end
+ end
+
+ private def convert_oli(paragraphs : Array(PostResponse::Paragraph)) : Array(Child)
+ if paragraphs.first? && paragraphs.first.type.is_a?(PostResponse::ParagraphType::OLI)
+ paragraph = paragraphs.shift
+ children = MarkupConverter.convert(paragraph.text, paragraph.markups)
+ [ListItem.new(children: children)] + convert_oli(paragraphs)
+ else
+ [] of Child
+ end
+ end
+end
diff --git a/src/actions/clients/local_client.cr b/src/clients/local_client.cr
similarity index 100%
rename from src/actions/clients/local_client.cr
rename to src/clients/local_client.cr
diff --git a/src/actions/clients/medium_client.cr b/src/clients/medium_client.cr
similarity index 94%
rename from src/actions/clients/medium_client.cr
rename to src/clients/medium_client.cr
index 4d140e1..6a2328d 100644
--- a/src/actions/clients/medium_client.cr
+++ b/src/clients/medium_client.cr
@@ -1,9 +1,6 @@
require "json"
class MediumClient
- # https://stackoverflow.com/questions/2669690/
- JSON_HIJACK_STRING = "])}while(1);"
-
def self.post_data(post_id : String) : PostResponse::Root
client = HTTP::Client.new("medium.com", tls: true)
response = client.post("/_/graphql", headers: headers, body: body(post_id))
diff --git a/src/components/page_content.cr b/src/components/page_content.cr
new file mode 100644
index 0000000..49fe832
--- /dev/null
+++ b/src/components/page_content.cr
@@ -0,0 +1,84 @@
+class PageContent < BaseComponent
+ include Nodes
+ needs page : Page
+
+ def render
+ page.nodes.each do |node|
+ render_child(node)
+ end
+ end
+
+ def render_children(children : Children)
+ children.each { |child| render_child(child) }
+ end
+
+ def render_child(node : BlockQuote)
+ blockquote { render_children(node.children) }
+ end
+
+ def render_child(node : Code)
+ code { render_children(node.children) }
+ end
+
+ def render_child(container : Container)
+ # Should never get called
+ raw ""
+ end
+
+ def render_child(node : Emphasis)
+ em { render_children(node.children) }
+ end
+
+ def render_child(container : Empty)
+ # Should never get called
+ raw ""
+ end
+
+ def render_child(node : Heading3)
+ h3 { render_children(node.children) }
+ end
+
+ def render_child(node : Heading4)
+ h4 { render_children(node.children) }
+ end
+
+ def render_child(child : IFrame)
+ div class: "embedded" do
+ a href: child.href do
+ text "Click to visit embedded content"
+ end
+ end
+ end
+
+ def render_child(child : Image)
+ img src: child.src
+ end
+
+ def render_child(node : ListItem)
+ li { render_children(node.children) }
+ end
+
+ def render_child(node : OrderedList)
+ ol { render_children(node.children) }
+ end
+
+ def render_child(node : Paragraph)
+ para { render_children(node.children) }
+ end
+
+ def render_child(node : Preformatted)
+ pre { render_children(node.children) }
+ end
+
+ def render_child(node : Strong)
+ strong { render_children(node.children) }
+ end
+
+ def render_child(child : Text)
+ text child.content
+ end
+
+ def render_child(node : UnorderedList)
+ ul { render_children(node.children) }
+ end
+end
diff --git a/src/components/post.cr b/src/components/post.cr
deleted file mode 100644
index 587d463..0000000
--- a/src/components/post.cr
+++ /dev/null
@@ -1,29 +0,0 @@
-class Post::Post < BaseComponent
- needs response : PostResponse::Root
-
- def render
- data = response.data.post.content.bodyModel.paragraphs
- data.each do |paragraph|
- case paragraph.type
- when PostResponse::ParagraphType::H3
- h3 paragraph.text
- when PostResponse::ParagraphType::H4
- h4 paragraph.text
- when PostResponse::ParagraphType::P
- para paragraph.text
- when PostResponse::ParagraphType::PRE
- pre paragraph.text
- when PostResponse::ParagraphType::BQ
- blockquote paragraph.text
- when PostResponse::ParagraphType::OLI
- li paragraph.text
- when PostResponse::ParagraphType::ULI
- li paragraph.text
- when PostResponse::ParagraphType::IFRAME
- mount IFrame, paragraph: paragraph
- else
- para "#{paragraph.type} not yet implimented"
- end
- end
- end
-end
diff --git a/src/constants.cr b/src/constants.cr
new file mode 100644
index 0000000..a136d90
--- /dev/null
+++ b/src/constants.cr
@@ -0,0 +1,2 @@
+# https://stackoverflow.com/questions/2669690/
+JSON_HIJACK_STRING = "])}while(1);"
diff --git a/src/models/media_response.cr b/src/models/media_response.cr
index 8ee915d..16d6749 100644
--- a/src/models/media_response.cr
+++ b/src/models/media_response.cr
@@ -13,6 +13,5 @@ class MediaResponse
class Value < Base
property href : String
- property iframeSrc : String
end
end
diff --git a/src/models/nodes.cr b/src/models/nodes.cr
new file mode 100644
index 0000000..99d02a1
--- /dev/null
+++ b/src/models/nodes.cr
@@ -0,0 +1,107 @@
+module Nodes
+ alias Leaf = Text | Image | IFrame
+ alias Child = Container | Leaf | Empty
+ alias Children = Array(Child)
+
+ class Container
+ getter children : Children
+
+ def initialize(@children : Children)
+ end
+
+ def ==(other : Container)
+ other.children == children
+ end
+
+ def empty?
+ children.empty? || children.each(&.empty?)
+ end
+ end
+
+ class Empty
+ def empty?
+ true
+ end
+ end
+
+ class BlockQuote < Container
+ end
+
+ class Code < Container
+ end
+
+ class Emphasis < Container
+ end
+
+ class Heading3 < Container
+ end
+
+ class Heading4 < Container
+ end
+
+ class ListItem < Container
+ end
+
+ class OrderedList < Container
+ end
+
+ class Paragraph < Container
+ end
+
+ class Preformatted < Container
+ end
+
+ class Strong < Container
+ end
+
+ class UnorderedList < Container
+ end
+
+ class Text
+ getter content : String
+
+ def initialize(@content : String)
+ end
+
+ def ==(other : Text)
+ other.content == content
+ end
+
+ def empty?
+ content.empty?
+ end
+ end
+
+ class Image
+ IMAGE_HOST = "https://cdn-images-1.medium.com"
+
+ getter src : String
+
+ def initialize(src : String)
+ @src = "#{IMAGE_HOST}/#{src}"
+ end
+
+ def ==(other : Image)
+ other.src == src
+ end
+
+ def empty?
+ false
+ end
+ end
+
+ class IFrame
+ getter href : String
+
+ def initialize(@href : String)
+ end
+
+ def ==(other : IFrame)
+ other.href == href
+ end
+
+ def empty?
+ false
+ end
+ end
+end
diff --git a/src/models/page.cr b/src/models/page.cr
new file mode 100644
index 0000000..06e103d
--- /dev/null
+++ b/src/models/page.cr
@@ -0,0 +1,6 @@
+class Page
+ getter nodes : Nodes::Children
+
+ def initialize(@nodes : Nodes::Children)
+ end
+end
diff --git a/src/models/post_response.cr b/src/models/post_response.cr
index 93c92f5..c8447cc 100644
--- a/src/models/post_response.cr
+++ b/src/models/post_response.cr
@@ -33,20 +33,43 @@ class PostResponse
class Paragraph < Base
property text : String
property type : ParagraphType
+ property markups : Array(Markup)
property iframe : IFrame?
property layout : String?
+ property metadata : Metadata?
end
enum ParagraphType
+ BQ
H3
H4
- P
- PRE
- BQ
- ULI
- OLI
IFRAME
IMG
+ OLI
+ P
+ PRE
+ ULI
+ end
+
+ class Markup < Base
+ property title : String?
+ property type : MarkupType
+ property href : String?
+ property start : Int32
+ property end : Int32
+ property anchorType : AnchorType?
+ end
+
+ enum MarkupType
+ A
+ CODE
+ EM
+ STRONG
+ end
+
+ enum AnchorType
+ LINK
+ USER
end
class IFrame < Base
@@ -58,5 +81,8 @@ class PostResponse
end
class Metadata < Base
+ property id : String
+ property originalWidth : Int32
+ property originalHeight : Int32
end
end
diff --git a/src/pages/articles/show_page.cr b/src/pages/articles/show_page.cr
index d6e78bb..c5c2b0e 100644
--- a/src/pages/articles/show_page.cr
+++ b/src/pages/articles/show_page.cr
@@ -1,7 +1,7 @@
class Articles::ShowPage < MainLayout
- needs post_response : PostResponse::Root
+ needs page : Page
def content
- mount Post::Post, response: post_response
+ mount PageContent, page: page
end
end