matt-harvey/tabulo

View on GitHub
lib/tabulo/cell.rb

Summary

Maintainability
C
7 hrs
Test Coverage
# encoding: utf-8

require "unicode/display_width"

module Tabulo

  # Represents a single cell within the body of a {Table}.
  class Cell

    # @return the underlying value for this Cell
    attr_reader :value

    # @!visibility private
    def initialize(
      alignment:,
      cell_data:,
      formatter:,
      left_padding:,
      padding_character:,
      right_padding:,
      styler:,
      truncation_indicator:,
      value:,
      wrap_preserve:,
      width:)

      @alignment = alignment
      @cell_data = cell_data
      @formatter = formatter
      @left_padding = left_padding
      @padding_character = padding_character
      @right_padding = right_padding
      @styler = styler
      @truncation_indicator = truncation_indicator
      @value = value
      @wrap_preserve = wrap_preserve
      @width = width
    end

    # @!visibility private
    def height
      subcells.size
    end

    # @!visibility private
    def padded_truncated_subcells(target_height)
      total_padding_amount = @left_padding + @right_padding
      truncated = (height > target_height)
      (0...target_height).map do |subcell_index|
        append_truncator = (truncated && (total_padding_amount != 0) && (subcell_index + 1 == target_height))
        padded_subcell(subcell_index, append_truncator)
      end
    end

    # @return [String] the content of the Cell, after applying the formatter for this Column (but
    #   without applying any wrapping or the styler).
    def formatted_content
      @formatted_content ||= apply_formatter
    end

    private

    def apply_formatter
      if @formatter.arity == 2
        @formatter.call(@value, @cell_data)
      else
        @formatter.call(@value)
      end
    end

    def apply_styler(content, line_index)
      case @styler.arity
      when 4
        @styler.call(@value, content, @cell_data, line_index)
      when 3
        @styler.call(@value, content, @cell_data)
      else
        @styler.call(@value, content)
      end
    end

    def subcells
      @subcells ||= calculate_subcells
    end

    def padded_subcell(subcell_index, append_truncator)
      lpad = @padding_character * @left_padding
      rpad =
        if append_truncator
          styled_truncation_indicator(subcell_index) + padding(@right_padding - 1)
        else
          padding(@right_padding)
        end
      inner = subcell_index < height ? subcells[subcell_index] : padding(@width)
      "#{lpad}#{inner}#{rpad}"
    end

    def padding(amount)
      @padding_character * amount
    end

    def styled_truncation_indicator(line_index)
      apply_styler(@truncation_indicator, line_index)
    end

    def calculate_subcells
      case @wrap_preserve
      when :rune
        line_index = 0
        formatted_content.split(Util::NEWLINE, -1).flat_map do |substr|
          subsubcells, subsubcell, subsubcell_width = [], String.new(""), 0

          substr.scan(/\X/).each do |rune|
            rune_width = Unicode::DisplayWidth.of(rune)
            if subsubcell_width + rune_width > @width
              subsubcells << style_and_align_cell_content(subsubcell, line_index)
              subsubcell_width = 0
              subsubcell.clear
              line_index += 1
            end

            subsubcell << rune
            subsubcell_width += rune_width
          end

          subsubcells << style_and_align_cell_content(subsubcell, line_index)
          line_index += 1
          subsubcells
        end
      when :word
        line_index = 0
        formatted_content.split(Util::NEWLINE, -1).flat_map do |substr|
          subsubcells, subsubcell, subsubcell_width = [], String.new(""), 0

          substr.split(/(?<= |\-|\—|\–⁠)\b/).each do |word|
            # Each word looks like "this " or like "this-".
            word_width = Unicode::DisplayWidth.of(word)
            combined_width = subsubcell_width + word_width
            if combined_width - 1 == @width && word[-1] == " "
              # do nothing, as we're on the final word of the line and
              # the space at the end will be chopped off.
            elsif combined_width > @width
              content = style_and_align_cell_content(subsubcell, line_index)
              if content.strip.length != 0
                subsubcells << content
                subsubcell_width = 0
                subsubcell.clear
                line_index += 1
              end
            end

            if word_width >= @width
              word.scan(/\X/).each do |rune|
                rune_width = Unicode::DisplayWidth.of(rune)
                if subsubcell_width + rune_width > @width
                  if rune != " "
                    content = style_and_align_cell_content(subsubcell, line_index)
                    subsubcells << content
                    subsubcell_width = 0
                    subsubcell.clear
                    line_index += 1
                  end
                end

                subsubcell << rune
                subsubcell_width += rune_width
              end
            else
              subsubcell << word
              subsubcell_width += word_width
            end
          end

          content = style_and_align_cell_content(subsubcell, line_index)
          subsubcells << content
          line_index += 1
          subsubcells
        end
      end
    end

    def style_and_align_cell_content(content, line_index)
      if @wrap_preserve == :word
        content.strip!
      end

      padding = Util.max(@width - Unicode::DisplayWidth.of(content), 0)
      left_padding, right_padding =
        case real_alignment
        when :center
          half_padding = padding / 2
          [padding - half_padding, half_padding]
        when :left
          [0, padding]
        when :right
          [padding, 0]
        end

      "#{' ' * left_padding}#{apply_styler(content, line_index)}#{' ' * right_padding}"
    end

    def real_alignment
      return @alignment unless @alignment == :auto

      case @value
      when Numeric
        :right
      when TrueClass, FalseClass
        :center
      else
        :left
      end
    end
  end
end