asciidoctor 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of asciidoctor might be problematic. Click here for more details.

Files changed (40) hide show
  1. data/README.asciidoc +11 -2
  2. data/asciidoctor.gemspec +3 -2
  3. data/lib/asciidoctor.rb +95 -62
  4. data/lib/asciidoctor/abstract_block.rb +7 -5
  5. data/lib/asciidoctor/abstract_node.rb +63 -15
  6. data/lib/asciidoctor/attribute_list.rb +3 -1
  7. data/lib/asciidoctor/backends/base_template.rb +17 -7
  8. data/lib/asciidoctor/backends/docbook45.rb +182 -150
  9. data/lib/asciidoctor/backends/html5.rb +138 -110
  10. data/lib/asciidoctor/block.rb +21 -18
  11. data/lib/asciidoctor/callouts.rb +3 -1
  12. data/lib/asciidoctor/cli/invoker.rb +3 -3
  13. data/lib/asciidoctor/cli/options.rb +6 -6
  14. data/lib/asciidoctor/debug.rb +7 -6
  15. data/lib/asciidoctor/document.rb +197 -25
  16. data/lib/asciidoctor/errors.rb +1 -1
  17. data/lib/asciidoctor/helpers.rb +29 -0
  18. data/lib/asciidoctor/inline.rb +11 -4
  19. data/lib/asciidoctor/lexer.rb +338 -182
  20. data/lib/asciidoctor/list_item.rb +14 -12
  21. data/lib/asciidoctor/reader.rb +423 -206
  22. data/lib/asciidoctor/renderer.rb +59 -15
  23. data/lib/asciidoctor/section.rb +7 -4
  24. data/lib/asciidoctor/substituters.rb +536 -511
  25. data/lib/asciidoctor/table.rb +473 -472
  26. data/lib/asciidoctor/version.rb +1 -1
  27. data/man/asciidoctor.1 +23 -14
  28. data/man/asciidoctor.ad +13 -7
  29. data/test/attributes_test.rb +42 -8
  30. data/test/blocks_test.rb +161 -1
  31. data/test/document_test.rb +134 -16
  32. data/test/invoker_test.rb +14 -6
  33. data/test/lexer_test.rb +45 -18
  34. data/test/lists_test.rb +79 -0
  35. data/test/paragraphs_test.rb +9 -1
  36. data/test/reader_test.rb +456 -19
  37. data/test/sections_test.rb +19 -0
  38. data/test/substitutions_test.rb +14 -12
  39. data/test/tables_test.rb +10 -10
  40. metadata +3 -5
@@ -1,506 +1,507 @@
1
1
  module Asciidoctor
