asciidoctor 0.0.9 → 0.1.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.

Files changed (42) hide show
  1. data/README.asciidoc +163 -41
  2. data/Rakefile +3 -1
  3. data/asciidoctor.gemspec +13 -5
  4. data/bin/asciidoctor +6 -3
  5. data/bin/asciidoctor-safe +13 -0
  6. data/lib/asciidoctor.rb +237 -26
  7. data/lib/asciidoctor/abstract_node.rb +27 -17
  8. data/lib/asciidoctor/attribute_list.rb +6 -0
  9. data/lib/asciidoctor/backends/base_template.rb +3 -4
  10. data/lib/asciidoctor/backends/docbook45.rb +114 -55
  11. data/lib/asciidoctor/backends/html5.rb +173 -104
  12. data/lib/asciidoctor/cli/invoker.rb +105 -0
  13. data/lib/asciidoctor/cli/options.rb +146 -0
  14. data/lib/asciidoctor/document.rb +135 -35
  15. data/lib/asciidoctor/lexer.rb +86 -33
  16. data/lib/asciidoctor/list_item.rb +2 -2
  17. data/lib/asciidoctor/reader.rb +6 -7
  18. data/lib/asciidoctor/section.rb +17 -5
  19. data/lib/asciidoctor/substituters.rb +216 -97
  20. data/lib/asciidoctor/table.rb +9 -2
  21. data/lib/asciidoctor/version.rb +1 -1
  22. data/man/asciidoctor.1 +212 -0
  23. data/man/asciidoctor.ad +156 -0
  24. data/test/attributes_test.rb +108 -5
  25. data/test/blocks_test.rb +102 -15
  26. data/test/document_test.rb +214 -3
  27. data/test/fixtures/encoding.asciidoc +4 -0
  28. data/test/fixtures/sample.asciidoc +26 -0
  29. data/test/invoker_test.rb +254 -0
  30. data/test/lexer_test.rb +53 -0
  31. data/test/links_test.rb +30 -0
  32. data/test/lists_test.rb +648 -9
  33. data/test/options_test.rb +68 -0
  34. data/test/paragraphs_test.rb +65 -1
  35. data/test/reader_test.rb +18 -4
  36. data/test/{headers_test.rb → sections_test.rb} +237 -0
  37. data/test/substitutions_test.rb +247 -5
  38. data/test/tables_test.rb +22 -4
  39. data/test/test_helper.rb +47 -3
  40. data/test/text_test.rb +20 -4
  41. metadata +34 -6
  42. data/noof.rb +0 -16
@@ -45,10 +45,11 @@ class Asciidoctor::Lexer
45
45
  def self.parse(reader, document)
46
46
  # process and plow away any attribute lines that proceed the first block so
47
47
  # we can get at the document title, if present, then begin parsing blocks
48
+ reader.skip_blank_lines
48
49
  attributes = parse_block_metadata_lines(reader, document)
49
50
 
50
51
  # by processing the header here, we enforce its position at head of the document
51
- next_level = is_next_line_section? reader
52
+ next_level = is_next_line_section? reader, attributes
52
53
  if next_level == 0
53
54
  title_info = parse_section_title(reader)
54
55
  document.title = title_info[1]
@@ -107,7 +108,7 @@ class Asciidoctor::Lexer
107
108
  # NOTE we could drop a hint in the attributes to indicate
108
109
  # that we are at a section title (so we don't have to check)
109
110
  if parent.is_a?(Document) && parent.blocks.empty? &&
110
- (parent.has_header? || !is_next_line_section?(reader))
111
+ (parent.has_header? || !is_next_line_section?(reader, attributes))
111
112
 
112
113
  if parent.has_header?
113
114
  preamble = Block.new(parent, :preamble)
@@ -147,7 +148,7 @@ class Asciidoctor::Lexer
147
148
  while reader.has_lines?
148
149
  parse_block_metadata_lines(reader, section, attributes)
149
150
 
150
- next_level = is_next_line_section?(reader)
151
+ next_level = is_next_line_section? reader, attributes
151
152
  if next_level
152
153
  doctype = parent.document.doctype
153
154
  if next_level == 0 && doctype != 'book'
@@ -236,14 +237,12 @@ class Asciidoctor::Lexer
236
237
  document = parent.document
237
238
  context = parent.is_a?(Block) ? parent.context : nil
238
239
  block = nil
239
- title = nil
240
- caption = nil
241
240
 
242
241
  while reader.has_lines? && block.nil?
243
242
  if parse_metadata && parse_block_metadata_line(reader, document, attributes, options)
244
243
  reader.next_line
245
244
  next
