thoughtbot--shoulda-matchers/lib/shoulda/matchers/util/word_wrap.rb

212 lines
4.6 KiB
Ruby

module Shoulda
module Matchers
# @private
module WordWrap
TERMINAL_WIDTH = 72
def word_wrap(document, options = {})
Document.new(document, **options).wrap
end
end
extend WordWrap
# @private
class Document
def initialize(document, indent: 0)
@document = document
@indent = indent
end
def wrap
wrapped_paragraphs.map { |lines| lines.join("\n") }.join("\n\n")
end
protected
attr_reader :document, :indent
private
def paragraphs
document.split(/\n{2,}/)
end
def wrapped_paragraphs
paragraphs.map do |paragraph|
Paragraph.new(paragraph, indent: indent).wrap
end
end
end
# @private
class Text < ::String
LIST_ITEM_REGEXP = /\A((?:[a-z0-9]+(?:\)|\.)|\*) )/.freeze
def indented?
self =~ /\A +/
end
def list_item?
self =~ LIST_ITEM_REGEXP
end
def match_as_list_item
match(LIST_ITEM_REGEXP)
end
end
# @private
class Paragraph
def initialize(paragraph, indent: 0)
@paragraph = Text.new(paragraph)
@indent = indent
end
def wrap
if paragraph.indented?
lines
elsif paragraph.list_item?
wrap_list_item
else
wrap_generic_paragraph
end
end
protected
attr_reader :paragraph, :indent
private
def wrap_list_item
wrap_lines(combine_list_item_lines(lines))
end
def lines
paragraph.split("\n").map { |line| Text.new(line) }
end
def combine_list_item_lines(lines)
lines.inject([]) do |combined_lines, line|
if line.list_item?
combined_lines << line
else
combined_lines.last << (" #{line}").squeeze(' ')
end
combined_lines
end
end
def wrap_lines(lines)
lines.map { |line| Line.new(line, indent: indent).wrap }
end
def wrap_generic_paragraph
Line.new(combine_paragraph_into_one_line, indent: indent).wrap
end
def combine_paragraph_into_one_line
paragraph.gsub(/\n/, ' ')
end
end
# @private
class Line
OFFSETS = { left: -1, right: +1 }.freeze
def initialize(line, indent: 0)
@indent = indent
@original_line = @line_to_wrap = Text.new(line)
@indentation = ' ' * indent
@indentation_read = false
end
def wrap
if line_to_wrap.indented?
[line_to_wrap]
else
lines = []
loop do
@previous_line_to_wrap = line_to_wrap
new_line = (indentation || '') + line_to_wrap
result = wrap_line(new_line)
lines << normalize_whitespace(result[:fitted_line])
unless @indentation_read
@indentation = read_indentation
@indentation_read = true
end
@line_to_wrap = result[:leftover]
if line_to_wrap.to_s.empty? || previous_line_to_wrap == line_to_wrap
break
end
end
lines
end
end
protected
attr_reader :indent, :original_line, :line_to_wrap, :indentation,
:previous_line_to_wrap
private
def read_indentation
initial_indentation = ' ' * indent
match = line_to_wrap.match_as_list_item
if match
initial_indentation + (' ' * match[1].length)
else
initial_indentation
end
end
def wrap_line(line)
index = nil
if line.length > Shoulda::Matchers::WordWrap::TERMINAL_WIDTH
index = determine_where_to_break_line(line, direction: :left)
if index == -1
index = determine_where_to_break_line(line, direction: :right)
end
end
if index.nil? || index == -1
fitted_line = line
leftover = ''
else
fitted_line = line[0..index].rstrip
leftover = line[index + 1..]
end
{ fitted_line: fitted_line, leftover: leftover }
end
def determine_where_to_break_line(line, args)
direction = args.fetch(:direction)
index = Shoulda::Matchers::WordWrap::TERMINAL_WIDTH
offset = OFFSETS.fetch(direction)
while line[index] !~ /\s/ && (0...line.length).cover?(index)
index += offset
end
index
end
def normalize_whitespace(string)
indentation + string.strip.squeeze(' ')
end
end
end
end