class Krill::LineWrap

Attributes

soft_hyphen[R]
space_count[R]

The number of spaces in the last wrapped line

zero_width_space[R]

Public Instance Methods

paragraph_finished?() click to toggle source

Whether this line is the last line in the paragraph

# File lib/krill/line_wrap.rb, line 16
def paragraph_finished?
  @newline_encountered || is_next_string_newline? || @arranger.finished?
end
tokenize(fragment) click to toggle source
# File lib/krill/line_wrap.rb, line 20
def tokenize(fragment)
  fragment.scan(scan_pattern)
end
width() click to toggle source

The width of the last wrapped line

# File lib/krill/line_wrap.rb, line 6
def width
  @accumulated_width || 0
end
wrap_line(width:, arranger:, kerning: nil, disable_wrap_by_char: nil) click to toggle source

Work in conjunction with the PDF::Formatted::Arranger defined in the :arranger option to determine what formatted text will fit within the width defined by the :width option

# File lib/krill/line_wrap.rb, line 28
def wrap_line(width:, arranger:, kerning: nil, disable_wrap_by_char: nil)
  initialize_line(
    kerning: kerning,
    width: width,
    arranger: arranger,
    disable_wrap_by_char: disable_wrap_by_char)

  while fragment = @arranger.next_string
    @fragment_output = ""

    next if empty_line?(fragment)

    break unless apply_font_settings_and_add_fragment_to_line(fragment)
  end

  @arranger.finalize_line
  @accumulated_width = @arranger.line_width
  @space_count = @arranger.space_count
  @arranger.line
end

Private Instance Methods

add_fragment_to_line(fragment, formatter) click to toggle source

returns true if all text was printed without running into the end of the line

# File lib/krill/line_wrap.rb, line 76
def add_fragment_to_line(fragment, formatter)
  if fragment == ""
    true
  elsif fragment == "\n"
    @newline_encountered = true
    false
  else
    tokenize(fragment).each do |segment|
      if segment == zero_width_space
        segment_width = 0
      else
        segment_width = formatter.width_of(segment, kerning: @kerning)
      end

      if @accumulated_width + segment_width <= @width
        @accumulated_width += segment_width
        if segment[-1] == soft_hyphen
          sh_width = formatter.width_of("#{soft_hyphen}", kerning: @kerning)
          @accumulated_width -= sh_width
        end
        @fragment_output += segment
      else
        end_of_the_line_reached(formatter, segment)
        fragment_finished(fragment)
        return false
      end
    end

    fragment_finished(fragment)
    true
  end
end
append_char(char, formatter) click to toggle source
# File lib/krill/line_wrap.rb, line 272
def append_char(char, formatter)
  # kerning doesn't make sense in the context of a single character
  char_width = formatter.compute_width_of(char, kerning: false)

  if @accumulated_width + char_width <= @width
    @accumulated_width += char_width
    @fragment_output << char
    true
  else
    false
  end
end
apply_font_settings_and_add_fragment_to_line(fragment) click to toggle source
# File lib/krill/line_wrap.rb, line 65
def apply_font_settings_and_add_fragment_to_line(fragment)
  # if font has changed from Unicode to non-Unicode, or vice versa,
  # the characters used for soft hyphens and zero-width spaces will
  # be different.
  set_soft_hyphen_and_zero_width_space(@arranger.current_formatter)
  add_fragment_to_line(fragment, @arranger.current_formatter)
end
break_chars(encoding = ::Encoding::UTF_8) click to toggle source
# File lib/krill/line_wrap.rb, line 158
def break_chars(encoding = ::Encoding::UTF_8)
  [
    whitespace(encoding),
    soft_hyphen(encoding),
    hyphen(encoding)
  ].join('')
end
determine_whether_to_pull_preceding_fragment_to_join_this_one(current_fragment) click to toggle source
# File lib/krill/line_wrap.rb, line 226
def determine_whether_to_pull_preceding_fragment_to_join_this_one(current_fragment)
  if @fragment_output.empty? && !current_fragment.empty? && @line_contains_more_than_one_word
    unless previous_fragment_ended_with_breakable? || fragment_begins_with_breakable?(current_fragment)
      @fragment_output = @previous_fragment_output_without_last_word
      update_output_based_on_last_fragment(@previous_fragment)
    end
  end
end
empty_line?(fragment) click to toggle source
# File lib/krill/line_wrap.rb, line 55
def empty_line?(fragment)
  empty = line_empty? && fragment.empty? && is_next_string_newline?
  @arranger.update_last_string("", "", soft_hyphen) if empty
  empty
end
end_of_the_line_reached(formatter, segment) click to toggle source
# File lib/krill/line_wrap.rb, line 260
def end_of_the_line_reached(formatter, segment)
  update_line_status_based_on_last_output
  wrap_by_char(formatter, segment) unless @disable_wrap_by_char || @line_contains_more_than_one_word
  @line_full = true
end
first_fragment_on_this_line?(fragment) click to toggle source
# File lib/krill/line_wrap.rb, line 51
def first_fragment_on_this_line?(fragment)
  line_empty? && fragment != "\n"
