asciidoctor 0.1.3 → 0.1.4
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 +387 -0
- data/README.adoc +358 -348
- data/asciidoctor.gemspec +30 -9
- data/bin/asciidoctor +3 -0
- data/bin/asciidoctor-safe +3 -0
- data/compat/asciidoc.conf +76 -4
- data/lib/asciidoctor.rb +174 -79
- data/lib/asciidoctor/abstract_block.rb +131 -101
- data/lib/asciidoctor/abstract_node.rb +108 -26
- data/lib/asciidoctor/attribute_list.rb +1 -1
- data/lib/asciidoctor/backends/_stylesheets.rb +204 -62
- data/lib/asciidoctor/backends/base_template.rb +11 -22
- data/lib/asciidoctor/backends/docbook45.rb +158 -163
- data/lib/asciidoctor/backends/docbook5.rb +103 -0
- data/lib/asciidoctor/backends/html5.rb +662 -445
- data/lib/asciidoctor/block.rb +54 -44
- data/lib/asciidoctor/cli/invoker.rb +41 -20
- data/lib/asciidoctor/cli/options.rb +66 -20
- data/lib/asciidoctor/debug.rb +1 -1
- data/lib/asciidoctor/document.rb +265 -100
- data/lib/asciidoctor/extensions.rb +443 -0
- data/lib/asciidoctor/helpers.rb +38 -6
- data/lib/asciidoctor/inline.rb +5 -5
- data/lib/asciidoctor/lexer.rb +532 -250
- data/lib/asciidoctor/{list_item.rb → list.rb} +33 -13
- data/lib/asciidoctor/path_resolver.rb +28 -2
- data/lib/asciidoctor/reader.rb +814 -455
- data/lib/asciidoctor/renderer.rb +128 -42
- data/lib/asciidoctor/section.rb +55 -41
- data/lib/asciidoctor/substituters.rb +380 -107
- data/lib/asciidoctor/table.rb +40 -30
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +32 -96
- data/man/{asciidoctor.ad → asciidoctor.adoc} +57 -48
- data/test/attributes_test.rb +200 -27
- data/test/blocks_test.rb +361 -22
- data/test/document_test.rb +496 -81
- data/test/extensions_test.rb +448 -0
- data/test/fixtures/basic-docinfo-footer.html +6 -0
- data/test/fixtures/basic-docinfo-footer.xml +8 -0
- data/test/fixtures/basic-docinfo.xml +3 -3
- data/test/fixtures/basic.asciidoc +1 -0
- data/test/fixtures/child-include.adoc +5 -0
- data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
- data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
- data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
- data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
- data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
- data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
- data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
- data/test/fixtures/docinfo-footer.html +1 -0
- data/test/fixtures/docinfo-footer.xml +9 -0
- data/test/fixtures/docinfo.xml +1 -0
- data/test/fixtures/grandchild-include.adoc +3 -0
- data/test/fixtures/parent-include-restricted.adoc +5 -0
- data/test/fixtures/parent-include.adoc +5 -0
- data/test/invoker_test.rb +82 -8
- data/test/lexer_test.rb +21 -3
- data/test/links_test.rb +34 -2
- data/test/lists_test.rb +304 -7
- data/test/options_test.rb +19 -3
- data/test/paragraphs_test.rb +13 -0
- data/test/paths_test.rb +22 -0
- data/test/preamble_test.rb +20 -0
- data/test/reader_test.rb +1096 -644
- data/test/renderer_test.rb +152 -12
- data/test/sections_test.rb +417 -76
- data/test/substitutions_test.rb +339 -138
- data/test/tables_test.rb +109 -4
- data/test/test_helper.rb +79 -13
- data/test/text_test.rb +111 -11
- metadata +54 -18
data/lib/asciidoctor/inline.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
module Asciidoctor
|
2
2
|
# Public: Methods for managing inline elements in AsciiDoc block
|
3
3
|
class Inline < AbstractNode
|
4
|
+
# Public: Get/Set the String name of the render template
|
5
|
+
attr_accessor :template_name
|
6
|
+
|
4
7
|
# Public: Get the text of this inline element
|
5
8
|
attr_reader :text
|
6
9
|
|
@@ -12,13 +15,10 @@ class Inline < AbstractNode
|
|
12
15
|
|
13
16
|
def initialize(parent, context, text = nil, opts = {})
|
14
17
|
super(parent, context)
|
18
|
+
@template_name = "inline_#{context}"
|
15
19
|
|
16
20
|
@text = text
|
17
21
|
|
18
|
-
#@id = opts[:id] if opts.has_key?(:id)
|
19
|
-
#@type = opts[:type] if opts.has_key?(:type)
|
20
|
-
#@target = opts[:target] if opts.has_key?(:target)
|
21
|
-
|
22
22
|
@id = opts[:id]
|
23
23
|
@type = opts[:type]
|
24
24
|
@target = opts[:target]
|
@@ -29,7 +29,7 @@ class Inline < AbstractNode
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def render
|
32
|
-
renderer.render(
|
32
|
+
renderer.render(@template_name, self).chomp
|
33
33
|
end
|
34
34
|
|
35
35
|
end
|
data/lib/asciidoctor/lexer.rb
CHANGED
@@ -77,10 +77,7 @@ class Lexer
|
|
77
77
|
# special case, block title is not allowed above document title,
|
78
78
|
# carry attributes over to the document body
|
79
79
|
if block_attributes.has_key?('title')
|
80
|
-
document.
|
81
|
-
document.save_attributes
|
82
|
-
block_attributes['invalid-header'] = true
|
83
|
-
return block_attributes
|
80
|
+
return document.finalize_header block_attributes, false
|
84
81
|
end
|
85
82
|
|
86
83
|
# yep, document title logic in AsciiDoc is just insanity
|
@@ -101,7 +98,7 @@ class Lexer
|
|
101
98
|
assigned_doctitle = doctitle
|
102
99
|
end
|
103
100
|
document.attributes['doctitle'] = section_title = doctitle
|
104
|
-
# QUESTION: should
|
101
|
+
# QUESTION: should the id assignment on Document be encapsulated in the Document class?
|
105
102
|
if document.id.nil? && block_attributes.has_key?('id')
|
106
103
|
document.id = block_attributes.delete('id')
|
107
104
|
end
|
@@ -118,13 +115,50 @@ class Lexer
|
|
118
115
|
if assigned_doctitle
|
119
116
|
document.attributes['doctitle'] = assigned_doctitle
|
120
117
|
end
|
118
|
+
|
119
|
+
# parse title and consume name section of manpage document
|
120
|
+
parse_manpage_header(reader, document) if document.doctype == 'manpage'
|
121
121
|
|
122
|
-
document
|
123
|
-
document.save_attributes
|
124
|
-
|
125
|
-
# NOTE these are the block-level attributes (not document attributes) that
|
122
|
+
# NOTE block_attributes are the block-level attributes (not document attributes) that
|
126
123
|
# precede the first line of content (document title, first section or first block)
|
127
|
-
block_attributes
|
124
|
+
document.finalize_header block_attributes
|
125
|
+
end
|
126
|
+
|
127
|
+
# Public: Parses the manpage header of the AsciiDoc source read from the Reader
|
128
|
+
#
|
129
|
+
# returns Nothing
|
130
|
+
def self.parse_manpage_header(reader, document)
|
131
|
+
if (m = document.attributes['doctitle'].match(REGEXP[:mantitle_manvolnum]))
|
132
|
+
document.attributes['mantitle'] = document.sub_attributes(m[1].rstrip.downcase)
|
133
|
+
document.attributes['manvolnum'] = m[2].strip
|
134
|
+
else
|
135
|
+
warn "asciidoctor: ERROR: #{reader.prev_line_info}: malformed manpage title"
|
136
|
+
end
|
137
|
+
|
138
|
+
reader.skip_blank_lines
|
139
|
+
|
140
|
+
if is_next_line_section?(reader, {})
|
141
|
+
name_section = initialize_section(reader, document, {})
|
142
|
+
if name_section.level == 1
|
143
|
+
name_section_buffer = reader.read_lines_until(:break_on_blank_lines => true).join.tr_s("\n ", ' ')
|
144
|
+
if (m = name_section_buffer.match(REGEXP[:manname_manpurpose]))
|
145
|
+
document.attributes['manname'] = m[1]
|
146
|
+
document.attributes['manpurpose'] = m[2]
|
147
|
+
# TODO parse multiple man names
|
148
|
+
|
149
|
+
if document.backend == 'manpage'
|
150
|
+
document.attributes['docname'] = document.attributes['manname']
|
151
|
+
document.attributes['outfilesuffix'] = ".#{document.attributes['manvolnum']}"
|
152
|
+
end
|
153
|
+
else
|
154
|
+
warn "asciidoctor: ERROR: #{reader.prev_line_info}: malformed name section body"
|
155
|
+
end
|
156
|
+
else
|
157
|
+
warn "asciidoctor: ERROR: #{reader.prev_line_info}: name section title must be at level 1"
|
158
|
+
end
|
159
|
+
else
|
160
|
+
warn "asciidoctor: ERROR: #{reader.prev_line_info}: name section expected"
|
161
|
+
end
|
128
162
|
end
|
129
163
|
|
130
164
|
# Public: Return the next section from the Reader.
|
@@ -176,7 +210,7 @@ class Lexer
|
|
176
210
|
(parent.has_header? || attributes.delete('invalid-header') || !is_next_line_section?(reader, attributes))
|
177
211
|
|
178
212
|
if parent.has_header?
|
179
|
-
preamble = Block.new(parent, :preamble)
|
213
|
+
preamble = Block.new(parent, :preamble, :content_model => :compound)
|
180
214
|
parent << preamble
|
181
215
|
end
|
182
216
|
section = parent
|
@@ -225,9 +259,9 @@ class Lexer
|
|
225
259
|
doctype = parent.document.doctype
|
226
260
|
if next_level > current_level || (section.is_a?(Document) && next_level == 0)
|
227
261
|
if next_level == 0 && doctype != 'book'
|
228
|
-
|
262
|
+
warn "asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections"
|
229
263
|
elsif !expected_next_levels.nil? && !expected_next_levels.include?(next_level)
|
230
|
-
|
264
|
+
warn "asciidoctor: WARNING: #{reader.line_info}: section title out of sequence: " +
|
231
265
|
"expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, " +
|
232
266
|
"got level #{next_level}"
|
233
267
|
end
|
@@ -236,7 +270,7 @@ class Lexer
|
|
236
270
|
section << new_section
|
237
271
|
else
|
238
272
|
if next_level == 0 && doctype != 'book'
|
239
|
-
|
273
|
+
warn "asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections"
|
240
274
|
end
|
241
275
|
# close this section (and break out of the nesting) to begin a new one
|
242
276
|
break
|
@@ -258,7 +292,7 @@ class Lexer
|
|
258
292
|
|
259
293
|
if preamble && !preamble.blocks?
|
260
294
|
# drop the preamble if it has no content
|
261
|
-
section.delete_at(0)
|
295
|
+
section.blocks.delete_at(0)
|
262
296
|
end
|
263
297
|
|
264
298
|
# The attributes returned here are orphaned attributes that fall at the end
|
@@ -305,7 +339,14 @@ class Lexer
|
|
305
339
|
#parse_sections = options.fetch(:parse_sections, false)
|
306
340
|
|
307
341
|
document = parent.document
|
308
|
-
|
342
|
+
if (extensions = document.extensions)
|
343
|
+
block_extensions = extensions.blocks?
|
344
|
+
macro_extensions = extensions.block_macros?
|
345
|
+
else
|
346
|
+
block_extensions = macro_extensions = false
|
347
|
+
end
|
348
|
+
#parent_context = parent.is_a?(Block) ? parent.context : nil
|
349
|
+
in_list = parent.is_a?(List)
|
309
350
|
block = nil
|
310
351
|
style = nil
|
311
352
|
explicit_style = nil
|
@@ -320,19 +361,20 @@ class Lexer
|
|
320
361
|
# break
|
321
362
|
end
|
322
363
|
|
323
|
-
# QUESTION introduce parsing context object?
|
324
|
-
this_line = reader.
|
364
|
+
# QUESTION should we introduce a parsing context object?
|
365
|
+
this_line = reader.read_line
|
325
366
|
delimited_block = false
|
326
367
|
block_context = nil
|
368
|
+
cloaked_context = nil
|
327
369
|
terminator = nil
|
328
370
|
# QUESTION put this inside call to rekey attributes?
|
329
371
|
if attributes[1]
|
330
|
-
style, explicit_style = parse_style_attribute(attributes)
|
372
|
+
style, explicit_style = parse_style_attribute(attributes, reader)
|
331
373
|
end
|
332
374
|
|
333
375
|
if delimited_blk_match = is_delimited_block?(this_line, true)
|
334
376
|
delimited_block = true
|
335
|
-
block_context = delimited_blk_match.context
|
377
|
+
block_context = cloaked_context = delimited_blk_match.context
|
336
378
|
terminator = delimited_blk_match.terminator
|
337
379
|
if !style
|
338
380
|
style = attributes['style'] = block_context.to_s
|
@@ -341,8 +383,10 @@ class Lexer
|
|
341
383
|
block_context = style.to_sym
|
342
384
|
elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
|
343
385
|
block_context = :admonition
|
386
|
+
elsif block_extensions && extensions.processor_registered_for_block?(style, block_context)
|
387
|
+
block_context = style.to_sym
|
344
388
|
else
|
345
|
-
|
389
|
+
warn "asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for #{block_context} block: #{style}"
|
346
390
|
style = block_context.to_s
|
347
391
|
end
|
348
392
|
end
|
@@ -365,16 +409,17 @@ class Lexer
|
|
365
409
|
end
|
366
410
|
|
367
411
|
# process lines normally
|
368
|
-
|
412
|
+
unless text_only
|
413
|
+
first_char = Compliance.markdown_syntax ? this_line.lstrip[0..0] : this_line[0..0]
|
369
414
|
# NOTE we're letting break lines (ruler, page_break, etc) have attributes
|
370
|
-
if (
|
371
|
-
|
415
|
+
if BREAK_LINES.has_key?(first_char) && this_line.length > 3 &&
|
416
|
+
(match = this_line.match(Compliance.markdown_syntax ? REGEXP[:break_line_plus] : REGEXP[:break_line]))
|
417
|
+
block = Block.new(parent, BREAK_LINES[first_char], :content_model => :empty)
|
372
418
|
break
|
373
419
|
|
374
|
-
# TODO make this a media_blk and handle image, video & audio
|
375
420
|
elsif (match = this_line.match(REGEXP[:media_blk_macro]))
|
376
421
|
blk_ctx = match[1].to_sym
|
377
|
-
block = Block.new(parent, blk_ctx)
|
422
|
+
block = Block.new(parent, blk_ctx, :content_model => :empty)
|
378
423
|
if blk_ctx == :image
|
379
424
|
posattrs = ['alt', 'width', 'height']
|
380
425
|
elsif blk_ctx == :video
|
@@ -394,53 +439,74 @@ class Lexer
|
|
394
439
|
:sub_input => true,
|
395
440
|
:sub_result => false,
|
396
441
|
:into => attributes)
|
397
|
-
target = block.sub_attributes(match[2])
|
442
|
+
target = block.sub_attributes(match[2], :attribute_missing => 'drop-line')
|
398
443
|
if target.empty?
|
399
|
-
|
400
|
-
|
444
|
+
if document.attributes.fetch('attribute-missing', COMPLIANCE[:attribute_missing]) == 'skip'
|
445
|
+
# retain as unparsed
|
446
|
+
return Block.new(parent, :paragraph, :source => [this_line.chomp])
|
447
|
+
else
|
448
|
+
# drop the line if target resolves to nothing
|
449
|
+
return nil
|
450
|
+
end
|
401
451
|
end
|
402
452
|
|
403
453
|
attributes['target'] = target
|
404
454
|
block.title = attributes.delete('title') if attributes.has_key?('title')
|
405
455
|
if blk_ctx == :image
|
406
456
|
document.register(:images, target)
|
407
|
-
attributes['alt'] ||= File.basename(target, File.extname(target))
|
457
|
+
attributes['alt'] ||= File.basename(target, File.extname(target)).tr('_-', ' ')
|
408
458
|
# QUESTION should video or audio have an auto-numbered caption?
|
409
459
|
block.assign_caption attributes.delete('caption'), 'figure'
|
410
460
|
end
|
411
461
|
break
|
412
462
|
|
413
463
|
# NOTE we're letting the toc macro have attributes
|
414
|
-
elsif (match = this_line.match(REGEXP[:toc]))
|
415
|
-
block = Block.new(parent, :toc)
|
464
|
+
elsif first_char == 't' && (match = this_line.match(REGEXP[:toc]))
|
465
|
+
block = Block.new(parent, :toc, :content_model => :empty)
|
416
466
|
block.parse_attributes(match[1], [], :sub_result => false, :into => attributes)
|
417
467
|
break
|
418
468
|
|
469
|
+
elsif macro_extensions && (match = this_line.match(REGEXP[:generic_blk_macro])) &&
|
470
|
+
extensions.processor_registered_for_block_macro?(match[1])
|
471
|
+
name = match[1]
|
472
|
+
target = match[2]
|
473
|
+
raw_attributes = match[3]
|
474
|
+
processor = extensions.load_block_macro_processor name, document
|
475
|
+
unless raw_attributes.empty?
|
476
|
+
document.parse_attributes(raw_attributes, processor.options.fetch(:pos_attrs, []),
|
477
|
+
:sub_input => true, :sub_result => false, :into => attributes)
|
478
|
+
end
|
479
|
+
if !(default_attrs = processor.options.fetch(:default_attrs, {})).empty?
|
480
|
+
default_attrs.each {|k, v| attributes[k] ||= v }
|
481
|
+
end
|
482
|
+
block = processor.process parent, target, attributes
|
483
|
+
return nil if block.nil?
|
484
|
+
break
|
419
485
|
end
|
420
486
|
end
|
421
487
|
|
422
488
|
# haven't found anything yet, continue
|
423
489
|
if (match = this_line.match(REGEXP[:colist]))
|
424
|
-
block =
|
490
|
+
block = List.new(parent, :colist)
|
425
491
|
attributes['style'] = 'arabic'
|
426
|
-
items = []
|
427
|
-
block.buffer = items
|
428
492
|
reader.unshift_line this_line
|
429
493
|
expected_index = 1
|
430
494
|
begin
|
431
495
|
# might want to move this check to a validate method
|
432
496
|
if match[1].to_i != expected_index
|
433
|
-
|
497
|
+
# FIXME this lineno - 2 hack means we need a proper look-behind cursor
|
498
|
+
warn "asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: callout list item index: expected #{expected_index} got #{match[1]}"
|
434
499
|
end
|
435
500
|
list_item = next_list_item(reader, block, match)
|
436
501
|
expected_index += 1
|
437
502
|
if !list_item.nil?
|
438
|
-
|
439
|
-
coids = document.callouts.callout_ids(items.size)
|
503
|
+
block << list_item
|
504
|
+
coids = document.callouts.callout_ids(block.items.size)
|
440
505
|
if !coids.empty?
|
441
506
|
list_item.attributes['coids'] = coids
|
442
507
|
else
|
443
|
-
|
508
|
+
# FIXME this lineno - 2 hack means we need a proper look-behind cursor
|
509
|
+
warn "asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: no callouts refer to list item #{block.items.size}"
|
444
510
|
end
|
445
511
|
end
|
446
512
|
end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist])
|
@@ -457,10 +523,10 @@ class Lexer
|
|
457
523
|
reader.unshift_line this_line
|
458
524
|
block = next_outline_list(reader, :olist, parent)
|
459
525
|
# QUESTION move this logic to next_outline_list?
|
460
|
-
if !
|
461
|
-
marker = block.
|
526
|
+
if !attributes['style'] && !block.attributes['style']
|
527
|
+
marker = block.items.first.marker
|
462
528
|
if marker.start_with? '.'
|
463
|
-
# first one makes more sense, but second
|
529
|
+
# first one makes more sense, but second one is AsciiDoc-compliant
|
464
530
|
#attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES.first).to_s
|
465
531
|
attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES.first).to_s
|
466
532
|
else
|
@@ -475,11 +541,12 @@ class Lexer
|
|
475
541
|
block = next_labeled_list(reader, match, parent)
|
476
542
|
break
|
477
543
|
|
478
|
-
elsif (style == 'float' || style == 'discrete') &&
|
544
|
+
elsif (style == 'float' || style == 'discrete') &&
|
545
|
+
is_section_title?(this_line, (Compliance.underline_style_section_titles ? reader.peek_line(true) : nil))
|
479
546
|
reader.unshift_line this_line
|
480
547
|
float_id, float_title, float_level, _ = parse_section_title(reader, document)
|
481
548
|
float_id ||= attributes['id'] if attributes.has_key?('id')
|
482
|
-
block = Block.new(parent, :floating_title)
|
549
|
+
block = Block.new(parent, :floating_title, :content_model => :empty)
|
483
550
|
if float_id.nil? || float_id.empty?
|
484
551
|
# FIXME remove hack of creating throwaway Section to get at the generate_id method
|
485
552
|
tmp_sect = Section.new(parent)
|
@@ -494,31 +561,40 @@ class Lexer
|
|
494
561
|
break
|
495
562
|
|
496
563
|
# FIXME create another set for "passthrough" styles
|
564
|
+
# FIXME make this more DRY!
|
497
565
|
elsif !style.nil? && style != 'normal'
|
498
566
|
if PARAGRAPH_STYLES.include?(style)
|
499
567
|
block_context = style.to_sym
|
568
|
+
cloaked_context = :paragraph
|
500
569
|
reader.unshift_line this_line
|
501
570
|
# advance to block parsing =>
|
502
571
|
break
|
503
572
|
elsif ADMONITION_STYLES.include?(style)
|
504
573
|
block_context = :admonition
|
574
|
+
cloaked_context = :paragraph
|
575
|
+
reader.unshift_line this_line
|
576
|
+
# advance to block parsing =>
|
577
|
+
break
|
578
|
+
elsif block_extensions && extensions.processor_registered_for_block?(style, :paragraph)
|
579
|
+
block_context = style.to_sym
|
580
|
+
cloaked_context = :paragraph
|
505
581
|
reader.unshift_line this_line
|
506
582
|
# advance to block parsing =>
|
507
583
|
break
|
508
584
|
else
|
509
|
-
|
585
|
+
warn "asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for paragraph: #{style}"
|
510
586
|
style = nil
|
511
587
|
# continue to process paragraph
|
512
588
|
end
|
513
589
|
end
|
514
590
|
|
515
|
-
break_at_list = (skipped == 0 &&
|
591
|
+
break_at_list = (skipped == 0 && in_list)
|
516
592
|
|
517
593
|
# a literal paragraph is contiguous lines starting at least one space
|
518
594
|
if style != 'normal' && this_line.match(REGEXP[:lit_par])
|
519
|
-
# So we need to actually include this one in the
|
595
|
+
# So we need to actually include this one in the read_lines group
|
520
596
|
reader.unshift_line this_line
|
521
|
-
|
597
|
+
lines = reader.read_lines_until(
|
522
598
|
:break_on_blank_lines => true,
|
523
599
|
:break_on_list_continuation => true,
|
524
600
|
:preserve_last_line => true) {|line|
|
@@ -530,20 +606,17 @@ class Lexer
|
|
530
606
|
(COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])))
|
531
607
|
}
|
532
608
|
|
533
|
-
reset_block_indent!
|
609
|
+
reset_block_indent! lines
|
534
610
|
|
535
|
-
block = Block.new(parent, :literal,
|
611
|
+
block = Block.new(parent, :literal, :content_model => :verbatim, :source => lines, :attributes => attributes)
|
536
612
|
# a literal gets special meaning inside of a definition list
|
537
|
-
|
538
|
-
|
539
|
-
# TODO this feels hacky, better way to distinguish from explicit literal block?
|
540
|
-
attributes['options'] << 'listparagraph'
|
541
|
-
end
|
613
|
+
# TODO this feels hacky, better way to distinguish from explicit literal block?
|
614
|
+
block.set_option('listparagraph') if in_list
|
542
615
|
|
543
616
|
# a paragraph is contiguous nonblank/noncontinuation lines
|
544
617
|
else
|
545
618
|
reader.unshift_line this_line
|
546
|
-
|
619
|
+
lines = reader.read_lines_until(
|
547
620
|
:break_on_blank_lines => true,
|
548
621
|
:break_on_list_continuation => true,
|
549
622
|
:preserve_last_line => true,
|
@@ -559,23 +632,23 @@ class Lexer
|
|
559
632
|
# NOTE we need this logic because we've asked the reader to skip
|
560
633
|
# line comments, which may leave us w/ an empty buffer if those
|
561
634
|
# were the only lines found
|
562
|
-
if
|
563
|
-
# call
|
564
|
-
reader.
|
635
|
+
if lines.empty?
|
636
|
+
# call advance since the reader preserved the last line
|
637
|
+
reader.advance
|
565
638
|
return nil
|
566
639
|
end
|
567
640
|
|
568
|
-
catalog_inline_anchors(
|
641
|
+
catalog_inline_anchors(lines.join, document)
|
569
642
|
|
570
|
-
first_line =
|
643
|
+
first_line = lines.first
|
571
644
|
if !text_only && (admonition_match = first_line.match(REGEXP[:admonition_inline]))
|
572
|
-
|
573
|
-
block = Block.new(parent, :admonition, buffer)
|
645
|
+
lines[0] = admonition_match.post_match.lstrip
|
574
646
|
attributes['style'] = admonition_match[1]
|
575
647
|
attributes['name'] = admonition_name = admonition_match[1].downcase
|
576
648
|
attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
|
577
|
-
|
578
|
-
|
649
|
+
block = Block.new(parent, :admonition, :source => lines, :attributes => attributes)
|
650
|
+
elsif !text_only && Compliance.markdown_syntax && first_line.start_with?('> ')
|
651
|
+
lines.map! {|line|
|
579
652
|
if line.start_with?('> ')
|
580
653
|
line[2..-1]
|
581
654
|
elsif line.chomp == '>'
|
@@ -585,10 +658,10 @@ class Lexer
|
|
585
658
|
end
|
586
659
|
}
|
587
660
|
|
588
|
-
if
|
589
|
-
attribution, citetitle =
|
590
|
-
|
591
|
-
|
661
|
+
if lines.last.start_with?('-- ')
|
662
|
+
attribution, citetitle = lines.pop[3..-1].split(', ', 2)
|
663
|
+
lines.pop while lines.last.chomp.empty?
|
664
|
+
lines[-1] = lines.last.chomp
|
592
665
|
else
|
593
666
|
attribution, citetitle = nil
|
594
667
|
end
|
@@ -597,27 +670,34 @@ class Lexer
|
|
597
670
|
attributes['citetitle'] = citetitle unless citetitle.nil?
|
598
671
|
# NOTE will only detect headings that are floating titles (not section titles)
|
599
672
|
# TODO could assume a floating title when inside a block context
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
673
|
+
# FIXME Reader needs to be created w/ line info
|
674
|
+
block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes)
|
675
|
+
elsif !text_only && lines.size > 1 && first_line.start_with?('"') &&
|
676
|
+
lines.last.start_with?('-- ') && lines[-2].chomp.end_with?('"')
|
677
|
+
lines[0] = first_line[1..-1]
|
678
|
+
attribution, citetitle = lines.pop[3..-1].split(', ', 2)
|
679
|
+
lines.pop while lines.last.chomp.empty?
|
680
|
+
lines[-1] = lines.last.chomp.chop
|
607
681
|
attributes['style'] = 'quote'
|
608
682
|
attributes['attribution'] = attribution unless attribution.nil?
|
609
683
|
attributes['citetitle'] = citetitle unless citetitle.nil?
|
610
|
-
block = Block.new(parent, :quote,
|
611
|
-
#block = Block.new(parent, :quote)
|
612
|
-
#block << Block.new(block, :paragraph,
|
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)
|
613
687
|
else
|
614
|
-
#
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
688
|
+
# if [normal] is used over an indented paragraph, unindent it
|
689
|
+
if style == 'normal' && ((first_char = lines.first[0..0]) == ' ' || first_char == "\t")
|
690
|
+
first_line = lines.first
|
691
|
+
first_line_shifted = first_line.lstrip
|
692
|
+
indent = line_length(first_line) - line_length(first_line_shifted)
|
693
|
+
lines[0] = first_line_shifted
|
694
|
+
# QUESTION should we fix the rest of the lines, since in XML output it's insignificant?
|
695
|
+
lines.size.times do |i|
|
696
|
+
lines[i] = lines[i][indent..-1] if i > 0
|
697
|
+
end
|
698
|
+
end
|
619
699
|
|
620
|
-
block = Block.new(parent, :paragraph,
|
700
|
+
block = Block.new(parent, :paragraph, :source => lines, :attributes => attributes)
|
621
701
|
end
|
622
702
|
end
|
623
703
|
|
@@ -636,21 +716,24 @@ class Lexer
|
|
636
716
|
when :admonition
|
637
717
|
attributes['name'] = admonition_name = style.downcase
|
638
718
|
attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
|
639
|
-
block = build_block(block_context, :
|
719
|
+
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
640
720
|
|
641
721
|
when :comment
|
642
|
-
|
722
|
+
build_block(block_context, :skip, terminator, parent, reader, attributes)
|
643
723
|
return nil
|
644
724
|
|
645
725
|
when :example
|
646
|
-
block = build_block(block_context, :
|
726
|
+
block = build_block(block_context, :compound, terminator, parent, reader, attributes, {:supports_caption => true})
|
647
727
|
|
648
728
|
when :listing, :fenced_code, :source
|
649
729
|
if block_context == :fenced_code
|
650
730
|
style = attributes['style'] = 'source'
|
651
|
-
|
652
|
-
|
653
|
-
|
731
|
+
language, linenums = this_line[3...-1].split(',', 2)
|
732
|
+
if language && !(language = language.strip).empty?
|
733
|
+
attributes['language'] = language
|
734
|
+
attributes['linenums'] = '' if linenums && !linenums.strip.empty?
|
735
|
+
end
|
736
|
+
terminator = terminator[0..2]
|
654
737
|
elsif block_context == :source
|
655
738
|
AttributeList.rekey(attributes, [nil, 'language', 'linenums'])
|
656
739
|
end
|
@@ -660,13 +743,14 @@ class Lexer
|
|
660
743
|
block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
|
661
744
|
|
662
745
|
when :pass
|
663
|
-
block = build_block(block_context, :
|
746
|
+
block = build_block(block_context, :raw, terminator, parent, reader, attributes)
|
664
747
|
|
665
748
|
when :open, :sidebar
|
666
|
-
block = build_block(block_context, :
|
749
|
+
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
667
750
|
|
668
751
|
when :table
|
669
|
-
|
752
|
+
cursor = reader.cursor
|
753
|
+
block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_line_comments => true), cursor
|
670
754
|
case terminator[0..0]
|
671
755
|
when ','
|
672
756
|
attributes['format'] = 'csv'
|
@@ -677,11 +761,26 @@ class Lexer
|
|
677
761
|
|
678
762
|
when :quote, :verse
|
679
763
|
AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
|
680
|
-
block = build_block(block_context, (block_context == :verse ? :verbatim : :
|
764
|
+
block = build_block(block_context, (block_context == :verse ? :verbatim : :compound), terminator, parent, reader, attributes)
|
681
765
|
|
682
766
|
else
|
683
|
-
|
684
|
-
|
767
|
+
if block_extensions && extensions.processor_registered_for_block?(block_context, cloaked_context)
|
768
|
+
processor = extensions.load_block_processor block_context, document
|
769
|
+
|
770
|
+
if (content_model = processor.options[:content_model]) != :skip
|
771
|
+
if !(pos_attrs = processor.options.fetch(:pos_attrs, [])).empty?
|
772
|
+
AttributeList.rekey(attributes, [nil].concat(pos_attrs))
|
773
|
+
end
|
774
|
+
if !(default_attrs = processor.options.fetch(:default_attrs, {})).empty?
|
775
|
+
default_attrs.each {|k, v| attributes[k] ||= v }
|
776
|
+
end
|
777
|
+
end
|
778
|
+
block = build_block(block_context, content_model, terminator, parent, reader, attributes, :processor => processor)
|
779
|
+
return nil if block.nil?
|
780
|
+
else
|
781
|
+
# this should only happen if there's a misconfiguration
|
782
|
+
raise "Unsupported block type #{block_context} at #{reader.line_info}"
|
783
|
+
end
|
685
784
|
end
|
686
785
|
end
|
687
786
|
end
|
@@ -689,21 +788,34 @@ class Lexer
|
|
689
788
|
# when looking for nested content, one or more line comments, comment
|
690
789
|
# blocks or trailing attribute lists could leave us without a block,
|
691
790
|
# so handle accordingly
|
692
|
-
# REVIEW we may no longer need this check
|
791
|
+
# REVIEW we may no longer need this nil check
|
693
792
|
if !block.nil?
|
694
793
|
# REVIEW seems like there is a better way to organize this wrap-up
|
695
794
|
block.id ||= attributes['id'] if attributes.has_key?('id')
|
696
795
|
block.title = attributes['title'] unless block.title?
|
697
796
|
block.caption ||= attributes.delete('caption')
|
797
|
+
# TODO eventualy remove the style attribute from the attributes hash
|
798
|
+
#block.style = attributes.delete('style')
|
799
|
+
block.style = attributes['style']
|
698
800
|
# AsciiDoc always use [id] as the reftext in HTML output,
|
699
801
|
# but I'd like to do better in Asciidoctor
|
700
802
|
if block.id && block.title? && !attributes.has_key?('reftext')
|
701
803
|
document.register(:ids, [block.id, block.title])
|
702
804
|
end
|
703
805
|
block.update_attributes(attributes)
|
704
|
-
|
705
|
-
|
706
|
-
|
806
|
+
block.lock_in_subs
|
807
|
+
|
808
|
+
#if document.attributes.has_key? :pending_attribute_entries
|
809
|
+
# document.attributes.delete(:pending_attribute_entries).each do |entry|
|
810
|
+
# entry.save_to block.attributes
|
811
|
+
# end
|
812
|
+
#end
|
813
|
+
|
814
|
+
if block.sub? :callouts
|
815
|
+
if !(catalog_callouts block.source, document)
|
816
|
+
# No need to look for callouts if they aren't there
|
817
|
+
block.remove_sub :callouts
|
818
|
+
end
|
707
819
|
end
|
708
820
|
end
|
709
821
|
|
@@ -713,48 +825,68 @@ class Lexer
|
|
713
825
|
# Public: Determines whether this line is the start of any of the delimited blocks
|
714
826
|
#
|
715
827
|
# returns the match data if this line is the first line of a delimited block or nil if not
|
716
|
-
def self.is_delimited_block?
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
828
|
+
def self.is_delimited_block? line, return_match_data = false
|
829
|
+
# highly optimized for best performance
|
830
|
+
line_len = line.length - 1
|
831
|
+
return nil unless line_len > 1 && DELIMITED_BLOCK_LEADERS.include?(line[0..1])
|
832
|
+
line = line.chomp
|
833
|
+
# counts endline character in line length
|
834
|
+
if line_len == 2
|
835
|
+
tip = line
|
836
|
+
tl = 2
|
837
|
+
elsif line_len < 3
|
838
|
+
return nil
|
839
|
+
else
|
840
|
+
if line_len < 5
|
841
|
+
tip = line
|
842
|
+
tl = line_len
|
723
843
|
else
|
724
844
|
tip = line[0..3]
|
725
845
|
tl = 4
|
846
|
+
end
|
726
847
|
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
848
|
+
# special case for fenced code blocks
|
849
|
+
if Compliance.markdown_syntax
|
850
|
+
tip_alt = tip.chop if tl == 4
|
851
|
+
if tip_alt == '```'
|
852
|
+
if tip.end_with? '`'
|
853
|
+
return nil
|
733
854
|
end
|
855
|
+
tip = tip_alt
|
856
|
+
tl = 3
|
857
|
+
elsif tip_alt == '~~~'
|
858
|
+
if tip.end_with? '~'
|
859
|
+
return nil
|
860
|
+
end
|
861
|
+
tip = tip_alt
|
862
|
+
tl = 3
|
734
863
|
end
|
735
864
|
end
|
865
|
+
end
|
736
866
|
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
context, masq = *DELIMITED_BLOCKS[tip]
|
751
|
-
BlockMatchData.new(context, masq, tip, match[0])
|
752
|
-
else
|
753
|
-
true
|
754
|
-
end
|
867
|
+
if DELIMITED_BLOCKS.has_key? tip
|
868
|
+
# tip is the full line when delimiter is minimum length
|
869
|
+
if tl == 3 || tl == line_len
|
870
|
+
if return_match_data
|
871
|
+
context, masq = *DELIMITED_BLOCKS[tip]
|
872
|
+
BlockMatchData.new(context, masq, tip, tip)
|
873
|
+
else
|
874
|
+
true
|
875
|
+
end
|
876
|
+
elsif %(#{tip}#{tip[-1..-1] * (line_len - tl)}) == line
|
877
|
+
if return_match_data
|
878
|
+
context, masq = *DELIMITED_BLOCKS[tip]
|
879
|
+
BlockMatchData.new(context, masq, tip, line)
|
755
880
|
else
|
756
|
-
|
881
|
+
true
|
757
882
|
end
|
883
|
+
#elsif match = line.match(REGEXP[:any_blk])
|
884
|
+
# if return_match_data
|
885
|
+
# context, masq = *DELIMITED_BLOCKS[tip]
|
886
|
+
# BlockMatchData.new(context, masq, tip, match[0])
|
887
|
+
# else
|
888
|
+
# true
|
889
|
+
# end
|
758
890
|
else
|
759
891
|
nil
|
760
892
|
end
|
@@ -766,53 +898,93 @@ class Lexer
|
|
766
898
|
# whether a block supports complex content should be a config setting
|
767
899
|
# if terminator is false, that means the all the lines in the reader should be parsed
|
768
900
|
# NOTE could invoke filter in here, before and after parsing
|
769
|
-
def self.build_block(block_context,
|
901
|
+
def self.build_block(block_context, content_model, terminator, parent, reader, attributes, options = {})
|
902
|
+
if content_model == :skip || content_model == :raw
|
903
|
+
skip_processing = content_model == :skip
|
904
|
+
parse_as_content_model = :simple
|
905
|
+
else
|
906
|
+
skip_processing = false
|
907
|
+
parse_as_content_model = content_model
|
908
|
+
end
|
909
|
+
|
770
910
|
if terminator.nil?
|
771
|
-
if
|
772
|
-
|
911
|
+
if parse_as_content_model == :verbatim
|
912
|
+
lines = reader.read_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true)
|
773
913
|
else
|
774
|
-
|
914
|
+
content_model = :simple if content_model == :compound
|
915
|
+
lines = reader.read_lines_until(
|
775
916
|
:break_on_blank_lines => true,
|
776
917
|
:break_on_list_continuation => true,
|
777
918
|
:preserve_last_line => true,
|
778
|
-
:skip_line_comments => true
|
919
|
+
:skip_line_comments => true,
|
920
|
+
:skip_processing => skip_processing) {|line|
|
779
921
|
COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line]))
|
780
922
|
}
|
781
|
-
# QUESTION check for empty
|
923
|
+
# QUESTION check for empty lines after grabbing lines for simple content model?
|
782
924
|
end
|
783
|
-
|
784
|
-
|
925
|
+
block_reader = nil
|
926
|
+
elsif parse_as_content_model != :compound
|
927
|
+
lines = reader.read_lines_until(:terminator => terminator, :chomp_last_line => true, :skip_processing => skip_processing)
|
928
|
+
block_reader = nil
|
929
|
+
# terminator is false when reader has already been prepared
|
785
930
|
elsif terminator == false
|
786
|
-
|
931
|
+
lines = nil
|
787
932
|
block_reader = reader
|
788
933
|
else
|
789
|
-
|
790
|
-
|
934
|
+
lines = nil
|
935
|
+
cursor = reader.cursor
|
936
|
+
block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_processing => skip_processing), cursor
|
791
937
|
end
|
792
938
|
|
793
|
-
if
|
794
|
-
|
939
|
+
if content_model == :skip
|
940
|
+
attributes.clear
|
941
|
+
return lines
|
942
|
+
end
|
943
|
+
|
944
|
+
if content_model == :verbatim && attributes.has_key?('indent')
|
945
|
+
reset_block_indent! lines, attributes['indent'].to_i
|
946
|
+
end
|
947
|
+
|
948
|
+
if (processor = options[:processor])
|
949
|
+
attributes.delete('style')
|
950
|
+
processor.options[:content_model] = content_model
|
951
|
+
block = processor.process(parent, block_reader || Reader.new(lines), attributes)
|
952
|
+
else
|
953
|
+
block = Block.new(parent, block_context, :content_model => content_model, :attributes => attributes, :source => lines)
|
795
954
|
end
|
796
955
|
|
797
|
-
block = Block.new(parent, block_context, buffer)
|
798
956
|
# should supports_caption be necessary?
|
799
957
|
if options.fetch(:supports_caption, false)
|
800
958
|
block.title = attributes.delete('title') if attributes.has_key?('title')
|
801
959
|
block.assign_caption attributes.delete('caption')
|
802
960
|
end
|
803
961
|
|
804
|
-
if
|
962
|
+
if content_model == :compound
|
805
963
|
# we can look for blocks until there are no more lines (and not worry
|
806
964
|
# about sections) since the reader is confined within the boundaries of a
|
807
965
|
# delimited block
|
808
|
-
|
809
|
-
parsed_block = next_block(block_reader, block)
|
810
|
-
block.blocks << parsed_block unless parsed_block.nil?
|
811
|
-
end
|
966
|
+
parse_blocks block_reader, block
|
812
967
|
end
|
813
968
|
block
|
814
969
|
end
|
815
970
|
|
971
|
+
# Public: Parse blocks from this reader until there are no more lines.
|
972
|
+
#
|
973
|
+
# This method calls Lexer#next_block until there are no more lines in the
|
974
|
+
# Reader. It does not consider sections because it's assumed the Reader only
|
975
|
+
# has lines which are within a delimited block region.
|
976
|
+
#
|
977
|
+
# reader - The Reader containing the lines to process
|
978
|
+
# parent - The parent Block to which to attach the parsed blocks
|
979
|
+
#
|
980
|
+
# Returns nothing.
|
981
|
+
def self.parse_blocks(reader, parent)
|
982
|
+
while reader.has_more_lines?
|
983
|
+
block = Lexer.next_block(reader, parent)
|
984
|
+
parent << block unless block.nil?
|
985
|
+
end
|
986
|
+
end
|
987
|
+
|
816
988
|
# Internal: Parse and construct an outline list Block from the current position of the Reader
|
817
989
|
#
|
818
990
|
# reader - The Reader from which to retrieve the outline list
|
@@ -821,39 +993,36 @@ class Lexer
|
|
821
993
|
#
|
822
994
|
# Returns the Block encapsulating the parsed outline (unordered or ordered) list
|
823
995
|
def self.next_outline_list(reader, list_type, parent)
|
824
|
-
list_block =
|
825
|
-
items = []
|
826
|
-
list_block.buffer = items
|
996
|
+
list_block = List.new(parent, list_type)
|
827
997
|
if parent.context == list_type
|
828
998
|
list_block.level = parent.level + 1
|
829
999
|
else
|
830
1000
|
list_block.level = 1
|
831
1001
|
end
|
832
|
-
Debug.debug { "Created #{list_type} block: #{list_block}" }
|
1002
|
+
#Debug.debug { "Created #{list_type} block: #{list_block}" }
|
833
1003
|
|
834
1004
|
while reader.has_more_lines? && (match = reader.peek_line.match(REGEXP[list_type]))
|
835
|
-
|
836
1005
|
marker = resolve_list_marker(list_type, match[1])
|
837
1006
|
|
838
1007
|
# if we are moving to the next item, and the marker is different
|
839
1008
|
# determine if we are moving up or down in nesting
|
840
|
-
if items
|
1009
|
+
if list_block.items? && marker != list_block.items.first.marker
|
841
1010
|
# assume list is nested by default, but then check to see if we are
|
842
1011
|
# popping out of a nested list by matching an ancestor's list marker
|
843
1012
|
this_item_level = list_block.level + 1
|
844
|
-
|
845
|
-
while
|
846
|
-
if marker ==
|
847
|
-
this_item_level =
|
1013
|
+
ancestor = parent
|
1014
|
+
while ancestor.context == list_type
|
1015
|
+
if marker == ancestor.items.first.marker
|
1016
|
+
this_item_level = ancestor.level
|
848
1017
|
break
|
849
1018
|
end
|
850
|
-
|
1019
|
+
ancestor = ancestor.parent
|
851
1020
|
end
|
852
1021
|
else
|
853
1022
|
this_item_level = list_block.level
|
854
1023
|
end
|
855
1024
|
|
856
|
-
if items
|
1025
|
+
if !list_block.items? || this_item_level == list_block.level
|
857
1026
|
list_item = next_list_item(reader, list_block, match)
|
858
1027
|
elsif this_item_level < list_block.level
|
859
1028
|
# leave this block
|
@@ -861,10 +1030,10 @@ class Lexer
|
|
861
1030
|
elsif this_item_level > list_block.level
|
862
1031
|
# If this next list level is down one from the
|
863
1032
|
# current Block's, append it to content of the current list item
|
864
|
-
items.last
|
1033
|
+
list_block.items.last << next_block(reader, list_block)
|
865
1034
|
end
|
866
1035
|
|
867
|
-
|
1036
|
+
list_block << list_item unless list_item.nil?
|
868
1037
|
list_item = nil
|
869
1038
|
|
870
1039
|
reader.skip_blank_lines
|
@@ -878,14 +1047,21 @@ class Lexer
|
|
878
1047
|
# text - The String of text in which to look for callouts
|
879
1048
|
# document - The current document on which the callouts are stored
|
880
1049
|
#
|
881
|
-
# Returns
|
1050
|
+
# Returns A Boolean indicating whether callouts were found
|
882
1051
|
def self.catalog_callouts(text, document)
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
1052
|
+
found = false
|
1053
|
+
if text.include? '<'
|
1054
|
+
text.scan(REGEXP[:callout_quick_scan]) {
|
1055
|
+
# alias match for Ruby 1.8.7 compat
|
1056
|
+
m = $~
|
1057
|
+
if m[0][0..0] != '\\'
|
1058
|
+
document.callouts.register(m[2])
|
1059
|
+
end
|
1060
|
+
# we have to mark as found even if it's escaped so it can be unescaped
|
1061
|
+
found = true
|
1062
|
+
}
|
1063
|
+
end
|
1064
|
+
found
|
889
1065
|
end
|
890
1066
|
|
891
1067
|
# Internal: Catalog any inline anchors found in the text, but don't process them
|
@@ -917,18 +1093,25 @@ class Lexer
|
|
917
1093
|
#
|
918
1094
|
# Returns the Block encapsulating the parsed labeled list
|
919
1095
|
def self.next_labeled_list(reader, match, parent)
|
920
|
-
|
921
|
-
|
922
|
-
block.buffer = pairs
|
1096
|
+
list_block = List.new(parent, :dlist)
|
1097
|
+
previous_pair = nil
|
923
1098
|
# allows us to capture until we find a labeled item
|
924
1099
|
# that uses the same delimiter (::, :::, :::: or ;;)
|
925
1100
|
sibling_pattern = REGEXP[:dlist_siblings][match[2]]
|
926
1101
|
|
927
1102
|
begin
|
928
|
-
|
1103
|
+
term, item = next_list_item(reader, list_block, match, sibling_pattern)
|
1104
|
+
if !previous_pair.nil? && previous_pair.last.nil?
|
1105
|
+
previous_pair.pop
|
1106
|
+
previous_pair[0] << term
|
1107
|
+
previous_pair << item
|
1108
|
+
else
|
1109
|
+
# FIXME this misses the automatic parent assignment
|
1110
|
+
list_block.items << (previous_pair = [[term], item])
|
1111
|
+
end
|
929
1112
|
end while reader.has_more_lines? && match = reader.peek_line.match(sibling_pattern)
|
930
1113
|
|
931
|
-
|
1114
|
+
list_block
|
932
1115
|
end
|
933
1116
|
|
934
1117
|
# Internal: Parse and construct the next ListItem for the current bulleted
|
@@ -957,25 +1140,46 @@ class Lexer
|
|
957
1140
|
has_text = !match[3].to_s.empty?
|
958
1141
|
else
|
959
1142
|
# Create list item using first line as the text of the list item
|
960
|
-
|
1143
|
+
text = match[2]
|
1144
|
+
checkbox = false
|
1145
|
+
if list_type == :ulist && text.start_with?('[')
|
1146
|
+
if text.start_with? '[ ] '
|
1147
|
+
checkbox = true
|
1148
|
+
checked = false
|
1149
|
+
text = text[3..-1].lstrip
|
1150
|
+
elsif text.start_with?('[*] ') || text.start_with?('[x] ')
|
1151
|
+
checkbox = true
|
1152
|
+
checked = true
|
1153
|
+
text = text[3..-1].lstrip
|
1154
|
+
end
|
1155
|
+
end
|
1156
|
+
list_item = ListItem.new(list_block, text)
|
1157
|
+
|
1158
|
+
if checkbox
|
1159
|
+
# FIXME checklist never makes it into the options attribute
|
1160
|
+
list_block.attributes['checklist-option'] = ''
|
1161
|
+
list_item.attributes['checkbox'] = ''
|
1162
|
+
list_item.attributes['checked'] = '' if checked
|
1163
|
+
end
|
961
1164
|
|
962
1165
|
if !sibling_trait
|
963
|
-
sibling_trait = resolve_list_marker(list_type, match[1], list_block.
|
1166
|
+
sibling_trait = resolve_list_marker(list_type, match[1], list_block.items.size, true, reader)
|
964
1167
|
end
|
965
1168
|
list_item.marker = sibling_trait
|
966
1169
|
has_text = true
|
967
1170
|
end
|
968
1171
|
|
969
1172
|
# first skip the line with the marker / term
|
970
|
-
reader.
|
971
|
-
|
1173
|
+
reader.advance
|
1174
|
+
cursor = reader.cursor
|
1175
|
+
list_item_reader = Reader.new read_lines_for_list_item(reader, list_type, sibling_trait, has_text), cursor
|
972
1176
|
if list_item_reader.has_more_lines?
|
973
|
-
comment_lines = list_item_reader.
|
1177
|
+
comment_lines = list_item_reader.skip_line_comments
|
974
1178
|
subsequent_line = list_item_reader.peek_line
|
975
|
-
list_item_reader.
|
1179
|
+
list_item_reader.unshift_lines comment_lines unless comment_lines.empty?
|
976
1180
|
|
977
1181
|
if !subsequent_line.nil?
|
978
|
-
continuation_connects_first_block = (subsequent_line ==
|
1182
|
+
continuation_connects_first_block = (subsequent_line == ::Asciidoctor::EOL)
|
979
1183
|
# if there's no continuation connecting the first block, then
|
980
1184
|
# treat the lines as paragraph text (activated when has_text = false)
|
981
1185
|
if !continuation_connects_first_block && list_type != :dlist
|
@@ -995,7 +1199,7 @@ class Lexer
|
|
995
1199
|
# list
|
996
1200
|
while list_item_reader.has_more_lines?
|
997
1201
|
new_block = next_block(list_item_reader, list_block, {}, options)
|
998
|
-
list_item
|
1202
|
+
list_item << new_block unless new_block.nil?
|
999
1203
|
end
|
1000
1204
|
|
1001
1205
|
list_item.fold_first(continuation_connects_first_block, content_adjacent)
|
@@ -1025,7 +1229,7 @@ class Lexer
|
|
1025
1229
|
# has_text - Whether the list item has text defined inline (always true except for labeled lists)
|
1026
1230
|
#
|
1027
1231
|
# Returns an Array of lines belonging to the current list item.
|
1028
|
-
def self.
|
1232
|
+
def self.read_lines_for_list_item(reader, list_type, sibling_trait = nil, has_text = true)
|
1029
1233
|
buffer = []
|
1030
1234
|
|
1031
1235
|
# three states for continuation: :inactive, :active & :frozen
|
@@ -1043,7 +1247,7 @@ class Lexer
|
|
1043
1247
|
detached_continuation = nil
|
1044
1248
|
|
1045
1249
|
while reader.has_more_lines?
|
1046
|
-
this_line = reader.
|
1250
|
+
this_line = reader.read_line
|
1047
1251
|
|
1048
1252
|
# if we've arrived at a sibling item in this list, we've captured
|
1049
1253
|
# the complete list item and can begin processing it
|
@@ -1057,7 +1261,7 @@ class Lexer
|
|
1057
1261
|
if continuation == :inactive
|
1058
1262
|
continuation = :active
|
1059
1263
|
has_text = true
|
1060
|
-
buffer[-1] =
|
1264
|
+
buffer[-1] = ::Asciidoctor::EOL unless within_nested_list
|
1061
1265
|
end
|
1062
1266
|
|
1063
1267
|
# dealing with adjacent list continuations (which is really a syntax error)
|
@@ -1078,7 +1282,7 @@ class Lexer
|
|
1078
1282
|
buffer << this_line
|
1079
1283
|
# grab all the lines in the block, leaving the delimiters in place
|
1080
1284
|
# we're being more strict here about the terminator, but I think that's a good thing
|
1081
|
-
buffer.concat reader.
|
1285
|
+
buffer.concat reader.read_lines_until(:terminator => match.terminator, :read_last_line => true)
|
1082
1286
|
continuation = :inactive
|
1083
1287
|
else
|
1084
1288
|
break
|
@@ -1095,7 +1299,7 @@ class Lexer
|
|
1095
1299
|
# list item will throw off the exit from it
|
1096
1300
|
if this_line.match(REGEXP[:lit_par])
|
1097
1301
|
reader.unshift_line this_line
|
1098
|
-
buffer.concat reader.
|
1302
|
+
buffer.concat reader.read_lines_until(
|
1099
1303
|
:preserve_last_line => true,
|
1100
1304
|
:break_on_blank_lines => true,
|
1101
1305
|
:break_on_list_continuation => true) {|line|
|
@@ -1122,7 +1326,7 @@ class Lexer
|
|
1122
1326
|
# advance to the next line of content
|
1123
1327
|
if this_line.chomp.empty?
|
1124
1328
|
reader.skip_blank_lines
|
1125
|
-
this_line = reader.
|
1329
|
+
this_line = reader.read_line
|
1126
1330
|
# if we hit eof or a sibling, stop reading
|
1127
1331
|
break if this_line.nil? || is_sibling_list_item?(this_line, list_type, sibling_trait)
|
1128
1332
|
end
|
@@ -1138,7 +1342,7 @@ class Lexer
|
|
1138
1342
|
# slurp up any literal paragraph offset by blank lines
|
1139
1343
|
if this_line.match(REGEXP[:lit_par])
|
1140
1344
|
reader.unshift_line this_line
|
1141
|
-
buffer.concat reader.
|
1345
|
+
buffer.concat reader.read_lines_until(
|
1142
1346
|
:preserve_last_line => true,
|
1143
1347
|
:break_on_blank_lines => true,
|
1144
1348
|
:break_on_list_continuation => true) {|line|
|
@@ -1211,13 +1415,15 @@ class Lexer
|
|
1211
1415
|
# parent - the parent Section or Document of this Section
|
1212
1416
|
# attributes - a Hash of attributes to assign to this section (default: {})
|
1213
1417
|
def self.initialize_section(reader, parent, attributes = {})
|
1214
|
-
|
1215
|
-
|
1418
|
+
document = parent.document
|
1419
|
+
sect_id, sect_title, sect_level, _ = parse_section_title(reader, document)
|
1420
|
+
section = Section.new parent, sect_level, document.attributes.has_key?('numbered')
|
1421
|
+
section.id = sect_id
|
1422
|
+
section.title = sect_title
|
1216
1423
|
# parse style, id and role from first positional attribute
|
1217
1424
|
if attributes[1]
|
1218
|
-
section.sectname, _ = parse_style_attribute(attributes)
|
1425
|
+
section.sectname, _ = parse_style_attribute(attributes, reader)
|
1219
1426
|
section.special = true
|
1220
|
-
document = parent.document
|
1221
1427
|
# HACK needs to be refactored so it's driven by config
|
1222
1428
|
if section.sectname == 'abstract' && document.doctype == 'book'
|
1223
1429
|
section.sectname = "sect1"
|
@@ -1231,6 +1437,9 @@ class Lexer
|
|
1231
1437
|
section.caption = "#{document.attributes['appendix-caption']} #{number}: "
|
1232
1438
|
Document::AttributeEntry.new('appendix-number', number).save_to(attributes)
|
1233
1439
|
end
|
1440
|
+
elsif sect_title.downcase == 'synopsis' && document.doctype == 'manpage'
|
1441
|
+
section.special = true
|
1442
|
+
section.sectname = 'synopsis'
|
1234
1443
|
else
|
1235
1444
|
section.sectname = "sect#{section.level}"
|
1236
1445
|
end
|
@@ -1276,7 +1485,7 @@ class Lexer
|
|
1276
1485
|
def self.is_next_line_section?(reader, attributes)
|
1277
1486
|
return false if !(val = attributes[1]).nil? && ['float', 'discrete'].include?(val)
|
1278
1487
|
return false if !reader.has_more_lines?
|
1279
|
-
is_section_title?(*reader.peek_lines(2))
|
1488
|
+
Compliance.underline_style_section_titles ? is_section_title?(*reader.peek_lines(2)) : is_section_title?(reader.peek_line)
|
1280
1489
|
end
|
1281
1490
|
|
1282
1491
|
# Internal: Convenience API for checking if the next line on the Reader is the document title
|
@@ -1299,7 +1508,7 @@ class Lexer
|
|
1299
1508
|
def self.is_section_title?(line1, line2 = nil)
|
1300
1509
|
if (level = is_single_line_section_title?(line1))
|
1301
1510
|
level
|
1302
|
-
elsif (level = is_two_line_section_title?(line1, line2))
|
1511
|
+
elsif line2 && (level = is_two_line_section_title?(line1, line2))
|
1303
1512
|
level
|
1304
1513
|
else
|
1305
1514
|
false
|
@@ -1307,7 +1516,8 @@ class Lexer
|
|
1307
1516
|
end
|
1308
1517
|
|
1309
1518
|
def self.is_single_line_section_title?(line1)
|
1310
|
-
|
1519
|
+
first_char = line1.nil? ? nil : line1[0..0]
|
1520
|
+
if (first_char == '=' || (Compliance.markdown_syntax && first_char == '#')) &&
|
1311
1521
|
(match = line1.match(REGEXP[:section_title]))
|
1312
1522
|
single_line_section_level match[1]
|
1313
1523
|
else
|
@@ -1319,7 +1529,7 @@ class Lexer
|
|
1319
1529
|
if !line1.nil? && !line2.nil? && SECTION_LEVELS.has_key?(line2[0..0]) &&
|
1320
1530
|
line2.match(REGEXP[:section_underline]) && line1.match(REGEXP[:section_name]) &&
|
1321
1531
|
# chomp so that a (non-visible) endline does not impact calculation
|
1322
|
-
(line1
|
1532
|
+
(line_length(line1) - line_length(line2)).abs <= 1
|
1323
1533
|
section_level line2
|
1324
1534
|
else
|
1325
1535
|
false
|
@@ -1370,23 +1580,24 @@ class Lexer
|
|
1370
1580
|
#--
|
1371
1581
|
# NOTE for efficiency, we don't reuse methods that check for a section title
|
1372
1582
|
def self.parse_section_title(reader, document)
|
1373
|
-
line1 = reader.
|
1583
|
+
line1 = reader.read_line
|
1374
1584
|
sect_id = nil
|
1375
1585
|
sect_title = nil
|
1376
1586
|
sect_level = -1
|
1377
1587
|
single_line = true
|
1378
1588
|
|
1379
|
-
|
1589
|
+
first_char = line1[0..0]
|
1590
|
+
if (first_char == '=' || (Compliance.markdown_syntax && first_char == '#')) &&
|
1380
1591
|
(match = line1.match(REGEXP[:section_title]))
|
1381
1592
|
sect_id = match[3]
|
1382
1593
|
sect_title = match[2]
|
1383
1594
|
sect_level = single_line_section_level match[1]
|
1384
|
-
|
1385
|
-
line2 = reader.peek_line
|
1595
|
+
elsif Compliance.underline_style_section_titles
|
1596
|
+
line2 = reader.peek_line true
|
1386
1597
|
if !line2.nil? && SECTION_LEVELS.has_key?(line2[0..0]) && line2.match(REGEXP[:section_underline]) &&
|
1387
1598
|
(name_match = line1.match(REGEXP[:section_name])) &&
|
1388
1599
|
# chomp so that a (non-visible) endline does not impact calculation
|
1389
|
-
(line1
|
1600
|
+
(line_length(line1) - line_length(line2)).abs <= 1
|
1390
1601
|
if anchor_match = name_match[1].match(REGEXP[:anchor_embedded])
|
1391
1602
|
sect_id = anchor_match[2]
|
1392
1603
|
sect_title = anchor_match[1]
|
@@ -1395,7 +1606,7 @@ class Lexer
|
|
1395
1606
|
end
|
1396
1607
|
sect_level = section_level line2
|
1397
1608
|
single_line = false
|
1398
|
-
reader.
|
1609
|
+
reader.advance
|
1399
1610
|
end
|
1400
1611
|
end
|
1401
1612
|
if sect_level >= 0
|
@@ -1404,6 +1615,15 @@ class Lexer
|
|
1404
1615
|
[sect_id, sect_title, sect_level, single_line]
|
1405
1616
|
end
|
1406
1617
|
|
1618
|
+
# Public: Calculate the number of unicode characters in the line, excluding the endline
|
1619
|
+
#
|
1620
|
+
# line - the String to calculate
|
1621
|
+
#
|
1622
|
+
# returns the number of unicode characters in the line
|
1623
|
+
def self.line_length(line)
|
1624
|
+
FORCE_UNICODE_LINE_LENGTH ? line.chomp.scan(/./u).length : line.chomp.length
|
1625
|
+
end
|
1626
|
+
|
1407
1627
|
# Public: Consume and parse the two header lines (line 1 = author info, line 2 = revision info).
|
1408
1628
|
#
|
1409
1629
|
# Returns the Hash of header metadata. If a Document object is supplied, the metadata
|
@@ -1425,8 +1645,8 @@ class Lexer
|
|
1425
1645
|
implicit_author = nil
|
1426
1646
|
implicit_authors = nil
|
1427
1647
|
|
1428
|
-
if reader.has_more_lines? && !reader.
|
1429
|
-
author_metadata = process_authors reader.
|
1648
|
+
if reader.has_more_lines? && !reader.next_line_empty?
|
1649
|
+
author_metadata = process_authors reader.read_line
|
1430
1650
|
|
1431
1651
|
unless author_metadata.empty?
|
1432
1652
|
# apply header subs and assign to document
|
@@ -1449,8 +1669,8 @@ class Lexer
|
|
1449
1669
|
|
1450
1670
|
rev_metadata = {}
|
1451
1671
|
|
1452
|
-
if reader.has_more_lines? && !reader.
|
1453
|
-
rev_line = reader.
|
1672
|
+
if reader.has_more_lines? && !reader.next_line_empty?
|
1673
|
+
rev_line = reader.read_line
|
1454
1674
|
if match = rev_line.match(REGEXP[:revision_info])
|
1455
1675
|
rev_metadata['revdate'] = match[2].strip
|
1456
1676
|
rev_metadata['revnumber'] = match[1].rstrip unless match[1].nil?
|
@@ -1640,7 +1860,7 @@ class Lexer
|
|
1640
1860
|
next_line = reader.peek_line
|
1641
1861
|
if (commentish = next_line.start_with?('//')) && (match = next_line.match(REGEXP[:comment_blk]))
|
1642
1862
|
terminator = match[0]
|
1643
|
-
reader.
|
1863
|
+
reader.read_lines_until(:skip_first_line => true, :preserve_last_line => true, :terminator => terminator, :skip_processing => true)
|
1644
1864
|
elsif commentish && next_line.match(REGEXP[:comment])
|
1645
1865
|
# do nothing, we'll skip it
|
1646
1866
|
elsif !options[:text] && (match = next_line.match(REGEXP[:attr_entry]))
|
@@ -1648,7 +1868,7 @@ class Lexer
|
|
1648
1868
|
elsif match = next_line.match(REGEXP[:anchor])
|
1649
1869
|
id, reftext = match[1].split(',')
|
1650
1870
|
attributes['id'] = id
|
1651
|
-
# AsciiDoc always
|
1871
|
+
# AsciiDoc always uses [id] as the reftext in HTML output,
|
1652
1872
|
# but I'd like to do better in Asciidoctor
|
1653
1873
|
#parent.document.register(:ids, id)
|
1654
1874
|
if reftext
|
@@ -1716,6 +1936,10 @@ class Lexer
|
|
1716
1936
|
# a nil value signals the attribute should be deleted (undefined)
|
1717
1937
|
value = nil
|
1718
1938
|
name = name.chop
|
1939
|
+
elsif name.start_with?('!')
|
1940
|
+
# a nil value signals the attribute should be deleted (undefined)
|
1941
|
+
value = nil
|
1942
|
+
name = name[1..-1]
|
1719
1943
|
end
|
1720
1944
|
|
1721
1945
|
name = sanitize_attribute_name(name)
|
@@ -1747,9 +1971,9 @@ class Lexer
|
|
1747
1971
|
# validate - Whether to validate the value of the marker
|
1748
1972
|
#
|
1749
1973
|
# Returns the String 0-index marker for this list item
|
1750
|
-
def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false)
|
1974
|
+
def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false, reader = nil)
|
1751
1975
|
if list_type == :olist && !marker.start_with?('.')
|
1752
|
-
resolve_ordered_list_marker(marker, ordinal, validate)
|
1976
|
+
resolve_ordered_list_marker(marker, ordinal, validate, reader)
|
1753
1977
|
elsif list_type == :colist
|
1754
1978
|
'<1>'
|
1755
1979
|
else
|
@@ -1778,7 +2002,7 @@ class Lexer
|
|
1778
2002
|
# # => 'A.'
|
1779
2003
|
#
|
1780
2004
|
# Returns the String of the first marker in this number series
|
1781
|
-
def self.resolve_ordered_list_marker(marker, ordinal = 0, validate = false)
|
2005
|
+
def self.resolve_ordered_list_marker(marker, ordinal = 0, validate = false, reader = nil)
|
1782
2006
|
number_style = ORDERED_LIST_STYLES.detect {|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) }
|
1783
2007
|
expected = actual = nil
|
1784
2008
|
case number_style
|
@@ -1817,8 +2041,7 @@ class Lexer
|
|
1817
2041
|
end
|
1818
2042
|
|
1819
2043
|
if validate && expected != actual
|
1820
|
-
|
1821
|
-
puts "asciidoctor: WARNING: list item index: expected #{expected}, got #{actual}"
|
2044
|
+
warn "asciidoctor: WARNING: #{reader.line_info}: list item index: expected #{expected}, got #{actual}"
|
1822
2045
|
end
|
1823
2046
|
|
1824
2047
|
marker
|
@@ -1872,11 +2095,19 @@ class Lexer
|
|
1872
2095
|
explicit_col_specs = false
|
1873
2096
|
end
|
1874
2097
|
|
1875
|
-
table_reader.skip_blank_lines
|
2098
|
+
skipped = table_reader.skip_blank_lines
|
1876
2099
|
|
1877
|
-
parser_ctx = Table::ParserContext.new(table, attributes)
|
2100
|
+
parser_ctx = Table::ParserContext.new(table_reader, table, attributes)
|
2101
|
+
loop_idx = -1
|
1878
2102
|
while table_reader.has_more_lines?
|
1879
|
-
|
2103
|
+
loop_idx += 1
|
2104
|
+
line = table_reader.read_line
|
2105
|
+
|
2106
|
+
if skipped == 0 && loop_idx.zero? && !attributes.has_key?('options') &&
|
2107
|
+
!(next_line = table_reader.peek_line).nil? && next_line == ::Asciidoctor::EOL
|
2108
|
+
table.has_header_option = true
|
2109
|
+
table.set_option 'header'
|
2110
|
+
end
|
1880
2111
|
|
1881
2112
|
if parser_ctx.format == 'psv'
|
1882
2113
|
if parser_ctx.starts_with_delimiter? line
|
@@ -1889,8 +2120,7 @@ class Lexer
|
|
1889
2120
|
if !next_cell_spec.nil?
|
1890
2121
|
parser_ctx.close_open_cell next_cell_spec
|
1891
2122
|
else
|
1892
|
-
# QUESTION do we not advance to next line? if so, when
|
1893
|
-
# will we if we came into this block?
|
2123
|
+
# QUESTION do we not advance to next line? if so, when will we if we came into this block?
|
1894
2124
|
end
|
1895
2125
|
end
|
1896
2126
|
end
|
@@ -1924,7 +2154,7 @@ class Lexer
|
|
1924
2154
|
# no other delimiters to see here
|
1925
2155
|
# suck up this line into the buffer and move on
|
1926
2156
|
parser_ctx.buffer = %(#{parser_ctx.buffer}#{line})
|
1927
|
-
# QUESTION make
|
2157
|
+
# QUESTION make stripping endlines in csv data an option? (unwrap-option?)
|
1928
2158
|
if parser_ctx.format == 'csv'
|
1929
2159
|
parser_ctx.buffer = %(#{parser_ctx.buffer.rstrip} )
|
1930
2160
|
end
|
@@ -1938,7 +2168,7 @@ class Lexer
|
|
1938
2168
|
end
|
1939
2169
|
end
|
1940
2170
|
|
1941
|
-
table_reader.skip_blank_lines unless parser_ctx.cell_open?
|
2171
|
+
skipped = table_reader.skip_blank_lines unless parser_ctx.cell_open?
|
1942
2172
|
|
1943
2173
|
if !table_reader.has_more_lines?
|
1944
2174
|
parser_ctx.close_cell true
|
@@ -1995,7 +2225,8 @@ class Lexer
|
|
1995
2225
|
end
|
1996
2226
|
end
|
1997
2227
|
|
1998
|
-
#
|
2228
|
+
# to_i permits us to support percentage width by stripping the %
|
2229
|
+
# NOTE this is slightly out of compliance w/ AsciiDoc, but makes way more sense
|
1999
2230
|
spec['width'] = !m[3].nil? ? m[3].to_i : 1
|
2000
2231
|
|
2001
2232
|
# make this an operation
|
@@ -2069,54 +2300,103 @@ class Lexer
|
|
2069
2300
|
# both the original style attribute and the parsed value from the first
|
2070
2301
|
# positional attribute.
|
2071
2302
|
#
|
2072
|
-
# attributes - The Hash of attributes to process
|
2303
|
+
# attributes - The Hash of attributes to process and update
|
2073
2304
|
#
|
2074
2305
|
# Examples
|
2075
2306
|
#
|
2076
2307
|
# puts attributes
|
2077
|
-
# => {1 => "abstract#intro.lead", "style" => "preamble"}
|
2308
|
+
# => {1 => "abstract#intro.lead%fragment", "style" => "preamble"}
|
2078
2309
|
#
|
2079
2310
|
# parse_style_attribute(attributes)
|
2080
2311
|
# => ["abstract", "preamble"]
|
2081
2312
|
#
|
2082
2313
|
# puts attributes
|
2083
|
-
# => {1 => "abstract#intro.lead", "style" => "abstract", "id" => "intro",
|
2314
|
+
# => {1 => "abstract#intro.lead", "style" => "abstract", "id" => "intro",
|
2315
|
+
# "role" => "lead", "options" => ["fragment"], "fragment-option" => ''}
|
2084
2316
|
#
|
2085
2317
|
# Returns a two-element Array of the parsed style from the
|
2086
2318
|
# first positional attribute and the original style that was
|
2087
2319
|
# replaced
|
2088
|
-
def self.parse_style_attribute(attributes)
|
2320
|
+
def self.parse_style_attribute(attributes, reader = nil)
|
2089
2321
|
original_style = attributes['style']
|
2090
2322
|
raw_style = attributes[1]
|
2323
|
+
# NOTE spaces are not allowed in shorthand, so if we find one, this ain't shorthand
|
2091
2324
|
if !raw_style || raw_style.include?(' ')
|
2092
2325
|
attributes['style'] = raw_style
|
2093
2326
|
[raw_style, original_style]
|
2094
|
-
# FIXME this logic could be condensed
|
2095
2327
|
else
|
2096
|
-
|
2097
|
-
|
2098
|
-
|
2099
|
-
|
2100
|
-
|
2101
|
-
if
|
2102
|
-
|
2103
|
-
|
2104
|
-
|
2328
|
+
type = :style
|
2329
|
+
collector = []
|
2330
|
+
parsed = {}
|
2331
|
+
# QUESTION should this be a private method? (though, it's never called if shorthand isn't used)
|
2332
|
+
save_current = lambda {
|
2333
|
+
if collector.empty?
|
2334
|
+
if type != :style
|
2335
|
+
warn "asciidoctor: WARNING:#{reader.nil? ? nil : " #{reader.prev_line_info}:"} invalid empty #{type} detected in style attribute"
|
2336
|
+
end
|
2105
2337
|
else
|
2106
|
-
|
2338
|
+
case type
|
2339
|
+
when :role, :option
|
2340
|
+
parsed[type] ||= []
|
2341
|
+
parsed[type].push collector.join
|
2342
|
+
when :id
|
2343
|
+
if parsed.has_key? :id
|
2344
|
+
warn "asciidoctor: WARNING:#{reader.nil? ? nil : " #{reader.prev_line_info}:"} multiple ids detected in style attribute"
|
2345
|
+
end
|
2346
|
+
parsed[type] = collector.join
|
2347
|
+
else
|
2348
|
+
parsed[type] = collector.join
|
2349
|
+
end
|
2350
|
+
collector = []
|
2107
2351
|
end
|
2108
|
-
|
2109
|
-
|
2110
|
-
|
2111
|
-
if
|
2112
|
-
|
2113
|
-
|
2114
|
-
|
2352
|
+
}
|
2353
|
+
|
2354
|
+
raw_style.split('').each do |c|
|
2355
|
+
if c == '.' || c == '#' || c == '%'
|
2356
|
+
save_current.call
|
2357
|
+
case c
|
2358
|
+
when '.'
|
2359
|
+
type = :role
|
2360
|
+
when '#'
|
2361
|
+
type = :id
|
2362
|
+
when '%'
|
2363
|
+
type = :option
|
2364
|
+
end
|
2115
2365
|
else
|
2116
|
-
|
2366
|
+
collector.push c
|
2117
2367
|
end
|
2118
|
-
|
2368
|
+
end
|
2369
|
+
|
2370
|
+
# small optimization if no shorthand is found
|
2371
|
+
if type == :style
|
2119
2372
|
parsed_style = attributes['style'] = raw_style
|
2373
|
+
else
|
2374
|
+
save_current.call
|
2375
|
+
|
2376
|
+
if parsed.has_key? :style
|
2377
|
+
parsed_style = attributes['style'] = parsed[:style]
|
2378
|
+
else
|
2379
|
+
parsed_style = nil
|
2380
|
+
end
|
2381
|
+
|
2382
|
+
if parsed.has_key? :id
|
2383
|
+
attributes['id'] = parsed[:id]
|
2384
|
+
end
|
2385
|
+
|
2386
|
+
if parsed.has_key? :role
|
2387
|
+
attributes['role'] = parsed[:role] * ' '
|
2388
|
+
end
|
2389
|
+
|
2390
|
+
if parsed.has_key? :option
|
2391
|
+
(options = parsed[:option]).each do |option|
|
2392
|
+
attributes["#{option}-option"] = ''
|
2393
|
+
end
|
2394
|
+
if (existing_opts = attributes['options'])
|
2395
|
+
attributes['options'] = (options + existing_opts.split(',')) * ','
|
2396
|
+
else
|
2397
|
+
attributes['options'] = options * ','
|
2398
|
+
end
|
2399
|
+
end
|
2120
2400
|
end
|
2121
2401
|
|
2122
2402
|
[parsed_style, original_style]
|
@@ -2159,6 +2439,8 @@ class Lexer
|
|
2159
2439
|
# # => end
|
2160
2440
|
#
|
2161
2441
|
# returns the Array of String lines with block offset removed
|
2442
|
+
#--
|
2443
|
+
# FIXME refactor gsub matchers into compiled regex
|
2162
2444
|
def self.reset_block_indent!(lines, indent = 0)
|
2163
2445
|
return if indent.nil? || lines.empty?
|
2164
2446
|
|