246
- elsif parse_sections && context.nil? && is_next_line_section?(reader)
245
+ elsif parse_sections && context.nil? && is_next_line_section?(reader, attributes)
247
246
  block, attributes = next_section(reader, parent, attributes)
248
247
  break
249
248
  end
@@ -275,6 +274,11 @@ class Asciidoctor::Lexer
275
274
  attributes['target'] = target
276
275
  document.register(:images, target)
277
276
  attributes['alt'] ||= File.basename(target, File.extname(target))
277
+ # hmmm, this assignment seems like a one-off
278
+ block.title = attributes['title']
279
+ if block.title? && attributes['caption'].nil?
280
+ attributes['caption'] = "Figure #{document.counter('figure-number')}. "
281
+ end
278
282
  else
279
283
  # drop the line if target resolves to nothing
280
284
  block = nil
@@ -362,6 +366,7 @@ class Asciidoctor::Lexer
362
366
  elsif match = this_line.match(REGEXP[:dlist])
363
367
  reader.unshift this_line
364
368
  block = next_labeled_list(reader, match, parent)
369
+ AttributeList.rekey(attributes, ['style'])
365
370
 
366
371
  elsif delimited_blk && (match = this_line.match(document.nested? ? REGEXP[:table_nested] : REGEXP[:table]))
367
372
  # table is surrounded by lines starting with a | followed by 3 or more '=' chars
@@ -369,6 +374,11 @@ class Asciidoctor::Lexer
369
374
  AttributeList.rekey(attributes, ['style'])
370
375
  table_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true)
371
376
  block = next_table(table_reader, parent, attributes)
377
+ # hmmm, this assignment seems like a one-off
378
+ block.title = attributes['title']
379
+ if block.title? && attributes['caption'].nil?
380
+ attributes['caption'] = "Table #{document.counter('table-number')}. "
381
+ end
372
382
 
373
383
  # FIXME violates DRY because it's a duplication of other block parsing
374
384
  elsif delimited_blk && (match = this_line.match(REGEXP[:example]))
@@ -381,6 +391,11 @@ class Asciidoctor::Lexer
381
391
  attributes['caption'] ||= admonition_style.capitalize
382
392
  else
383
393
  block = Block.new(parent, :example)
394
+ # hmmm, this assignment seems like a one-off
395
+ block.title = attributes['title']
396
+ if block.title? && attributes['caption'].nil?
397
+ attributes['caption'] = "Example #{document.counter('example-number')}. "
398
+ end
384
399
  end
385
400
  buffer = Reader.new reader.grab_lines_until(:terminator => terminator)
386
401
 
@@ -487,7 +502,26 @@ class Asciidoctor::Lexer
487
502
  buffer.last.chomp! unless buffer.empty?
488
503
  block = Block.new(parent, quote_context, buffer)
489
504
 
490
- else # paragraph, contiguous nonblank/noncontinuation lines
505
+ # a floating (i.e., discrete) title
506
+ elsif ['float', 'discrete'].include?(attributes[1]) && is_section_title?(this_line, reader.peek_line)
507
+ attributes['style'] = attributes[1]
508
+ reader.unshift this_line
509
+ float_id, float_title, float_level, _ = parse_section_title reader
510
+ block = Block.new(parent, :floating_title)
511
+ if float_id.nil? || float_id.empty?
512
+ # FIXME remove hack of creating throwaway Section to get at the generate_id method
513
+ tmp_sect = Section.new(parent)
514
+ tmp_sect.title = float_title
515
+ block.id = tmp_sect.generate_id
516
+ else
517
+ block.id = float_id
518
+ @document.register(:ids, [float_id, float_title])
519
+ end
520
+ block.level = float_level
521
+ block.title = float_title
522
+
523
+ # a paragraph - contiguous nonblank/noncontinuation lines
524
+ else
491
525
  reader.unshift this_line
