lib/asciidoctor/table.rb in asciidoctor-1.5.8 vs lib/asciidoctor/table.rb in asciidoctor-2.0.0.rc.1
- old
+ new
@@ -1,12 +1,12 @@
-# encoding: UTF-8
+# frozen_string_literal: true
module Asciidoctor
# Public: Methods and constants for managing AsciiDoc table content in a document.
# It supports all three of AsciiDoc's table formats: psv, dsv and csv.
class Table < AbstractBlock
- # multipler / divisor for tuning precision of calculated result
- DEFAULT_PRECISION_FACTOR = 10000.0
+ # precision of column widths
+ DEFAULT_PRECISION = 4
# Public: A data object that encapsulates the collection of rows (head, foot, body) for a table
class Rows
attr_accessor :head, :foot, :body
@@ -16,20 +16,30 @@
@body = body
end
alias [] send
- # Public: Returns the rows grouped by section.
+ # Public: Retrieve the rows grouped by section as a nested Array.
#
# Creates a 2-dimensional array of two element entries. The first element
# is the section name as a symbol. The second element is the Array of rows
# in that section. The entries are in document order (head, foot, body).
#
# Returns a 2-dimentional Array of rows grouped by section.
def by_section
[[:head, @head], [:body, @body], [:foot, @foot]]
end
+
+ # Public: Retrieve the rows as a Hash.
+ #
+ # The keys are the names of the section groups and the values are the Array of rows in that section.
+ # The keys are in document order (head, foot, body).
+ #
+ # Returns a Hash of rows grouped by section.
+ def to_h
+ { head: @head, body: @body, foot: @foot }
+ end
end
# Public: Get/Set the columns for this table
attr_accessor :columns
@@ -46,11 +56,11 @@
def initialize parent, attributes
super parent, :table
@rows = Rows.new
@columns = []
- @has_header_option = attributes.key? 'header-option'
+ @has_header_option = attributes['header-option'] ? true : false
# smells like we need a utility method here
# to resolve an integer width from potential bogus input
if (pcwidth = attributes['width'])
if (pcwidth_intval = pcwidth.to_i) > 100 || pcwidth_intval < 1
@@ -65,11 +75,11 @@
# FIXME calculate more accurately (only used in DocBook output)
@attributes['tableabswidth'] ||=
((@attributes['tablepcwidth'].to_f / 100) * @document.attributes['pagewidth']).round
end
- attributes['orientation'] = 'landscape' if attributes.key? 'rotate-option'
+ attributes['orientation'] = 'landscape' if attributes['rotate-option']
end
# Internal: Returns whether the current row being processed is
# the header row
def header_row?
@@ -109,45 +119,35 @@
#
# width_base - the total of the relative column values used for calculating percentage widths (default: nil)
#
# returns nothing
def assign_column_widths width_base = nil, autowidth_cols = nil
- pf = DEFAULT_PRECISION_FACTOR
+ precision = DEFAULT_PRECISION
total_width = col_pcwidth = 0
if width_base
if autowidth_cols
if width_base > 100
autowidth = 0
logger.warn %(total column width must not exceed 100% when using autowidth columns; got #{width_base}%)
else
- autowidth = ((100.0 - width_base) / autowidth_cols.size * pf).to_i / pf
+ autowidth = ((100.0 - width_base) / autowidth_cols.size).truncate precision
autowidth = autowidth.to_i if autowidth.to_i == autowidth
width_base = 100
end
autowidth_attrs = { 'width' => autowidth, 'autowidth-option' => '' }
autowidth_cols.each {|col| col.update_attributes autowidth_attrs }
end
- @columns.each {|col| total_width += (col_pcwidth = col.assign_width nil, width_base, pf) }
+ @columns.each {|col| total_width += (col_pcwidth = col.assign_width nil, width_base, precision) }
else
- col_pcwidth = ((100 * pf / @columns.size).to_i) / pf
- # or...
- #col_pcwidth = (100.0 / @columns.size).truncate 4
+ col_pcwidth = (100.0 / @columns.size).truncate precision
col_pcwidth = col_pcwidth.to_i if col_pcwidth.to_i == col_pcwidth
- @columns.each {|col| total_width += col.assign_width col_pcwidth }
+ @columns.each {|col| total_width += col.assign_width col_pcwidth, nil, precision }
end
# donate balance, if any, to final column (using half up rounding)
- unless total_width == 100
- @columns[-1].assign_width(((100 - total_width + col_pcwidth) * pf).round / pf)
- # or (manual half up rounding)...
- #numerator = (raw_numerator = (100 - total_width + col_pcwidth) * pf).to_i
- #numerator += 1 if raw_numerator >= numerator + 0.5
- #@columns[-1].assign_width numerator / pf
- # or...
- #@columns[-1].assign_width((100 - total_width + col_pcwidth).round 4)
- end
+ @columns[-1].assign_width(((100 - total_width + col_pcwidth).round precision), nil, precision) unless total_width == 100
nil
end
# Internal: Partition the rows into header, footer and body as determined
@@ -167,26 +167,26 @@
# QUESTION why does AsciiDoc use an array for head? is it
# possible to have more than one based on the syntax?
@rows.head = [head]
end
- if num_body_rows > 0 && attrs.key?('footer-option')
+ if num_body_rows > 0 && attrs['footer-option']
@rows.foot = [@rows.body.pop]
end
nil
end
end
# Public: Methods to manage the columns of an AsciiDoc table. In particular, it
# keeps track of the column specs
class Table::Column < AbstractNode
- # Public: Get/Set the Symbol style for this column.
+ # Public: Get/Set the style Symbol for this column.
attr_accessor :style
def initialize table, index, attributes = {}
- super table, :column
+ super table, :table_column
@style = attributes['style']
attributes['colnumber'] = index + 1
attributes['width'] ||= 1
attributes['halign'] ||= 'left'
attributes['valign'] ||= 'top'
@@ -199,51 +199,50 @@
# Internal: Calculate and assign the widths (percentage and absolute) for this column
#
# This method assigns the colpcwidth and colabswidth attributes.
#
# returns the resolved colpcwidth value
- def assign_width col_pcwidth, width_base = nil, pf = 10000.0
+ def assign_width col_pcwidth, width_base, precision
if width_base
- col_pcwidth = ((@attributes['width'].to_f / width_base) * 100 * pf).to_i / pf
- # or...
- #col_pcwidth = (@attributes['width'].to_f * 100.0 / width_base).truncate 4
+ col_pcwidth = (@attributes['width'].to_f * 100.0 / width_base).truncate precision
col_pcwidth = col_pcwidth.to_i if col_pcwidth.to_i == col_pcwidth
end
@attributes['colpcwidth'] = col_pcwidth
if parent.attributes.key? 'tableabswidth'
# FIXME calculate more accurately (only used in DocBook output)
@attributes['colabswidth'] = ((col_pcwidth / 100.0) * parent.attributes['tableabswidth']).round
end
col_pcwidth
end
+
+ def block?
+ false
+ end
+
+ def inline?
+ false
+ end
end
# Public: Methods for managing the a cell in an AsciiDoc table.
-class Table::Cell < AbstractNode
- # Public: Gets/Sets the location in the AsciiDoc source where this cell begins
- attr_reader :source_location
+class Table::Cell < AbstractBlock
+ DOUBLE_LF = LF * 2
- # Public: Get/Set the Symbol style for this cell (default: nil)
- attr_accessor :style
-
- # Public: Substitutions to be applied to content in this cell
- attr_accessor :subs
-
# Public: An Integer of the number of columns this cell will span (default: nil)
attr_accessor :colspan
# Public: An Integer of the number of rows this cell will span (default: nil)
attr_accessor :rowspan
# Public: An alias to the parent block (which is always a Column)
alias column parent
- # Public: The internal Asciidoctor::Document for a cell that has the asciidoc style
+ # Internal: Returns the nested Document in an AsciiDoc table cell (only set when style is :asciidoc)
attr_reader :inner_document
def initialize column, cell_text, attributes = {}, opts = {}
- super column, :cell
+ super column, :table_cell
@source_location = opts[:cursor].dup if @document.sourcemap
if column
cell_style = column.attributes['style'] unless (in_header_row = column.table.header_row?)
# REVIEW feels hacky to inherit all attributes from column
update_attributes column.attributes
@@ -267,11 +266,12 @@
# NOTE this only works if we remain in the same file
inner_document_cursor.advance lines_advanced
else
cell_text = cell_text.lstrip
end
- elsif (literal = cell_style == :literal) || cell_style == :verse
+ elsif cell_style == :literal
+ literal = true
cell_text = cell_text.rstrip
# QUESTION should we use same logic as :asciidoc cell? strip leading space if text doesn't start with newline?
cell_text = cell_text.slice 1, cell_text.length while cell_text.start_with? LF
else
normal_psv = true
@@ -301,19 +301,21 @@
unless unprocessed_line1 == preprocessed_lines[0] && preprocessed_lines.size < 2
inner_document_lines.shift
inner_document_lines.unshift(*preprocessed_lines) unless preprocessed_lines.empty?
end
end unless inner_document_lines.empty?
- @inner_document = Document.new(inner_document_lines, :header_footer => false, :parent => @document, :cursor => inner_document_cursor)
+ @inner_document = Document.new(inner_document_lines, header_footer: false, parent: @document, cursor: inner_document_cursor)
@document.attributes['doctitle'] = parent_doctitle unless parent_doctitle.nil?
@subs = nil
elsif literal
+ @content_model = :verbatim
@subs = BASIC_SUBS
else
if normal_psv && (cell_text.start_with? '[[') && LeadingInlineAnchorRx =~ cell_text
Parser.catalog_inline_anchor $1, $2, self, opts[:cursor], @document
end
+ @content_model = :simple
@subs = NORMAL_SUBS
end
@text = cell_text
@style = cell_style
end
@@ -343,16 +345,22 @@
#
# This method should not be used for cells in the head row or that have the literal or verse style.
#
# Returns the converted String for this Cell
def content
- if @style == :asciidoc
+ if (cell_style = @style) == :asciidoc
@inner_document.convert
- else
- text.split(BlankLineRx).map do |p|
- !@style || @style == :header ? p : Inline.new(parent, :quoted, p, :type => @style).convert
+ elsif @text.include? DOUBLE_LF
+ (text.split BlankLineRx).map do |para|
+ cell_style && cell_style != :header ? (Inline.new parent, :quoted, para, type: cell_style).convert : para
end
+ elsif (subbed_text = text).empty?
+ []
+ elsif cell_style && cell_style != :header
+ [(Inline.new parent, :quoted, subbed_text, type: cell_style).convert]
+ else
+ [subbed_text]
end
end
# Public: Get the source file where this block started
def file
@@ -386,12 +394,12 @@
# Public: A Hash mapping the AsciiDoc table formats to default delimiters
DELIMITERS = {
'psv' => ['|', /\|/],
'csv' => [',', /,/],
'dsv' => [':', /:/],
- 'tsv' => [%(\t), /\t/],
- '!sv' => ['!', /!/]
+ 'tsv' => [?\t, /\t/],
+ '!sv' => ['!', /!/],
}
# Public: The Table currently being parsed
attr_accessor :table
@@ -425,28 +433,28 @@
@format = 'csv'
elsif (@format = xsv) == 'psv' && table.document.nested?
xsv = '!sv'
end
else
- logger.error message_with_context %(illegal table format: #{xsv}), :source_location => reader.cursor_at_prev_line
+ logger.error message_with_context %(illegal table format: #{xsv}), source_location: reader.cursor_at_prev_line
@format, xsv = 'psv', (table.document.nested? ? '!sv' : 'psv')
end
else
@format, xsv = 'psv', (table.document.nested? ? '!sv' : 'psv')
end
if attributes.key? 'separator'
if (sep = attributes['separator']).nil_or_empty?
- @delimiter, @delimiter_re = DELIMITERS[xsv]
+ @delimiter, @delimiter_rx = DELIMITERS[xsv]
# QUESTION should we support any other escape codes or multiple tabs?
elsif sep == '\t'
- @delimiter, @delimiter_re = DELIMITERS['tsv']
+ @delimiter, @delimiter_rx = DELIMITERS['tsv']
else
- @delimiter, @delimiter_re = sep, /#{::Regexp.escape sep}/
+ @delimiter, @delimiter_rx = sep, /#{::Regexp.escape sep}/
end
else
- @delimiter, @delimiter_re = DELIMITERS[xsv]
+ @delimiter, @delimiter_rx = DELIMITERS[xsv]
end
@colcount = table.columns.empty? ? -1 : table.columns.size
@buffer = ''
@cellspecs = []
@@ -468,11 +476,11 @@
# Public: Checks whether the line provided contains the cell delimiter
# used by this table.
#
# returns Regexp MatchData if the line contains the delimiter, false otherwise
def match_delimiter(line)
- @delimiter_re.match(line)
+ @delimiter_rx.match(line)
end
# Public: Skip past the matched delimiter because it's inside quoted text.
#
# Returns nothing
@@ -580,11 +588,11 @@
cell_text = @buffer
@buffer = ''
if (cellspec = take_cellspec)
repeat = cellspec.delete('repeatcol') || 1
else
- logger.error message_with_context 'table missing leading separator; recovering automatically', :source_location => Reader::Cursor.new(*@start_cursor_data)
+ logger.error message_with_context 'table missing leading separator; recovering automatically', source_location: Reader::Cursor.new(*@start_cursor_data)
cellspec = {}
repeat = 1
end
else
cell_text = @buffer.strip
@@ -597,11 +605,11 @@
# unquote
if (cell_text = cell_text.slice(1, cell_text.length - 2))
# trim whitespace and collapse escaped quotes
cell_text = cell_text.strip.squeeze('"')
else
- logger.error message_with_context 'unclosed quote in CSV data; setting cell to empty', :source_location => @reader.cursor_at_prev_line
+ logger.error message_with_context 'unclosed quote in CSV data; setting cell to empty', source_location: @reader.cursor_at_prev_line
cell_text = ''
end
else
# collapse escaped quotes
cell_text = cell_text.squeeze('"')
@@ -620,16 +628,16 @@
end
end
else
# QUESTION is this right for cells that span columns?
unless (column = @table.columns[@current_row.size])
- logger.error message_with_context 'dropping cell because it exceeds specified number of columns', :source_location => @reader.cursor_before_mark
+ logger.error message_with_context 'dropping cell because it exceeds specified number of columns', source_location: @reader.cursor_before_mark
return
end
end
- cell = Table::Cell.new(column, cell_text, cellspec, :cursor => @reader.cursor_before_mark)
+ cell = Table::Cell.new(column, cell_text, cellspec, cursor: @reader.cursor_before_mark)
@reader.mark
unless !cell.rowspan || cell.rowspan == 1
activate_rowspan(cell.rowspan, (cell.colspan || 1))
end
@column_visits += (cell.colspan || 1)
@@ -640,11 +648,13 @@
end
@cell_open = false
nil
end
- # Public: Close the row by adding it to the Table and resetting the row
+ private
+
+ # Internal: Close the row by adding it to the Table and resetting the row
# Array and counter variables.
#
# returns nothing
def close_row
@table.rows.body << @current_row
@@ -656,27 +666,24 @@
@active_rowspans.shift
@active_rowspans[0] ||= 0
nil
end
- # Public: Activate a rowspan. The rowspan Array is consulted when
+ # Internal: Activate a rowspan. The rowspan Array is consulted when
# determining the effective number of cells in the current row.
#
# returns nothing
def activate_rowspan(rowspan, colspan)
- 1.upto(rowspan - 1).each {|i|
- # longhand assignment used for Opal compatibility
- @active_rowspans[i] = (@active_rowspans[i] || 0) + colspan
- }
+ 1.upto(rowspan - 1) {|i| @active_rowspans[i] = (@active_rowspans[i] || 0) + colspan }
nil
end
- # Public: Check whether we've met the number of effective columns for the current row.
+ # Internal: Check whether we've met the number of effective columns for the current row.
def end_of_row?
@colcount == -1 || effective_column_visits == @colcount
end
- # Public: Calculate the effective column visits, which consists of the number of
+ # Internal: Calculate the effective column visits, which consists of the number of
# cells plus any active rowspans.
def effective_column_visits
@column_visits + @active_rowspans[0]
end