end
fragment_begins_with_breakable?(fragment) click to toggle source
# File lib/krill/line_wrap.rb, line 248
def fragment_begins_with_breakable?(fragment)
  fragment =~ /^[#{break_chars}]/
end
fragment_finished(fragment) click to toggle source
# File lib/krill/line_wrap.rb, line 208
def fragment_finished(fragment)
  if fragment == "\n"
    @newline_encountered = true
    @line_empty = false
  else
    update_output_based_on_last_fragment(fragment, soft_hyphen)
    update_line_status_based_on_last_output
    determine_whether_to_pull_preceding_fragment_to_join_this_one(fragment)
  end
  remember_this_fragment_for_backward_looking_ops
end
hyphen(_encoding = ::Encoding::UTF_8) click to toggle source
# File lib/krill/line_wrap.rb, line 174
def hyphen(_encoding = ::Encoding::UTF_8)
  '-'
end
initialize_line(kerning:, width:, arranger:, disable_wrap_by_char:) click to toggle source
# File lib/krill/line_wrap.rb, line 184
def initialize_line(kerning:, width:, arranger:, disable_wrap_by_char:)
  @kerning = kerning
  @width = width

  @disable_wrap_by_char = disable_wrap_by_char

  @accumulated_width = 0
  @line_empty = true
  @line_contains_more_than_one_word = false

  @arranger = arranger
  @arranger.initialize_line

  @newline_encountered = false
  @line_full = false
end
is_next_string_newline?() click to toggle source
# File lib/krill/line_wrap.rb, line 61
def is_next_string_newline?
  @arranger.preview_next_string == "\n"
end
line_empty?() click to toggle source
# File lib/krill/line_wrap.rb, line 180
def line_empty?
  @line_empty && @accumulated_width == 0
end
line_finished?() click to toggle source
# File lib/krill/line_wrap.rb, line 252
def line_finished?
  @line_full || paragraph_finished?
end
previous_fragment_ended_with_breakable?() click to toggle source
# File lib/krill/line_wrap.rb, line 244
def previous_fragment_ended_with_breakable?
  @previous_fragment_ended_with_breakable
end
remember_this_fragment_for_backward_looking_ops() click to toggle source
# File lib/krill/line_wrap.rb, line 235
def remember_this_fragment_for_backward_looking_ops
  @previous_fragment = @fragment_output.dup
  pf = @previous_fragment
  @previous_fragment_ended_with_breakable = pf =~ /[#{break_chars}]$/
  last_word = pf.slice(/[^#{break_chars}]*$/)
  last_word_length = last_word.nil? ? 0 : last_word.length
  @previous_fragment_output_without_last_word = pf.slice(0, pf.length - last_word_length)
end
scan_pattern(encoding = ::Encoding::UTF_8) click to toggle source

The pattern used to determine chunks of text to place on a given line

# File lib/krill/line_wrap.rb, line 113
def scan_pattern(encoding = ::Encoding::UTF_8)
  ebc = break_chars(encoding)
  eshy = soft_hyphen(encoding)
  ehy = hyphen(encoding)
  ews = whitespace(encoding)

  patterns = [
    "[^#{ebc}]+#{eshy}",
    "[^#{ebc}]+#{ehy}+",
    "[^#{ebc}]+",
    "[#{ews}]+",
    "#{ehy}+[^#{ebc}]*",
    eshy.to_s
  ]

  pattern = patterns
    .map { |p| p.encode(encoding) }
    .join('|')

  Regexp.new(pattern)
end
set_soft_hyphen_and_zero_width_space(formatter) click to toggle source
# File lib/krill/line_wrap.rb, line 201
def set_soft_hyphen_and_zero_width_space(formatter)
  # this is done once per fragment, after the font settings for the fragment are applied --
  #   it could actually be skipped if the font hasn't changed
  @soft_hyphen = formatter.normalize_encoding(SHY)
  @zero_width_space = formatter.unicode? ? ZWSP : ""
end
update_line_status_based_on_last_output() click to toggle source
# File lib/krill/line_wrap.rb, line 256
def update_line_status_based_on_last_output
  @line_contains_more_than_one_word = true if @fragment_output =~ word_division_scan_pattern
end
update_output_based_on_last_fragment(fragment, normalized_soft_hyphen = nil) click to toggle source
# File lib/krill/line_wrap.rb, line 220
def update_output_based_on_last_fragment(fragment, normalized_soft_hyphen = nil)
  remaining_text = fragment.slice(@fragment_output.length..fragment.length).lstrip
  fail CannotFit if line_finished? && line_empty? && @fragment_output.empty? && !fragment.strip.empty?
  @arranger.update_last_string(@fragment_output, remaining_text, normalized_soft_hyphen)
end
whitespace(encoding = ::Encoding::UTF_8) click to toggle source
# File lib/krill/line_wrap.rb, line 170
def whitespace(encoding = ::Encoding::UTF_8)
  "\s\t#{zero_width_space(encoding)}".encode(encoding)
end
word_division_scan_pattern(encoding = ::Encoding::UTF_8) click to toggle source

The pattern used to determine whether any word breaks exist on a current line, which in turn determines whether character level word breaking is needed

# File lib/krill/line_wrap.rb, line 139
def word_division_scan_pattern(encoding = ::Encoding::UTF_8)
  common_whitespaces = ["\t", "\n", "\v", "\r", ' '].map do |c|
    c.encode(encoding)
  end

  Regexp.union(
    common_whitespaces +
    [
      zero_width_space(encoding),
      soft_hyphen(encoding),
      hyphen(encoding)
    ].compact
  )
end
wrap_by_char(formatter, segment) click to toggle source
# File lib/krill/line_wrap.rb, line 266
def wrap_by_char(formatter, segment)
  segment.each_char do |char|
    break unless append_char(char, formatter)
  end
end