492
526
  buffer = reader.grab_lines_until(:break_on_blank_lines => true, :preserve_last_line => true, :skip_line_comments => true) {|line|
493
527
  delimited_block?(line) || line.match(REGEXP[:attr_line]) ||
@@ -524,8 +558,8 @@ class Asciidoctor::Lexer
524
558
  # so handle accordingly
525
559
  if !block.nil?
526
560
  block.id = attributes['id'] if attributes.has_key?('id')
527
- block.title ||= (attributes['title'] || title)
528
- block.caption ||= caption unless block.is_a?(Section)
561
+ block.title = attributes['title'] unless block.title?
562
+ block.caption ||= attributes['caption'] unless block.is_a?(Section)
529
563
  # AsciiDoc always use [id] as the reftext in HTML output,
530
564
  # but I'd like to do better in Asciidoctor
531
565
  if block.id && block.title? && !attributes.has_key?('reftext')
@@ -812,22 +846,23 @@ class Asciidoctor::Lexer
812
846
 
813
847
  # a delimited block immediately breaks the list unless preceded
814
848
  # by a list continuation (they are harsh like that ;0)
815
- if (match = delimited_block?(this_line)) ||
816
- # technically attr_line only breaks if ensuing line is not a list item
817
- # which really means attr_line only breaks if it's acting as a block delimiter
818
- (list_type == :dlist && match = this_line.match(REGEXP[:attr_line]))
819
- terminator = match[0]
849
+ if match = delimited_block?(this_line)
820
850
  if continuation == :active
821
851
  buffer << this_line
822
852
  # grab all the lines in the block, leaving the delimiters in place
823
853
  # we're being more strict here about the terminator, but I think that's a good thing
854
+ terminator = match[0]
824
855
  buffer.concat reader.grab_lines_until(:terminator => terminator, :grab_last_line => true)
825
856
  continuation = :inactive
826
857
  else
827
858
  break
828
859
  end
860
+ # technically attr_line only breaks if ensuing line is not a list item
861
+ # which really means attr_line only breaks if it's acting as a block delimiter
862
+ elsif list_type == :dlist && continuation != :active && this_line.match(REGEXP[:attr_line])
863
+ break
829
864
  else
830
- if continuation == :active && !this_line.strip.empty?
865
+ if continuation == :active && !this_line.chomp.empty?
831
866
  # literal paragraphs have special considerations (and this is one of
832
867
  # two entry points into one)
833
868
  # if we don't process it as a whole, then a line in it that looks like a
@@ -837,7 +872,15 @@ class Asciidoctor::Lexer
837
872
  buffer.concat reader.grab_lines_until(
838
873
  :preserve_last_line => true,
839
874
  :break_on_blank_lines => true,
840
- :break_on_list_continuation => true)
875
+ :break_on_list_continuation => true) {|line|
876
+ # we may be in an indented list disguised as a literal paragraph
877
+ # so we need to make sure we don't slurp up a legitimate sibling
878
+ list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
879
+ }
880
+ continuation = :inactive
881
+ # let block metadata play out until we find the block
882
+ elsif this_line.match(REGEXP[:blk_title]) || this_line.match(REGEXP[:attr_line])
883
+ buffer << this_line
841
884
  else
842
885
  if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).detect {|ctx| this_line.match(REGEXP[ctx]) }
843
886
  within_nested_list = true
@@ -847,12 +890,12 @@ class Asciidoctor::Lexer
847
890
  end
848
891
  end
849
892
  buffer << this_line
893
+ continuation = :inactive
850
894
  end
851
- continuation = :inactive
852
- elsif !prev_line.nil? && prev_line.strip.empty?
895
+ elsif !prev_line.nil? && prev_line.chomp.empty?
853
896
  # advance to the next line of content
854
- if this_line.strip.empty?
855
- reader.skip_blank
897
+ if this_line.chomp.empty?
898
+ reader.skip_blank_lines
856
899
  this_line = reader.get_line
857
900
  # if we hit eof or a sibling, stop reading
858
901
  break if this_line.nil? || is_sibling_list_item?(this_line, list_type, sibling_trait)
@@ -872,13 +915,15 @@ class Asciidoctor::Lexer
872
915
  buffer.concat reader.grab_lines_until(
873
916
  :preserve_last_line => true,
874
917
  :break_on_blank_lines => true,
875
- :break_on_list_continuation => true)
918
+ :break_on_list_continuation => true) {|line|
919
+ # we may be in an indented list disguised as a literal paragraph
920
+ # so we need to make sure we don't slurp up a legitimate sibling
921
+ list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
922
+ }
876
923
  # TODO any way to combine this with the check after skipping blank lines?
877
924
  elsif is_sibling_list_item?(this_line, list_type, sibling_trait)
878
- #buffer.pop unless within_nested_list
879
925
  break
880
926
  elsif nested_list_type = NESTABLE_LIST_CONTEXTS.detect {|ctx| this_line.match(REGEXP[ctx]) }
881
- #buffer.pop unless within_nested_list
882
927
  buffer << this_line
883
928
  within_nested_list = true
884
929
  if nested_list_type == :dlist && $~[3].to_s.empty?
@@ -896,7 +941,7 @@ class Asciidoctor::Lexer
896
941
  end
897
942
  end
898
943
  else
899
- has_text = true if !this_line.strip.empty?
944
+ has_text = true if !this_line.chomp.empty?
900
945
  if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).detect {|ctx| this_line.match(REGEXP[ctx]) }
901
946
  within_nested_list = true
902
947
  if nested_list_type == :dlist && $~[3].to_s.empty?
@@ -916,16 +961,16 @@ class Asciidoctor::Lexer
916
961
  buffer.delete_at detached_continuation
917
962
  end
918
963
 
919
- # QUESTION should we strip these trailing endlines?
920
- #buffer.pop while buffer.last == "\n"
964
+ # strip trailing blank lines to prevent empty blocks
965
+ buffer.pop while !buffer.empty? && buffer.last.strip.empty?
921
966
 
922
967
  # We do need to replace the optional trailing continuation
923
968
  # a blank line would have served the same purpose in the document
924
969
  if !buffer.empty? && buffer.last.chomp == LIST_CONTINUATION
925
970
  buffer.pop
926
971
  end
927
- #puts "BUFFER>#{buffer.join}<BUFFER"
928
- #puts "BUFFER>#{buffer}<BUFFER"
972
+ #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.join}<BUFFER"
973
+ #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer}<BUFFER"
929
974
 
930
975
  buffer
931
976
  end
@@ -949,6 +994,15 @@ class Asciidoctor::Lexer
949
994
  section.id ||= section.generate_id
950
995
  end
951
996
 
997
+ if attributes[1]
998
+ section.sectname = attributes[1]
999
+ section.special = true
1000
+ if section.sectname == 'appendix'
1001
+ attributes['caption'] ||= "Appendix #{parent.document.counter('appendix-number', 'A')}: "
1002
+ end
1003
+ else
1004
+ section.sectname = "sect#{section.level}"
1005
+ end
952
1006
  section.update_attributes(attributes)
953
1007
  reader.skip_blank
954
1008
 
@@ -978,14 +1032,12 @@ class Asciidoctor::Lexer
978
1032
 
979
1033
  # Internal: Checks if the next line on the Reader is a section title
980
1034
  #
981
- # This is a more efficient version of #is_section_title? and should
982
- # eventually replace its usage.
983
- #
984
1035
  # reader - the source Reader
985
1036
  #
986
1037
  # returns the section level if the Reader is positioned at a section title,
987
1038
  # false otherwise
988
- def self.is_next_line_section?(reader)
1039
+ def self.is_next_line_section?(reader, attributes)
1040
+ return false if !attributes[1].nil? && ['float', 'discrete'].include?(attributes[1])
989
1041
  if reader.has_lines?
990
1042
  line1 = reader.get_line
991
1043
  line2 = reader.peek_line
@@ -1079,7 +1131,7 @@ class Asciidoctor::Lexer
1079
1131
  line1 = reader.get_line
1080
1132
  sect_id = nil
1081
1133
  sect_title = nil
1082
- sect_level = 0
1134
+ sect_level = -1
1083
1135
  single_line = true
1084
1136
 
1085
1137
  if match = line1.match(REGEXP[:section_title])
@@ -1188,6 +1240,7 @@ class Asciidoctor::Lexer
1188
1240
  # returns the Hash of attributes including any metadata found
1189
1241
  def self.parse_block_metadata_lines(reader, parent, attributes = {}, options = {})
1190
1242
  while parse_block_metadata_line(reader, parent, attributes, options)
1243
+ # discard the line just processed
1191
1244
  reader.next_line
1192
1245
  reader.skip_blank_lines
1193
1246
  end
@@ -46,10 +46,10 @@ class Asciidoctor::ListItem < Asciidoctor::AbstractBlock
46
46
 
47
47
  block = blocks.shift
48
48
  unless @text.to_s.empty?
49
- block.buffer.unshift(@text)
49
+ block.buffer.unshift("#@text\n")
50
50
  end
51
51
 
52
- @text = block.buffer.join("\n")
52
+ @text = block.buffer.join
53
53
  end
54
54
  nil
55
55
  end
@@ -37,9 +37,6 @@ class Asciidoctor::Reader
37
37
  @lines = []
38
38
  end
39
39
 
40
- # just in case we got some nils floating at the end of our lines after reading a funky document
41
- @lines.pop until @lines.empty? || !@lines.last.nil?
42
-
43
40
  @source = @lines.join
44
41
  end
45
42
 
@@ -303,6 +300,8 @@ class Asciidoctor::Reader
303
300
  @lines = []
304
301
 
305
302
  raw_source.each do |line|
303
+ # normalize line ending to LF (purging occurrences of CRLF)
304
+ line = "#{line.rstrip}\n"
306
305
  if skip_to
307
306
  skip_to = nil if line.match(skip_to)