2
- # Public: Methods and constants for managing AsciiDoc table content in a document.
3
- # It supports all three of AsciiDoc's table formats: psv, dsv and csv.
4
- class Table < AbstractBlock
5
-
6
- # Public: A String key that specifies the default table format in AsciiDoc (psv)
7
- DEFAULT_DATA_FORMAT = 'psv'
8
-
9
- # Public: An Array of String keys that represent the table formats in AsciiDoc
10
- DATA_FORMATS = ['psv', 'dsv', 'csv']
11
-
12
- # Public: A Hash mapping the AsciiDoc table formats to their default delimiters
13
- DEFAULT_DELIMITERS = {
14
- 'psv' => '|',
15
- 'dsv' => ':',
16
- 'csv' => ','
17
- }
18
-
19
- # Public: A Hash mapping styles abbreviations to styles that can be applied
20
- # to a table column or cell
21
- TEXT_STYLES = {
22
- 'd' => :none,
23
- 's' => :strong,
24
- 'e' => :emphasis,
25
- 'm' => :monospaced,
26
- 'h' => :header,
27
- 'l' => :literal,
28
- 'v' => :verse,
29
- 'a' => :asciidoc
30
- }
31
-
32
- # Public: A Hash mapping alignment abbreviations to alignments (horizontal
33
- # and vertial) that can be applies to a table column or cell
34
- ALIGNMENTS = {
35
- :h => {
36
- '<' => 'left',
37
- '>' => 'right',
38
- '^' => 'center'
39
- },
40
- :v => {
41
- '<' => 'top',
42
- '>' => 'bottom',
43
- '^' => 'middle'
44
- }
2
+ # Public: Methods and constants for managing AsciiDoc table content in a document.
3
+ # It supports all three of AsciiDoc's table formats: psv, dsv and csv.
4
+ class Table < AbstractBlock
5
+
6
+ # Public: A String key that specifies the default table format in AsciiDoc (psv)
7
+ DEFAULT_DATA_FORMAT = 'psv'
8
+
9
+ # Public: An Array of String keys that represent the table formats in AsciiDoc
10
+ DATA_FORMATS = ['psv', 'dsv', 'csv']
11
+
12
+ # Public: A Hash mapping the AsciiDoc table formats to their default delimiters
13
+ DEFAULT_DELIMITERS = {
14
+ 'psv' => '|',
15
+ 'dsv' => ':',
16
+ 'csv' => ','
17
+ }
18
+
19
+ # Public: A Hash mapping styles abbreviations to styles that can be applied
20
+ # to a table column or cell
21
+ TEXT_STYLES = {
22
+ 'd' => :none,
23
+ 's' => :strong,
24
+ 'e' => :emphasis,
25
+ 'm' => :monospaced,
26
+ 'h' => :header,
27
+ 'l' => :literal,
28
+ 'v' => :verse,
29
+ 'a' => :asciidoc
30
+ }
31
+
32
+ # Public: A Hash mapping alignment abbreviations to alignments (horizontal
33
+ # and vertial) that can be applies to a table column or cell
34
+ ALIGNMENTS = {
35
+ :h => {
36
+ '<' => 'left',
37
+ '>' => 'right',
38
+ '^' => 'center'
39
+ },
40
+ :v => {
41
+ '<' => 'top',
42
+ '>' => 'bottom',
43
+ '^' => 'middle'
45
44
  }
46
-
47
- # Public: A compiled Regexp to match a blank line
48
- BLANK_LINE_PATTERN = /\n[[:blank:]]*\n/
49
-
50
- # Public: Get/Set the String caption (unused, necessary for compatibility w/ next_block)
51
- attr_accessor :caption
52
-
53
- # Public: Get/Set the columns for this table
54
- attr_accessor :columns
55
-
56
- # Public: Get/Set the Rows struct for this table (encapsulates head, foot
57
- # and body rows)
58
- attr_accessor :rows
59
-
60
- def initialize(parent, attributes)
61
- super(parent, :table)
62
- # QUESTION since caption is on block, should it go to AbstractBlock?
63
- @caption = nil
64
- @rows = Rows.new([], [], [])
65
- @columns = []
66
-
67
- unless @attributes.has_key? 'tablepcwidth'
68
- # smell like we need a utility method here
69
- # to resolve an integer width from potential bogus input
70
- pcwidth = attributes['width']
71
- pcwidth_intval = pcwidth.to_i.abs
72
- if pcwidth_intval == 0 && pcwidth != "0" || pcwidth_intval > 100
73
- pcwidth_intval = 100
74
- end
75
- @attributes['tablepcwidth'] = pcwidth_intval
76
- end
45
+ }
77
46
 
78
- if @document.attributes.has_key? 'pagewidth'
79
- @attributes['tableabswidth'] ||=
80
- ((@attributes['tablepcwidth'].to_f / 100) * @document.attributes['pagewidth']).round
47
+ # Public: A compiled Regexp to match a blank line
48
+ BLANK_LINE_PATTERN = /\n[[:blank:]]*\n/
49
+
50
+ # Public: Get/Set the String caption (unused, necessary for compatibility w/ next_block)
51
+ attr_accessor :caption
52
+
53
+ # Public: Get/Set the columns for this table
54
+ attr_accessor :columns
55
+
56
+ # Public: Get/Set the Rows struct for this table (encapsulates head, foot
57
+ # and body rows)
58
+ attr_accessor :rows
59
+
60
+ def initialize(parent, attributes)
61
+ super(parent, :table)
62
+ # QUESTION since caption is on block, should it go to AbstractBlock?
63
+ @caption = nil
64
+ @rows = Rows.new([], [], [])
65
+ @columns = []
66
+
67
+ unless @attributes.has_key? 'tablepcwidth'
68
+ # smell like we need a utility method here
69
+ # to resolve an integer width from potential bogus input
70
+ pcwidth = attributes['width']
71
+ pcwidth_intval = pcwidth.to_i.abs
72
+ if pcwidth_intval == 0 && pcwidth != "0" || pcwidth_intval > 100
73
+ pcwidth_intval = 100
81
74
  end
75
+ @attributes['tablepcwidth'] = pcwidth_intval
82
76
  end
83
-
84
- # Internal: Creates the Column objects from the column spec
85
- #
86
- # returns nothing
87
- def create_columns(col_specs)
88
- total_width = 0
89
- @columns = col_specs.inject([]) {|collector, col_spec|
90
- total_width += col_spec['width']
91
- collector << Column.new(self, collector.size, col_spec)
92
- collector
93
- }
94
-
95
- if !@columns.empty?
96
- @attributes['colcount'] = @columns.size
97
- even_width = (100.0 / @columns.size).floor
98
- @columns.each {|c| c.assign_width(total_width, even_width) }
99
- end
100
77
 
101
- nil
78
+ if @document.attributes.has_key? 'pagewidth'
79
+ @attributes['tableabswidth'] ||=
80
+ ((@attributes['tablepcwidth'].to_f / 100) * @document.attributes['pagewidth']).round
102
81
  end
103
-
104
- # Internal: Partition the rows into header, footer and body as determined
105
- # by the options on the table
106
- #
107
- # returns nothing
108
- def partition_header_footer(attributes)
109
- # set rowcount before splitting up body rows
110
- @attributes['rowcount'] = @rows.body.size
111
-
112
- if !rows.body.empty? && attributes.has_key?('header-option')
113
- head = rows.body.shift
114
- # styles aren't applied to header row
115
- head.each {|c| c.attributes.delete('style') }
116
- # QUESTION why does AsciiDoc use an array for head? is it
117
- # possible to have more than one based on the syntax?
118
- rows.head = [head]
119
- end
120
-
121
- if !rows.body.empty? && attributes.has_key?('footer-option')
122
- rows.foot = [rows.body.pop]
123
- end
124
-
125
- nil
126
- end
127
-
128
- # Public: Get the rendered String content for this Block. If the block
129
- # has child blocks, the content method should cause them to be
130
- # rendered and returned as content that can be included in the
131
- # parent block's template.
132
- def render
133
- Asciidoctor.debug { "Now attempting to render for table my own bad #{self}" }
134
- Asciidoctor.debug { "Parent is #{@parent}" }
135
- Asciidoctor.debug { "Renderer is #{renderer}" }
136
- renderer.render('block_table', self)
82
+ end
83
+
84
+ # Internal: Creates the Column objects from the column spec
85
+ #
86
+ # returns nothing
87
+ def create_columns(col_specs)
88
+ total_width = 0
89
+ @columns = col_specs.inject([]) {|collector, col_spec|
90
+ total_width += col_spec['width']
91
+ collector << Column.new(self, collector.size, col_spec)
92
+ collector
93
+ }
94
+
95
+ if !@columns.empty?
96
+ @attributes['colcount'] = @columns.size
97
+ even_width = (100.0 / @columns.size).floor
98
+ @columns.each {|c| c.assign_width(total_width, even_width) }
137
99
  end
138
-
100
+
101
+ nil
139
102
  end
140
-
141
- # Public: A struct that encapsulates the collection of rows (head, foot, body) for a table
142
- Table::Rows = Struct.new(:head, :foot, :body)
143
-
144
- # Public: Methods to manage the columns of an AsciiDoc table. In particular, it
145
- # keeps track of the column specs
146
- class Table::Column < AbstractNode
147
- def initialize(table, index, attributes = {})
148
- super(table, :column)
149
- attributes['colnumber'] = index + 1
150
- attributes['width'] ||= 1
151
- attributes['halign'] ||= 'left'
152
- attributes['valign'] ||= 'top'
153
- update_attributes(attributes)
103
+
104
+ # Internal: Partition the rows into header, footer and body as determined
105
+ # by the options on the table
106
+ #
107
+ # returns nothing
108
+ def partition_header_footer(attributes)
109
+ # set rowcount before splitting up body rows
110
+ @attributes['rowcount'] = @rows.body.size
111
+
112
+ if !rows.body.empty? && attributes.has_key?('header-option')
113
+ head = rows.body.shift
114
+ # styles aren't applied to header row
115
+ head.each {|c| c.attributes.delete('style') }
116
+ # QUESTION why does AsciiDoc use an array for head? is it
117
+ # possible to have more than one based on the syntax?
118
+ rows.head = [head]
154
119
  end
155
-
156
- # Internal: Calculate and assign the widths (percentage and absolute) for this column
157
- #
158
- # This method assigns the colpcwidth and colabswidth attributes.
159
- #
160
- # returns nothing
161
- def assign_width(total_width, even_width)
162
- if total_width > 0
163
- width = ((@attributes['width'].to_f / total_width) * 100).floor
164
- else
165
- width = even_width
166
- end
167
- @attributes['colpcwidth'] = width
168
- if parent.attributes.has_key? 'tableabswidth'
169
- @attributes['colabswidth'] = ((width.to_f / 100) * parent.attributes['tableabswidth']).round
170
- end
171
120
 
172
- nil
121
+ if !rows.body.empty? && attributes.has_key?('footer-option')
122
+ rows.foot = [rows.body.pop]
173
123
  end
124
+
125
+ nil
174
126
  end
175
-
176
- # Public: Methods for managing the a cell in an AsciiDoc table.
177
- class Table::Cell < AbstractNode
178
127
 
179
- # Public: An Integer of the number of columns this cell will span (default: nil)
180
- attr_accessor :colspan
128
+ # Public: Get the rendered String content for this Block. If the block
129
+ # has child blocks, the content method should cause them to be
130
+ # rendered and returned as content that can be included in the
131
+ # parent block's template.
132
+ def render
133
+ Debug.debug { "Now attempting to render for table my own bad #{self}" }
134
+ Debug.debug { "Parent is #{@parent}" }
135
+ Debug.debug { "Renderer is #{renderer}" }
136
+ @document.playback_attributes @attributes
137
+ renderer.render('block_table', self)
138
+ end
181
139
 
182
- # Public: An Integer of the number of rows this cell will span (default: nil)
183
- attr_accessor :rowspan
140
+ end
184
141
 
185
- # Public: An alias to the parent block (which is always a Column)
186
- alias :column :parent
142
+ # Public: A struct that encapsulates the collection of rows (head, foot, body) for a table
143
+ Table::Rows = Struct.new(:head, :foot, :body)
187
144
 
188
- # Public: The internal Asciidoctor::Document for a cell that has the asciidoc style
189
- attr_reader :inner_document
190
-
191
- def initialize(column, text, attributes = {})
192
- super(column, :cell)
193
- @text = text
194
- @colspan = nil
195
- @rowspan = nil
196
- # TODO feels hacky
197
- if !column.nil?
198
- update_attributes(column.attributes)
199
- end
200
- if !attributes.nil?
201
- if attributes.has_key? 'colspan'
202
- @colspan = attributes['colspan']
203
- attributes.delete('colspan')
204
- end
205
- if attributes.has_key? 'rowspan'
206
- @rowspan = attributes['rowspan']
207
- attributes.delete('rowspan')
208
- end
209
- update_attributes(attributes)
210
- end
211
- if @attributes['style'] == :asciidoc
212
- @inner_document = Document.new(@text, :header_footer => false, :parent => @document)
213
- end
214
- end
215
-
216
- # Public: Get the text with normal substitutions applied for this cell. Used for cells in the head rows
217
- def text
218
- apply_normal_subs(@text)
219
- end
220
-
221
- # Public: Handles the body data (tbody, tfoot), applying styles and partitioning into paragraphs
222
- def content
223
- style = attr('style')
224
- if style == :asciidoc
225
- @inner_document.render
226
- else
227
- text.split(Table::BLANK_LINE_PATTERN).map {|p|
228
- !style || style == :header ? p : Inline.new(parent, :quoted, p, :type => attr('style')).render
229
- }
230
- end
145
+ # Public: Methods to manage the columns of an AsciiDoc table. In particular, it
146
+ # keeps track of the column specs
147
+ class Table::Column < AbstractNode
148
+ def initialize(table, index, attributes = {})
149
+ super(table, :column)
150
+ attributes['colnumber'] = index + 1
151
+ attributes['width'] ||= 1
152
+ attributes['halign'] ||= 'left'
153
+ attributes['valign'] ||= 'top'
154
+ update_attributes(attributes)
155
+ end
156
+
157
+ # Internal: Calculate and assign the widths (percentage and absolute) for this column
158
+ #
159
+ # This method assigns the colpcwidth and colabswidth attributes.
160
+ #
161
+ # returns nothing
162
+ def assign_width(total_width, even_width)
163
+ if total_width > 0
164
+ width = ((@attributes['width'].to_f / total_width) * 100).floor
165
+ else
166
+ width = even_width
231
167
  end
232
-
233
- def to_s
234
- "#{super.to_s} - [text: #@text, colspan: #{@colspan || 1}, rowspan: #{@rowspan || 1}, attributes: #@attributes]"
168
+ @attributes['colpcwidth'] = width
169
+ if parent.attributes.has_key? 'tableabswidth'
170
+ @attributes['colabswidth'] = ((width.to_f / 100) * parent.attributes['tableabswidth']).round
235
171
  end
172
+
173
+ nil
236
174
  end
237
-
238
- # Public: Methods for managing the parsing of an AsciiDoc table. Instances of this
239
- # class are primarily responsible for tracking the buffer of a cell as the parser
240
- # moves through the lines of the table using tail recursion. When a cell boundary
241
- # is located, the previous cell is closed, an instance of Table::Cell is
242
- # instantiated, the row is closed if the cell satisifies the column count and,
243
- # finally, a new buffer is allocated to track the next cell.
244
- class Table::ParserContext
245
-
246
- # Public: The Table currently being parsed
247
- attr_accessor :table
248
-
249
- # Public: The AsciiDoc table format (psv, dsv or csv)
250
- attr_accessor :format
251
-
252
- # Public: Get the expected column count for a row
253
- #
254
- # col_count is the number of columns to pull into a row
255
- # A value of -1 means we use the number of columns found
256
- # in the first line as the col_count
257
- attr_reader :col_count
258
-
259
- # Public: The String buffer of the currently open cell
260
- attr_accessor :buffer
261
-
262
- # Public: The cell delimiter for this table.
263
- attr_reader :delimiter
264
-
265
- # Public: The cell delimiter compiled Regexp for this table.
266
- attr_reader :delimiter_re
267
-
268
- def initialize(table, attributes = {})
269
- @table = table
270
- if attributes.has_key? 'format'
271
- @format = attributes['format']
272
- if !Table::DATA_FORMATS.include? @format
273
- raise "Illegal table format: #@format"
274
- end
275
- else
276
- @format = Table::DEFAULT_DATA_FORMAT
175
+ end
176
+
177
+ # Public: Methods for managing the a cell in an AsciiDoc table.
178
+ class Table::Cell < AbstractNode
179
+
180
+ # Public: An Integer of the number of columns this cell will span (default: nil)
181
+ attr_accessor :colspan
182
+
183
+ # Public: An Integer of the number of rows this cell will span (default: nil)
184
+ attr_accessor :rowspan
185
+
186
+ # Public: An alias to the parent block (which is always a Column)
187
+ alias :column :parent
188
+
189
+ # Public: The internal Asciidoctor::Document for a cell that has the asciidoc style
190
+ attr_reader :inner_document
191
+
192
+ def initialize(column, text, attributes = {})
193
+ super(column, :cell)
194
+ @text = text
195
+ @colspan = nil
196
+ @rowspan = nil
197
+ # TODO feels hacky
198
+ if !column.nil?
199
+ update_attributes(column.attributes)
200
+ end
201
+ if !attributes.nil?
202
+ if attributes.has_key? 'colspan'
203
+ @colspan = attributes['colspan']
204
+ attributes.delete('colspan')
277
205
  end
278
-
279
- if @format == 'psv' && !attributes.has_key?('separator') && table.document.nested?
280
- @delimiter = '!'
281
- else
282
- @delimiter = attributes.fetch('separator', Table::DEFAULT_DELIMITERS[@format])
206
+ if attributes.has_key? 'rowspan'
207
+ @rowspan = attributes['rowspan']
208
+ attributes.delete('rowspan')
283
209
  end
284
- @delimiter_re = /#{Regexp.escape @delimiter}/
285
- @col_count = table.columns.empty? ? -1 : table.columns.size
286
- @buffer = ''
287
- @cell_specs = []
288
- @cell_open = false
289
- @active_rowspans = [0]
290
- @col_visits = 0
291
- @current_row = []
292
- @linenum = -1
293
- end
294
-
295
- # Public: Checks whether the line provided starts with the cell delimiter
296
- # used by this table.
297
- #
298
- # returns true if the line starts with the delimiter, false otherwise
299
- def starts_with_delimiter?(line)
300
- line.start_with? @delimiter
301
- end
302
-
303
- # Public: Checks whether the line provided contains the cell delimiter
304
- # used by this table.
305
- #
306
- # returns MatchData if the line contains the delimiter, false otherwise
307
- def match_delimiter(line)
308
- line.match @delimiter_re
309
- end
310
-
311
- # Public: Skip beyond the matched delimiter because it was a false positive
312
- # (either because it was escaped or in a quoted context)
313
- #
314
- # returns the String after the match
315
- def skip_matched_delimiter(match, escaped = false)
316
- @buffer << (escaped ? match.pre_match.chop : match.pre_match) << @delimiter
317
- match.post_match
318
- end
319
-
320
- # Public: Determines whether the buffer has unclosed quotes. Used for CSV data.
321
- #
322
- # returns true if the buffer has unclosed quotes, false if it doesn't or it
323
- # isn't quoted data
324
- def buffer_has_unclosed_quotes?(append = nil)
325
- record = "#@buffer#{append}".strip
326
- record.start_with?('"') && !record.start_with?('""') && !record.end_with?('"')
327
- end
328
-
329
- # Public: Determines whether the buffer contains quoted data. Used for CSV data.
330
- #
331
- # returns true if the buffer starts with a double quote (and not an escaped double quote),
332
- # false otherwise
333
- def buffer_quoted?
334
- @buffer.lstrip!
335
- @buffer.start_with?('"') && !@buffer.start_with?('""')
336
- end
337
-
338
- # Public: Takes a cell spec from the stack. Cell specs precede the delimiter, so a
339
- # stack is used to carry over the spec from the previous cell to the current cell
340
- # when the cell is being closed.
341
- #
342
- # returns The cell spec Hash captured from parsing the previous cell
343
- def take_cell_spec()
344
- @cell_specs.shift
345
- end
346
-
347
- # Public: Puts a cell spec onto the stack. Cell specs precede the delimiter, so a
348
- # stack is used to carry over the spec to the next cell.
349
- #
350
- # returns nothing
351
- def push_cell_spec(cell_spec = {})
352
- # this shouldn't be nil, but we check anyway
353
- @cell_specs << (cell_spec || {})
354
- nil
355
- end
356
-
357
- # Public: Marks that the cell should be kept open. Used when the end of the line is
358
- # reached and the cell may contain additional text.
359
- #
360
- # returns nothing
361
- def keep_cell_open
362
- @cell_open = true
363
- nil
210
+ update_attributes(attributes)
364
211
  end
365
-
366
- # Public: Marks the cell as closed so that the parser knows to instantiate a new cell
367
- # instance and add it to the current row.
368
- #
369
- # returns nothing
370
- def mark_cell_closed
371
- @cell_open = false
372
- nil
212
+ if @attributes['style'] == :asciidoc
213
+ @inner_document = Document.new(@text, :header_footer => false, :parent => @document)
373
214
  end
374
-
375
- # Public: Checks whether the current cell is still open
376
- #
377
- # returns true if the cell is marked as open, false otherwise
378
- def cell_open?
379
- @cell_open
215
+ end
216
+
217
+ # Public: Get the text with normal substitutions applied for this cell. Used for cells in the head rows
218
+ def text
219
+ apply_normal_subs(@text)
220
+ end
221
+
222
+ # Public: Handles the body data (tbody, tfoot), applying styles and partitioning into paragraphs
223
+ def content
224
+ style = attr('style')
225
+ if style == :asciidoc
226
+ @inner_document.render
227
+ else
228
+ text.split(Table::BLANK_LINE_PATTERN).map {|p|
229
+ !style || style == :header ? p : Inline.new(parent, :quoted, p, :type => attr('style')).render
230
+ }
380
231
  end
381
-
382
- # Public: Checks whether the current cell has been marked as closed
383
- #
384
- # returns true if the cell is marked as closed, false otherwise
385
- def cell_closed?
386
- !@cell_open
232
+ end
233
+
234
+ def to_s
235
+ "#{super.to_s} - [text: #@text, colspan: #{@colspan || 1}, rowspan: #{@rowspan || 1}, attributes: #@attributes]"
236
+ end
237
+ end
238
+
239
+ # Public: Methods for managing the parsing of an AsciiDoc table. Instances of this
240
+ # class are primarily responsible for tracking the buffer of a cell as the parser
241
+ # moves through the lines of the table using tail recursion. When a cell boundary
242
+ # is located, the previous cell is closed, an instance of Table::Cell is
243
+ # instantiated, the row is closed if the cell satisifies the column count and,
244
+ # finally, a new buffer is allocated to track the next cell.
245
+ class Table::ParserContext
246
+
247
+ # Public: The Table currently being parsed
248
+ attr_accessor :table
249
+
250
+ # Public: The AsciiDoc table format (psv, dsv or csv)
251
+ attr_accessor :format
252
+
253
+ # Public: Get the expected column count for a row
254
+ #
255
+ # col_count is the number of columns to pull into a row
256
+ # A value of -1 means we use the number of columns found
257
+ # in the first line as the col_count
258
+ attr_reader :col_count
259
+
260
+ # Public: The String buffer of the currently open cell
261
+ attr_accessor :buffer
262
+
263
+ # Public: The cell delimiter for this table.
264
+ attr_reader :delimiter
265
+
266
+ # Public: The cell delimiter compiled Regexp for this table.
267
+ attr_reader :delimiter_re
268
+
269
+ def initialize(table, attributes = {})
270
+ @table = table
271
+ if attributes.has_key? 'format'
272
+ @format = attributes['format']
273
+ if !Table::DATA_FORMATS.include? @format
274
+ raise "Illegal table format: #@format"
275
+ end
276
+ else
277
+ @format = Table::DEFAULT_DATA_FORMAT
387
278
  end
388
-
389
- # Public: If the current cell is open, close it. In additional, push the
390
- # cell spec captured from the end of this cell onto the stack for use
391
- # by the next cell.
392
- #
393
- # returns nothing
394
- def close_open_cell(next_cell_spec = {})
395
- push_cell_spec next_cell_spec
396
- close_cell(true) if cell_open?
397
- next_line
398
- nil
279
+
280
+ if @format == 'psv' && !attributes.has_key?('separator') && table.document.nested?
281
+ @delimiter = '!'
282
+ else
283
+ @delimiter = attributes.fetch('separator', Table::DEFAULT_DELIMITERS[@format])
399
284
  end
400
-
401
- # Public: Close the current cell, instantiate a new Table::Cell, add it to
402
- # the current row and, if the number of expected columns for the current
403
- # row has been met, close the row and begin a new one.
404
- #
405
- # returns nothing
406
- def close_cell(eol = false)
407
- cell_text = @buffer.strip
408
- @buffer = ''
409
- if format == 'psv'
410
- cell_spec = take_cell_spec
411
- if cell_spec.nil?
412
- puts 'asciidoctor: ERROR: table missing leading separator, recovering automatically'
413
- cell_spec = {}
414
- repeat = 1
415
- else
416
- repeat = cell_spec.fetch('repeatcol', 1)
417
- cell_spec.delete('repeatcol')
418
- end
419
- else
420
- cell_spec = nil
285
+ @delimiter_re = /#{Regexp.escape @delimiter}/
286
+ @col_count = table.columns.empty? ? -1 : table.columns.size
287
+ @buffer = ''
288
+ @cell_specs = []
289
+ @cell_open = false
290
+ @active_rowspans = [0]
291
+ @col_visits = 0
292
+ @current_row = []
293
+ @linenum = -1
294
+ end
295
+
296
+ # Public: Checks whether the line provided starts with the cell delimiter
297
+ # used by this table.
298
+ #
299
+ # returns true if the line starts with the delimiter, false otherwise
300
+ def starts_with_delimiter?(line)
301
+ line.start_with? @delimiter
302
+ end
303
+
304
+ # Public: Checks whether the line provided contains the cell delimiter
305
+ # used by this table.
306
+ #
307
+ # returns MatchData if the line contains the delimiter, false otherwise
308
+ def match_delimiter(line)
309
+ line.match @delimiter_re
310
+ end
311
+
312
+ # Public: Skip beyond the matched delimiter because it was a false positive
313
+ # (either because it was escaped or in a quoted context)
314
+ #
315
+ # returns the String after the match
316
+ def skip_matched_delimiter(match, escaped = false)
317
+ @buffer << (escaped ? match.pre_match.chop : match.pre_match) << @delimiter
318
+ match.post_match
319
+ end
320
+
321
+ # Public: Determines whether the buffer has unclosed quotes. Used for CSV data.
322
+ #
323
+ # returns true if the buffer has unclosed quotes, false if it doesn't or it
324
+ # isn't quoted data
325
+ def buffer_has_unclosed_quotes?(append = nil)
326
+ record = "#@buffer#{append}".strip
327
+ record.start_with?('"') && !record.start_with?('""') && !record.end_with?('"')
328
+ end
329
+
330
+ # Public: Determines whether the buffer contains quoted data. Used for CSV data.
331
+ #
332
+ # returns true if the buffer starts with a double quote (and not an escaped double quote),
333
+ # false otherwise
334
+ def buffer_quoted?
335
+ @buffer.lstrip!
336
+ @buffer.start_with?('"') && !@buffer.start_with?('""')
337
+ end
338
+
339
+ # Public: Takes a cell spec from the stack. Cell specs precede the delimiter, so a
340
+ # stack is used to carry over the spec from the previous cell to the current cell
341
+ # when the cell is being closed.
342
+ #
343
+ # returns The cell spec Hash captured from parsing the previous cell
344
+ def take_cell_spec()
345
+ @cell_specs.shift
346
+ end
347
+
348
+ # Public: Puts a cell spec onto the stack. Cell specs precede the delimiter, so a
349
+ # stack is used to carry over the spec to the next cell.
350
+ #
351
+ # returns nothing
352
+ def push_cell_spec(cell_spec = {})
353
+ # this shouldn't be nil, but we check anyway
354
+ @cell_specs << (cell_spec || {})
355
+ nil
356
+ end
357
+
358
+ # Public: Marks that the cell should be kept open. Used when the end of the line is
359
+ # reached and the cell may contain additional text.
360
+ #
361
+ # returns nothing
362
+ def keep_cell_open
363
+ @cell_open = true
364
+ nil
365
+ end
366
+
367
+ # Public: Marks the cell as closed so that the parser knows to instantiate a new cell
368
+ # instance and add it to the current row.
369
+ #
370
+ # returns nothing
371
+ def mark_cell_closed
372
+ @cell_open = false
373
+ nil
374
+ end
375
+
376
+ # Public: Checks whether the current cell is still open
377
+ #
378
+ # returns true if the cell is marked as open, false otherwise
379
+ def cell_open?
380
+ @cell_open
381
+ end
382
+
383
+ # Public: Checks whether the current cell has been marked as closed
384
+ #
385
+ # returns true if the cell is marked as closed, false otherwise
386
+ def cell_closed?
387
+ !@cell_open
388
+ end
389
+
390
+ # Public: If the current cell is open, close it. In additional, push the
391
+ # cell spec captured from the end of this cell onto the stack for use
392
+ # by the next cell.
393
+ #
394
+ # returns nothing
395
+ def close_open_cell(next_cell_spec = {})
396
+ push_cell_spec next_cell_spec
397
+ close_cell(true) if cell_open?
398
+ advance
399
+ nil
400
+ end
401
+
402
+ # Public: Close the current cell, instantiate a new Table::Cell, add it to
403
+ # the current row and, if the number of expected columns for the current
404
+ # row has been met, close the row and begin a new one.
405
+ #
406
+ # returns nothing
407
+ def close_cell(eol = false)
408
+ cell_text = @buffer.strip
409
+ @buffer = ''
410
+ if format == 'psv'
411
+ cell_spec = take_cell_spec
412
+ if cell_spec.nil?
413
+ puts 'asciidoctor: ERROR: table missing leading separator, recovering automatically'
414
+ cell_spec = {}
421
415
  repeat = 1
422
- if format == 'csv'
423
- if !cell_text.empty? && cell_text.include?('"')
424
- # this may not be perfect logic, but it hits the 99%
425
- if cell_text.start_with?('"') && cell_text.end_with?('"')
426
- # unquote
427
- cell_text = cell_text[1..-2].strip
428
- end
429
-
430
- # collapses escaped quotes
431
- cell_text = cell_text.tr_s('"', '"')
416
+ else
417
+ repeat = cell_spec.fetch('repeatcol', 1)
418
+ cell_spec.delete('repeatcol')
419
+ end
420
+ else
421
+ cell_spec = nil
422
+ repeat = 1
423
+ if format == 'csv'
424
+ if !cell_text.empty? && cell_text.include?('"')
425
+ # this may not be perfect logic, but it hits the 99%
426
+ if cell_text.start_with?('"') && cell_text.end_with?('"')
427
+ # unquote
428
+ cell_text = cell_text[1..-2].strip
432
429
  end
430
+
431
+ # collapses escaped quotes
432
+ cell_text = cell_text.tr_s('"', '"')
433
433
  end
434
434
  end
435
-
436
- 1.upto(repeat) {|i|
437
- # make column resolving an operation
438
- if @col_count == -1
439
- @table.columns << Table::Column.new(@table, @current_row.size + i - 1)
440
- column = @table.columns.last
441
- else
442
- # QUESTION is this right for cells that span columns?
443
- column = @table.columns[@current_row.size]
444
- end
445
-
446
- cell = Table::Cell.new(column, cell_text, cell_spec)
447
- unless cell.rowspan.nil? || cell.rowspan == 1
448
- activate_rowspan(cell.rowspan, (cell.colspan || 1))
449
- end
450
- @col_visits += (cell.colspan || 1)
451
- @current_row << cell
452
- # don't close the row if we're on the first line and the column count has not been set explicitly
453
- # TODO perhaps the col_count/linenum logic should be in end_of_row? (or a should_end_row? method)
454
- close_row if end_of_row? && (@col_count != -1 || @linenum > 0 || (eol && i == repeat))
455
- }
456
- @open_cell = false
457
- nil
458
- end
459
-
460
- # Public: Close the row by adding it to the Table and resetting the row
461
- # Array and counter variables.
462
- #
463
- # returns nothing
464
- def close_row
465
- @table.rows.body << @current_row
466
- # don't have to account for active rowspans here
467
- # since we know this is first row
468
- @col_count = @col_visits if @col_count == -1
469
- @col_visits = 0
470
- @current_row = []
471
- @active_rowspans.shift
472
- @active_rowspans[0] ||= 0
473
- nil
474
- end
475
-
476
- # Public: Activate a rowspan. The rowspan Array is consulted when
477
- # determining the effective number of cells in the current row.
478
- #
479
- # returns nothing
480
- def activate_rowspan(rowspan, colspan)
481
- 1.upto(rowspan - 1).each {|i|
482
- @active_rowspans[i] ||= 0
483
- @active_rowspans[i] += colspan
484
- }
485
- nil
486
- end
487
-
488
- # Public: Check whether we've met the number of effective columns for the current row.
489
- def end_of_row?
490
- @col_count == -1 || effective_col_visits == @col_count
491
435
  end
492
-
493
- # Public: Calculate the effective column visits, which consists of the number of
494
- # cells plus any active rowspans.
495
- def effective_col_visits
496
- @col_visits + @active_rowspans.first
497
- end
498
-
499
- # Internal: Advance to the next line (which may come after the parser begins processing
500
- # the next line if the last cell had wrapped content).
501
- def next_line
502
- @linenum += 1
503
- end
504
-
436
+
437
+ 1.upto(repeat) {|i|
438
+ # make column resolving an operation
439
+ if @col_count == -1
440
+ @table.columns << Table::Column.new(@table, @current_row.size + i - 1)
441
+ column = @table.columns.last
442
+ else
443
+ # QUESTION is this right for cells that span columns?
444
+ column = @table.columns[@current_row.size]
445
+ end
446
+
447
+ cell = Table::Cell.new(column, cell_text, cell_spec)
448
+ unless cell.rowspan.nil? || cell.rowspan == 1
449
+ activate_rowspan(cell.rowspan, (cell.colspan || 1))
450
+ end
451
+ @col_visits += (cell.colspan || 1)
452
+ @current_row << cell
453
+ # don't close the row if we're on the first line and the column count has not been set explicitly
454
+ # TODO perhaps the col_count/linenum logic should be in end_of_row? (or a should_end_row? method)
455
+ close_row if end_of_row? && (@col_count != -1 || @linenum > 0 || (eol && i == repeat))
456
+ }
457
+ @open_cell = false
458
+ nil
459
+ end
460
+
461
+ # Public: Close the row by adding it to the Table and resetting the row
462
+ # Array and counter variables.
463
+ #
464
+ # returns nothing
465
+ def close_row
466
+ @table.rows.body << @current_row
467
+ # don't have to account for active rowspans here
468
+ # since we know this is first row
469
+ @col_count = @col_visits if @col_count == -1
470
+ @col_visits = 0
471
+ @current_row = []
472
+ @active_rowspans.shift
473
+ @active_rowspans[0] ||= 0
474
+ nil
475
+ end
476
+
477
+ # Public: Activate a rowspan. The rowspan Array is consulted when
478
+ # determining the effective number of cells in the current row.
479
+ #
480
+ # returns nothing
481
+ def activate_rowspan(rowspan, colspan)
482
+ 1.upto(rowspan - 1).each {|i|
483
+ @active_rowspans[i] ||= 0
484
+ @active_rowspans[i] += colspan
485
+ }
486
+ nil
487
+ end
488
+
489
+ # Public: Check whether we've met the number of effective columns for the current row.
490
+ def end_of_row?
491
+ @col_count == -1 || effective_col_visits == @col_count
505
492
  end
493
+
494
+ # Public: Calculate the effective column visits, which consists of the number of
495
+ # cells plus any active rowspans.
496
+ def effective_col_visits
497
+ @col_visits + @active_rowspans.first
498
+ end
499
+
500
+ # Internal: Advance to the next line (which may come after the parser begins processing
501
+ # the next line if the last cell had wrapped content).
502
+ def advance
503
+ @linenum += 1
504
+ end
505
+
506
+ end
506
507
  end