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
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

View file

@ -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