lib/kramdown/parser/kramdown/table.rb



# -*- coding: utf-8 -*-
#
#--
# Copyright (C) 2009-2014 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#

require 'kramdown/parser/kramdown/block_boundary'

module Kramdown
  module Parser
    class Kramdown

      TABLE_SEP_LINE = /^([+|: -]*?-[+|: -]*?)[ \t]*\n/
      TABLE_HSEP_ALIGN = /[ ]?(:?)-+(:?)[ ]?/
      TABLE_FSEP_LINE = /^[+|: =]*?=[+|: =]*?[ \t]*\n/
      TABLE_ROW_LINE = /^(.*?)[ \t]*\n/
      TABLE_PIPE_CHECK = /(?:\||.*?[^\\\n]\|)/
      TABLE_LINE = /#{TABLE_PIPE_CHECK}.*?\n/
      TABLE_START = /^#{OPT_SPACE}(?=\S)#{TABLE_LINE}/

      # Parse the table at the current location.
      def parse_table
        return false if !after_block_boundary?

        saved_pos = @src.save_pos
        orig_pos = @src.pos
        table = new_block_el(:table, nil, nil, :alignment => [], :location => @src.current_line_number)
        leading_pipe = (@src.check(TABLE_LINE) =~ /^\s*\|/)
        @src.scan(TABLE_SEP_LINE)

        rows = []
        has_footer = false
        columns = 0

        add_container = lambda do |type, force|
          if !has_footer || type != :tbody || force
            cont = Element.new(type)
            cont.children, rows = rows, []
            table.children << cont
          end
        end

        while !@src.eos?
          break if !@src.check(TABLE_LINE)
          if @src.scan(TABLE_SEP_LINE) && !rows.empty?
            if table.options[:alignment].empty? && !has_footer
              add_container.call(:thead, false)
              table.options[:alignment] = @src[1].scan(TABLE_HSEP_ALIGN).map do |left, right|
                (left.empty? && right.empty? && :default) || (right.empty? && :left) || (left.empty? && :right) || :center
              end
            else # treat as normal separator line
              add_container.call(:tbody, false)
            end
          elsif @src.scan(TABLE_FSEP_LINE)
            add_container.call(:tbody, true) if !rows.empty?
            has_footer = true
          elsif @src.scan(TABLE_ROW_LINE)
            trow = Element.new(:tr)

            # parse possible code spans on the line and correctly split the line into cells
            env = save_env
            cells = []
            @src[1].split(/(<code.*?>.*?<\/code>)/).each_with_index do |str, i|
              if i % 2 == 1
                (cells.empty? ? cells : cells.last) << str
              else
                reset_env(:src => Kramdown::Utils::StringScanner.new(str, @src.current_line_number))
                root = Element.new(:root)
                parse_spans(root, nil, [:codespan])

                root.children.each do |c|
                  if c.type == :raw_text
                    # Only on Ruby 1.9: f, *l = c.value.split(/(?<!\\)\|/).map {|t| t.gsub(/\\\|/, '|')}
                    f, *l = c.value.split(/\\\|/, -1).map {|t| t.split(/\|/, -1)}.inject([]) do |memo, t|
                      memo.last << "|#{t.shift}" if memo.size > 0
                      memo.concat(t)
                    end
                    (cells.empty? ? cells : cells.last) << f
                    cells.concat(l)
                  else
                    delim = (c.value.scan(/`+/).max || '') + '`'
                    tmp = "#{delim}#{' ' if delim.size > 1}#{c.value}#{' ' if delim.size > 1}#{delim}"
                    (cells.empty? ? cells : cells.last) << tmp
                  end
                end
              end
            end
            restore_env(env)

            cells.shift if leading_pipe && cells.first.strip.empty?
            cells.pop if cells.last.strip.empty?
            cells.each do |cell_text|
              tcell = Element.new(:td)
              tcell.children << Element.new(:raw_text, cell_text.strip)
              trow.children << tcell
            end
            columns = [columns, cells.length].max
            rows << trow
          else
            break
          end
        end

        if !before_block_boundary?
          @src.revert_pos(saved_pos)
          return false
        end

        # Parse all lines of the table with the code span parser
        env = save_env
        l_src = ::Kramdown::Utils::StringScanner.new(extract_string(orig_pos...(@src.pos-1), @src),
                                                     @src.current_line_number)
        reset_env(:src => l_src)
        root = Element.new(:root)
        parse_spans(root, nil, [:codespan, :span_html])
        restore_env(env)

        # Check if each line has at least one unescaped pipe that is not inside a code span/code
        # HTML element
        # Note: It doesn't matter that we parse *all* span HTML elements because the row splitting
        # algorithm above only takes <code> elements into account!
        pipe_on_line = false
        while (c = root.children.shift)
          lines = c.value.split(/\n/)
          if c.type == :codespan
            if lines.size > 2 || (lines.size == 2 && !pipe_on_line)
              break
            elsif lines.size == 2 && pipe_on_line
              pipe_on_line = false
            end
          else
            break if lines.size > 1 && !pipe_on_line && lines.first !~ /^#{TABLE_PIPE_CHECK}/
            pipe_on_line = (lines.size > 1 ? false : pipe_on_line) || (lines.last =~ /^#{TABLE_PIPE_CHECK}/)
          end
        end
        @src.revert_pos(saved_pos) and return false if !pipe_on_line

        add_container.call(has_footer ? :tfoot : :tbody, false) if !rows.empty?

        if !table.children.any? {|el| el.type == :tbody}
          warning("Found table without body on line #{table.options[:location]} - ignoring it")
          @src.revert_pos(saved_pos)
          return false
        end

        # adjust all table rows to have equal number of columns, same for alignment defs
        table.children.each do |kind|
          kind.children.each do |row|
            (columns - row.children.length).times do
              row.children << Element.new(:td)
            end
          end
        end
        if table.options[:alignment].length > columns
          table.options[:alignment] = table.options[:alignment][0...columns]
        else
          table.options[:alignment] += [:default] * (columns - table.options[:alignment].length)
        end

        @tree.children << table

        true
      end
      define_parser(:table, TABLE_START)

    end
  end
end