308
307
  elsif continuing_value
@@ -310,7 +309,7 @@ class Asciidoctor::Reader
310
309
  # Lines that start with whitespace and end with a '+' are
311
310
  # a continuation, so gobble them up into `value`
312
311
  if line.match(REGEXP[:attr_continue])
313
- continuing_value += ' ' + $1
312
+ continuing_value += ' ' + $1.rstrip
314
313
  # An empty line ends a continuation
315
314
  elsif line.strip.empty?
316
315
  raw_source.unshift(line)
@@ -343,7 +342,7 @@ class Asciidoctor::Reader
343
342
  # attribute value continuation line; grab lines until we run out
344
343
  # of continuation lines
345
344
  continuing_key = key
346
- continuing_value = $1 # strip off the spaces and +
345
+ continuing_value = $1.rstrip # strip off the spaces and +
347
346
  else
348
347
  unless attribute_overridden? key
349
348
  @document.attributes[key] = apply_attribute_value_subs(value)
@@ -360,9 +359,9 @@ class Asciidoctor::Reader
360
359
  elsif !line.match(REGEXP[:endif_macro])
361
360
  while line.match(REGEXP[:attr_conditional])
362
361
  value = @document.attributes.has_key?($1) ? $2 : ''
363
- line.sub!(conditional_regexp, value)
362
+ line.sub!(REGEXP[:attr_conditional], value)
364
363
  end
365
- # leave line comments in as they play a role in flow (such as a list divider)
364
+ # NOTE leave line comments in as they play a role in flow (such as a list divider)
366
365
  @lines << line
367
366
  end
368
367
  end
@@ -22,6 +22,12 @@ class Asciidoctor::Section < Asciidoctor::AbstractBlock
22
22
  # Public: Get/Set the Integer index of this section within the parent block
23
23
  attr_accessor :index
24
24
 
25
+ # Public: Get/Set the section name of this section
26
+ attr_accessor :sectname
27
+
28
+ # Public: Get/Set the flag to indicate whether this is a special section or a child of one
29
+ attr_accessor :special
30
+
25
31
  # Public: Initialize an Asciidoctor::Section object.
26
32
  #
27
33
  # parent - The parent Asciidoc Object.
@@ -30,6 +36,11 @@ class Asciidoctor::Section < Asciidoctor::AbstractBlock
30
36
  if level.nil? && !parent.nil?
31
37
  @level = parent.level + 1
32
38
  end
39
+ if parent.is_a?(::Asciidoctor::Section) && parent.special
40
+ @special = true
41
+ else
42
+ @special = false
43
+ end
33
44
  @index = 0
34
45
  end
35
46
 
@@ -58,16 +69,17 @@ class Asciidoctor::Section < Asciidoctor::AbstractBlock
58
69
  # another_section.generate_id
59
70
  # => "_foo_1"
60
71
  def generate_id
61
- if @document.attr?('sectids')
62
- base_id = @document.attr('idprefix', '_') + title.downcase.gsub(/&#[0-9]+;/, '_').
63
- gsub(/\W+/, '_').tr_s('_', '_').gsub(/^_?(.*?)_?$/, '\1')
72
+ if @document.attr? 'sectids'
73
+ separator = @document.attr('idseparator', '_')
74
+ base_id = @document.attr('idprefix', '_') + title.downcase.gsub(/&#[0-9]+;/, separator).
75
+ gsub(/\W+/, separator).tr_s(separator, separator).chomp(separator)
64
76
  gen_id = base_id
65
77
  cnt = 2
66
78
  while @document.references[:ids].has_key? gen_id
67
- gen_id = "#{base_id}_#{cnt}"
79
+ gen_id = "#{base_id}#{separator}#{cnt}"
68
80
  cnt += 1
69
81
  end
70
- @document.references[:ids][gen_id] = title
82
+ @document.register(:ids, [gen_id, title])
71
83
  gen_id
72
84
  else
73
85
  nil
@@ -250,14 +250,25 @@ module Asciidoctor
250
250
  reject = false
251
251
  subject = line.dup
252
252
  subject.gsub!(REGEXP[:attr_ref]) {
253
+ # alias match for Ruby 1.8.7 compat
254
+ m = $~
255
+ key = m[2].downcase
256
+ # escaped attribute
253
257
  if !$1.empty? || !$3.empty?
254
258
  "{#$2}"
255
- elsif document.attributes.has_key? $2
256
- document.attributes[$2]
257
- elsif INTRINSICS.has_key? $2
258
- INTRINSICS[$2]
259
+ elsif m[2].start_with?('counter:')
260
+ args = m[2].split(':')
261
+ @document.counter(args[1], args[2])
262
+ elsif m[2].start_with?('counter2:')
263
+ args = m[2].split(':')
264
+ @document.counter(args[1], args[2])
265
+ ''
266
+ elsif document.attributes.has_key? key
267
+ @document.attributes[key]
268
+ elsif INTRINSICS.has_key? key
269
+ INTRINSICS[key]
259
270
  else
260
- Asciidoctor.debug { "Missing attribute: #$2, line marked for removal" }
271
+ Asciidoctor.debug { "Missing attribute: #{m[2]}, line marked for removal" }
261
272
  reject = true
262
273
  break '{undefined}'
263
274
  end
@@ -281,101 +292,202 @@ module Asciidoctor
281
292
 
282
293
  result = text.dup
283
294
 
284
- # inline images, image:target.ext[Alt]
285
- result.gsub!(REGEXP[:image_macro]) {
286
- # alias match for Ruby 1.8.7 compat
287
- m = $~
288
- # honor the escape
289
- if m[0].start_with? '\\'
290
- next m[0][1..-1]
291
- end
292
- target = sub_attributes(m[1])
293
- @document.register(:images, target)
294
- attrs = parse_attributes(m[2], ['alt', 'width', 'height'])
295
- if !attrs.has_key?('alt') || attrs['alt'].empty?
296
- attrs['alt'] = File.basename(target, File.extname(target))
297
- end
298
- Inline.new(self, :image, nil, :target => target, :attributes => attrs).render
299
- } unless !result.include?('image:')
295
+ # some look ahead assertions to cut unnecessary regex calls
296
+ found = {}
297
+ found[:square_bracket] = result.include?('[')
298
+ found[:round_bracket] = result.include?('(')
299
+ found[:macroish] = (found[:square_bracket] && result.include?(':'))
300
+ found[:macroish_short_form] = (found[:square_bracket] && result.include?(':['))
301
+ found[:uri] = result.include?('://')
302
+
303
+ if found[:macroish] && result.include?('image:')
304
+ # image:filename.png[Alt Text]
305
+ result.gsub!(REGEXP[:image_macro]) {
306
+ # alias match for Ruby 1.8.7 compat
307
+ m = $~
308
+ # honor the escape
309
+ if m[0].start_with? '\\'
310
+ next m[0][1..-1]
311
+ end
312
+ target = sub_attributes(m[1])
313
+ @document.register(:images, target)
314
+ attrs = parse_attributes(m[2], ['alt', 'width', 'height'])
315
+ if !attrs.has_key?('alt') || attrs['alt'].empty?
316
+ attrs['alt'] = File.basename(target, File.extname(target))
317
+ end
318
+ Inline.new(self, :image, nil, :target => target, :attributes => attrs).render
319
+ }
320
+ end
300
321
 
301
- # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>)
302
- result.gsub!(REGEXP[:link_inline]) {
303
- # alias match for Ruby 1.8.7 compat
304
- m = $~
305
- # honor the escape
306
- if m[2].start_with? '\\'
307
- next "#{m[1]}#{m[2][1..-1]}#{m[3]}"
308
- # not a valid macro syntax w/o trailing square brackets
309
- # we probably shouldn't even get here...our regex is doing too much
310
- elsif m[1] == 'link:' && m[3].nil?
311
- next m[0]
312
- end
313
- prefix = (m[1] != 'link:' ? m[1] : '')
314
- target = m[2]
315
- # strip the <> around the link
316
- if prefix.end_with? '&lt;'
317
- prefix = prefix[0..-5]
318
- end
319
- if target.end_with? '&gt;'
320
- target = target[0..-5]
321
- end
322
- @document.register(:links, target)
323
- text = !m[3].nil? ? sub_attributes(m[3].gsub('\]', ']')) : ''
324
- "#{prefix}#{Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target).render}"
325
- } unless !result.include?('http')
322
+ if found[:macroish_short_form] || found[:round_bracket]
323
+ # indexterm:[Tigers,Big cats]
324
+ # (((Tigers,Big cats)))
325
+ result.gsub!(REGEXP[:indexterm_macro]) {
326
+ # alias match for Ruby 1.8.7 compat
327
+ m = $~
328
+ # honor the escape
329
+ if m[0].start_with? '\\'
330
+ next m[0][1..-1]
331
+ end
326
332
 
327
- # inline link macros, link:target[text]
328
- result.gsub!(REGEXP[:link_macro]) {
329
- # alias match for Ruby 1.8.7 compat
330
- m = $~
331
- # honor the escape
332
- if m[0].start_with? '\\'
333
- next m[0][1..-1]
334
- end
335
- target = m[1]
336
- @document.register(:links, target)
337
- text = sub_attributes(m[2].gsub('\]', ']'))
338
- Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target).render
339
- } unless !result.include?('link:')
333
+ terms = (m[1] || m[2]).strip.tr("\n", ' ').gsub('\]', ']').split(REGEXP[:csv_delimiter])
334
+ document.register(:indexterms, [*terms])
335
+ Inline.new(self, :indexterm, text, :attributes => {'terms' => terms}).render
336
+ }
337
+
338
+ # indexterm2:[Tigers]
339
+ # ((Tigers))
340
+ result.gsub!(REGEXP[:indexterm2_macro]) {
341
+ # alias match for Ruby 1.8.7 compat
342
+ m = $~
343
+ # honor the escape
344
+ if m[0].start_with? '\\'
345
+ next m[0][1..-1]
346
+ end
340
347
 
341
- result.gsub!(REGEXP[:xref_macro]) {
342
- # alias match for Ruby 1.8.7 compat
343
- m = $~
344
- # honor the escape
345
- if m[0].start_with? '\\'
346
- next m[0][1..-1]
347
- end
348
- if !m[1].nil?
349
- id, reftext = m[1].split(',', 2)
350
- id.sub!(/^("|)(.*)\1$/, '\2')
351
- reftext.sub!(/^("|)(.*)\1$/m, '\2') unless reftext.nil?
352
- else
353
- id = m[2]
354
- reftext = !m[3].empty? ? m[3] : nil
355
- end
356
- Inline.new(self, :anchor, reftext, :type => :xref, :target => id).render
357
- }
348
+ text = (m[1] || m[2]).strip.tr("\n", ' ').gsub('\]', ']')
349
+ document.register(:indexterms, [text])
350
+ Inline.new(self, :indexterm, text, :type => :visible).render
351
+ }
352
+ end
358
353
 
359
- result.gsub!(REGEXP[:anchor_macro]) {
360
- # alias match for Ruby 1.8.7 compat
361
- m = $~
362
- # honor the escape
363
- if m[0].start_with? '\\'
364
- next m[0][1..-1]
365
- end
366
- id, reftext = m[1].split(',')
367
- id.sub!(/^("|)(.*)\1$/, '\2')
368
- if reftext.nil?
369
- reftext = "[#{id}]"
370
- else
371
- reftext.sub!(/^("|)(.*)\1$/m, '\2')
372
- end
373
- # NOTE the reftext should also match what's in our references dic
374
- if !@document.references[:ids].has_key? id
375
- Asciidoctor.debug { "Missing reference for anchor #{id}" }
376
- end
377
- Inline.new(self, :anchor, reftext, :type => :ref, :target => id).render
378
- } unless !result.include?('[[')
354
+ if found[:uri]
355
+ # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>)
356
+ result.gsub!(REGEXP[:link_inline]) {
357
+ # alias match for Ruby 1.8.7 compat
358
+ m = $~
359
+ # honor the escape
360
+ if m[2].start_with? '\\'
361
+ next "#{m[1]}#{m[2][1..-1]}#{m[3]}"
362
+ # not a valid macro syntax w/o trailing square brackets
363
+ # we probably shouldn't even get here...our regex is doing too much
364
+ elsif m[1] == 'link:' && m[3].nil?
365
+ next m[0]
366
+ end
367
+ prefix = (m[1] != 'link:' ? m[1] : '')
368
+ target = m[2]
369
+ # strip the <> around the link
370
+ if prefix.end_with? '&lt;'
371
+ prefix = prefix[0..-5]
372
+ end
373
+ if target.end_with? '&gt;'
374
+ target = target[0..-5]
375
+ end
376
+ @document.register(:links, target)
377
+ text = !m[3].nil? ? sub_attributes(m[3].gsub('\]', ']')) : ''
378
+ "#{prefix}#{Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target).render}"
379
+ }
380
+ end
381
+
382
+ if found[:macroish] && result.include?('link:')
383
+ # inline link macros, link:target[text]
384
+ result.gsub!(REGEXP[:link_macro]) {
385
+ # alias match for Ruby 1.8.7 compat
386
+ m = $~
387
+ # honor the escape
388
+ if m[0].start_with? '\\'
389
+ next m[0][1..-1]
390
+ end
391
+ target = m[1]
392
+ @document.register(:links, target)
393
+ text = sub_attributes(m[2].gsub('\]', ']'))
394
+ Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target).render
395
+ }
396
+ end
397
+
398
+ if found[:macroish_short_form] && result.include?('footnote')
399
+ result.gsub!(REGEXP[:footnote_macro]) {
400
+ # alias match for Ruby 1.8.7 compat
401
+ m = $~
402
+ # honor the escape
403
+ if m[0].start_with? '\\'
404
+ next m[0][1..-1]
405
+ end
406
+ if m[1] == 'footnote'
407
+ # hmmmm
408
+ text = restore_passthroughs(m[2])
409
+ id = nil
410
+ index = @document.counter('footnote-number')
411
+ @document.register(:footnotes, Document::Footnote.new(index, id, text))
412
+ type = nil
413
+ target = nil
414
+ else
415
+ id, text = m[2].split(/ *, */, 2)
416
+ if !text.nil?
417
+ # hmmmm
418
+ text = restore_passthroughs(text)
419
+ index = @document.counter('footnote-number')
420
+ @document.register(:footnotes, Document::Footnote.new(index, id, text))
421
+ type = :ref
422
+ target = nil
423
+ else
424
+ fn = @document.references[:footnotes].find {|fn| fn.id == id }
425
+ target = id
426
+ id = nil
427
+ index = fn.index
428
+ text = fn.text
429
+ type = :xref
430
+ end
431
+ end
432
+ Inline.new(self, :footnote, text, :attributes => {'index' => index}, :id => id, :target => target, :type => type).render
433
+ }
434
+ end
435
+
436
+ if found[:macroish] || result.include?('&lt;&lt;')
437
+ result.gsub!(REGEXP[:xref_macro]) {
438
+ # alias match for Ruby 1.8.7 compat
439
+ m = $~
440
+ # honor the escape
441
+ if m[0].start_with? '\\'
442
+ next m[0][1..-1]
443
+ end
444
+ if !m[1].nil?
445
+ id, reftext = m[1].split(REGEXP[:csv_delimiter], 2)
446
+ id.sub!(/^("|)(.*)\1$/, '\2')
447
+ reftext.sub!(/^("|)(.*)\1$/m, '\2') unless reftext.nil?
448
+ else
449
+ id = m[2]
450
+ reftext = !m[3].empty? ? m[3] : nil
451
+ end
452
+ Inline.new(self, :anchor, reftext, :type => :xref, :target => id).render
453
+ }
454
+ end
455
+
456
+ if found[:square_bracket] && result.include?('[[[')
457
+ result.gsub!(REGEXP[:biblio_macro]) {
458
+ # alias match for Ruby 1.8.7 compat
459
+ m = $~
460
+ # honor the escape
461
+ if m[0].start_with? '\\'
462
+ next m[0][1..-1]
463
+ end
464
+ id = reftext = m[1]
465
+ Inline.new(self, :anchor, reftext, :type => :bibref, :target => id).render
466
+ }
467
+ end
468
+
469
+ if found[:square_bracket] && result.include?('[[')
470
+ result.gsub!(REGEXP[:anchor_macro]) {
471
+ # alias match for Ruby 1.8.7 compat
472
+ m = $~
473
+ # honor the escape
474
+ if m[0].start_with? '\\'
475
+ next m[0][1..-1]
476
+ end
477
+ id, reftext = m[1].split(REGEXP[:csv_delimiter])
478
+ id.sub!(/^("|)(.*)\1$/, '\2')
479
+ if reftext.nil?
480
+ reftext = "[#{id}]"
481
+ else
482
+ reftext.sub!(/^("|)(.*)\1$/m, '\2')
483
+ end
484
+ # NOTE the reftext should also match what's in our references dic
485
+ if !@document.references[:ids].has_key? id
486
+ Asciidoctor.debug { "Missing reference for anchor #{id}" }
487
+ end
488
+ Inline.new(self, :anchor, reftext, :type => :ref, :target => id).render
489
+ }
490
+ end
379
491
 
380
492
  result
381
493
  end
@@ -403,7 +515,14 @@ module Asciidoctor
403
515
  #
404
516
  # returns The String with the post replacements rendered using the backend templates
405
517
  def sub_post_replacements(text)
406
- text.gsub(REGEXP[:line_break]) { Inline.new(self, :break, $1, :type => :line).render }
518
+ if @document.attr? 'hardbreaks'
519
+ lines = text.lines.entries
520
+ return text if lines.size == 1
521
+ last = lines.pop
522
+ "#{lines.map {|line| Inline.new(self, :break, line.rstrip.chomp(' +'), :type => :line).render } * "\n"}\n#{last}"
523
+ else
524
+ text.gsub(REGEXP[:line_break]) { Inline.new(self, :break, $1, :type => :line).render }
525
+ end
407
526
  end
408
527
 
409
528
  # Internal: Transform (render) a quoted text region