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
|
include Nodes
|
||||||
|
|
||||||
describe MarkupConverter do
|
describe MarkupConverter do
|
||||||
|
describe "#convert" do
|
||||||
it "returns just text with no markups" do
|
it "returns just text with no markups" do
|
||||||
json = <<-JSON
|
markups = [] of PostResponse::Markup
|
||||||
{
|
|
||||||
"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 = MarkupConverter.convert(text: "Hello, world", markups: markups)
|
||||||
|
|
||||||
result.should eq([Text.new(content: "Hello, world")])
|
result.should eq([Text.new(content: "Hello, world")])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns just text with multiple markups" do
|
it "returns text with multiple markups" do
|
||||||
json = <<-JSON
|
markups = Array(PostResponse::Markup).from_json <<-JSON
|
||||||
{
|
[
|
||||||
"text": "strong and emphasized only",
|
|
||||||
"type": "P",
|
|
||||||
"markups": [
|
|
||||||
{
|
{
|
||||||
"title": null,
|
"title": null,
|
||||||
"type": "STRONG",
|
"type": "STRONG",
|
||||||
|
@ -46,16 +33,10 @@ describe MarkupConverter do
|
||||||
"rel": null,
|
"rel": null,
|
||||||
"anchorType": null
|
"anchorType": null
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"href": null,
|
|
||||||
"iframe": null,
|
|
||||||
"layout": null,
|
|
||||||
"metadata": null
|
|
||||||
}
|
|
||||||
JSON
|
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([
|
result.should eq([
|
||||||
Strong.new(children: [Text.new(content: "strong")] of Child),
|
Strong.new(children: [Text.new(content: "strong")] of Child),
|
||||||
|
@ -65,12 +46,9 @@ describe MarkupConverter do
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns just text with a code markup" do
|
it "returns text with a code markup" do
|
||||||
json = <<-JSON
|
markups = Array(PostResponse::Markup).from_json <<-JSON
|
||||||
{
|
[
|
||||||
"text": "inline code",
|
|
||||||
"type": "P",
|
|
||||||
"markups": [
|
|
||||||
{
|
{
|
||||||
"title": null,
|
"title": null,
|
||||||
"type": "CODE",
|
"type": "CODE",
|
||||||
|
@ -80,16 +58,10 @@ describe MarkupConverter do
|
||||||
"rel": null,
|
"rel": null,
|
||||||
"anchorType": null
|
"anchorType": null
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"href": null,
|
|
||||||
"iframe": null,
|
|
||||||
"layout": null,
|
|
||||||
"metadata": null
|
|
||||||
}
|
|
||||||
JSON
|
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([
|
result.should eq([
|
||||||
Text.new(content: "inline "),
|
Text.new(content: "inline "),
|
||||||
|
@ -97,12 +69,9 @@ describe MarkupConverter do
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "renders an A LINK markup" do
|
it "renders an A-LINK markup" do
|
||||||
json = <<-JSON
|
markups = Array(PostResponse::Markup).from_json <<-JSON
|
||||||
{
|
[
|
||||||
"text": "I am a Link",
|
|
||||||
"type": "P",
|
|
||||||
"markups": [
|
|
||||||
{
|
{
|
||||||
"title": "",
|
"title": "",
|
||||||
"type": "A",
|
"type": "A",
|
||||||
|
@ -112,17 +81,10 @@ describe MarkupConverter do
|
||||||
"rel": "",
|
"rel": "",
|
||||||
"anchorType": "LINK"
|
"anchorType": "LINK"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"href": null,
|
|
||||||
"iframe": null,
|
|
||||||
"layout": null,
|
|
||||||
"metadata": null
|
|
||||||
}
|
|
||||||
JSON
|
JSON
|
||||||
|
|
||||||
paragraph = PostResponse::Paragraph.from_json(json)
|
result = MarkupConverter.convert(text: "I am a Link", markups: markups)
|
||||||
|
|
||||||
result = MarkupConverter.convert(text: paragraph.text, markups: paragraph.markups)
|
|
||||||
|
|
||||||
result.should eq([
|
result.should eq([
|
||||||
Text.new("I am a "),
|
Text.new("I am a "),
|
||||||
|
@ -130,12 +92,9 @@ describe MarkupConverter do
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "renders an A USER markup" do
|
it "renders an A-USER markup" do
|
||||||
json = <<-JSON
|
markups = Array(PostResponse::Markup).from_json <<-JSON
|
||||||
{
|
[
|
||||||
"text": "Hi Dr Nick!",
|
|
||||||
"type": "P",
|
|
||||||
"markups": [
|
|
||||||
{
|
{
|
||||||
"title": null,
|
"title": null,
|
||||||
"type": "A",
|
"type": "A",
|
||||||
|
@ -146,17 +105,10 @@ describe MarkupConverter do
|
||||||
"rel": null,
|
"rel": null,
|
||||||
"anchorType": "USER"
|
"anchorType": "USER"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"href": null,
|
|
||||||
"iframe": null,
|
|
||||||
"layout": null,
|
|
||||||
"metadata": null
|
|
||||||
}
|
|
||||||
JSON
|
JSON
|
||||||
|
|
||||||
paragraph = PostResponse::Paragraph.from_json(json)
|
result = MarkupConverter.convert(text: "Hi Dr Nick!", markups: markups)
|
||||||
|
|
||||||
result = MarkupConverter.convert(text: paragraph.text, markups: paragraph.markups)
|
|
||||||
|
|
||||||
result.should eq([
|
result.should eq([
|
||||||
Text.new("Hi "),
|
Text.new("Hi "),
|
||||||
|
@ -164,4 +116,81 @@ describe MarkupConverter do
|
||||||
Text.new("!"),
|
Text.new("!"),
|
||||||
])
|
])
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
struct StringSplit
|
struct RangeWithMarkup
|
||||||
getter pre, content, post
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -19,64 +20,53 @@ class MarkupConverter
|
||||||
end
|
end
|
||||||
|
|
||||||
def convert : Array(Child)
|
def convert : Array(Child)
|
||||||
if markups.empty?
|
ranges.flat_map do |range_with_markups|
|
||||||
return [Text.new(content: text)] of Child
|
text_to_wrap = text[range_with_markups.range]
|
||||||
|
wrap_in_markups(text_to_wrap, range_with_markups.markups)
|
||||||
end
|
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
|
end
|
||||||
text_splits.in_groups_of(2, "").map_with_index do |split, index|
|
|
||||||
plain, to_be_marked = split
|
private def ranges
|
||||||
markup = markups.fetch(index, Text.new(""))
|
markup_boundaries = markups.flat_map { |markup| [markup.start, markup.end] }
|
||||||
if markup.is_a?(Text)
|
bookended_markup_boundaries = ([0] + markup_boundaries + [text.size]).uniq.sort
|
||||||
[Text.new(plain)] of Child
|
bookended_markup_boundaries.each_cons(2).map do |boundaries|
|
||||||
else
|
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
|
case markup.type
|
||||||
when PostResponse::MarkupType::A
|
when PostResponse::MarkupType::A
|
||||||
if href = markup.href
|
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
|
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
|
else
|
||||||
container = Empty.new
|
Empty.new
|
||||||
end
|
end
|
||||||
when PostResponse::MarkupType::CODE
|
when PostResponse::MarkupType::CODE
|
||||||
container = construct_markup(text: to_be_marked, container: Code)
|
Code.new(children: [child] of Child)
|
||||||
when PostResponse::MarkupType::EM
|
when PostResponse::MarkupType::EM
|
||||||
container = construct_markup(text: to_be_marked, container: Emphasis)
|
Emphasis.new(children: [child] of Child)
|
||||||
when PostResponse::MarkupType::STRONG
|
when PostResponse::MarkupType::STRONG
|
||||||
container = construct_markup(text: to_be_marked, container: Strong)
|
Strong.new(children: [child] of Child)
|
||||||
else
|
else
|
||||||
container = construct_markup(text: to_be_marked, container: Code)
|
Code.new(children: [child] of Child)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue