asciidoctor 0.1.4 → 1.5.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +209 -25
- data/{LICENSE → LICENSE.adoc} +4 -3
- data/README.adoc +392 -395
- data/Rakefile +94 -137
- data/benchmark/benchmark.rb +127 -0
- data/benchmark/sample-data/mdbasics.adoc +334 -0
- data/bin/asciidoctor +5 -8
- data/bin/asciidoctor-safe +4 -8
- data/compat/asciidoc.conf +78 -11
- data/compat/font-awesome-3-compat.css +397 -0
- data/data/stylesheets/asciidoctor-default.css +399 -0
- data/data/stylesheets/coderay-asciidoctor.css +89 -0
- data/features/open_block.feature +92 -0
- data/features/pass_block.feature +66 -0
- data/features/step_definitions.rb +42 -0
- data/features/text_formatting.feature +55 -0
- data/features/xref.feature +116 -0
- data/lib/asciidoctor.rb +1155 -605
- data/lib/asciidoctor/abstract_block.rb +157 -71
- data/lib/asciidoctor/abstract_node.rb +150 -93
- data/lib/asciidoctor/attribute_list.rb +85 -90
- data/lib/asciidoctor/block.rb +51 -24
- data/lib/asciidoctor/callouts.rb +4 -7
- data/lib/asciidoctor/cli.rb +3 -0
- data/lib/asciidoctor/cli/invoker.rb +86 -76
- data/lib/asciidoctor/cli/options.rb +111 -61
- data/lib/asciidoctor/converter.rb +232 -0
- data/lib/asciidoctor/converter/base.rb +58 -0
- data/lib/asciidoctor/converter/composite.rb +66 -0
- data/lib/asciidoctor/converter/docbook45.rb +94 -0
- data/lib/asciidoctor/converter/docbook5.rb +684 -0
- data/lib/asciidoctor/converter/factory.rb +225 -0
- data/lib/asciidoctor/converter/html5.rb +1081 -0
- data/lib/asciidoctor/converter/template.rb +296 -0
- data/lib/asciidoctor/core_ext.rb +7 -0
- data/lib/asciidoctor/core_ext/object/nil_or_empty.rb +23 -0
- data/lib/asciidoctor/core_ext/string/chr.rb +6 -0
- data/lib/asciidoctor/core_ext/symbol/length.rb +6 -0
- data/lib/asciidoctor/document.rb +590 -304
- data/lib/asciidoctor/extensions.rb +1100 -308
- data/lib/asciidoctor/helpers.rb +109 -46
- data/lib/asciidoctor/inline.rb +16 -9
- data/lib/asciidoctor/list.rb +23 -15
- data/lib/asciidoctor/opal_ext.rb +4 -0
- data/lib/asciidoctor/opal_ext/comparable.rb +38 -0
- data/lib/asciidoctor/opal_ext/dir.rb +13 -0
- data/lib/asciidoctor/opal_ext/error.rb +2 -0
- data/lib/asciidoctor/opal_ext/file.rb +125 -0
- data/lib/asciidoctor/{lexer.rb → parser.rb} +646 -455
- data/lib/asciidoctor/path_resolver.rb +141 -77
- data/lib/asciidoctor/reader.rb +257 -187
- data/lib/asciidoctor/section.rb +12 -16
- data/lib/asciidoctor/stylesheets.rb +91 -0
- data/lib/asciidoctor/substitutors.rb +1548 -0
- data/lib/asciidoctor/table.rb +73 -57
- data/lib/asciidoctor/timings.rb +39 -0
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +22 -14
- data/man/asciidoctor.adoc +18 -10
- data/test/attributes_test.rb +314 -14
- data/test/blocks_test.rb +763 -118
- data/test/converter_test.rb +352 -0
- data/test/document_test.rb +518 -199
- data/test/extensions_test.rb +273 -103
- data/test/fixtures/asciidoc_index.txt +27 -13
- data/test/fixtures/basic-docinfo.xml +1 -1
- data/test/fixtures/chapter-a.adoc +3 -0
- data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +6 -0
- data/test/fixtures/docinfo.xml +1 -1
- data/test/fixtures/include-file.asciidoc +2 -0
- data/test/fixtures/master.adoc +5 -0
- data/test/invoker_test.rb +173 -61
- data/test/links_test.rb +97 -21
- data/test/lists_test.rb +181 -22
- data/test/options_test.rb +86 -2
- data/test/paragraphs_test.rb +47 -5
- data/test/{lexer_test.rb → parser_test.rb} +128 -57
- data/test/paths_test.rb +36 -1
- data/test/preamble_test.rb +25 -17
- data/test/reader_test.rb +404 -249
- data/test/sections_test.rb +623 -58
- data/test/substitutions_test.rb +609 -132
- data/test/tables_test.rb +198 -24
- data/test/test_helper.rb +101 -31
- data/test/text_test.rb +88 -31
- metadata +160 -64
- data/Gemfile +0 -12
- data/Guardfile +0 -18
- data/asciidoctor.gemspec +0 -143
- data/lib/asciidoctor/backends/_stylesheets.rb +0 -466
- data/lib/asciidoctor/backends/base_template.rb +0 -114
- data/lib/asciidoctor/backends/docbook45.rb +0 -774
- data/lib/asciidoctor/backends/docbook5.rb +0 -103
- data/lib/asciidoctor/backends/html5.rb +0 -1214
- data/lib/asciidoctor/renderer.rb +0 -259
- data/lib/asciidoctor/substituters.rb +0 -1083
- data/test/fixtures/asciidoc.txt +0 -105
- data/test/fixtures/ascshort.txt +0 -32
- data/test/fixtures/list_elements.asciidoc +0 -10
- data/test/renderer_test.rb +0 -162
@@ -1,11 +1,11 @@
|
|
1
1
|
module Asciidoctor
|
2
2
|
# Public: Methods to parse lines of AsciiDoc into an object hierarchy
|
3
3
|
# representing the structure of the document. All methods are class methods and
|
4
|
-
# should be invoked from the
|
5
|
-
# No
|
6
|
-
# instantiate a
|
4
|
+
# should be invoked from the Parser class. The main entry point is ::next_block.
|
5
|
+
# No Parser instances shall be discovered running around. (Any attempt to
|
6
|
+
# instantiate a Parser will be futile).
|
7
7
|
#
|
8
|
-
# The object hierarchy created by the
|
8
|
+
# The object hierarchy created by the Parser consists of zero or more Section
|
9
9
|
# and Block objects. Section objects may be nested and a Section object
|
10
10
|
# contains zero or more Block objects. Block objects may be nested, but may
|
11
11
|
# only contain other Block objects. Block objects which represent lists may
|
@@ -14,18 +14,18 @@ module Asciidoctor
|
|
14
14
|
# Examples
|
15
15
|
#
|
16
16
|
# # Create a Reader for the AsciiDoc lines and retrieve the next block from it.
|
17
|
-
# #
|
17
|
+
# # Parser.next_block requires a parent, so we begin by instantiating an empty Document.
|
18
18
|
#
|
19
19
|
# doc = Document.new
|
20
20
|
# reader = Reader.new lines
|
21
|
-
# block =
|
21
|
+
# block = Parser.next_block(reader, doc)
|
22
22
|
# block.class
|
23
23
|
# # => Asciidoctor::Block
|
24
|
-
class
|
24
|
+
class Parser
|
25
25
|
|
26
|
-
BlockMatchData = Struct.new
|
26
|
+
BlockMatchData = Struct.new :context, :masq, :tip, :terminator
|
27
27
|
|
28
|
-
# Public: Make sure the
|
28
|
+
# Public: Make sure the Parser object doesn't get initialized.
|
29
29
|
#
|
30
30
|
# Raises RuntimeError if this constructor is invoked.
|
31
31
|
def initialize
|
@@ -34,7 +34,7 @@ class Lexer
|
|
34
34
|
|
35
35
|
# Public: Parses AsciiDoc source read from the Reader into the Document
|
36
36
|
#
|
37
|
-
# This method is the main entry-point into the
|
37
|
+
# This method is the main entry-point into the Parser when parsing a full document.
|
38
38
|
# It first looks for and, if found, processes the document title. It then
|
39
39
|
# proceeds to iterate through the lines in the Reader, parsing the document
|
40
40
|
# into nested Sections and Blocks.
|
@@ -50,8 +50,21 @@ class Lexer
|
|
50
50
|
unless options[:header_only]
|
51
51
|
while reader.has_more_lines?
|
52
52
|
new_section, block_attributes = next_section(reader, document, block_attributes)
|
53
|
-
document << new_section
|
53
|
+
document << new_section if new_section
|
54
54
|
end
|
55
|
+
# NOTE we could try to avoid creating a preamble in the first place, though
|
56
|
+
# that would require reworking assumptions in next_section since the preamble
|
57
|
+
# is treated like an untitled section
|
58
|
+
# NOTE logic relocated to end of next_section
|
59
|
+
#if Compliance.unwrap_standalone_preamble &&
|
60
|
+
# document.blocks.size == 1 && (first_block = document.blocks[0]).context == :preamble &&
|
61
|
+
# first_block.blocks? && (document.doctype != 'book' || first_block.blocks[0].style != 'abstract')
|
62
|
+
# preamble = document.blocks.shift
|
63
|
+
# while (child_block = preamble.blocks.shift)
|
64
|
+
# child_block.parent = document
|
65
|
+
# document << child_block
|
66
|
+
# end
|
67
|
+
#end
|
55
68
|
end
|
56
69
|
|
57
70
|
document
|
@@ -83,7 +96,7 @@ class Lexer
|
|
83
96
|
# yep, document title logic in AsciiDoc is just insanity
|
84
97
|
# definitely an area for spec refinement
|
85
98
|
assigned_doctitle = nil
|
86
|
-
unless (val = document.attributes
|
99
|
+
unless (val = document.attributes['doctitle']).nil_or_empty?
|
87
100
|
document.title = val
|
88
101
|
assigned_doctitle = val
|
89
102
|
end
|
@@ -92,20 +105,24 @@ class Lexer
|
|
92
105
|
# check if the first line is the document title
|
93
106
|
# if so, add a header to the document and parse the header metadata
|
94
107
|
if is_next_line_document_title?(reader, block_attributes)
|
95
|
-
|
108
|
+
source_location = reader.cursor if document.sourcemap
|
109
|
+
document.id, _, doctitle, _, single_line = parse_section_title(reader, document)
|
96
110
|
unless assigned_doctitle
|
97
111
|
document.title = doctitle
|
98
112
|
assigned_doctitle = doctitle
|
99
113
|
end
|
114
|
+
# default to compat-mode if document uses atx-style doctitle
|
115
|
+
document.set_attribute 'compat-mode', '' unless single_line
|
116
|
+
document.header.source_location = source_location if source_location
|
100
117
|
document.attributes['doctitle'] = section_title = doctitle
|
101
118
|
# QUESTION: should the id assignment on Document be encapsulated in the Document class?
|
102
|
-
|
119
|
+
unless document.id
|
103
120
|
document.id = block_attributes.delete('id')
|
104
121
|
end
|
105
122
|
parse_header_metadata(reader, document)
|
106
123
|
end
|
107
124
|
|
108
|
-
if !(val = document.attributes
|
125
|
+
if !(val = document.attributes['doctitle']).nil_or_empty? &&
|
109
126
|
val != section_title
|
110
127
|
document.title = val
|
111
128
|
assigned_doctitle = val
|
@@ -128,11 +145,11 @@ class Lexer
|
|
128
145
|
#
|
129
146
|
# returns Nothing
|
130
147
|
def self.parse_manpage_header(reader, document)
|
131
|
-
if (m = document.attributes['doctitle']
|
148
|
+
if (m = ManpageTitleVolnumRx.match(document.attributes['doctitle']))
|
132
149
|
document.attributes['mantitle'] = document.sub_attributes(m[1].rstrip.downcase)
|
133
150
|
document.attributes['manvolnum'] = m[2].strip
|
134
151
|
else
|
135
|
-
warn
|
152
|
+
warn %(asciidoctor: ERROR: #{reader.prev_line_info}: malformed manpage title)
|
136
153
|
end
|
137
154
|
|
138
155
|
reader.skip_blank_lines
|
@@ -140,24 +157,24 @@ class Lexer
|
|
140
157
|
if is_next_line_section?(reader, {})
|
141
158
|
name_section = initialize_section(reader, document, {})
|
142
159
|
if name_section.level == 1
|
143
|
-
name_section_buffer = reader.read_lines_until(:break_on_blank_lines => true).join.tr_s(
|
144
|
-
if (m =
|
145
|
-
document.attributes['manname'] = m[1]
|
160
|
+
name_section_buffer = reader.read_lines_until(:break_on_blank_lines => true).join(' ').tr_s(' ', ' ')
|
161
|
+
if (m = ManpageNamePurposeRx.match(name_section_buffer))
|
162
|
+
document.attributes['manname'] = document.sub_attributes m[1]
|
146
163
|
document.attributes['manpurpose'] = m[2]
|
147
164
|
# TODO parse multiple man names
|
148
165
|
|
149
166
|
if document.backend == 'manpage'
|
150
167
|
document.attributes['docname'] = document.attributes['manname']
|
151
|
-
document.attributes['outfilesuffix'] =
|
168
|
+
document.attributes['outfilesuffix'] = %(.#{document.attributes['manvolnum']})
|
152
169
|
end
|
153
170
|
else
|
154
|
-
warn
|
171
|
+
warn %(asciidoctor: ERROR: #{reader.prev_line_info}: malformed name section body)
|
155
172
|
end
|
156
173
|
else
|
157
|
-
warn
|
174
|
+
warn %(asciidoctor: ERROR: #{reader.prev_line_info}: name section title must be at level 1)
|
158
175
|
end
|
159
176
|
else
|
160
|
-
warn
|
177
|
+
warn %(asciidoctor: ERROR: #{reader.prev_line_info}: name section expected)
|
161
178
|
end
|
162
179
|
end
|
163
180
|
|
@@ -184,33 +201,35 @@ class Lexer
|
|
184
201
|
# Examples
|
185
202
|
#
|
186
203
|
# source
|
187
|
-
# # => "Greetings\n
|
204
|
+
# # => "= Greetings\n\nThis is my doc.\n\n== Salutations\n\nIt is awesome."
|
188
205
|
#
|
189
|
-
# reader = Reader.new source
|
206
|
+
# reader = Reader.new source, nil, :normalize => true
|
190
207
|
# # create empty document to parent the section
|
191
208
|
# # and hold attributes extracted from header
|
192
209
|
# doc = Document.new
|
193
210
|
#
|
194
|
-
#
|
211
|
+
# Parser.next_section(reader, doc).first.title
|
195
212
|
# # => "Greetings"
|
196
213
|
#
|
197
|
-
#
|
214
|
+
# Parser.next_section(reader, doc).first.title
|
198
215
|
# # => "Salutations"
|
199
216
|
#
|
200
217
|
# returns a two-element Array containing the Section and Hash of orphaned attributes
|
201
218
|
def self.next_section(reader, parent, attributes = {})
|
202
219
|
preamble = false
|
220
|
+
part = false
|
221
|
+
intro = false
|
203
222
|
|
204
223
|
# FIXME if attributes[1] is a verbatim style, then don't check for section
|
205
224
|
|
206
225
|
# check if we are at the start of processing the document
|
207
226
|
# NOTE we could drop a hint in the attributes to indicate
|
208
227
|
# that we are at a section title (so we don't have to check)
|
209
|
-
if parent.
|
210
|
-
(parent.has_header? || attributes.delete('invalid-header') || !is_next_line_section?(reader, attributes))
|
211
|
-
|
212
|
-
if
|
213
|
-
preamble = Block.new(parent, :preamble, :content_model => :compound)
|
228
|
+
if parent.context == :document && parent.blocks.empty? &&
|
229
|
+
((has_header = parent.has_header?) || attributes.delete('invalid-header') || !is_next_line_section?(reader, attributes))
|
230
|
+
doctype = parent.doctype
|
231
|
+
if has_header || (doctype == 'book' && attributes[1] != 'abstract')
|
232
|
+
preamble = intro = Block.new(parent, :preamble, :content_model => :compound)
|
214
233
|
parent << preamble
|
215
234
|
end
|
216
235
|
section = parent
|
@@ -219,21 +238,26 @@ class Lexer
|
|
219
238
|
if parent.attributes.has_key? 'fragment'
|
220
239
|
expected_next_levels = nil
|
221
240
|
# small tweak to allow subsequent level-0 sections for book doctype
|
222
|
-
elsif
|
241
|
+
elsif doctype == 'book'
|
223
242
|
expected_next_levels = [0, 1]
|
224
243
|
else
|
225
244
|
expected_next_levels = [1]
|
226
245
|
end
|
227
246
|
else
|
247
|
+
doctype = parent.document.doctype
|
228
248
|
section = initialize_section(reader, parent, attributes)
|
229
249
|
# clear attributes, except for title which carries over
|
230
250
|
# section title to next block of content
|
231
|
-
attributes =
|
251
|
+
attributes = (title = attributes['title']) ? { 'title' => title } : {}
|
232
252
|
current_level = section.level
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
253
|
+
if current_level == 0 && doctype == 'book'
|
254
|
+
part = !section.special
|
255
|
+
# subsections in preface & appendix in multipart books start at level 2
|
256
|
+
if section.special && (['preface', 'appendix'].include? section.sectname)
|
257
|
+
expected_next_levels = [current_level + 2]
|
258
|
+
else
|
259
|
+
expected_next_levels = [current_level + 1]
|
260
|
+
end
|
237
261
|
else
|
238
262
|
expected_next_levels = [current_level + 1]
|
239
263
|
end
|
@@ -253,46 +277,103 @@ class Lexer
|
|
253
277
|
while reader.has_more_lines?
|
254
278
|
parse_block_metadata_lines(reader, section, attributes)
|
255
279
|
|
256
|
-
next_level = is_next_line_section? reader, attributes
|
257
|
-
if next_level
|
280
|
+
if (next_level = is_next_line_section? reader, attributes)
|
258
281
|
next_level += section.document.attr('leveloffset', 0).to_i
|
259
|
-
|
260
|
-
if next_level > current_level || (section.is_a?(Document) && next_level == 0)
|
282
|
+
if next_level > current_level || (section.context == :document && next_level == 0)
|
261
283
|
if next_level == 0 && doctype != 'book'
|
262
|
-
warn
|
263
|
-
elsif
|
264
|
-
warn
|
265
|
-
|
266
|
-
|
284
|
+
warn %(asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections)
|
285
|
+
elsif expected_next_levels && !expected_next_levels.include?(next_level)
|
286
|
+
warn %(asciidoctor: WARNING: #{reader.line_info}: section title out of sequence: ) +
|
287
|
+
%(expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, ) +
|
288
|
+
%(got level #{next_level})
|
267
289
|
end
|
268
290
|
# the attributes returned are those that are orphaned
|
269
291
|
new_section, attributes = next_section(reader, section, attributes)
|
270
292
|
section << new_section
|
271
293
|
else
|
272
294
|
if next_level == 0 && doctype != 'book'
|
273
|
-
warn
|
295
|
+
warn %(asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections)
|
274
296
|
end
|
275
297
|
# close this section (and break out of the nesting) to begin a new one
|
276
298
|
break
|
277
299
|
end
|
278
300
|
else
|
279
301
|
# just take one block or else we run the risk of overrunning section boundaries
|
280
|
-
|
281
|
-
if
|
282
|
-
|
302
|
+
block_line_info = reader.line_info
|
303
|
+
if (new_block = next_block reader, (intro || section), attributes, :parse_metadata => false)
|
304
|
+
# REVIEW this may be doing too much
|
305
|
+
if part
|
306
|
+
if !section.blocks?
|
307
|
+
# if this block wasn't marked as [partintro], emulate behavior as if it had
|
308
|
+
if new_block.style != 'partintro'
|
309
|
+
# emulate [partintro] paragraph
|
310
|
+
if new_block.context == :paragraph
|
311
|
+
new_block.context = :open
|
312
|
+
new_block.style = 'partintro'
|
313
|
+
# emulate [partintro] open block
|
314
|
+
else
|
315
|
+
intro = Block.new section, :open, :content_model => :compound
|
316
|
+
intro.style = 'partintro'
|
317
|
+
new_block.parent = intro
|
318
|
+
section << intro
|
319
|
+
end
|
320
|
+
end
|
321
|
+
elsif section.blocks.size == 1
|
322
|
+
first_block = section.blocks[0]
|
323
|
+
# open the [partintro] open block for appending
|
324
|
+
if !intro && first_block.content_model == :compound
|
325
|
+
#new_block.parent = (intro = first_block)
|
326
|
+
warn %(asciidoctor: ERROR: #{block_line_info}: illegal block content outside of partintro block)
|
327
|
+
# rebuild [partintro] paragraph as an open block
|
328
|
+
elsif first_block.content_model != :compound
|
329
|
+
intro = Block.new section, :open, :content_model => :compound
|
330
|
+
intro.style = 'partintro'
|
331
|
+
section.blocks.shift
|
332
|
+
if first_block.style == 'partintro'
|
333
|
+
first_block.context = :paragraph
|
334
|
+
first_block.style = nil
|
335
|
+
end
|
336
|
+
first_block.parent = intro
|
337
|
+
intro << first_block
|
338
|
+
new_block.parent = intro
|
339
|
+
section << intro
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
(intro || section) << new_block
|
283
345
|
attributes = {}
|
284
|
-
else
|
285
|
-
|
286
|
-
|
346
|
+
#else
|
347
|
+
# # don't clear attributes if we don't find a block because they may
|
348
|
+
# # be trailing attributes that didn't get associated with a block
|
287
349
|
end
|
288
350
|
end
|
289
351
|
|
290
352
|
reader.skip_blank_lines
|
291
353
|
end
|
292
354
|
|
293
|
-
if
|
355
|
+
if part
|
356
|
+
unless section.blocks? && section.blocks[-1].context == :section
|
357
|
+
warn %(asciidoctor: ERROR: #{reader.line_info}: invalid part, must have at least one section (e.g., chapter, appendix, etc.))
|
358
|
+
end
|
359
|
+
# NOTE we could try to avoid creating a preamble in the first place, though
|
360
|
+
# that would require reworking assumptions in next_section since the preamble
|
361
|
+
# is treated like an untitled section
|
362
|
+
elsif preamble # implies parent == document
|
363
|
+
document = parent
|
364
|
+
if preamble.blocks?
|
365
|
+
# unwrap standalone preamble (i.e., no sections), if permissible
|
366
|
+
if Compliance.unwrap_standalone_preamble && document.blocks.size == 1 && doctype != 'book'
|
367
|
+
document.blocks.shift
|
368
|
+
while (child_block = preamble.blocks.shift)
|
369
|
+
child_block.parent = document
|
370
|
+
document << child_block
|
371
|
+
end
|
372
|
+
end
|
294
373
|
# drop the preamble if it has no content
|
295
|
-
|
374
|
+
else
|
375
|
+
document.blocks.shift
|
376
|
+
end
|
296
377
|
end
|
297
378
|
|
298
379
|
# The attributes returned here are orphaned attributes that fall at the end
|
@@ -324,13 +405,12 @@ class Lexer
|
|
324
405
|
skipped = reader.skip_blank_lines
|
325
406
|
|
326
407
|
# bail if we've reached the end of the parent block or document
|
327
|
-
return
|
408
|
+
return unless reader.has_more_lines?
|
328
409
|
|
329
|
-
text_only = options[:text]
|
330
410
|
# check for option to find list item text only
|
331
411
|
# if skipped a line, assume a list continuation was
|
332
412
|
# used and block content is acceptable
|
333
|
-
if text_only && skipped > 0
|
413
|
+
if (text_only = options[:text]) && skipped > 0
|
334
414
|
options.delete(:text)
|
335
415
|
text_only = false
|
336
416
|
end
|
@@ -341,27 +421,30 @@ class Lexer
|
|
341
421
|
document = parent.document
|
342
422
|
if (extensions = document.extensions)
|
343
423
|
block_extensions = extensions.blocks?
|
344
|
-
|
424
|
+
block_macro_extensions = extensions.block_macros?
|
345
425
|
else
|
346
|
-
block_extensions =
|
426
|
+
block_extensions = block_macro_extensions = false
|
347
427
|
end
|
348
428
|
#parent_context = parent.is_a?(Block) ? parent.context : nil
|
349
|
-
in_list = parent.is_a?
|
429
|
+
in_list = (parent.is_a? List)
|
350
430
|
block = nil
|
351
431
|
style = nil
|
352
432
|
explicit_style = nil
|
433
|
+
sourcemap = document.sourcemap
|
434
|
+
source_location = nil
|
353
435
|
|
354
|
-
while
|
436
|
+
while !block && reader.has_more_lines?
|
355
437
|
# if parsing metadata, read until there is no more to read
|
356
438
|
if parse_metadata && parse_block_metadata_line(reader, document, attributes, options)
|
357
439
|
reader.advance
|
358
440
|
next
|
359
|
-
#elsif parse_sections && parent_context
|
441
|
+
#elsif parse_sections && !parent_context && is_next_line_section?(reader, attributes)
|
360
442
|
# block, attributes = next_section(reader, parent, attributes)
|
361
443
|
# break
|
362
444
|
end
|
363
445
|
|
364
446
|
# QUESTION should we introduce a parsing context object?
|
447
|
+
source_location = reader.cursor if sourcemap
|
365
448
|
this_line = reader.read_line
|
366
449
|
delimited_block = false
|
367
450
|
block_context = nil
|
@@ -372,7 +455,7 @@ class Lexer
|
|
372
455
|
style, explicit_style = parse_style_attribute(attributes, reader)
|
373
456
|
end
|
374
457
|
|
375
|
-
if delimited_blk_match = is_delimited_block?
|
458
|
+
if (delimited_blk_match = is_delimited_block? this_line, true)
|
376
459
|
delimited_block = true
|
377
460
|
block_context = cloaked_context = delimited_blk_match.context
|
378
461
|
terminator = delimited_blk_match.terminator
|
@@ -383,16 +466,16 @@ class Lexer
|
|
383
466
|
block_context = style.to_sym
|
384
467
|
elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
|
385
468
|
block_context = :admonition
|
386
|
-
elsif block_extensions && extensions.
|
469
|
+
elsif block_extensions && extensions.registered_for_block?(style, block_context)
|
387
470
|
block_context = style.to_sym
|
388
471
|
else
|
389
|
-
warn
|
472
|
+
warn %(asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for #{block_context} block: #{style})
|
390
473
|
style = block_context.to_s
|
391
474
|
end
|
392
475
|
end
|
393
476
|
end
|
394
477
|
|
395
|
-
|
478
|
+
unless delimited_block
|
396
479
|
|
397
480
|
# this loop only executes once; used for flow control
|
398
481
|
# break once a block is found or at end of loop
|
@@ -401,7 +484,7 @@ class Lexer
|
|
401
484
|
while true
|
402
485
|
|
403
486
|
# process lines verbatim
|
404
|
-
if
|
487
|
+
if style && Compliance.strict_verbatim_paragraphs && VERBATIM_STYLES.include?(style)
|
405
488
|
block_context = style.to_sym
|
406
489
|
reader.unshift_line this_line
|
407
490
|
# advance to block parsing =>
|
@@ -410,14 +493,14 @@ class Lexer
|
|
410
493
|
|
411
494
|
# process lines normally
|
412
495
|
unless text_only
|
413
|
-
first_char = Compliance.markdown_syntax ? this_line.lstrip
|
414
|
-
# NOTE we're letting break lines (
|
415
|
-
if
|
416
|
-
(
|
417
|
-
block = Block.new(parent,
|
496
|
+
first_char = Compliance.markdown_syntax ? this_line.lstrip.chr : this_line.chr
|
497
|
+
# NOTE we're letting break lines (horizontal rule, page_break, etc) have attributes
|
498
|
+
if (LAYOUT_BREAK_LINES.has_key? first_char) && this_line.length >= 3 &&
|
499
|
+
(Compliance.markdown_syntax ? LayoutBreakLinePlusRx : LayoutBreakLineRx) =~ this_line
|
500
|
+
block = Block.new(parent, LAYOUT_BREAK_LINES[first_char], :content_model => :empty)
|
418
501
|
break
|
419
502
|
|
420
|
-
elsif (match =
|
503
|
+
elsif this_line.end_with?(']') && (match = MediaBlockMacroRx.match(this_line))
|
421
504
|
blk_ctx = match[1].to_sym
|
422
505
|
block = Block.new(parent, blk_ctx, :content_model => :empty)
|
423
506
|
if blk_ctx == :image
|
@@ -428,7 +511,7 @@ class Lexer
|
|
428
511
|
posattrs = []
|
429
512
|
end
|
430
513
|
|
431
|
-
unless style
|
514
|
+
unless !style || explicit_style
|
432
515
|
attributes['alt'] = style if blk_ctx == :image
|
433
516
|
attributes.delete('style')
|
434
517
|
style = nil
|
@@ -441,52 +524,66 @@ class Lexer
|
|
441
524
|
:into => attributes)
|
442
525
|
target = block.sub_attributes(match[2], :attribute_missing => 'drop-line')
|
443
526
|
if target.empty?
|
444
|
-
if
|
445
|
-
|
446
|
-
return Block.new(parent, :paragraph, :source => [this_line
|
527
|
+
# retain as unparsed if attribute-missing is skip
|
528
|
+
if document.attributes.fetch('attribute-missing', Compliance.attribute_missing) == 'skip'
|
529
|
+
return Block.new(parent, :paragraph, :content_model => :simple, :source => [this_line])
|
530
|
+
# otherwise, drop the line
|
447
531
|
else
|
448
|
-
|
449
|
-
return
|
532
|
+
attributes.clear
|
533
|
+
return
|
450
534
|
end
|
451
535
|
end
|
452
536
|
|
453
537
|
attributes['target'] = target
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
538
|
+
# now done down below
|
539
|
+
#block.title = attributes.delete('title') if attributes.has_key?('title')
|
540
|
+
#if blk_ctx == :image
|
541
|
+
# if attributes.has_key? 'scaledwidth'
|
542
|
+
# # append % to scaledwidth if ends in number (no units present)
|
543
|
+
# if (48..57).include?((attributes['scaledwidth'][-1] || 0).ord)
|
544
|
+
# attributes['scaledwidth'] = %(#{attributes['scaledwidth']}%)
|
545
|
+
# end
|
546
|
+
# end
|
547
|
+
# document.register(:images, target)
|
548
|
+
# attributes['alt'] ||= ::File.basename(target, ::File.extname(target)).tr('_-', ' ')
|
549
|
+
# # QUESTION should video or audio have an auto-numbered caption?
|
550
|
+
# block.assign_caption attributes.delete('caption'), 'figure'
|
551
|
+
#end
|
461
552
|
break
|
462
553
|
|
463
554
|
# NOTE we're letting the toc macro have attributes
|
464
|
-
elsif first_char == 't' && (match =
|
555
|
+
elsif first_char == 't' && (match = TocBlockMacroRx.match(this_line))
|
465
556
|
block = Block.new(parent, :toc, :content_model => :empty)
|
466
557
|
block.parse_attributes(match[1], [], :sub_result => false, :into => attributes)
|
467
558
|
break
|
468
559
|
|
469
|
-
elsif
|
470
|
-
extensions.
|
471
|
-
name = match[1]
|
560
|
+
elsif block_macro_extensions && (match = GenericBlockMacroRx.match(this_line)) &&
|
561
|
+
(extension = extensions.registered_for_block_macro?(match[1]))
|
472
562
|
target = match[2]
|
473
563
|
raw_attributes = match[3]
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
564
|
+
if extension.config[:content_model] == :attributes
|
565
|
+
unless raw_attributes.empty?
|
566
|
+
document.parse_attributes(raw_attributes, (extension.config[:pos_attrs] || []),
|
567
|
+
:sub_input => true, :sub_result => false, :into => attributes)
|
568
|
+
end
|
569
|
+
else
|
570
|
+
attributes['text'] = raw_attributes
|
478
571
|
end
|
479
|
-
if
|
572
|
+
if (default_attrs = extension.config[:default_attrs])
|
480
573
|
default_attrs.each {|k, v| attributes[k] ||= v }
|
481
574
|
end
|
482
|
-
block =
|
483
|
-
|
575
|
+
if (block = extension.process_method[parent, target, attributes.dup])
|
576
|
+
attributes.replace block.attributes
|
577
|
+
else
|
578
|
+
attributes.clear
|
579
|
+
return
|
580
|
+
end
|
484
581
|
break
|
485
582
|
end
|
486
583
|
end
|
487
584
|
|
488
585
|
# haven't found anything yet, continue
|
489
|
-
if (match =
|
586
|
+
if (match = CalloutListRx.match(this_line))
|
490
587
|
block = List.new(parent, :colist)
|
491
588
|
attributes['style'] = 'arabic'
|
492
589
|
reader.unshift_line this_line
|
@@ -495,48 +592,48 @@ class Lexer
|
|
495
592
|
# might want to move this check to a validate method
|
496
593
|
if match[1].to_i != expected_index
|
497
594
|
# FIXME this lineno - 2 hack means we need a proper look-behind cursor
|
498
|
-
warn
|
595
|
+
warn %(asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: callout list item index: expected #{expected_index} got #{match[1]})
|
499
596
|
end
|
500
597
|
list_item = next_list_item(reader, block, match)
|
501
598
|
expected_index += 1
|
502
|
-
if
|
599
|
+
if list_item
|
503
600
|
block << list_item
|
504
601
|
coids = document.callouts.callout_ids(block.items.size)
|
505
602
|
if !coids.empty?
|
506
603
|
list_item.attributes['coids'] = coids
|
507
604
|
else
|
508
605
|
# FIXME this lineno - 2 hack means we need a proper look-behind cursor
|
509
|
-
warn
|
606
|
+
warn %(asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: no callouts refer to list item #{block.items.size})
|
510
607
|
end
|
511
608
|
end
|
512
|
-
end while reader.has_more_lines? && match = reader.peek_line
|
609
|
+
end while reader.has_more_lines? && (match = CalloutListRx.match(reader.peek_line))
|
513
610
|
|
514
611
|
document.callouts.next_list
|
515
612
|
break
|
516
613
|
|
517
|
-
elsif
|
614
|
+
elsif UnorderedListRx =~ this_line
|
518
615
|
reader.unshift_line this_line
|
519
616
|
block = next_outline_list(reader, :ulist, parent)
|
520
617
|
break
|
521
618
|
|
522
|
-
elsif (match =
|
619
|
+
elsif (match = OrderedListRx.match(this_line))
|
523
620
|
reader.unshift_line this_line
|
524
621
|
block = next_outline_list(reader, :olist, parent)
|
525
622
|
# QUESTION move this logic to next_outline_list?
|
526
623
|
if !attributes['style'] && !block.attributes['style']
|
527
|
-
marker = block.items.
|
624
|
+
marker = block.items[0].marker
|
528
625
|
if marker.start_with? '.'
|
529
626
|
# first one makes more sense, but second one is AsciiDoc-compliant
|
530
|
-
#attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES
|
531
|
-
attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES
|
627
|
+
#attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES[0]).to_s
|
628
|
+
attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES[0]).to_s
|
532
629
|
else
|
533
|
-
style = ORDERED_LIST_STYLES.detect{|s|
|
534
|
-
attributes['style'] = (style || ORDERED_LIST_STYLES
|
630
|
+
style = ORDERED_LIST_STYLES.detect{|s| OrderedListMarkerRxMap[s] =~ marker }
|
631
|
+
attributes['style'] = (style || ORDERED_LIST_STYLES[0]).to_s
|
535
632
|
end
|
536
633
|
end
|
537
634
|
break
|
538
635
|
|
539
|
-
elsif (match =
|
636
|
+
elsif (match = DefinitionListRx.match(this_line))
|
540
637
|
reader.unshift_line this_line
|
541
638
|
block = next_labeled_list(reader, match, parent)
|
542
639
|
break
|
@@ -544,10 +641,11 @@ class Lexer
|
|
544
641
|
elsif (style == 'float' || style == 'discrete') &&
|
545
642
|
is_section_title?(this_line, (Compliance.underline_style_section_titles ? reader.peek_line(true) : nil))
|
546
643
|
reader.unshift_line this_line
|
547
|
-
float_id, float_title, float_level, _ = parse_section_title(reader, document)
|
644
|
+
float_id, float_reftext, float_title, float_level, _ = parse_section_title(reader, document)
|
645
|
+
attributes['reftext'] = float_reftext if float_reftext
|
548
646
|
float_id ||= attributes['id'] if attributes.has_key?('id')
|
549
647
|
block = Block.new(parent, :floating_title, :content_model => :empty)
|
550
|
-
if float_id.
|
648
|
+
if float_id.nil_or_empty?
|
551
649
|
# FIXME remove hack of creating throwaway Section to get at the generate_id method
|
552
650
|
tmp_sect = Section.new(parent)
|
553
651
|
tmp_sect.title = float_title
|
@@ -555,14 +653,13 @@ class Lexer
|
|
555
653
|
else
|
556
654
|
block.id = float_id
|
557
655
|
end
|
558
|
-
document.register(:ids, [block.id, float_title]) if block.id
|
559
656
|
block.level = float_level
|
560
657
|
block.title = float_title
|
561
658
|
break
|
562
659
|
|
563
660
|
# FIXME create another set for "passthrough" styles
|
564
661
|
# FIXME make this more DRY!
|
565
|
-
elsif
|
662
|
+
elsif style && style != 'normal'
|
566
663
|
if PARAGRAPH_STYLES.include?(style)
|
567
664
|
block_context = style.to_sym
|
568
665
|
cloaked_context = :paragraph
|
@@ -575,14 +672,14 @@ class Lexer
|
|
575
672
|
reader.unshift_line this_line
|
576
673
|
# advance to block parsing =>
|
577
674
|
break
|
578
|
-
elsif block_extensions && extensions.
|
675
|
+
elsif block_extensions && extensions.registered_for_block?(style, :paragraph)
|
579
676
|
block_context = style.to_sym
|
580
677
|
cloaked_context = :paragraph
|
581
678
|
reader.unshift_line this_line
|
582
679
|
# advance to block parsing =>
|
583
680
|
break
|
584
681
|
else
|
585
|
-
warn
|
682
|
+
warn %(asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for paragraph: #{style})
|
586
683
|
style = nil
|
587
684
|
# continue to process paragraph
|
588
685
|
end
|
@@ -591,7 +688,7 @@ class Lexer
|
|
591
688
|
break_at_list = (skipped == 0 && in_list)
|
592
689
|
|
593
690
|
# a literal paragraph is contiguous lines starting at least one space
|
594
|
-
if style != 'normal' && this_line
|
691
|
+
if style != 'normal' && LiteralParagraphRx =~ this_line
|
595
692
|
# So we need to actually include this one in the read_lines group
|
596
693
|
reader.unshift_line this_line
|
597
694
|
lines = reader.read_lines_until(
|
@@ -602,8 +699,8 @@ class Lexer
|
|
602
699
|
# and therefore we should not break at a list item
|
603
700
|
# (this won't stop breaking on item of same level since we've already parsed them out)
|
604
701
|
# QUESTION can we turn this block into a lambda or function call?
|
605
|
-
(break_at_list && line
|
606
|
-
(
|
702
|
+
(break_at_list && AnyListRx =~ line) ||
|
703
|
+
(Compliance.block_terminates_paragraph && (is_delimited_block?(line) || BlockAttributeLineRx =~ line))
|
607
704
|
}
|
608
705
|
|
609
706
|
reset_block_indent! lines
|
@@ -625,8 +722,8 @@ class Lexer
|
|
625
722
|
# and therefore we should not break at a list item
|
626
723
|
# (this won't stop breaking on item of same level since we've already parsed them out)
|
627
724
|
# QUESTION can we turn this block into a lambda or function call?
|
628
|
-
(break_at_list && line
|
629
|
-
(
|
725
|
+
(break_at_list && AnyListRx =~ line) ||
|
726
|
+
(Compliance.block_terminates_paragraph && (is_delimited_block?(line) || BlockAttributeLineRx =~ line))
|
630
727
|
}
|
631
728
|
|
632
729
|
# NOTE we need this logic because we've asked the reader to skip
|
@@ -635,59 +732,57 @@ class Lexer
|
|
635
732
|
if lines.empty?
|
636
733
|
# call advance since the reader preserved the last line
|
637
734
|
reader.advance
|
638
|
-
return
|
735
|
+
return
|
639
736
|
end
|
640
737
|
|
641
|
-
catalog_inline_anchors(lines.join, document)
|
738
|
+
catalog_inline_anchors(lines.join(EOL), document)
|
642
739
|
|
643
|
-
first_line = lines
|
644
|
-
if !text_only && (admonition_match =
|
740
|
+
first_line = lines[0]
|
741
|
+
if !text_only && (admonition_match = AdmonitionParagraphRx.match(first_line))
|
645
742
|
lines[0] = admonition_match.post_match.lstrip
|
646
743
|
attributes['style'] = admonition_match[1]
|
647
744
|
attributes['name'] = admonition_name = admonition_match[1].downcase
|
648
|
-
attributes['caption'] ||= document.attributes[
|
649
|
-
block = Block.new(parent, :admonition, :source => lines, :attributes => attributes)
|
745
|
+
attributes['caption'] ||= document.attributes[%(#{admonition_name}-caption)]
|
746
|
+
block = Block.new(parent, :admonition, :content_model => :simple, :source => lines, :attributes => attributes)
|
650
747
|
elsif !text_only && Compliance.markdown_syntax && first_line.start_with?('> ')
|
651
748
|
lines.map! {|line|
|
652
|
-
if line
|
653
|
-
line[2..-1]
|
654
|
-
elsif line.chomp == '>'
|
749
|
+
if line == '>'
|
655
750
|
line[1..-1]
|
751
|
+
elsif line.start_with? '> '
|
752
|
+
line[2..-1]
|
656
753
|
else
|
657
754
|
line
|
658
755
|
end
|
659
756
|
}
|
660
757
|
|
661
|
-
if lines.
|
758
|
+
if lines[-1].start_with? '-- '
|
662
759
|
attribution, citetitle = lines.pop[3..-1].split(', ', 2)
|
663
|
-
lines.pop while lines.
|
664
|
-
lines[-1] = lines.last.chomp
|
760
|
+
lines.pop while lines[-1].empty?
|
665
761
|
else
|
666
762
|
attribution, citetitle = nil
|
667
763
|
end
|
668
764
|
attributes['style'] = 'quote'
|
669
|
-
attributes['attribution'] = attribution
|
670
|
-
attributes['citetitle'] = citetitle
|
765
|
+
attributes['attribution'] = attribution if attribution
|
766
|
+
attributes['citetitle'] = citetitle if citetitle
|
671
767
|
# NOTE will only detect headings that are floating titles (not section titles)
|
672
768
|
# TODO could assume a floating title when inside a block context
|
673
769
|
# FIXME Reader needs to be created w/ line info
|
674
770
|
block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes)
|
675
771
|
elsif !text_only && lines.size > 1 && first_line.start_with?('"') &&
|
676
|
-
lines.
|
772
|
+
lines[-1].start_with?('-- ') && lines[-2].end_with?('"')
|
677
773
|
lines[0] = first_line[1..-1]
|
678
774
|
attribution, citetitle = lines.pop[3..-1].split(', ', 2)
|
679
|
-
lines.pop while lines.
|
680
|
-
|
775
|
+
lines.pop while lines[-1].empty?
|
776
|
+
# strip trailing quote
|
777
|
+
lines[-1] = lines[-1].chop
|
681
778
|
attributes['style'] = 'quote'
|
682
|
-
attributes['attribution'] = attribution
|
683
|
-
attributes['citetitle'] = citetitle
|
684
|
-
block = Block.new(parent, :quote, :source => lines, :attributes => attributes)
|
685
|
-
#block = Block.new(parent, :quote, :content_model => :compound, :attributes => attributes)
|
686
|
-
#block << Block.new(block, :paragraph, :source => lines)
|
779
|
+
attributes['attribution'] = attribution if attribution
|
780
|
+
attributes['citetitle'] = citetitle if citetitle
|
781
|
+
block = Block.new(parent, :quote, :content_model => :simple, :source => lines, :attributes => attributes)
|
687
782
|
else
|
688
783
|
# if [normal] is used over an indented paragraph, unindent it
|
689
|
-
if style == 'normal' && ((first_char = lines
|
690
|
-
first_line = lines
|
784
|
+
if style == 'normal' && ((first_char = lines[0].chr) == ' ' || first_char == TAB)
|
785
|
+
first_line = lines[0]
|
691
786
|
first_line_shifted = first_line.lstrip
|
692
787
|
indent = line_length(first_line) - line_length(first_line_shifted)
|
693
788
|
lines[0] = first_line_shifted
|
@@ -697,7 +792,7 @@ class Lexer
|
|
697
792
|
end
|
698
793
|
end
|
699
794
|
|
700
|
-
block = Block.new(parent, :paragraph, :source => lines, :attributes => attributes)
|
795
|
+
block = Block.new(parent, :paragraph, :content_model => :simple, :source => lines, :attributes => attributes)
|
701
796
|
end
|
702
797
|
end
|
703
798
|
|
@@ -707,7 +802,7 @@ class Lexer
|
|
707
802
|
end
|
708
803
|
|
709
804
|
# either delimited block or styled paragraph
|
710
|
-
if block
|
805
|
+
if !block && block_context
|
711
806
|
# abstract and partintro should be handled by open block
|
712
807
|
# FIXME kind of hackish...need to sort out how to generalize this
|
713
808
|
block_context = :open if block_context == :abstract || block_context == :partintro
|
@@ -715,29 +810,36 @@ class Lexer
|
|
715
810
|
case block_context
|
716
811
|
when :admonition
|
717
812
|
attributes['name'] = admonition_name = style.downcase
|
718
|
-
attributes['caption'] ||= document.attributes[
|
813
|
+
attributes['caption'] ||= document.attributes[%(#{admonition_name}-caption)]
|
719
814
|
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
720
815
|
|
721
816
|
when :comment
|
722
817
|
build_block(block_context, :skip, terminator, parent, reader, attributes)
|
723
|
-
return
|
818
|
+
return
|
724
819
|
|
725
820
|
when :example
|
726
|
-
block = build_block(block_context, :compound, terminator, parent, reader, attributes
|
821
|
+
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
727
822
|
|
728
823
|
when :listing, :fenced_code, :source
|
729
824
|
if block_context == :fenced_code
|
730
825
|
style = attributes['style'] = 'source'
|
731
|
-
language, linenums = this_line[3
|
826
|
+
language, linenums = this_line[3..-1].split(',', 2)
|
732
827
|
if language && !(language = language.strip).empty?
|
733
828
|
attributes['language'] = language
|
734
829
|
attributes['linenums'] = '' if linenums && !linenums.strip.empty?
|
830
|
+
elsif (default_language = document.attributes['source-language'])
|
831
|
+
attributes['language'] = default_language
|
735
832
|
end
|
736
833
|
terminator = terminator[0..2]
|
737
834
|
elsif block_context == :source
|
738
835
|
AttributeList.rekey(attributes, [nil, 'language', 'linenums'])
|
836
|
+
unless attributes.has_key? 'language'
|
837
|
+
if (default_language = document.attributes['source-language'])
|
838
|
+
attributes['language'] = default_language
|
839
|
+
end
|
840
|
+
end
|
739
841
|
end
|
740
|
-
block = build_block(:listing, :verbatim, terminator, parent, reader, attributes
|
842
|
+
block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
|
741
843
|
|
742
844
|
when :literal
|
743
845
|
block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
|
@@ -745,13 +847,25 @@ class Lexer
|
|
745
847
|
when :pass
|
746
848
|
block = build_block(block_context, :raw, terminator, parent, reader, attributes)
|
747
849
|
|
850
|
+
when :stem, :latexmath, :asciimath
|
851
|
+
if block_context == :stem
|
852
|
+
attributes['style'] = if (explicit_stem_syntax = attributes[2])
|
853
|
+
explicit_stem_syntax.include?('tex') ? 'latexmath' : 'asciimath'
|
854
|
+
elsif (default_stem_syntax = document.attributes['stem']).nil_or_empty?
|
855
|
+
'asciimath'
|
856
|
+
else
|
857
|
+
default_stem_syntax
|
858
|
+
end
|
859
|
+
end
|
860
|
+
block = build_block(:stem, :raw, terminator, parent, reader, attributes)
|
861
|
+
|
748
862
|
when :open, :sidebar
|
749
863
|
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
750
864
|
|
751
865
|
when :table
|
752
866
|
cursor = reader.cursor
|
753
867
|
block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_line_comments => true), cursor
|
754
|
-
case terminator
|
868
|
+
case terminator.chr
|
755
869
|
when ','
|
756
870
|
attributes['format'] = 'csv'
|
757
871
|
when ':'
|
@@ -764,22 +878,24 @@ class Lexer
|
|
764
878
|
block = build_block(block_context, (block_context == :verse ? :verbatim : :compound), terminator, parent, reader, attributes)
|
765
879
|
|
766
880
|
else
|
767
|
-
if block_extensions && extensions.
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
if !(pos_attrs = processor.options.fetch(:pos_attrs, [])).empty?
|
881
|
+
if block_extensions && (extension = extensions.registered_for_block?(block_context, cloaked_context))
|
882
|
+
# TODO pass cloaked_context to extension somehow (perhaps a new instance for each cloaked_context?)
|
883
|
+
if (content_model = extension.config[:content_model]) != :skip
|
884
|
+
if !(pos_attrs = extension.config[:pos_attrs] || []).empty?
|
772
885
|
AttributeList.rekey(attributes, [nil].concat(pos_attrs))
|
773
886
|
end
|
774
|
-
if
|
887
|
+
if (default_attrs = extension.config[:default_attrs])
|
775
888
|
default_attrs.each {|k, v| attributes[k] ||= v }
|
776
889
|
end
|
777
890
|
end
|
778
|
-
block = build_block
|
779
|
-
|
891
|
+
block = build_block block_context, content_model, terminator, parent, reader, attributes, :extension => extension
|
892
|
+
unless block && content_model != :skip
|
893
|
+
attributes.clear
|
894
|
+
return
|
895
|
+
end
|
780
896
|
else
|
781
897
|
# this should only happen if there's a misconfiguration
|
782
|
-
raise
|
898
|
+
raise %(Unsupported block type #{block_context} at #{reader.line_info})
|
783
899
|
end
|
784
900
|
end
|
785
901
|
end
|
@@ -789,20 +905,38 @@ class Lexer
|
|
789
905
|
# blocks or trailing attribute lists could leave us without a block,
|
790
906
|
# so handle accordingly
|
791
907
|
# REVIEW we may no longer need this nil check
|
792
|
-
|
908
|
+
# FIXME we've got to clean this up, it's horrible!
|
909
|
+
if block
|
910
|
+
block.source_location = source_location if source_location
|
793
911
|
# REVIEW seems like there is a better way to organize this wrap-up
|
794
|
-
block.id ||= attributes['id'] if attributes.has_key?('id')
|
795
912
|
block.title = attributes['title'] unless block.title?
|
796
|
-
|
913
|
+
# FIXME HACK don't hardcode logic for alt, caption and scaledwidth on images down here
|
914
|
+
if block.context == :image
|
915
|
+
resolved_target = attributes['target']
|
916
|
+
block.document.register(:images, resolved_target)
|
917
|
+
attributes['alt'] ||= ::File.basename(resolved_target, ::File.extname(resolved_target)).tr('_-', ' ')
|
918
|
+
attributes['alt'] = block.sub_specialcharacters attributes['alt']
|
919
|
+
block.assign_caption attributes.delete('caption'), 'figure'
|
920
|
+
if (scaledwidth = attributes['scaledwidth'])
|
921
|
+
# append % to scaledwidth if ends in number (no units present)
|
922
|
+
if (48..57).include?((scaledwidth[-1] || 0).ord)
|
923
|
+
attributes['scaledwidth'] = %(#{scaledwidth}%)
|
924
|
+
end
|
925
|
+
end
|
926
|
+
else
|
927
|
+
block.caption ||= attributes.delete('caption')
|
928
|
+
end
|
797
929
|
# TODO eventualy remove the style attribute from the attributes hash
|
798
930
|
#block.style = attributes.delete('style')
|
799
931
|
block.style = attributes['style']
|
800
932
|
# AsciiDoc always use [id] as the reftext in HTML output,
|
801
933
|
# but I'd like to do better in Asciidoctor
|
802
|
-
if
|
803
|
-
|
934
|
+
if (block_id = (block.id ||= attributes['id']))
|
935
|
+
# TODO sub reftext
|
936
|
+
document.register(:ids, [block_id, (attributes['reftext'] || (block.title? ? block.title : nil))])
|
804
937
|
end
|
805
|
-
|
938
|
+
# FIXME remove the need for this update!
|
939
|
+
block.attributes.update(attributes) unless attributes.empty?
|
806
940
|
block.lock_in_subs
|
807
941
|
|
808
942
|
#if document.attributes.has_key? :pending_attribute_entries
|
@@ -812,7 +946,7 @@ class Lexer
|
|
812
946
|
#end
|
813
947
|
|
814
948
|
if block.sub? :callouts
|
815
|
-
|
949
|
+
unless (catalog_callouts block.source, document)
|
816
950
|
# No need to look for callouts if they aren't there
|
817
951
|
block.remove_sub :callouts
|
818
952
|
end
|
@@ -827,17 +961,14 @@ class Lexer
|
|
827
961
|
# returns the match data if this line is the first line of a delimited block or nil if not
|
828
962
|
def self.is_delimited_block? line, return_match_data = false
|
829
963
|
# highly optimized for best performance
|
830
|
-
line_len = line.length
|
831
|
-
|
832
|
-
line = line.chomp
|
833
|
-
# counts endline character in line length
|
964
|
+
return unless (line_len = line.length) > 1 && (DELIMITED_BLOCK_LEADERS.include? line[0..1])
|
965
|
+
# catches open block
|
834
966
|
if line_len == 2
|
835
967
|
tip = line
|
836
968
|
tl = 2
|
837
|
-
elsif line_len < 3
|
838
|
-
return nil
|
839
969
|
else
|
840
|
-
|
970
|
+
# catches all other delimited blocks, including fenced code
|
971
|
+
if line_len <= 4
|
841
972
|
tip = line
|
842
973
|
tl = line_len
|
843
974
|
else
|
@@ -846,27 +977,27 @@ class Lexer
|
|
846
977
|
end
|
847
978
|
|
848
979
|
# special case for fenced code blocks
|
980
|
+
# REVIEW review this logic
|
981
|
+
fenced_code = false
|
849
982
|
if Compliance.markdown_syntax
|
850
|
-
|
851
|
-
if
|
852
|
-
if tip.end_with?
|
853
|
-
return
|
854
|
-
end
|
855
|
-
tip = tip_alt
|
856
|
-
tl = 3
|
857
|
-
elsif tip_alt == '~~~'
|
858
|
-
if tip.end_with? '~'
|
859
|
-
return nil
|
983
|
+
tip_3 = (tl == 4 ? tip.chop : tip)
|
984
|
+
if tip_3 == '```'
|
985
|
+
if tl == 4 && tip.end_with?('`')
|
986
|
+
return
|
860
987
|
end
|
861
|
-
tip =
|
988
|
+
tip = tip_3
|
862
989
|
tl = 3
|
990
|
+
fenced_code = true
|
863
991
|
end
|
864
992
|
end
|
993
|
+
|
994
|
+
# short circuit if not a fenced code block
|
995
|
+
return if tl == 3 && !fenced_code
|
865
996
|
end
|
866
997
|
|
867
998
|
if DELIMITED_BLOCKS.has_key? tip
|
868
999
|
# tip is the full line when delimiter is minimum length
|
869
|
-
if tl
|
1000
|
+
if tl < 4 || tl == line_len
|
870
1001
|
if return_match_data
|
871
1002
|
context, masq = *DELIMITED_BLOCKS[tip]
|
872
1003
|
BlockMatchData.new(context, masq, tip, tip)
|
@@ -880,7 +1011,8 @@ class Lexer
|
|
880
1011
|
else
|
881
1012
|
true
|
882
1013
|
end
|
883
|
-
#
|
1014
|
+
# only enable if/when we decide to support non-congruent block delimiters
|
1015
|
+
#elsif (match = BlockDelimiterRx.match(line))
|
884
1016
|
# if return_match_data
|
885
1017
|
# context, masq = *DELIMITED_BLOCKS[tip]
|
886
1018
|
# BlockMatchData.new(context, masq, tip, match[0])
|
@@ -918,13 +1050,13 @@ class Lexer
|
|
918
1050
|
:preserve_last_line => true,
|
919
1051
|
:skip_line_comments => true,
|
920
1052
|
:skip_processing => skip_processing) {|line|
|
921
|
-
|
1053
|
+
Compliance.block_terminates_paragraph && (is_delimited_block?(line) || BlockAttributeLineRx =~ line)
|
922
1054
|
}
|
923
1055
|
# QUESTION check for empty lines after grabbing lines for simple content model?
|
924
1056
|
end
|
925
1057
|
block_reader = nil
|
926
1058
|
elsif parse_as_content_model != :compound
|
927
|
-
lines = reader.read_lines_until(:terminator => terminator, :
|
1059
|
+
lines = reader.read_lines_until(:terminator => terminator, :skip_processing => skip_processing)
|
928
1060
|
block_reader = nil
|
929
1061
|
# terminator is false when reader has already been prepared
|
930
1062
|
elsif terminator == false
|
@@ -938,24 +1070,37 @@ class Lexer
|
|
938
1070
|
|
939
1071
|
if content_model == :skip
|
940
1072
|
attributes.clear
|
1073
|
+
# FIXME we shouldn't be mixing return types
|
941
1074
|
return lines
|
942
1075
|
end
|
943
1076
|
|
944
|
-
if content_model == :verbatim && attributes
|
945
|
-
reset_block_indent! lines,
|
1077
|
+
if content_model == :verbatim && (indent = attributes['indent'])
|
1078
|
+
reset_block_indent! lines, indent.to_i
|
946
1079
|
end
|
947
1080
|
|
948
|
-
if (
|
1081
|
+
if (extension = options[:extension])
|
1082
|
+
# QUESTION do we want to delete the style?
|
949
1083
|
attributes.delete('style')
|
950
|
-
|
951
|
-
|
1084
|
+
if (block = extension.process_method[parent, block_reader || (Reader.new lines), attributes.dup])
|
1085
|
+
attributes.replace block.attributes
|
1086
|
+
# FIXME if the content model is set to compound, but we only have simple in this context, then
|
1087
|
+
# forcefully set the content_model to simple to prevent parsing blocks from children
|
1088
|
+
# TODO document this behavior!!
|
1089
|
+
if block.content_model == :compound && !(lines = block.lines).nil_or_empty?
|
1090
|
+
content_model = :compound
|
1091
|
+
block_reader = Reader.new lines
|
1092
|
+
end
|
1093
|
+
else
|
1094
|
+
# FIXME need a test to verify this returns nil at the right time
|
1095
|
+
return
|
1096
|
+
end
|
952
1097
|
else
|
953
|
-
block = Block.new(parent, block_context, :content_model => content_model, :
|
1098
|
+
block = Block.new(parent, block_context, :content_model => content_model, :source => lines, :attributes => attributes)
|
954
1099
|
end
|
955
1100
|
|
956
|
-
# should
|
957
|
-
if
|
958
|
-
block.title = attributes.delete
|
1101
|
+
# QUESTION should we have an explicit map or can we rely on check for *-caption attribute?
|
1102
|
+
if (attributes.has_key? 'title') && (block.document.attr? %(#{block.context}-caption))
|
1103
|
+
block.title = attributes.delete 'title'
|
959
1104
|
block.assign_caption attributes.delete('caption')
|
960
1105
|
end
|
961
1106
|
|
@@ -970,7 +1115,7 @@ class Lexer
|
|
970
1115
|
|
971
1116
|
# Public: Parse blocks from this reader until there are no more lines.
|
972
1117
|
#
|
973
|
-
# This method calls
|
1118
|
+
# This method calls Parser#next_block until there are no more lines in the
|
974
1119
|
# Reader. It does not consider sections because it's assumed the Reader only
|
975
1120
|
# has lines which are within a delimited block region.
|
976
1121
|
#
|
@@ -980,8 +1125,8 @@ class Lexer
|
|
980
1125
|
# Returns nothing.
|
981
1126
|
def self.parse_blocks(reader, parent)
|
982
1127
|
while reader.has_more_lines?
|
983
|
-
block =
|
984
|
-
parent << block
|
1128
|
+
block = Parser.next_block(reader, parent)
|
1129
|
+
parent << block if block
|
985
1130
|
end
|
986
1131
|
end
|
987
1132
|
|
@@ -1001,18 +1146,18 @@ class Lexer
|
|
1001
1146
|
end
|
1002
1147
|
#Debug.debug { "Created #{list_type} block: #{list_block}" }
|
1003
1148
|
|
1004
|
-
while reader.has_more_lines? && (match = reader.peek_line
|
1149
|
+
while reader.has_more_lines? && (match = ListRxMap[list_type].match(reader.peek_line))
|
1005
1150
|
marker = resolve_list_marker(list_type, match[1])
|
1006
1151
|
|
1007
1152
|
# if we are moving to the next item, and the marker is different
|
1008
1153
|
# determine if we are moving up or down in nesting
|
1009
|
-
if list_block.items? && marker != list_block.items.
|
1154
|
+
if list_block.items? && marker != list_block.items[0].marker
|
1010
1155
|
# assume list is nested by default, but then check to see if we are
|
1011
1156
|
# popping out of a nested list by matching an ancestor's list marker
|
1012
1157
|
this_item_level = list_block.level + 1
|
1013
1158
|
ancestor = parent
|
1014
1159
|
while ancestor.context == list_type
|
1015
|
-
if marker == ancestor.items.
|
1160
|
+
if marker == ancestor.items[0].marker
|
1016
1161
|
this_item_level = ancestor.level
|
1017
1162
|
break
|
1018
1163
|
end
|
@@ -1030,10 +1175,10 @@ class Lexer
|
|
1030
1175
|
elsif this_item_level > list_block.level
|
1031
1176
|
# If this next list level is down one from the
|
1032
1177
|
# current Block's, append it to content of the current list item
|
1033
|
-
list_block.items
|
1178
|
+
list_block.items[-1] << next_block(reader, list_block)
|
1034
1179
|
end
|
1035
1180
|
|
1036
|
-
list_block << list_item
|
1181
|
+
list_block << list_item if list_item
|
1037
1182
|
list_item = nil
|
1038
1183
|
|
1039
1184
|
reader.skip_blank_lines
|
@@ -1051,10 +1196,10 @@ class Lexer
|
|
1051
1196
|
def self.catalog_callouts(text, document)
|
1052
1197
|
found = false
|
1053
1198
|
if text.include? '<'
|
1054
|
-
text.scan(
|
1199
|
+
text.scan(CalloutQuickScanRx) {
|
1055
1200
|
# alias match for Ruby 1.8.7 compat
|
1056
1201
|
m = $~
|
1057
|
-
if m[0]
|
1202
|
+
if m[0].chr != '\\'
|
1058
1203
|
document.callouts.register(m[2])
|
1059
1204
|
end
|
1060
1205
|
# we have to mark as found even if it's escaped so it can be unescaped
|
@@ -1071,17 +1216,21 @@ class Lexer
|
|
1071
1216
|
#
|
1072
1217
|
# Returns nothing
|
1073
1218
|
def self.catalog_inline_anchors(text, document)
|
1074
|
-
text.
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1219
|
+
if text.include? '['
|
1220
|
+
text.scan(InlineAnchorRx) {
|
1221
|
+
# alias match for Ruby 1.8.7 compat
|
1222
|
+
m = $~
|
1223
|
+
next if m[0].start_with? '\\'
|
1224
|
+
id = m[1] || m[3]
|
1225
|
+
reftext = m[2] || m[4]
|
1226
|
+
# enable if we want to allow double quoted values
|
1227
|
+
#id = id.sub(DoubleQuotedRx, '\2')
|
1228
|
+
#if reftext
|
1229
|
+
# reftext = reftext.sub(DoubleQuotedMultiRx, '\2')
|
1230
|
+
#end
|
1231
|
+
document.register(:ids, [id, reftext])
|
1232
|
+
}
|
1233
|
+
end
|
1085
1234
|
nil
|
1086
1235
|
end
|
1087
1236
|
|
@@ -1097,11 +1246,11 @@ class Lexer
|
|
1097
1246
|
previous_pair = nil
|
1098
1247
|
# allows us to capture until we find a labeled item
|
1099
1248
|
# that uses the same delimiter (::, :::, :::: or ;;)
|
1100
|
-
sibling_pattern =
|
1249
|
+
sibling_pattern = DefinitionListSiblingRx[match[2]]
|
1101
1250
|
|
1102
1251
|
begin
|
1103
1252
|
term, item = next_list_item(reader, list_block, match, sibling_pattern)
|
1104
|
-
if
|
1253
|
+
if previous_pair && !previous_pair[-1]
|
1105
1254
|
previous_pair.pop
|
1106
1255
|
previous_pair[0] << term
|
1107
1256
|
previous_pair << item
|
@@ -1109,7 +1258,7 @@ class Lexer
|
|
1109
1258
|
# FIXME this misses the automatic parent assignment
|
1110
1259
|
list_block.items << (previous_pair = [[term], item])
|
1111
1260
|
end
|
1112
|
-
end while reader.has_more_lines? && match = reader.peek_line
|
1261
|
+
end while reader.has_more_lines? && (match = sibling_pattern.match(reader.peek_line))
|
1113
1262
|
|
1114
1263
|
list_block
|
1115
1264
|
end
|
@@ -1132,22 +1281,20 @@ class Lexer
|
|
1132
1281
|
# Returns the next ListItem or ListItem pair (depending on the list type)
|
1133
1282
|
# for the parent list Block.
|
1134
1283
|
def self.next_list_item(reader, list_block, match, sibling_trait = nil)
|
1135
|
-
list_type = list_block.context
|
1136
|
-
|
1137
|
-
if list_type == :dlist
|
1284
|
+
if (list_type = list_block.context) == :dlist
|
1138
1285
|
list_term = ListItem.new(list_block, match[1])
|
1139
1286
|
list_item = ListItem.new(list_block, match[3])
|
1140
|
-
has_text = !match[3].
|
1287
|
+
has_text = !match[3].nil_or_empty?
|
1141
1288
|
else
|
1142
1289
|
# Create list item using first line as the text of the list item
|
1143
1290
|
text = match[2]
|
1144
1291
|
checkbox = false
|
1145
1292
|
if list_type == :ulist && text.start_with?('[')
|
1146
|
-
if text.start_with?
|
1293
|
+
if text.start_with?('[ ] ')
|
1147
1294
|
checkbox = true
|
1148
1295
|
checked = false
|
1149
1296
|
text = text[3..-1].lstrip
|
1150
|
-
elsif text.start_with?('[
|
1297
|
+
elsif text.start_with?('[x] ') || text.start_with?('[*] ')
|
1151
1298
|
checkbox = true
|
1152
1299
|
checked = true
|
1153
1300
|
text = text[3..-1].lstrip
|
@@ -1162,9 +1309,7 @@ class Lexer
|
|
1162
1309
|
list_item.attributes['checked'] = '' if checked
|
1163
1310
|
end
|
1164
1311
|
|
1165
|
-
|
1166
|
-
sibling_trait = resolve_list_marker(list_type, match[1], list_block.items.size, true, reader)
|
1167
|
-
end
|
1312
|
+
sibling_trait ||= resolve_list_marker(list_type, match[1], list_block.items.size, true, reader)
|
1168
1313
|
list_item.marker = sibling_trait
|
1169
1314
|
has_text = true
|
1170
1315
|
end
|
@@ -1179,13 +1324,13 @@ class Lexer
|
|
1179
1324
|
list_item_reader.unshift_lines comment_lines unless comment_lines.empty?
|
1180
1325
|
|
1181
1326
|
if !subsequent_line.nil?
|
1182
|
-
continuation_connects_first_block =
|
1327
|
+
continuation_connects_first_block = subsequent_line.empty?
|
1183
1328
|
# if there's no continuation connecting the first block, then
|
1184
1329
|
# treat the lines as paragraph text (activated when has_text = false)
|
1185
1330
|
if !continuation_connects_first_block && list_type != :dlist
|
1186
1331
|
has_text = false
|
1187
1332
|
end
|
1188
|
-
content_adjacent = !subsequent_line.
|
1333
|
+
content_adjacent = !continuation_connects_first_block && !subsequent_line.empty?
|
1189
1334
|
else
|
1190
1335
|
continuation_connects_first_block = false
|
1191
1336
|
content_adjacent = false
|
@@ -1199,7 +1344,7 @@ class Lexer
|
|
1199
1344
|
# list
|
1200
1345
|
while list_item_reader.has_more_lines?
|
1201
1346
|
new_block = next_block(list_item_reader, list_block, {}, options)
|
1202
|
-
list_item << new_block
|
1347
|
+
list_item << new_block if new_block
|
1203
1348
|
end
|
1204
1349
|
|
1205
1350
|
list_item.fold_first(continuation_connects_first_block, content_adjacent)
|
@@ -1255,17 +1400,17 @@ class Lexer
|
|
1255
1400
|
# the termination of the list
|
1256
1401
|
break if is_sibling_list_item?(this_line, list_type, sibling_trait)
|
1257
1402
|
|
1258
|
-
prev_line = buffer.empty? ? nil : buffer
|
1403
|
+
prev_line = buffer.empty? ? nil : buffer[-1]
|
1259
1404
|
|
1260
1405
|
if prev_line == LIST_CONTINUATION
|
1261
1406
|
if continuation == :inactive
|
1262
1407
|
continuation = :active
|
1263
1408
|
has_text = true
|
1264
|
-
buffer[-1] =
|
1409
|
+
buffer[-1] = '' unless within_nested_list
|
1265
1410
|
end
|
1266
1411
|
|
1267
1412
|
# dealing with adjacent list continuations (which is really a syntax error)
|
1268
|
-
if this_line
|
1413
|
+
if this_line == LIST_CONTINUATION
|
1269
1414
|
if continuation != :frozen
|
1270
1415
|
continuation = :frozen
|
1271
1416
|
buffer << this_line
|
@@ -1277,7 +1422,7 @@ class Lexer
|
|
1277
1422
|
|
1278
1423
|
# a delimited block immediately breaks the list unless preceded
|
1279
1424
|
# by a list continuation (they are harsh like that ;0)
|
1280
|
-
if match = is_delimited_block?(this_line, true)
|
1425
|
+
if (match = is_delimited_block?(this_line, true))
|
1281
1426
|
if continuation == :active
|
1282
1427
|
buffer << this_line
|
1283
1428
|
# grab all the lines in the block, leaving the delimiters in place
|
@@ -1287,17 +1432,18 @@ class Lexer
|
|
1287
1432
|
else
|
1288
1433
|
break
|
1289
1434
|
end
|
1290
|
-
# technically
|
1291
|
-
# which really means
|
1292
|
-
|
1435
|
+
# technically BlockAttributeLineRx only breaks if ensuing line is not a list item
|
1436
|
+
# which really means BlockAttributeLineRx only breaks if it's acting as a block delimiter
|
1437
|
+
# FIXME to be AsciiDoc compliant, we shouldn't break if style in attribute line is "literal" (i.e., [literal])
|
1438
|
+
elsif list_type == :dlist && continuation != :active && BlockAttributeLineRx =~ this_line
|
1293
1439
|
break
|
1294
1440
|
else
|
1295
|
-
if continuation == :active && !this_line.
|
1441
|
+
if continuation == :active && !this_line.empty?
|
1296
1442
|
# literal paragraphs have special considerations (and this is one of
|
1297
1443
|
# two entry points into one)
|
1298
1444
|
# if we don't process it as a whole, then a line in it that looks like a
|
1299
1445
|
# list item will throw off the exit from it
|
1300
|
-
if this_line
|
1446
|
+
if LiteralParagraphRx =~ this_line
|
1301
1447
|
reader.unshift_line this_line
|
1302
1448
|
buffer.concat reader.read_lines_until(
|
1303
1449
|
:preserve_last_line => true,
|
@@ -1309,12 +1455,12 @@ class Lexer
|
|
1309
1455
|
}
|
1310
1456
|
continuation = :inactive
|
1311
1457
|
# let block metadata play out until we find the block
|
1312
|
-
elsif this_line
|
1458
|
+
elsif BlockTitleRx =~ this_line || BlockAttributeLineRx =~ this_line || AttributeEntryRx =~ this_line
|
1313
1459
|
buffer << this_line
|
1314
1460
|
else
|
1315
|
-
if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).detect {|ctx|
|
1461
|
+
if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).detect {|ctx| ListRxMap[ctx] =~ this_line }
|
1316
1462
|
within_nested_list = true
|
1317
|
-
if nested_list_type == :dlist && $~[3].
|
1463
|
+
if nested_list_type == :dlist && $~[3].nil_or_empty?
|
1318
1464
|
# get greedy again
|
1319
1465
|
has_text = false
|
1320
1466
|
end
|
@@ -1322,16 +1468,16 @@ class Lexer
|
|
1322
1468
|
buffer << this_line
|
1323
1469
|
continuation = :inactive
|
1324
1470
|
end
|
1325
|
-
elsif !prev_line.nil? && prev_line.
|
1471
|
+
elsif !prev_line.nil? && prev_line.empty?
|
1326
1472
|
# advance to the next line of content
|
1327
|
-
if this_line.
|
1473
|
+
if this_line.empty?
|
1328
1474
|
reader.skip_blank_lines
|
1329
1475
|
this_line = reader.read_line
|
1330
1476
|
# if we hit eof or a sibling, stop reading
|
1331
1477
|
break if this_line.nil? || is_sibling_list_item?(this_line, list_type, sibling_trait)
|
1332
1478
|
end
|
1333
1479
|
|
1334
|
-
if this_line
|
1480
|
+
if this_line == LIST_CONTINUATION
|
1335
1481
|
detached_continuation = buffer.size
|
1336
1482
|
buffer << this_line
|
1337
1483
|
else
|
@@ -1339,8 +1485,19 @@ class Lexer
|
|
1339
1485
|
# for all other lists, has_text is always true
|
1340
1486
|
# in this block, we have to see whether we stay in the list
|
1341
1487
|
if has_text
|
1488
|
+
# TODO any way to combine this with the check after skipping blank lines?
|
1489
|
+
if is_sibling_list_item?(this_line, list_type, sibling_trait)
|
1490
|
+
break
|
1491
|
+
elsif nested_list_type = NESTABLE_LIST_CONTEXTS.detect {|ctx| ListRxMap[ctx] =~ this_line }
|
1492
|
+
buffer << this_line
|
1493
|
+
within_nested_list = true
|
1494
|
+
if nested_list_type == :dlist && $~[3].nil_or_empty?
|
1495
|
+
# get greedy again
|
1496
|
+
has_text = false
|
1497
|
+
end
|
1342
1498
|
# slurp up any literal paragraph offset by blank lines
|
1343
|
-
|
1499
|
+
# NOTE we have to check for indented list items first
|
1500
|
+
elsif LiteralParagraphRx =~ this_line
|
1344
1501
|
reader.unshift_line this_line
|
1345
1502
|
buffer.concat reader.read_lines_until(
|
1346
1503
|
:preserve_last_line => true,
|
@@ -1350,16 +1507,6 @@ class Lexer
|
|
1350
1507
|
# so we need to make sure we don't slurp up a legitimate sibling
|
1351
1508
|
list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
|
1352
1509
|
}
|
1353
|
-
# TODO any way to combine this with the check after skipping blank lines?
|
1354
|
-
elsif is_sibling_list_item?(this_line, list_type, sibling_trait)
|
1355
|
-
break
|
1356
|
-
elsif nested_list_type = NESTABLE_LIST_CONTEXTS.detect {|ctx| this_line.match(REGEXP[ctx]) }
|
1357
|
-
buffer << this_line
|
1358
|
-
within_nested_list = true
|
1359
|
-
if nested_list_type == :dlist && $~[3].to_s.empty?
|
1360
|
-
# get greedy again
|
1361
|
-
has_text = false
|
1362
|
-
end
|
1363
1510
|
else
|
1364
1511
|
break
|
1365
1512
|
end
|
@@ -1371,10 +1518,10 @@ class Lexer
|
|
1371
1518
|
end
|
1372
1519
|
end
|
1373
1520
|
else
|
1374
|
-
has_text = true if !this_line.
|
1375
|
-
if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).detect {|ctx|
|
1521
|
+
has_text = true if !this_line.empty?
|
1522
|
+
if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).detect {|ctx| ListRxMap[ctx] =~ this_line }
|
1376
1523
|
within_nested_list = true
|
1377
|
-
if nested_list_type == :dlist && $~[3].
|
1524
|
+
if nested_list_type == :dlist && $~[3].nil_or_empty?
|
1378
1525
|
# get greedy again
|
1379
1526
|
has_text = false
|
1380
1527
|
end
|
@@ -1385,23 +1532,21 @@ class Lexer
|
|
1385
1532
|
this_line = nil
|
1386
1533
|
end
|
1387
1534
|
|
1388
|
-
reader.unshift_line this_line if
|
1535
|
+
reader.unshift_line this_line if this_line
|
1389
1536
|
|
1390
1537
|
if detached_continuation
|
1391
1538
|
buffer.delete_at detached_continuation
|
1392
1539
|
end
|
1393
1540
|
|
1394
1541
|
# strip trailing blank lines to prevent empty blocks
|
1395
|
-
buffer.pop while !buffer.empty? && buffer.
|
1542
|
+
buffer.pop while !buffer.empty? && buffer[-1].empty?
|
1396
1543
|
|
1397
1544
|
# We do need to replace the optional trailing continuation
|
1398
1545
|
# a blank line would have served the same purpose in the document
|
1399
|
-
if !buffer.empty? && buffer
|
1400
|
-
buffer.pop
|
1401
|
-
end
|
1546
|
+
buffer.pop if !buffer.empty? && buffer[-1] == LIST_CONTINUATION
|
1402
1547
|
|
1403
|
-
#
|
1404
|
-
#
|
1548
|
+
#warn "BUFFER[#{list_type},#{sibling_trait}]>#{buffer * EOL}<BUFFER"
|
1549
|
+
#warn "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.inspect}<BUFFER"
|
1405
1550
|
|
1406
1551
|
buffer
|
1407
1552
|
end
|
@@ -1416,35 +1561,37 @@ class Lexer
|
|
1416
1561
|
# attributes - a Hash of attributes to assign to this section (default: {})
|
1417
1562
|
def self.initialize_section(reader, parent, attributes = {})
|
1418
1563
|
document = parent.document
|
1419
|
-
|
1420
|
-
|
1564
|
+
source_location = reader.cursor if document.sourcemap
|
1565
|
+
sect_id, sect_reftext, sect_title, sect_level, _ = parse_section_title(reader, document)
|
1566
|
+
attributes['reftext'] = sect_reftext if sect_reftext
|
1567
|
+
section = Section.new parent, sect_level, document.attributes.has_key?('sectnums')
|
1568
|
+
section.source_location = source_location if source_location
|
1421
1569
|
section.id = sect_id
|
1422
1570
|
section.title = sect_title
|
1423
1571
|
# parse style, id and role from first positional attribute
|
1424
1572
|
if attributes[1]
|
1425
|
-
|
1426
|
-
|
1427
|
-
|
1428
|
-
|
1429
|
-
section.
|
1430
|
-
|
1431
|
-
section.
|
1432
|
-
|
1433
|
-
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1437
|
-
section.
|
1438
|
-
Document::AttributeEntry.new('appendix-number', number).save_to(attributes)
|
1573
|
+
style, _ = parse_style_attribute attributes, reader
|
1574
|
+
# handle case where only id and/or role are given (e.g., #idname.rolename)
|
1575
|
+
if style
|
1576
|
+
section.sectname = style
|
1577
|
+
section.special = true
|
1578
|
+
# HACK needs to be refactored so it's driven by config
|
1579
|
+
if section.sectname == 'abstract' && document.doctype == 'book'
|
1580
|
+
section.sectname = 'sect1'
|
1581
|
+
section.special = false
|
1582
|
+
section.level = 1
|
1583
|
+
end
|
1584
|
+
else
|
1585
|
+
section.sectname = %(sect#{section.level})
|
1439
1586
|
end
|
1440
1587
|
elsif sect_title.downcase == 'synopsis' && document.doctype == 'manpage'
|
1441
1588
|
section.special = true
|
1442
1589
|
section.sectname = 'synopsis'
|
1443
1590
|
else
|
1444
|
-
section.sectname =
|
1591
|
+
section.sectname = %(sect#{section.level})
|
1445
1592
|
end
|
1446
1593
|
|
1447
|
-
if section.id
|
1594
|
+
if !section.id && (id = attributes['id'])
|
1448
1595
|
section.id = id
|
1449
1596
|
else
|
1450
1597
|
# generate an id if one was not *embedded* in the heading line
|
@@ -1453,7 +1600,8 @@ class Lexer
|
|
1453
1600
|
end
|
1454
1601
|
|
1455
1602
|
if section.id
|
1456
|
-
|
1603
|
+
# TODO sub reftext
|
1604
|
+
section.document.register(:ids, [section.id, (attributes['reftext'] || section.title)])
|
1457
1605
|
end
|
1458
1606
|
section.update_attributes(attributes)
|
1459
1607
|
reader.skip_blank_lines
|
@@ -1466,7 +1614,7 @@ class Lexer
|
|
1466
1614
|
#
|
1467
1615
|
# line - the String line from under the section title.
|
1468
1616
|
def self.section_level(line)
|
1469
|
-
SECTION_LEVELS[line
|
1617
|
+
SECTION_LEVELS[line.chr]
|
1470
1618
|
end
|
1471
1619
|
|
1472
1620
|
#--
|
@@ -1483,8 +1631,10 @@ class Lexer
|
|
1483
1631
|
# returns the section level if the Reader is positioned at a section title,
|
1484
1632
|
# false otherwise
|
1485
1633
|
def self.is_next_line_section?(reader, attributes)
|
1486
|
-
|
1487
|
-
|
1634
|
+
if !(val = attributes[1]).nil? && ((ord_0 = val[0].ord) == 100 || ord_0 == 102) && val =~ FloatingTitleStyleRx
|
1635
|
+
return false
|
1636
|
+
end
|
1637
|
+
return false unless reader.has_more_lines?
|
1488
1638
|
Compliance.underline_style_section_titles ? is_section_title?(*reader.peek_lines(2)) : is_section_title?(reader.peek_line)
|
1489
1639
|
end
|
1490
1640
|
|
@@ -1516,9 +1666,9 @@ class Lexer
|
|
1516
1666
|
end
|
1517
1667
|
|
1518
1668
|
def self.is_single_line_section_title?(line1)
|
1519
|
-
first_char = line1
|
1669
|
+
first_char = line1 ? line1.chr : nil
|
1520
1670
|
if (first_char == '=' || (Compliance.markdown_syntax && first_char == '#')) &&
|
1521
|
-
(match =
|
1671
|
+
(match = AtxSectionRx.match(line1))
|
1522
1672
|
single_line_section_level match[1]
|
1523
1673
|
else
|
1524
1674
|
false
|
@@ -1526,8 +1676,8 @@ class Lexer
|
|
1526
1676
|
end
|
1527
1677
|
|
1528
1678
|
def self.is_two_line_section_title?(line1, line2)
|
1529
|
-
if
|
1530
|
-
line2
|
1679
|
+
if line1 && line2 && SECTION_LEVELS.has_key?(line2.chr) &&
|
1680
|
+
line2 =~ SetextSectionLineRx && line1 =~ SetextSectionTitleRx &&
|
1531
1681
|
# chomp so that a (non-visible) endline does not impact calculation
|
1532
1682
|
(line_length(line1) - line_length(line2)).abs <= 1
|
1533
1683
|
section_level line2
|
@@ -1547,9 +1697,9 @@ class Lexer
|
|
1547
1697
|
# Examples
|
1548
1698
|
#
|
1549
1699
|
# reader.lines
|
1550
|
-
# # => ["Foo
|
1700
|
+
# # => ["Foo", "~~~"]
|
1551
1701
|
#
|
1552
|
-
# title, level,
|
1702
|
+
# id, reftext, title, level, single = parse_section_title(reader, document)
|
1553
1703
|
#
|
1554
1704
|
# title
|
1555
1705
|
# # => "Foo"
|
@@ -1561,9 +1711,9 @@ class Lexer
|
|
1561
1711
|
# # => false
|
1562
1712
|
#
|
1563
1713
|
# line1
|
1564
|
-
# # => "==== Foo
|
1714
|
+
# # => "==== Foo"
|
1565
1715
|
#
|
1566
|
-
# title, level,
|
1716
|
+
# id, reftext, title, level, single = parse_section_title(reader, document)
|
1567
1717
|
#
|
1568
1718
|
# title
|
1569
1719
|
# # => "Foo"
|
@@ -1574,8 +1724,8 @@ class Lexer
|
|
1574
1724
|
# single
|
1575
1725
|
# # => true
|
1576
1726
|
#
|
1577
|
-
# returns an Array of [String, Integer, String, Boolean], representing the
|
1578
|
-
# id, title, level and line count of the Section, or nil.
|
1727
|
+
# returns an Array of [String, String, Integer, String, Boolean], representing the
|
1728
|
+
# id, reftext, title, level and line count of the Section, or nil.
|
1579
1729
|
#
|
1580
1730
|
#--
|
1581
1731
|
# NOTE for efficiency, we don't reuse methods that check for a section title
|
@@ -1584,25 +1734,33 @@ class Lexer
|
|
1584
1734
|
sect_id = nil
|
1585
1735
|
sect_title = nil
|
1586
1736
|
sect_level = -1
|
1737
|
+
sect_reftext = nil
|
1587
1738
|
single_line = true
|
1588
1739
|
|
1589
|
-
first_char = line1
|
1740
|
+
first_char = line1.chr
|
1590
1741
|
if (first_char == '=' || (Compliance.markdown_syntax && first_char == '#')) &&
|
1591
|
-
(match =
|
1592
|
-
sect_id = match[3]
|
1593
|
-
sect_title = match[2]
|
1742
|
+
(match = AtxSectionRx.match(line1))
|
1594
1743
|
sect_level = single_line_section_level match[1]
|
1744
|
+
sect_title = match[2]
|
1745
|
+
if sect_title.end_with?(']]') && (anchor_match = InlineSectionAnchorRx.match(sect_title))
|
1746
|
+
if anchor_match[2].nil?
|
1747
|
+
sect_title = anchor_match[1]
|
1748
|
+
sect_id = anchor_match[3]
|
1749
|
+
sect_reftext = anchor_match[4]
|
1750
|
+
end
|
1751
|
+
end
|
1595
1752
|
elsif Compliance.underline_style_section_titles
|
1596
|
-
line2 = reader.peek_line
|
1597
|
-
|
1598
|
-
(name_match = line1.match(REGEXP[:section_name])) &&
|
1753
|
+
if (line2 = reader.peek_line(true)) && SECTION_LEVELS.has_key?(line2.chr) && line2 =~ SetextSectionLineRx &&
|
1754
|
+
(name_match = SetextSectionTitleRx.match(line1)) &&
|
1599
1755
|
# chomp so that a (non-visible) endline does not impact calculation
|
1600
1756
|
(line_length(line1) - line_length(line2)).abs <= 1
|
1601
|
-
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1757
|
+
sect_title = name_match[1]
|
1758
|
+
if sect_title.end_with?(']]') && (anchor_match = InlineSectionAnchorRx.match(sect_title))
|
1759
|
+
if anchor_match[2].nil?
|
1760
|
+
sect_title = anchor_match[1]
|
1761
|
+
sect_id = anchor_match[3]
|
1762
|
+
sect_reftext = anchor_match[4]
|
1763
|
+
end
|
1606
1764
|
end
|
1607
1765
|
sect_level = section_level line2
|
1608
1766
|
single_line = false
|
@@ -1612,7 +1770,7 @@ class Lexer
|
|
1612
1770
|
if sect_level >= 0
|
1613
1771
|
sect_level += document.attr('leveloffset', 0).to_i
|
1614
1772
|
end
|
1615
|
-
[sect_id, sect_title, sect_level, single_line]
|
1773
|
+
[sect_id, sect_reftext, sect_title, sect_level, single_line]
|
1616
1774
|
end
|
1617
1775
|
|
1618
1776
|
# Public: Calculate the number of unicode characters in the line, excluding the endline
|
@@ -1621,7 +1779,7 @@ class Lexer
|
|
1621
1779
|
#
|
1622
1780
|
# returns the number of unicode characters in the line
|
1623
1781
|
def self.line_length(line)
|
1624
|
-
FORCE_UNICODE_LINE_LENGTH ? line.
|
1782
|
+
FORCE_UNICODE_LINE_LENGTH ? line.scan(UnicodeCharScanRx).length : line.length
|
1625
1783
|
end
|
1626
1784
|
|
1627
1785
|
# Public: Consume and parse the two header lines (line 1 = author info, line 2 = revision info).
|
@@ -1634,7 +1792,8 @@ class Lexer
|
|
1634
1792
|
#
|
1635
1793
|
# Examples
|
1636
1794
|
#
|
1637
|
-
#
|
1795
|
+
# data = ["Author Name <author@example.org>\n", "v1.0, 2012-12-21: Coincide w/ end of world.\n"]
|
1796
|
+
# parse_header_metadata(Reader.new data, nil, :normalize => true)
|
1638
1797
|
# # => {'author' => 'Author Name', 'firstname' => 'Author', 'lastname' => 'Name', 'email' => 'author@example.org',
|
1639
1798
|
# # 'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.'}
|
1640
1799
|
def self.parse_header_metadata(reader, document = nil)
|
@@ -1649,12 +1808,12 @@ class Lexer
|
|
1649
1808
|
author_metadata = process_authors reader.read_line
|
1650
1809
|
|
1651
1810
|
unless author_metadata.empty?
|
1652
|
-
|
1653
|
-
|
1654
|
-
author_metadata.
|
1655
|
-
|
1656
|
-
|
1657
|
-
|
1811
|
+
if document
|
1812
|
+
# apply header subs and assign to document
|
1813
|
+
author_metadata.each do |key, val|
|
1814
|
+
unless document.attributes.has_key? key
|
1815
|
+
document.attributes[key] = ((val.is_a? ::String) ? document.apply_header_subs(val) : val)
|
1816
|
+
end
|
1658
1817
|
end
|
1659
1818
|
|
1660
1819
|
implicit_author = document.attributes['author']
|
@@ -1671,7 +1830,7 @@ class Lexer
|
|
1671
1830
|
|
1672
1831
|
if reader.has_more_lines? && !reader.next_line_empty?
|
1673
1832
|
rev_line = reader.read_line
|
1674
|
-
if match =
|
1833
|
+
if (match = RevisionInfoLineRx.match(rev_line))
|
1675
1834
|
rev_metadata['revdate'] = match[2].strip
|
1676
1835
|
rev_metadata['revnumber'] = match[1].rstrip unless match[1].nil?
|
1677
1836
|
rev_metadata['revremark'] = match[3].rstrip unless match[3].nil?
|
@@ -1682,12 +1841,12 @@ class Lexer
|
|
1682
1841
|
end
|
1683
1842
|
|
1684
1843
|
unless rev_metadata.empty?
|
1685
|
-
|
1686
|
-
|
1687
|
-
rev_metadata.
|
1688
|
-
|
1689
|
-
|
1690
|
-
|
1844
|
+
if document
|
1845
|
+
# apply header subs and assign to document
|
1846
|
+
rev_metadata.each do |key, val|
|
1847
|
+
unless document.attributes.has_key? key
|
1848
|
+
document.attributes[key] = document.apply_header_subs(val)
|
1849
|
+
end
|
1691
1850
|
end
|
1692
1851
|
end
|
1693
1852
|
|
@@ -1700,7 +1859,7 @@ class Lexer
|
|
1700
1859
|
reader.skip_blank_lines
|
1701
1860
|
end
|
1702
1861
|
|
1703
|
-
if
|
1862
|
+
if document
|
1704
1863
|
# process author attribute entries that override (or stand in for) the implicit author line
|
1705
1864
|
author_metadata = nil
|
1706
1865
|
if document.attributes.has_key?('author') &&
|
@@ -1713,21 +1872,21 @@ class Lexer
|
|
1713
1872
|
author_metadata = process_authors author_line, true
|
1714
1873
|
else
|
1715
1874
|
authors = []
|
1716
|
-
author_key =
|
1875
|
+
author_key = %(author_#{authors.size + 1})
|
1717
1876
|
while document.attributes.has_key? author_key
|
1718
1877
|
authors << document.attributes[author_key]
|
1719
|
-
author_key =
|
1878
|
+
author_key = %(author_#{authors.size + 1})
|
1720
1879
|
end
|
1721
1880
|
if authors.size == 1
|
1722
1881
|
# do not allow multiple, process as names only
|
1723
|
-
author_metadata = process_authors authors
|
1882
|
+
author_metadata = process_authors authors[0], true, false
|
1724
1883
|
elsif authors.size > 1
|
1725
1884
|
# allow multiple, process as names only
|
1726
1885
|
author_metadata = process_authors authors.join('; '), true
|
1727
1886
|
end
|
1728
1887
|
end
|
1729
1888
|
|
1730
|
-
|
1889
|
+
if author_metadata
|
1731
1890
|
document.attributes.update author_metadata
|
1732
1891
|
|
1733
1892
|
# special case
|
@@ -1752,9 +1911,8 @@ class Lexer
|
|
1752
1911
|
def self.process_authors(author_line, names_only = false, multiple = true)
|
1753
1912
|
author_metadata = {}
|
1754
1913
|
keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
|
1755
|
-
author_entries = multiple ? author_line.split
|
1914
|
+
author_entries = multiple ? (author_line.split ';').map {|line| line.strip } : [author_line]
|
1756
1915
|
author_entries.each_with_index do |author_entry, idx|
|
1757
|
-
author_entry.strip!
|
1758
1916
|
next if author_entry.empty?
|
1759
1917
|
key_map = {}
|
1760
1918
|
if idx.zero?
|
@@ -1763,7 +1921,7 @@ class Lexer
|
|
1763
1921
|
end
|
1764
1922
|
else
|
1765
1923
|
keys.each do |key|
|
1766
|
-
key_map[key.to_sym] =
|
1924
|
+
key_map[key.to_sym] = %(#{key}_#{idx + 1})
|
1767
1925
|
end
|
1768
1926
|
end
|
1769
1927
|
|
@@ -1771,7 +1929,7 @@ class Lexer
|
|
1771
1929
|
if names_only
|
1772
1930
|
# splitting on ' ' will collapse repeating spaces
|
1773
1931
|
segments = author_entry.split(' ', 3)
|
1774
|
-
elsif (match =
|
1932
|
+
elsif (match = AuthorInfoLineRx.match(author_entry))
|
1775
1933
|
segments = match.to_a
|
1776
1934
|
segments.shift
|
1777
1935
|
end
|
@@ -1792,7 +1950,7 @@ class Lexer
|
|
1792
1950
|
end
|
1793
1951
|
author_metadata[key_map[:email]] = segments[3] unless names_only || segments[3].nil?
|
1794
1952
|
else
|
1795
|
-
author_metadata[key_map[:author]] = author_metadata[key_map[:firstname]] = fname = author_entry.strip.
|
1953
|
+
author_metadata[key_map[:author]] = author_metadata[key_map[:firstname]] = fname = author_entry.strip.tr_s(' ', ' ')
|
1796
1954
|
author_metadata[key_map[:authorinitials]] = fname[0, 1]
|
1797
1955
|
end
|
1798
1956
|
|
@@ -1800,13 +1958,13 @@ class Lexer
|
|
1800
1958
|
# only assign the _1 attributes if there are multiple authors
|
1801
1959
|
if idx == 1
|
1802
1960
|
keys.each do |key|
|
1803
|
-
author_metadata[
|
1961
|
+
author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.has_key? key
|
1804
1962
|
end
|
1805
1963
|
end
|
1806
1964
|
if idx.zero?
|
1807
1965
|
author_metadata['authors'] = author_metadata[key_map[:author]]
|
1808
1966
|
else
|
1809
|
-
author_metadata['authors'] =
|
1967
|
+
author_metadata['authors'] = %(#{author_metadata['authors']}, #{author_metadata[key_map[:author]]})
|
1810
1968
|
end
|
1811
1969
|
end
|
1812
1970
|
|
@@ -1856,30 +2014,28 @@ class Lexer
|
|
1856
2014
|
#
|
1857
2015
|
# returns true if the line contains metadata, otherwise false
|
1858
2016
|
def self.parse_block_metadata_line(reader, parent, attributes, options = {})
|
1859
|
-
return false
|
2017
|
+
return false unless reader.has_more_lines?
|
1860
2018
|
next_line = reader.peek_line
|
1861
|
-
if (commentish = next_line.start_with?('//')) && (match =
|
2019
|
+
if (commentish = next_line.start_with?('//')) && (match = CommentBlockRx.match(next_line))
|
1862
2020
|
terminator = match[0]
|
1863
2021
|
reader.read_lines_until(:skip_first_line => true, :preserve_last_line => true, :terminator => terminator, :skip_processing => true)
|
1864
|
-
elsif commentish && next_line
|
2022
|
+
elsif commentish && CommentLineRx =~ next_line
|
1865
2023
|
# do nothing, we'll skip it
|
1866
|
-
elsif !options[:text] && (match =
|
2024
|
+
elsif !options[:text] && next_line.start_with?(':') && (match = AttributeEntryRx.match(next_line))
|
1867
2025
|
process_attribute_entry(reader, parent, attributes, match)
|
1868
|
-
elsif
|
1869
|
-
|
1870
|
-
|
1871
|
-
|
1872
|
-
|
1873
|
-
|
1874
|
-
|
1875
|
-
attributes['reftext'] = reftext
|
1876
|
-
parent.document.register(:ids, [id, reftext])
|
2026
|
+
elsif (in_square_brackets = next_line.start_with?('[') && next_line.end_with?(']')) && (match = BlockAnchorRx.match(next_line))
|
2027
|
+
unless match[1].nil_or_empty?
|
2028
|
+
attributes['id'] = match[1]
|
2029
|
+
# AsciiDoc always uses [id] as the reftext in HTML output,
|
2030
|
+
# but I'd like to do better in Asciidoctor
|
2031
|
+
# registration is deferred until the block or section is processed
|
2032
|
+
attributes['reftext'] = match[2] unless match[2].nil?
|
1877
2033
|
end
|
1878
|
-
elsif match =
|
2034
|
+
elsif in_square_brackets && (match = BlockAttributeListRx.match(next_line))
|
1879
2035
|
parent.document.parse_attributes(match[1], [], :sub_input => true, :into => attributes)
|
1880
2036
|
# NOTE title doesn't apply to section, but we need to stash it for the first block
|
1881
2037
|
# TODO should issue an error if this is found above the document title
|
1882
|
-
elsif !options[:text] && (match =
|
2038
|
+
elsif !options[:text] && (match = BlockTitleRx.match(next_line))
|
1883
2039
|
attributes['title'] = match[1]
|
1884
2040
|
else
|
1885
2041
|
return false
|
@@ -1898,25 +2054,26 @@ class Lexer
|
|
1898
2054
|
end
|
1899
2055
|
|
1900
2056
|
def self.process_attribute_entry(reader, parent, attributes = nil, match = nil)
|
1901
|
-
match ||= reader.has_more_lines? ? reader.peek_line
|
2057
|
+
match ||= (reader.has_more_lines? ? AttributeEntryRx.match(reader.peek_line) : nil)
|
1902
2058
|
if match
|
1903
2059
|
name = match[1]
|
1904
|
-
value = match[2]
|
1905
|
-
|
1906
|
-
|
1907
|
-
|
1908
|
-
|
1909
|
-
|
1910
|
-
|
1911
|
-
|
1912
|
-
|
1913
|
-
|
1914
|
-
|
2060
|
+
unless (value = match[2] || '').empty?
|
2061
|
+
if value.end_with?(line_continuation = LINE_CONTINUATION) ||
|
2062
|
+
value.end_with?(line_continuation = LINE_CONTINUATION_LEGACY)
|
2063
|
+
value = value.chop.rstrip
|
2064
|
+
while reader.advance
|
2065
|
+
break if (next_line = reader.peek_line.strip).empty?
|
2066
|
+
if (keep_open = next_line.end_with? line_continuation)
|
2067
|
+
next_line = next_line.chop.rstrip
|
2068
|
+
end
|
2069
|
+
separator = (value.end_with? LINE_BREAK) ? EOL : ' '
|
2070
|
+
value = %(#{value}#{separator}#{next_line})
|
2071
|
+
break unless keep_open
|
1915
2072
|
end
|
1916
2073
|
end
|
1917
2074
|
end
|
1918
2075
|
|
1919
|
-
store_attribute(name, value, parent
|
2076
|
+
store_attribute(name, value, (parent ? parent.document : nil), attributes)
|
1920
2077
|
true
|
1921
2078
|
else
|
1922
2079
|
false
|
@@ -1932,6 +2089,7 @@ class Lexer
|
|
1932
2089
|
#
|
1933
2090
|
# returns a 2-element array containing the attribute name and value
|
1934
2091
|
def self.store_attribute(name, value, doc = nil, attrs = nil)
|
2092
|
+
# TODO move processing of attribute value to utility method
|
1935
2093
|
if name.end_with?('!')
|
1936
2094
|
# a nil value signals the attribute should be deleted (undefined)
|
1937
2095
|
value = nil
|
@@ -1944,11 +2102,25 @@ class Lexer
|
|
1944
2102
|
|
1945
2103
|
name = sanitize_attribute_name(name)
|
1946
2104
|
accessible = true
|
1947
|
-
|
1948
|
-
|
2105
|
+
if doc
|
2106
|
+
# alias numbered attribute to sectnums
|
2107
|
+
if name == 'numbered'
|
2108
|
+
name = 'sectnums'
|
2109
|
+
# support relative leveloffset values
|
2110
|
+
elsif name == 'leveloffset'
|
2111
|
+
if value
|
2112
|
+
case value.chr
|
2113
|
+
when '+'
|
2114
|
+
value = ((doc.attr 'leveloffset', 0).to_i + (value[1..-1] || 0).to_i).to_s
|
2115
|
+
when '-'
|
2116
|
+
value = ((doc.attr 'leveloffset', 0).to_i - (value[1..-1] || 0).to_i).to_s
|
2117
|
+
end
|
2118
|
+
end
|
2119
|
+
end
|
2120
|
+
accessible = value ? doc.set_attribute(name, value) : doc.delete_attribute(name)
|
1949
2121
|
end
|
1950
2122
|
|
1951
|
-
|
2123
|
+
if accessible && attrs
|
1952
2124
|
Document::AttributeEntry.new(name, value).save_to(attrs)
|
1953
2125
|
end
|
1954
2126
|
|
@@ -1998,12 +2170,12 @@ class Lexer
|
|
1998
2170
|
# Examples
|
1999
2171
|
#
|
2000
2172
|
# marker = 'B.'
|
2001
|
-
#
|
2173
|
+
# Parser.resolve_ordered_list_marker(marker, 1, true)
|
2002
2174
|
# # => 'A.'
|
2003
2175
|
#
|
2004
2176
|
# Returns the String of the first marker in this number series
|
2005
2177
|
def self.resolve_ordered_list_marker(marker, ordinal = 0, validate = false, reader = nil)
|
2006
|
-
number_style = ORDERED_LIST_STYLES.detect {|s|
|
2178
|
+
number_style = ORDERED_LIST_STYLES.detect {|s| OrderedListMarkerRxMap[s] =~ marker }
|
2007
2179
|
expected = actual = nil
|
2008
2180
|
case number_style
|
2009
2181
|
when :arabic
|
@@ -2041,7 +2213,7 @@ class Lexer
|
|
2041
2213
|
end
|
2042
2214
|
|
2043
2215
|
if validate && expected != actual
|
2044
|
-
warn
|
2216
|
+
warn %(asciidoctor: WARNING: #{reader.line_info}: list item index: expected #{expected}, got #{actual})
|
2045
2217
|
end
|
2046
2218
|
|
2047
2219
|
marker
|
@@ -2057,15 +2229,15 @@ class Lexer
|
|
2057
2229
|
# Returns a Boolean indicating whether this line is a sibling list item given
|
2058
2230
|
# the criteria provided
|
2059
2231
|
def self.is_sibling_list_item?(line, list_type, sibling_trait)
|
2060
|
-
if sibling_trait.is_a?
|
2232
|
+
if sibling_trait.is_a? ::Regexp
|
2061
2233
|
matcher = sibling_trait
|
2062
2234
|
expected_marker = false
|
2063
2235
|
else
|
2064
|
-
matcher =
|
2236
|
+
matcher = ListRxMap[list_type]
|
2065
2237
|
expected_marker = sibling_trait
|
2066
2238
|
end
|
2067
2239
|
|
2068
|
-
if m =
|
2240
|
+
if (m = matcher.match(line))
|
2069
2241
|
if expected_marker
|
2070
2242
|
expected_marker == resolve_list_marker(list_type, m[1])
|
2071
2243
|
else
|
@@ -2085,8 +2257,10 @@ class Lexer
|
|
2085
2257
|
# returns an instance of Asciidoctor::Table parsed from the provided reader
|
2086
2258
|
def self.next_table(table_reader, parent, attributes)
|
2087
2259
|
table = Table.new(parent, attributes)
|
2088
|
-
|
2089
|
-
|
2260
|
+
if (attributes.has_key? 'title')
|
2261
|
+
table.title = attributes.delete 'title'
|
2262
|
+
table.assign_caption attributes.delete('caption')
|
2263
|
+
end
|
2090
2264
|
|
2091
2265
|
if attributes.has_key? 'cols'
|
2092
2266
|
table.create_columns(parse_col_specs(attributes['cols']))
|
@@ -2104,7 +2278,7 @@ class Lexer
|
|
2104
2278
|
line = table_reader.read_line
|
2105
2279
|
|
2106
2280
|
if skipped == 0 && loop_idx.zero? && !attributes.has_key?('options') &&
|
2107
|
-
!(next_line = table_reader.peek_line).nil? && next_line
|
2281
|
+
!(next_line = table_reader.peek_line).nil? && next_line.empty?
|
2108
2282
|
table.has_header_option = true
|
2109
2283
|
table.set_option 'header'
|
2110
2284
|
end
|
@@ -2115,7 +2289,7 @@ class Lexer
|
|
2115
2289
|
# push an empty cell spec if boundary at start of line
|
2116
2290
|
parser_ctx.close_open_cell
|
2117
2291
|
else
|
2118
|
-
next_cell_spec, line = parse_cell_spec(line, :start)
|
2292
|
+
next_cell_spec, line = parse_cell_spec(line, :start, parser_ctx.delimiter)
|
2119
2293
|
# if the cell spec is not null, then we're at a cell boundary
|
2120
2294
|
if !next_cell_spec.nil?
|
2121
2295
|
parser_ctx.close_open_cell next_cell_spec
|
@@ -2125,8 +2299,10 @@ class Lexer
|
|
2125
2299
|
end
|
2126
2300
|
end
|
2127
2301
|
|
2128
|
-
|
2129
|
-
|
2302
|
+
seen = false
|
2303
|
+
while !seen || !line.empty?
|
2304
|
+
seen = true
|
2305
|
+
if (m = parser_ctx.match_delimiter(line))
|
2130
2306
|
if parser_ctx.format == 'csv'
|
2131
2307
|
if parser_ctx.buffer_has_unclosed_quotes?(m.pre_match)
|
2132
2308
|
# throw it back, it's too small
|
@@ -2153,7 +2329,7 @@ class Lexer
|
|
2153
2329
|
else
|
2154
2330
|
# no other delimiters to see here
|
2155
2331
|
# suck up this line into the buffer and move on
|
2156
|
-
parser_ctx.buffer = %(#{parser_ctx.buffer}#{line})
|
2332
|
+
parser_ctx.buffer = %(#{parser_ctx.buffer}#{line}#{EOL})
|
2157
2333
|
# QUESTION make stripping endlines in csv data an option? (unwrap-option?)
|
2158
2334
|
if parser_ctx.format == 'csv'
|
2159
2335
|
parser_ctx.buffer = %(#{parser_ctx.buffer.rstrip} )
|
@@ -2200,27 +2376,24 @@ class Lexer
|
|
2200
2376
|
# returns a Hash of attributes that specify how to format
|
2201
2377
|
# and layout the cells in the table.
|
2202
2378
|
def self.parse_col_specs(records)
|
2203
|
-
|
2204
|
-
|
2205
|
-
|
2206
|
-
|
2207
|
-
1.upto(m[0].to_i) {
|
2208
|
-
specs << {'width' => 1}
|
2209
|
-
}
|
2210
|
-
return specs
|
2379
|
+
# check for deprecated syntax: single number, equal column spread
|
2380
|
+
# REVIEW could use records == records.to_i.to_s instead of regexp
|
2381
|
+
if DigitsRx =~ records
|
2382
|
+
return ::Array.new(records.to_i) { { 'width' => 1 } }
|
2211
2383
|
end
|
2212
2384
|
|
2385
|
+
specs = []
|
2213
2386
|
records.split(',').each {|record|
|
2214
2387
|
# TODO might want to use scan rather than this mega-regexp
|
2215
|
-
if m =
|
2388
|
+
if (m = ColumnSpecRx.match(record))
|
2216
2389
|
spec = {}
|
2217
2390
|
if m[2]
|
2218
2391
|
# make this an operation
|
2219
2392
|
colspec, rowspec = m[2].split '.'
|
2220
|
-
if !colspec.
|
2393
|
+
if !colspec.nil_or_empty? && Table::ALIGNMENTS[:h].has_key?(colspec)
|
2221
2394
|
spec['halign'] = Table::ALIGNMENTS[:h][colspec]
|
2222
2395
|
end
|
2223
|
-
if !rowspec.
|
2396
|
+
if !rowspec.nil_or_empty? && Table::ALIGNMENTS[:v].has_key?(rowspec)
|
2224
2397
|
spec['valign'] = Table::ALIGNMENTS[:v][rowspec]
|
2225
2398
|
end
|
2226
2399
|
end
|
@@ -2248,47 +2421,65 @@ class Lexer
|
|
2248
2421
|
#
|
2249
2422
|
# The cell specs dictate the cell's alignments, styles or filters,
|
2250
2423
|
# colspan, rowspan and/or repeating content.
|
2424
|
+
#
|
2425
|
+
# The default spec when pos == :end is {} since we already know we're at a
|
2426
|
+
# delimiter. When pos == :start, we *may* be at a delimiter, nil indicates
|
2427
|
+
# we're not.
|
2251
2428
|
#
|
2252
2429
|
# returns the Hash of attributes that indicate how to layout
|
2253
2430
|
# and style this cell in the table.
|
2254
|
-
def self.parse_cell_spec(line, pos = :start)
|
2255
|
-
|
2256
|
-
|
2257
|
-
|
2258
|
-
|
2259
|
-
|
2260
|
-
|
2261
|
-
|
2262
|
-
|
2263
|
-
|
2264
|
-
|
2265
|
-
|
2266
|
-
if m[1]
|
2267
|
-
colspec, rowspec = m[1].split '.'
|
2268
|
-
colspec = colspec.to_s.empty? ? 1 : colspec.to_i
|
2269
|
-
rowspec = rowspec.to_s.empty? ? 1 : rowspec.to_i
|
2270
|
-
if m[2] == '+'
|
2271
|
-
spec['colspan'] = colspec unless colspec == 1
|
2272
|
-
spec['rowspan'] = rowspec unless rowspec == 1
|
2273
|
-
elsif m[2] == '*'
|
2274
|
-
spec['repeatcol'] = colspec unless colspec == 1
|
2431
|
+
def self.parse_cell_spec(line, pos = :start, delimiter = nil)
|
2432
|
+
m = nil
|
2433
|
+
rest = ''
|
2434
|
+
|
2435
|
+
case pos
|
2436
|
+
when :start
|
2437
|
+
if line.include? delimiter
|
2438
|
+
spec_part, rest = line.split delimiter, 2
|
2439
|
+
if (m = CellSpecStartRx.match spec_part)
|
2440
|
+
return [{}, rest] if m[0].empty?
|
2441
|
+
else
|
2442
|
+
return [nil, line]
|
2275
2443
|
end
|
2444
|
+
else
|
2445
|
+
return [nil, line]
|
2276
2446
|
end
|
2277
|
-
|
2278
|
-
if m
|
2279
|
-
|
2280
|
-
|
2281
|
-
|
2282
|
-
|
2283
|
-
|
2284
|
-
spec['valign'] = Table::ALIGNMENTS[:v][rowspec]
|
2285
|
-
end
|
2447
|
+
when :end
|
2448
|
+
if (m = CellSpecEndRx.match line)
|
2449
|
+
# NOTE return the line stripped of trailing whitespace if no cellspec is found in this case
|
2450
|
+
return [{}, line.rstrip] if m[0].lstrip.empty?
|
2451
|
+
rest = m.pre_match
|
2452
|
+
else
|
2453
|
+
return [{}, line]
|
2286
2454
|
end
|
2455
|
+
end
|
2287
2456
|
|
2288
|
-
|
2289
|
-
|
2457
|
+
spec = {}
|
2458
|
+
if m[1]
|
2459
|
+
colspec, rowspec = m[1].split '.'
|
2460
|
+
colspec = colspec.nil_or_empty? ? 1 : colspec.to_i
|
2461
|
+
rowspec = rowspec.nil_or_empty? ? 1 : rowspec.to_i
|
2462
|
+
if m[2] == '+'
|
2463
|
+
spec['colspan'] = colspec unless colspec == 1
|
2464
|
+
spec['rowspan'] = rowspec unless rowspec == 1
|
2465
|
+
elsif m[2] == '*'
|
2466
|
+
spec['repeatcol'] = colspec unless colspec == 1
|
2467
|
+
end
|
2468
|
+
end
|
2469
|
+
|
2470
|
+
if m[3]
|
2471
|
+
colspec, rowspec = m[3].split '.'
|
2472
|
+
if !colspec.nil_or_empty? && Table::ALIGNMENTS[:h].has_key?(colspec)
|
2473
|
+
spec['halign'] = Table::ALIGNMENTS[:h][colspec]
|
2290
2474
|
end
|
2291
|
-
|
2475
|
+
if !rowspec.nil_or_empty? && Table::ALIGNMENTS[:v].has_key?(rowspec)
|
2476
|
+
spec['valign'] = Table::ALIGNMENTS[:v][rowspec]
|
2477
|
+
end
|
2478
|
+
end
|
2479
|
+
|
2480
|
+
if m[4] && Table::TEXT_STYLES.has_key?(m[4])
|
2481
|
+
spec['style'] = Table::TEXT_STYLES[m[4]]
|
2482
|
+
end
|
2292
2483
|
|
2293
2484
|
[spec, rest]
|
2294
2485
|
end
|
@@ -2321,10 +2512,7 @@ class Lexer
|
|
2321
2512
|
original_style = attributes['style']
|
2322
2513
|
raw_style = attributes[1]
|
2323
2514
|
# NOTE spaces are not allowed in shorthand, so if we find one, this ain't shorthand
|
2324
|
-
if
|
2325
|
-
attributes['style'] = raw_style
|
2326
|
-
[raw_style, original_style]
|
2327
|
-
else
|
2515
|
+
if raw_style && !raw_style.include?(' ') && Compliance.shorthand_property_syntax
|
2328
2516
|
type = :style
|
2329
2517
|
collector = []
|
2330
2518
|
parsed = {}
|
@@ -2332,7 +2520,7 @@ class Lexer
|
|
2332
2520
|
save_current = lambda {
|
2333
2521
|
if collector.empty?
|
2334
2522
|
if type != :style
|
2335
|
-
warn
|
2523
|
+
warn %(asciidoctor: WARNING:#{reader.nil? ? nil : " #{reader.prev_line_info}:"} invalid empty #{type} detected in style attribute)
|
2336
2524
|
end
|
2337
2525
|
else
|
2338
2526
|
case type
|
@@ -2341,7 +2529,7 @@ class Lexer
|
|
2341
2529
|
parsed[type].push collector.join
|
2342
2530
|
when :id
|
2343
2531
|
if parsed.has_key? :id
|
2344
|
-
warn
|
2532
|
+
warn %(asciidoctor: WARNING:#{reader.nil? ? nil : " #{reader.prev_line_info}:"} multiple ids detected in style attribute)
|
2345
2533
|
end
|
2346
2534
|
parsed[type] = collector.join
|
2347
2535
|
else
|
@@ -2351,7 +2539,7 @@ class Lexer
|
|
2351
2539
|
end
|
2352
2540
|
}
|
2353
2541
|
|
2354
|
-
raw_style.
|
2542
|
+
raw_style.each_char do |c|
|
2355
2543
|
if c == '.' || c == '#' || c == '%'
|
2356
2544
|
save_current.call
|
2357
2545
|
case c
|
@@ -2389,7 +2577,7 @@ class Lexer
|
|
2389
2577
|
|
2390
2578
|
if parsed.has_key? :option
|
2391
2579
|
(options = parsed[:option]).each do |option|
|
2392
|
-
attributes[
|
2580
|
+
attributes[%(#{option}-option)] = ''
|
2393
2581
|
end
|
2394
2582
|
if (existing_opts = attributes['options'])
|
2395
2583
|
attributes['options'] = (options + existing_opts.split(',')) * ','
|
@@ -2400,6 +2588,9 @@ class Lexer
|
|
2400
2588
|
end
|
2401
2589
|
|
2402
2590
|
[parsed_style, original_style]
|
2591
|
+
else
|
2592
|
+
attributes['style'] = raw_style
|
2593
|
+
[raw_style, original_style]
|
2403
2594
|
end
|
2404
2595
|
end
|
2405
2596
|
|
@@ -2427,13 +2618,13 @@ class Lexer
|
|
2427
2618
|
# end
|
2428
2619
|
# EOS
|
2429
2620
|
#
|
2430
|
-
# source.
|
2431
|
-
# # => [" def names
|
2621
|
+
# source.split("\n")
|
2622
|
+
# # => [" def names", " @names.split ' '", " end"]
|
2432
2623
|
#
|
2433
|
-
#
|
2434
|
-
# # => ["def names
|
2624
|
+
# Parser.reset_block_indent(source.split "\n")
|
2625
|
+
# # => ["def names", " @names.split ' '", "end"]
|
2435
2626
|
#
|
2436
|
-
# puts
|
2627
|
+
# puts Parser.reset_block_indent(source.split "\n") * "\n"
|
2437
2628
|
# # => def names
|
2438
2629
|
# # => @names.split ' '
|
2439
2630
|
# # => end
|
@@ -2442,7 +2633,7 @@ class Lexer
|
|
2442
2633
|
#--
|
2443
2634
|
# FIXME refactor gsub matchers into compiled regex
|
2444
2635
|
def self.reset_block_indent!(lines, indent = 0)
|
2445
|
-
return if indent
|
2636
|
+
return if !indent || lines.empty?
|
2446
2637
|
|
2447
2638
|
tab_detected = false
|
2448
2639
|
# TODO make tab size configurable
|
@@ -2450,10 +2641,10 @@ class Lexer
|
|
2450
2641
|
# strip leading block indent
|
2451
2642
|
offsets = lines.map do |line|
|
2452
2643
|
# break if the first char is non-whitespace
|
2453
|
-
break [] unless line.
|
2454
|
-
if line.include?
|
2644
|
+
break [] unless line.chr.lstrip.empty?
|
2645
|
+
if line.include? TAB
|
2455
2646
|
tab_detected = true
|
2456
|
-
line = line.gsub(
|
2647
|
+
line = line.gsub(TAB_PATTERN, tab_expansion)
|
2457
2648
|
end
|
2458
2649
|
if (flush_line = line.lstrip).empty?
|
2459
2650
|
nil
|
@@ -2467,8 +2658,8 @@ class Lexer
|
|
2467
2658
|
unless offsets.empty? || (offsets = offsets.compact).empty?
|
2468
2659
|
if (offset = offsets.min) > 0
|
2469
2660
|
lines.map! {|line|
|
2470
|
-
line = line.gsub(
|
2471
|
-
line[offset..-1]
|
2661
|
+
line = line.gsub(TAB_PATTERN, tab_expansion) if tab_detected
|
2662
|
+
line[offset..-1].to_s
|
2472
2663
|
}
|
2473
2664
|
end
|
2474
2665
|
end
|
@@ -2498,7 +2689,7 @@ class Lexer
|
|
2498
2689
|
# sanitize_attribute_name('Foo 3 #-Billy')
|
2499
2690
|
# => 'foo3-billy'
|
2500
2691
|
def self.sanitize_attribute_name(name)
|
2501
|
-
name.gsub(
|
2692
|
+
name.gsub(InvalidAttributeNameCharsRx, '').downcase
|
2502
2693
|
end
|
2503
2694
|
|
2504
2695
|
# Internal: Converts a Roman numeral to an integer value.
|