lib/asciidoctor/table.rb in asciidoctor-1.5.6.2 vs lib/asciidoctor/table.rb in asciidoctor-1.5.7
- old
+ new
@@ -1,10 +1,12 @@
# encoding: UTF-8
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
# Public: A data object that encapsulates the collection of rows (head, foot, body) for a table
class Rows
attr_accessor :head, :foot, :body
@@ -22,11 +24,11 @@
# 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], [:foot, @foot], [:body, @body]]
+ [[:head, @head], [:body, @body], [:foot, @foot]]
end
end
# Public: Get/Set the columns for this table
attr_accessor :columns
@@ -46,11 +48,11 @@
@rows = Rows.new
@columns = []
@has_header_option = attributes.key? 'header-option'
- # smell like we need a utility method here
+ # 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
pcwidth_intval = 100 unless pcwidth_intval == 0 && (pcwidth == '0' || pcwidth == '0%')
end
@@ -77,18 +79,25 @@
# Internal: Creates the Column objects from the column spec
#
# returns nothing
def create_columns colspecs
cols = []
+ autowidth_cols = nil
width_base = 0
colspecs.each do |colspec|
- width_base += colspec['width']
+ colwidth = colspec['width']
cols << (Column.new self, cols.size, colspec)
+ if colwidth < 0
+ (autowidth_cols ||= []) << cols[-1]
+ else
+ width_base += colwidth
+ end
end
- unless (@columns = cols).empty?
- @attributes['colcount'] = cols.size
- assign_column_widths(width_base == 0 ? nil : width_base)
+ if (num_cols = (@columns = cols).size) > 0
+ @attributes['colcount'] = num_cols
+ width_base = nil unless width_base > 0 || autowidth_cols
+ assign_column_widths width_base, autowidth_cols
end
nil
end
# Internal: Assign column widths to columns
@@ -99,15 +108,27 @@
# This method assumes there's at least one column in the columns array.
#
# 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
- pf = 10.0 ** 4 # precision factor (multipler / divisor) for managing precision of calculated result
+ def assign_column_widths width_base = nil, autowidth_cols = nil
+ pf = DEFAULT_PRECISION_FACTOR
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 = 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) }
else
col_pcwidth = ((100 * pf / @columns.size).to_i) / pf
# or...
#col_pcwidth = (100.0 / @columns.size).truncate 4
@@ -196,13 +217,19 @@
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
+
# 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
@@ -213,38 +240,51 @@
# Public: The internal Asciidoctor::Document for a cell that has the asciidoc style
attr_reader :inner_document
def initialize column, cell_text, attributes = {}, opts = {}
super column, :cell
+ @source_location = opts[:cursor].dup if @document.sourcemap
if column
- cell_style = (in_header_row = column.table.header_row?) ? nil : column.attributes['style']
+ 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
- else
- in_header_row = cell_style = nil
end
+ # NOTE if attributes is defined, we know this is a psv cell; implies text should be stripped
if attributes
- @colspan = attributes.delete 'colspan'
- @rowspan = attributes.delete 'rowspan'
- # TODO eventually remove the style attribute from the attributes hash
- #cell_style = attributes.delete 'style' unless in_header_row || !(attributes.key? 'style')
- cell_style = attributes['style'] unless in_header_row || !(attributes.key? 'style')
- if opts[:strip_text]
- if cell_style == :literal || cell_style == :verse
- cell_text = cell_text.rstrip
- cell_text = cell_text.slice 1, cell_text.length - 1 while cell_text.start_with? LF
+ if attributes.empty?
+ @colspan = @rowspan = nil
+ else
+ @colspan, @rowspan = (attributes.delete 'colspan'), (attributes.delete 'rowspan')
+ # TODO delete style attribute from @attributes if set
+ cell_style = attributes['style'] || cell_style unless in_header_row
+ update_attributes attributes
+ end
+ if cell_style == :asciidoc
+ asciidoc = true
+ inner_document_cursor = opts[:cursor]
+ if (cell_text = cell_text.rstrip).start_with? LF
+ lines_advanced = 1
+ lines_advanced += 1 while (cell_text = cell_text.slice 1, cell_text.length).start_with? LF
+ # NOTE this only works if we remain in the same file
+ inner_document_cursor.advance lines_advanced
else
- cell_text = cell_text.strip
+ cell_text = cell_text.lstrip
end
+ elsif (literal = cell_style == :literal) || cell_style == :verse
+ 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
+ # NOTE AsciidoctorJ uses nil cell_text to create an empty cell
+ cell_text = cell_text ? cell_text.strip : ''
end
- update_attributes attributes
else
- @colspan = nil
- @rowspan = nil
+ @colspan = @rowspan = nil
end
# NOTE only true for non-header rows
- if cell_style == :asciidoc
+ if asciidoc
# FIXME hide doctitle from nested document; temporary workaround to fix
# nested document seeing doctitle and assuming it has its own document title
parent_doctitle = @document.attributes.delete('doctitle')
# NOTE we need to process the first line of content as it may not have been processed
# the included content cannot expect to match conditional terminators in the remaining
@@ -257,12 +297,20 @@
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 => opts[: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
+ @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
+ @subs = NORMAL_SUBS
end
@text = cell_text
@style = cell_style
end
@@ -273,11 +321,11 @@
#
# This method shouldn't be used for cells that have the AsciiDoc style.
#
# Returns the converted String text for this Cell
def text
- apply_subs @text, (@style == :literal ? BASIC_SUBS : NORMAL_SUBS)
+ apply_subs @text, @subs
end
# Public: Set the String text.
#
# This method shouldn't be used for cells that have the AsciiDoc style.
@@ -300,10 +348,20 @@
!@style || @style == :header ? p : Inline.new(parent, :quoted, p, :type => @style).convert
end
end
end
+ # Public: Get the source file where this block started
+ def file
+ @source_location && @source_location.file
+ end
+
+ # Public: Get the source line number where this block started
+ def lineno
+ @source_location && @source_location.lineno
+ end
+
def to_s
"#{super.to_s} - [text: #@text, colspan: #{@colspan || 1}, rowspan: #{@rowspan || 1}, attributes: #@attributes]"
end
end
@@ -312,10 +370,12 @@
# moves through the lines of the table using tail recursion. When a cell boundary
# is located, the previous cell is closed, an instance of Table::Cell is
# instantiated, the row is closed if the cell satisifies the column count and,
# finally, a new buffer is allocated to track the next cell.
class Table::ParserContext
+ include Logging
+
# Public: An Array of String keys that represent the table formats in AsciiDoc
#--
# QUESTION should we recognize !sv as a valid format value?
FORMATS = ['psv', 'csv', 'dsv', 'tsv'].to_set
@@ -349,25 +409,23 @@
# Public: The cell delimiter compiled Regexp for this table.
attr_reader :delimiter_re
def initialize reader, table, attributes = {}
- @reader = reader
+ @start_cursor_data = (@reader = reader).mark
@table = table
- # IMPORTANT if reader.cursor becomes a reference, this assignment would require .dup
- @last_cursor = reader.cursor
if attributes.key? 'format'
if FORMATS.include?(xsv = attributes['format'])
if xsv == 'tsv'
# NOTE tsv is just an alias for csv with a tab separator
@format = 'csv'
elsif (@format = xsv) == 'psv' && table.document.nested?
xsv = '!sv'
end
else
- warn %(asciidoctor: ERROR: #{reader.prev_line_info}: illegal table format: #{xsv})
+ 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
@@ -411,30 +469,32 @@
@delimiter_re.match(line)
end
# Public: Skip past the matched delimiter because it's inside quoted text.
#
- # returns the String after the match
- def skip_past_delimiter(match)
- @buffer = %(#{@buffer}#{match.pre_match}#{@delimiter})
- match.post_match
+ # Returns nothing
+ def skip_past_delimiter(pre)
+ @buffer = %(#{@buffer}#{pre}#{@delimiter})
+ nil
end
# Public: Skip past the matched delimiter because it's escaped.
#
- # returns the String after the match
- def skip_past_escaped_delimiter(match)
- @buffer = %(#{@buffer}#{match.pre_match.chop}#{@delimiter})
- match.post_match
+ # Returns nothing
+ def skip_past_escaped_delimiter(pre)
+ @buffer = %(#{@buffer}#{pre.chop}#{@delimiter})
+ nil
end
# Public: Determines whether the buffer has unclosed quotes. Used for CSV data.
#
# returns true if the buffer has unclosed quotes, false if it doesn't or it
# isn't quoted data
def buffer_has_unclosed_quotes? append = nil
- if (record = append ? (buffer + append).strip : buffer.strip).start_with? '"'
+ if (record = append ? (@buffer + append).strip : @buffer.strip) == '"'
+ true
+ elsif record.start_with? '"'
if ((trailing_quote = record.end_with? '"') && (record.end_with? '""')) || (record.start_with? '""')
((record = record.gsub '""', '').start_with? '"') && !(record.end_with? '"')
else
!trailing_quote
end
@@ -511,32 +571,30 @@
# row has been met, close the row and begin a new one.
#
# returns nothing
def close_cell(eol = false)
if @format == 'psv'
- strip_text = true
cell_text = @buffer
@buffer = ''
if (cellspec = take_cellspec)
repeat = cellspec.delete('repeatcol') || 1
else
- warn %(asciidoctor: ERROR: #{@last_cursor.line_info}: table missing leading separator, recovering automatically)
+ logger.error message_with_context 'table missing leading separator; recovering automatically', :source_location => Reader::Cursor.new(*@start_cursor_data)
cellspec = {}
repeat = 1
end
else
- strip_text = false
cell_text = @buffer.strip
@buffer = ''
cellspec = nil
repeat = 1
if @format == 'csv'
if !cell_text.empty? && cell_text.include?('"')
# this may not be perfect logic, but it hits the 99%
if cell_text.start_with?('"') && cell_text.end_with?('"')
# unquote
- cell_text = cell_text[1...-1].strip
+ cell_text = cell_text.slice(1, cell_text.length - 2).strip
end
# collapse escaped quotes
cell_text = cell_text.squeeze('"')
end
@@ -554,17 +612,17 @@
end
end
else
# QUESTION is this right for cells that span columns?
unless (column = @table.columns[@current_row.size])
- warn %(asciidoctor: ERROR: #{@last_cursor.line_info}: dropping cell because it exceeds specified number of columns)
+ 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 => @last_cursor, :strip_text => strip_text)
- @last_cursor = @reader.cursor
+ 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)
@current_row << cell
@@ -618,8 +676,7 @@
# Internal: Advance to the next line (which may come after the parser begins processing
# the next line if the last cell had wrapped content).
def advance
@linenum += 1
end
-
end
end