Overlapping refactor
Example: * Text: "strong and emphasized only" * Markups: * Strong: 0..10 * Emphasis: 7..21 First, get all the borders of the markups, including the start (0) and end (text.size) indexes of the text in order: ``` [0, 7, 10, 21, 26] ``` Then attach markups to each range. Note that the ranges are exclusive; they don't include the final number: * 0...7: Strong * 7...10: Strong, Emphasized * 10...21: Emphasized * 21...26: N/A Bundle each range and it's related markups into a value object RangeWithMarkup and return the list. Loop through that list and recursively apply each markup to each segment of text: * Apply a `Strong` markup to the text "strong " * Apply a `Strong` markup to the text "and" * Wrap that in an `Emphasis` markup * Apply an `Emphasis` markup to the text " emphasized" * Leave the text " only" as is --- This has the side effect of breaking up the nodes more than they need to be broken up. For example right now the algorithm creates this HTML: ``` <strong>strong </strong><em><strong>and</strong></em> ``` instead of: ``` <strong>strong <em>and</em></strong> ``` But that's a task for another day.
This commit is contained in:
parent
31f7d6956c
commit
09995cde5c
2 changed files with 172 additions and 153 deletions
|
@ -3,31 +3,18 @@ require "../spec_helper"
|
|||
include Nodes
|
||||
|
||||
describe MarkupConverter do
|
||||
describe "#convert" 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)
|
||||
markups = [] of PostResponse::Markup
|
||||
|
||||
result = MarkupConverter.convert(text: paragraph.text, markups: paragraph.markups)
|
||||
result = MarkupConverter.convert(text: "Hello, world", markups: 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": [
|
||||
it "returns text with multiple markups" do
|
||||
markups = Array(PostResponse::Markup).from_json <<-JSON
|
||||
[
|
||||
{
|
||||
"title": null,
|
||||
"type": "STRONG",
|
||||
|
@ -46,16 +33,10 @@ describe MarkupConverter do
|
|||
"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 = MarkupConverter.convert(text: "strong and emphasized only", markups: markups)
|
||||
|
||||
result.should eq([
|
||||
Strong.new(children: [Text.new(content: "strong")] of Child),
|
||||
|
@ -65,12 +46,9 @@ describe MarkupConverter do
|
|||
])
|
||||
end
|
||||
|
||||
it "returns just text with a code markup" do
|
||||
json = <<-JSON
|
||||
{
|
||||
"text": "inline code",
|
||||
"type": "P",
|
||||
"markups": [
|
||||
it "returns text with a code markup" do
|
||||
markups = Array(PostResponse::Markup).from_json <<-JSON
|
||||
[
|
||||
{
|
||||
"title": null,
|
||||
"type": "CODE",
|
||||
|
@ -80,16 +58,10 @@ describe MarkupConverter do
|
|||
"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 = MarkupConverter.convert(text: "inline code", markups: markups)
|
||||
|
||||
result.should eq([
|
||||
Text.new(content: "inline "),
|
||||
|
@ -97,12 +69,9 @@ describe MarkupConverter do
|
|||
])
|
||||
end
|
||||
|
||||
it "renders an A LINK markup" do
|
||||
json = <<-JSON
|
||||
{
|
||||
"text": "I am a Link",
|
||||
"type": "P",
|
||||
"markups": [
|
||||
it "renders an A-LINK markup" do
|
||||
markups = Array(PostResponse::Markup).from_json <<-JSON
|
||||
[
|
||||
{
|
||||
"title": "",
|
||||
"type": "A",
|
||||
|
@ -112,17 +81,10 @@ describe MarkupConverter do
|
|||
"rel": "",
|
||||
"anchorType": "LINK"
|
||||
}
|
||||
],
|
||||
"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 = MarkupConverter.convert(text: "I am a Link", markups: markups)
|
||||
|
||||
result.should eq([
|
||||
Text.new("I am a "),
|
||||
|
@ -130,12 +92,9 @@ describe MarkupConverter do
|
|||
])
|
||||
end
|
||||
|
||||
it "renders an A USER markup" do
|
||||
json = <<-JSON
|
||||
{
|
||||
"text": "Hi Dr Nick!",
|
||||
"type": "P",
|
||||
"markups": [
|
||||
it "renders an A-USER markup" do
|
||||
markups = Array(PostResponse::Markup).from_json <<-JSON
|
||||
[
|
||||
{
|
||||
"title": null,
|
||||
"type": "A",
|
||||
|
@ -146,17 +105,10 @@ describe MarkupConverter do
|
|||
"rel": null,
|
||||
"anchorType": "USER"
|
||||
}
|
||||
],
|
||||
"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 = MarkupConverter.convert(text: "Hi Dr Nick!", markups: markups)
|
||||
|
||||
result.should eq([
|
||||
Text.new("Hi "),
|
||||
|
@ -164,4 +116,81 @@ describe MarkupConverter do
|
|||
Text.new("!"),
|
||||
])
|
||||
end
|
||||
|
||||
it "renders overlapping markups" do
|
||||
markups = Array(PostResponse::Markup).from_json <<-JSON
|
||||
[
|
||||
{
|
||||
"title": null,
|
||||
"type": "STRONG",
|
||||
"href": null,
|
||||
"userId": null,
|
||||
"start": 7,
|
||||
"end": 15,
|
||||
"rel": null,
|
||||
"anchorType": null
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"type": "EM",
|
||||
"href": null,
|
||||
"userId": null,
|
||||
"start": 0,
|
||||
"end": 10,
|
||||
"rel": null,
|
||||
"anchorType": null
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
result = MarkupConverter.convert(text: "Italic and bold", markups: markups)
|
||||
|
||||
result.should eq([
|
||||
Emphasis.new(children: [Text.new("Italic ")] of Child),
|
||||
Emphasis.new(children: [
|
||||
Strong.new(children: [Text.new("and")] of Child),
|
||||
] of Child),
|
||||
Strong.new(children: [Text.new(" bold")] of Child),
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#wrap_in_markups" do
|
||||
it "returns text wrapped in multiple markups" do
|
||||
markups = Array(PostResponse::Markup).from_json <<-JSON
|
||||
[
|
||||
{
|
||||
"title": null,
|
||||
"type": "STRONG",
|
||||
"href": null,
|
||||
"start": 0,
|
||||
"end": 17,
|
||||
"rel": null,
|
||||
"anchorType": null
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"type": "A",
|
||||
"href": null,
|
||||
"userId": "abc123",
|
||||
"start": 13,
|
||||
"end": 17,
|
||||
"rel": null,
|
||||
"anchorType": "USER"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
converter = MarkupConverter.new(text: "it's ya boi, jack", markups: markups)
|
||||
|
||||
result = converter.wrap_in_markups("jack", markups)
|
||||
|
||||
result.should eq([
|
||||
UserAnchor.new(children: [
|
||||
Strong.new([
|
||||
Text.new("jack"),
|
||||
] of Child),
|
||||
] of Child, userId: "abc123"),
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
struct StringSplit
|
||||
getter pre, content, post
|
||||
struct RangeWithMarkup
|
||||
getter range : Range(Int32, Int32)
|
||||
getter markups : Array(PostResponse::Markup)
|
||||
|
||||
def initialize(@pre : String, @content : String, @post : String)
|
||||
def initialize(@range : Range, @markups : Array(PostResponse::Markup))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -19,64 +20,53 @@ class MarkupConverter
|
|||
end
|
||||
|
||||
def convert : Array(Child)
|
||||
if markups.empty?
|
||||
return [Text.new(content: text)] of Child
|
||||
ranges.flat_map do |range_with_markups|
|
||||
text_to_wrap = text[range_with_markups.range]
|
||||
wrap_in_markups(text_to_wrap, range_with_markups.markups)
|
||||
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
|
||||
|
||||
private def ranges
|
||||
markup_boundaries = markups.flat_map { |markup| [markup.start, markup.end] }
|
||||
bookended_markup_boundaries = ([0] + markup_boundaries + [text.size]).uniq.sort
|
||||
bookended_markup_boundaries.each_cons(2).map do |boundaries|
|
||||
range = (boundaries[0]...boundaries[1])
|
||||
covered_markups = markups.select do |markup|
|
||||
range.covers?(markup.start) || range.covers?(markup.end - 1)
|
||||
end
|
||||
RangeWithMarkup.new(range, covered_markups)
|
||||
end.to_a
|
||||
end
|
||||
|
||||
def wrap_in_markups(child : String | Child, markups : Array(PostResponse::Markup)) : Array(Child)
|
||||
if child.is_a?(String)
|
||||
child = Text.new(child)
|
||||
end
|
||||
if markups.first?.nil?
|
||||
return [child] of Child
|
||||
end
|
||||
marked_up = markup_node_in_container(child, markups[0])
|
||||
wrap_in_markups(marked_up, markups[1..])
|
||||
end
|
||||
|
||||
private def markup_node_in_container(child : Child, markup : PostResponse::Markup)
|
||||
case markup.type
|
||||
when PostResponse::MarkupType::A
|
||||
if href = markup.href
|
||||
container = Anchor.new(children: [Text.new(to_be_marked)] of Child, href: href)
|
||||
Anchor.new(href: href, children: [child] of Child)
|
||||
elsif userId = markup.userId
|
||||
container = UserAnchor.new(children: [Text.new(to_be_marked)] of Child, userId: userId)
|
||||
UserAnchor.new(userId: userId, children: [child] of Child)
|
||||
else
|
||||
container = Empty.new
|
||||
Empty.new
|
||||
end
|
||||
when PostResponse::MarkupType::CODE
|
||||
container = construct_markup(text: to_be_marked, container: Code)
|
||||
Code.new(children: [child] of Child)
|
||||
when PostResponse::MarkupType::EM
|
||||
container = construct_markup(text: to_be_marked, container: Emphasis)
|
||||
Emphasis.new(children: [child] of Child)
|
||||
when PostResponse::MarkupType::STRONG
|
||||
container = construct_markup(text: to_be_marked, container: Strong)
|
||||
Strong.new(children: [child] of Child)
|
||||
else
|
||||
container = construct_markup(text: to_be_marked, container: 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)
|
||||
Code.new(children: [child] of Child)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue