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.
- data/README.asciidoc +11 -2
- data/asciidoctor.gemspec +3 -2
- data/lib/asciidoctor.rb +95 -62
- data/lib/asciidoctor/abstract_block.rb +7 -5
- data/lib/asciidoctor/abstract_node.rb +63 -15
- data/lib/asciidoctor/attribute_list.rb +3 -1
- data/lib/asciidoctor/backends/base_template.rb +17 -7
- data/lib/asciidoctor/backends/docbook45.rb +182 -150
- data/lib/asciidoctor/backends/html5.rb +138 -110
- data/lib/asciidoctor/block.rb +21 -18
- data/lib/asciidoctor/callouts.rb +3 -1
- data/lib/asciidoctor/cli/invoker.rb +3 -3
- data/lib/asciidoctor/cli/options.rb +6 -6
- data/lib/asciidoctor/debug.rb +7 -6
- data/lib/asciidoctor/document.rb +197 -25
- data/lib/asciidoctor/errors.rb +1 -1
- data/lib/asciidoctor/helpers.rb +29 -0
- data/lib/asciidoctor/inline.rb +11 -4
- data/lib/asciidoctor/lexer.rb +338 -182
- data/lib/asciidoctor/list_item.rb +14 -12
- data/lib/asciidoctor/reader.rb +423 -206
- data/lib/asciidoctor/renderer.rb +59 -15
- data/lib/asciidoctor/section.rb +7 -4
- data/lib/asciidoctor/substituters.rb +536 -511
- data/lib/asciidoctor/table.rb +473 -472
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +23 -14
- data/man/asciidoctor.ad +13 -7
- data/test/attributes_test.rb +42 -8
- data/test/blocks_test.rb +161 -1
- data/test/document_test.rb +134 -16
- data/test/invoker_test.rb +14 -6
- data/test/lexer_test.rb +45 -18
- data/test/lists_test.rb +79 -0
- data/test/paragraphs_test.rb +9 -1
- data/test/reader_test.rb +456 -19
- data/test/sections_test.rb +19 -0
- data/test/substitutions_test.rb +14 -12
- data/test/tables_test.rb +10 -10
- metadata +3 -5
data/lib/asciidoctor/table.rb
CHANGED
@@ -1,506 +1,507 @@
|
|
1
1
|
module Asciidoctor
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
#
|
142
|
-
|
143
|
-
|
144
|
-
#
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
attributes
|
153
|
-
|
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
|
-
|
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
|
-
|
180
|
-
|
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
|
-
|
183
|
-
attr_accessor :rowspan
|
140
|
+
end
|
184
141
|
|
185
|
-
|
186
|
-
|
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
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
-
|
234
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
#
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
#
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
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
|
-
|
280
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
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
|
-
|
383
|
-
|
384
|
-
#
|
385
|
-
|
386
|
-
|
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
|
-
|
390
|
-
|
391
|
-
|
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
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
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
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
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
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
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
|