asciidoctor 0.0.7 → 0.0.9

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