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 <ul> 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 <ol> 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 %(<p>hi</p>) + 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 %(<p>Hello, <em>World!</em></p><ul><li>List!</li><li>Again!</li></ul>) + 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 %(<blockquote>Wayne Gretzky. Michael Scott.</blockquote>) + 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 %(<code>foo = bar</code>) + 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 %(<p>This is <em>neat!</em></p>) + 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 %(<h3>Title!</h3>) + 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 %(<h4>In Conclusion...</h4>) + 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 %(<p><img src="https://cdn-images-1.medium.com/image.png"></p>) + 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 %(<p><div class="embedded"><a href="https://example.com">Click to visit embedded content</a></div></p>) + 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 %(<ol><li>One</li><li>Two</li></ol>) + 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 %(<p>Hello, world!</p>) + 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 %(<pre>New\nline</pre>) + 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 %(<strong>Oh yeah!</strong>) + 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 %(<ul><li>Apple</li><li>Banana</li></ul>) + 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);</x>" - 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 "<!-- a Container was rendered -->" + end + + def render_child(node : Emphasis) + em { render_children(node.children) } + end + + def render_child(container : Empty) + # Should never get called + raw "<!-- an Empty was rendered -->" + 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);</x>" 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