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:
Edward Loveall 2021-07-18 20:21:44 -04:00
parent 31f7d6956c
commit 09995cde5c
No known key found for this signature in database
GPG Key ID: 789A4AE983AC8901
2 changed files with 172 additions and 153 deletions

View File

@ -3,31 +3,18 @@ require "../spec_helper"
include Nodes include Nodes
describe MarkupConverter do describe MarkupConverter do
it "returns just text with no markups" do describe "#convert" do
json = <<-JSON it "returns just text with no markups" do
{ 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,31 +33,22 @@ describe MarkupConverter do
"rel": null, "rel": null,
"anchorType": null "anchorType": null
} }
], ]
"href": null, JSON
"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([ result.should eq([
Strong.new(children: [Text.new(content: "strong")] of Child), Strong.new(children: [Text.new(content: "strong")] of Child),
Text.new(content: " and "), Text.new(content: " and "),
Emphasis.new(children: [Text.new(content: "emphasized")] of Child), Emphasis.new(children: [Text.new(content: "emphasized")] of Child),
Text.new(content: " only"), Text.new(content: " only"),
]) ])
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,29 +58,20 @@ describe MarkupConverter do
"rel": null, "rel": null,
"anchorType": null "anchorType": null
} }
], ]
"href": null, JSON
"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([ result.should eq([
Text.new(content: "inline "), Text.new(content: "inline "),
Code.new(children: [Text.new(content: "code")] of Child), Code.new(children: [Text.new(content: "code")] of Child),
]) ])
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,30 +81,20 @@ describe MarkupConverter do
"rel": "", "rel": "",
"anchorType": "LINK" "anchorType": "LINK"
} }
], ]
"href": null, JSON
"iframe": null,
"layout": null,
"metadata": null
}
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([
Text.new("I am a "),
Anchor.new(children: [Text.new("Link")] of Child, href: "https://example.com"),
])
end
result.should eq([ it "renders an A-USER markup" do
Text.new("I am a "), markups = Array(PostResponse::Markup).from_json <<-JSON
Anchor.new(children: [Text.new("Link")] of Child, href: "https://example.com"), [
])
end
it "renders an A USER markup" do
json = <<-JSON
{
"text": "Hi Dr Nick!",
"type": "P",
"markups": [
{ {
"title": null, "title": null,
"type": "A", "type": "A",
@ -146,22 +105,92 @@ describe MarkupConverter do
"rel": null, "rel": null,
"anchorType": "USER" "anchorType": "USER"
} }
], ]
"href": null, JSON
"iframe": null,
"layout": null,
"metadata": null
}
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([
Text.new("Hi "),
UserAnchor.new(children: [Text.new("Dr Nick")] of Child, userId: "abc123"),
Text.new("!"),
])
end
result.should eq([ it "renders overlapping markups" do
Text.new("Hi "), markups = Array(PostResponse::Markup).from_json <<-JSON
UserAnchor.new(children: [Text.new("Dr Nick")] of Child, userId: "abc123"), [
Text.new("!"), {
]) "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 end

View File

@ -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 end
text_splits = markups.reduce([text]) do |splits, markup|
individual_split = split_string(markup.start - offset, markup.end - offset, splits.pop) private def ranges
offset = markup.end markup_boundaries = markups.flat_map { |markup| [markup.start, markup.end] }
splits.push(individual_split.pre) bookended_markup_boundaries = ([0] + markup_boundaries + [text.size]).uniq.sort
splits.push(individual_split.content) bookended_markup_boundaries.each_cons(2).map do |boundaries|
splits.push(individual_split.post) range = (boundaries[0]...boundaries[1])
end covered_markups = markups.select do |markup|
text_splits.in_groups_of(2, "").map_with_index do |split, index| range.covers?(markup.start) || range.covers?(markup.end - 1)
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::A
if href = markup.href
container = Anchor.new(children: [Text.new(to_be_marked)] of Child, href: href)
elsif userId = markup.userId
container = UserAnchor.new(children: [Text.new(to_be_marked)] of Child, userId: userId)
else
container = Empty.new
end
when PostResponse::MarkupType::CODE
container = construct_markup(text: to_be_marked, container: Code)
when PostResponse::MarkupType::EM
container = construct_markup(text: to_be_marked, container: Emphasis)
when PostResponse::MarkupType::STRONG
container = construct_markup(text: to_be_marked, container: Strong)
else
container = construct_markup(text: to_be_marked, container: Code)
end
[Text.new(plain), container] of Child
end end
end.flatten.reject(&.empty?) RangeWithMarkup.new(range, covered_markups)
end.to_a
end end
private def construct_markup(text : String, container : Container.class) : Child def wrap_in_markups(child : String | Child, markups : Array(PostResponse::Markup)) : Array(Child)
container.new(children: [Text.new(content: text)] of 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 end
private def split_string(start : Int32, finish : Int32, string : String) private def markup_node_in_container(child : Child, markup : PostResponse::Markup)
if start.zero? case markup.type
pre = "" when PostResponse::MarkupType::A
if href = markup.href
Anchor.new(href: href, children: [child] of Child)
elsif userId = markup.userId
UserAnchor.new(userId: userId, children: [child] of Child)
else
Empty.new
end
when PostResponse::MarkupType::CODE
Code.new(children: [child] of Child)
when PostResponse::MarkupType::EM
Emphasis.new(children: [child] of Child)
when PostResponse::MarkupType::STRONG
Strong.new(children: [child] of Child)
else else
pre = string[0...start] Code.new(children: [child] of Child)
end 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