asciidoctor 1.5.2 → 1.5.3

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +107 -1
  3. data/LICENSE.adoc +1 -1
  4. data/README.adoc +155 -230
  5. data/Rakefile +2 -1
  6. data/bin/asciidoctor +5 -1
  7. data/data/stylesheets/asciidoctor-default.css +37 -29
  8. data/data/stylesheets/coderay-asciidoctor.css +3 -3
  9. data/features/text_formatting.feature +2 -0
  10. data/lib/asciidoctor.rb +46 -21
  11. data/lib/asciidoctor/abstract_block.rb +14 -8
  12. data/lib/asciidoctor/abstract_node.rb +77 -24
  13. data/lib/asciidoctor/attribute_list.rb +1 -1
  14. data/lib/asciidoctor/block.rb +2 -3
  15. data/lib/asciidoctor/cli/options.rb +14 -15
  16. data/lib/asciidoctor/converter/docbook45.rb +8 -8
  17. data/lib/asciidoctor/converter/docbook5.rb +25 -17
  18. data/lib/asciidoctor/converter/factory.rb +6 -1
  19. data/lib/asciidoctor/converter/html5.rb +159 -117
  20. data/lib/asciidoctor/converter/manpage.rb +671 -0
  21. data/lib/asciidoctor/converter/template.rb +24 -17
  22. data/lib/asciidoctor/document.rb +89 -47
  23. data/lib/asciidoctor/extensions.rb +22 -21
  24. data/lib/asciidoctor/helpers.rb +73 -16
  25. data/lib/asciidoctor/list.rb +26 -5
  26. data/lib/asciidoctor/parser.rb +179 -122
  27. data/lib/asciidoctor/path_resolver.rb +6 -10
  28. data/lib/asciidoctor/reader.rb +37 -34
  29. data/lib/asciidoctor/stylesheets.rb +16 -10
  30. data/lib/asciidoctor/substitutors.rb +98 -21
  31. data/lib/asciidoctor/table.rb +21 -17
  32. data/lib/asciidoctor/timings.rb +3 -3
  33. data/lib/asciidoctor/version.rb +1 -1
  34. data/man/asciidoctor.1 +155 -89
  35. data/man/asciidoctor.adoc +19 -11
  36. data/test/attributes_test.rb +86 -0
  37. data/test/blocks_test.rb +203 -15
  38. data/test/converter_test.rb +15 -2
  39. data/test/document_test.rb +290 -36
  40. data/test/extensions_test.rb +22 -3
  41. data/test/fixtures/circle.svg +8 -0
  42. data/test/fixtures/subs-docinfo.html +2 -0
  43. data/test/fixtures/subs.adoc +7 -0
  44. data/test/invoker_test.rb +25 -0
  45. data/test/links_test.rb +17 -0
  46. data/test/lists_test.rb +173 -0
  47. data/test/options_test.rb +2 -2
  48. data/test/paragraphs_test.rb +2 -2
  49. data/test/parser_test.rb +56 -13
  50. data/test/reader_test.rb +35 -3
  51. data/test/sections_test.rb +59 -0
  52. data/test/substitutions_test.rb +53 -14
  53. data/test/tables_test.rb +158 -2
  54. data/test/test_helper.rb +7 -2
  55. metadata +22 -11
  56. data/benchmark/benchmark.rb +0 -129
  57. data/benchmark/sample-data/mdbasics.adoc +0 -334
  58. data/lib/asciidoctor/opal_ext.rb +0 -26
  59. data/lib/asciidoctor/opal_ext/comparable.rb +0 -38
  60. data/lib/asciidoctor/opal_ext/dir.rb +0 -13
  61. data/lib/asciidoctor/opal_ext/error.rb +0 -2
  62. data/lib/asciidoctor/opal_ext/file.rb +0 -145
@@ -5,24 +5,39 @@ module Helpers
5
5
  #
6
6
  # Attempts to load the library specified in the first argument using the
7
7
  # Kernel#require. Rescues the LoadError if the library is not available and
8
- # passes a message to Kernel#fail to communicate to the user that processing
9
- # is being aborted. If a gem_name is specified, the failure message
10
- # communicates that a required gem is not installed.
11
- #
12
- # name - the String name of the library to require.
13
- # gem - a Boolean that indicates whether this library is provided by a RubyGem,
14
- # or the String name of the RubyGem if it differs from the library name
15
- # (default: true)
16
- #
17
- # returns the return value of Kernel#require if the library is available,
18
- # otherwise Kernel#fail is called with an appropriate message.
19
- def self.require_library name, gem = true
8
+ # passes a message to Kernel#fail if on_failure is :abort or Kernel#warn if
9
+ # on_failure is :warn to communicate to the user that processing is being
10
+ # aborted or functionality is disabled, respectively. If a gem_name is
11
+ # specified, the message communicates that a required gem is not installed.
12
+ #
13
+ # name - the String name of the library to require.
14
+ # gem_name - a Boolean that indicates whether this library is provided by a RubyGem,
15
+ # or the String name of the RubyGem if it differs from the library name
16
+ # (default: true)
17
+ # on_failure - a Symbol that indicates how to handle a load failure (:abort, :warn, :ignore) (default: :abort)
18
+ #
19
+ # returns The return value of Kernel#require if the library is available and can be, or was previously, loaded.
20
+ # Otherwise, Kernel#fail is called with an appropriate message if on_failure is :abort.
21
+ # Otherwise, Kernel#warn is called with an appropriate message and nil returned if on_failure is :warn.
22
+ # Otherwise, nil is returned.
23
+ def self.require_library name, gem_name = true, on_failure = :abort
20
24
  require name
21
25
  rescue ::LoadError => e
22
- if gem
23
- fail %(asciidoctor: FAILED: required gem '#{gem == true ? name : gem}' is not installed. Processing aborted.)
26
+ if gem_name
27
+ gem_name = name if gem_name == true
28
+ case on_failure
29
+ when :abort
30
+ fail %(asciidoctor: FAILED: required gem '#{gem_name}' is not installed. Processing aborted.)
31
+ when :warn
32
+ warn %(asciidoctor: WARNING: optional gem '#{gem_name}' is not installed. Functionality disabled.)
33
+ end
24
34
  else
25
- fail %(asciidoctor: FAILED: #{e.message.chomp '.'}. Processing aborted.)
35
+ case on_failure
36
+ when :abort
37
+ fail %(asciidoctor: FAILED: #{e.message.chomp '.'}. Processing aborted.)
38
+ when :warn
39
+ warn %(asciidoctor: WARNING: #{e.message.chomp '.'}. Functionality disabled.)
40
+ end
26
41
  end
27
42
  end
28
43
 
@@ -109,6 +124,30 @@ module Helpers
109
124
  data.each_line.map {|line| line.rstrip }
110
125
  end
111
126
 
127
+ # Public: Efficiently checks whether the specified String resembles a URI
128
+ #
129
+ # Uses the Asciidoctor::UriSniffRx regex to check whether the String begins
130
+ # with a URI prefix (e.g., http://). No validation of the URI is performed.
131
+ #
132
+ # str - the String to check
133
+ #
134
+ # returns true if the String is a URI, false if it is not
135
+ def self.uriish? str
136
+ (str.include? ':') && str =~ UriSniffRx
137
+ end
138
+
139
+ # Public: Efficiently retrieves the URI prefix of the specified String
140
+ #
141
+ # Uses the Asciidoctor::UriSniffRx regex to match the URI prefix in the
142
+ # specified String (e.g., http://), if present.
143
+ #
144
+ # str - the String to check
145
+ #
146
+ # returns the string URI prefix if the string is a URI, otherwise nil
147
+ def self.uri_prefix str
148
+ (str.include? ':') && str =~ UriSniffRx ? $& : nil
149
+ end
150
+
112
151
  # Matches the characters in a URI to encode
113
152
  REGEXP_ENCODE_URI_CHARS = /[^\w\-.!~*';:@=+$,()\[\]]/
114
153
 
@@ -134,10 +173,28 @@ module Helpers
134
173
  #
135
174
  # Returns the String filename with the file extension removed
136
175
  def self.rootname(file_name)
137
- # alternatively, this could be written as ::File.basename file_name, ((::File.extname file_name) || '')
138
176
  (ext = ::File.extname(file_name)).empty? ? file_name : file_name[0...-ext.length]
139
177
  end
140
178
 
179
+ # Public: Retrieves the basename of the filename, optionally removing the extension, if present
180
+ #
181
+ # file_name - The String file name to process
182
+ # drop_extname - A Boolean flag indicating whether to drop the extension (default: false)
183
+ #
184
+ # Examples
185
+ #
186
+ # Helpers.basename('images/tiger.png', true)
187
+ # # => "tiger"
188
+ #
189
+ # Returns the String filename with leading directories removed and, if specified, the extension removed
190
+ def self.basename(file_name, drop_extname = false)
191
+ if drop_extname
192
+ ::File.basename file_name, ((::File.extname file_name) || '')
193
+ else
194
+ ::File.basename file_name
195
+ end
196
+ end
197
+
141
198
  def self.mkdir_p(dir)
142
199
  unless ::File.directory? dir
143
200
  parent_dir = ::File.dirname(dir)
@@ -1,19 +1,24 @@
1
1
  # encoding: UTF-8
2
2
  module Asciidoctor
3
- # Public: Methods for managing AsciiDoc lists (ordered, unordered and labeled lists)
3
+ # Public: Methods for managing AsciiDoc lists (ordered, unordered and description lists)
4
4
  class List < AbstractBlock
5
5
 
6
6
  # Public: Create alias for blocks
7
7
  alias :items :blocks
8
+ # Public: Get the items in this list as an Array
9
+ alias :content :blocks
10
+ # Public: Create alias to check if this list has blocks
8
11
  alias :items? :blocks?
9
12
 
10
13
  def initialize parent, context
11
14
  super
12
15
  end
13
16
 
14
- # Public: Get the items in this list as an Array
15
- def content
16
- @blocks
17
+ # Check whether this list is an outline list (unordered or ordered).
18
+ #
19
+ # Return true if this list is an outline list. Otherwise, return false.
20
+ def outline?
21
+ @context == :ulist || @context == :olist
17
22
  end
18
23
 
19
24
  def convert
@@ -59,6 +64,22 @@ class ListItem < AbstractBlock
59
64
  apply_subs @text
60
65
  end
61
66
 
67
+ # Check whether this list item has simple content (no nested blocks aside from a single outline list).
68
+ # Primarily relevant for outline lists.
69
+ #
70
+ # Return true if the list item contains no blocks or it contains a single outline list. Otherwise, return false.
71
+ def simple?
72
+ @blocks.empty? || (@blocks.size == 1 && List === (blk = @blocks[0]) && blk.outline?)
73
+ end
74
+
75
+ # Check whether this list item has compound content (nested blocks aside from a single outline list).
76
+ # Primarily relevant for outline lists.
77
+ #
78
+ # Return true if the list item contains blocks other than a single outline list. Otherwise, return false.
79
+ def compound?
80
+ !simple?
81
+ end
82
+
62
83
  # Public: Fold the first paragraph block into the text
63
84
  #
64
85
  # Here are the rules for when a folding occurs:
@@ -71,7 +92,7 @@ class ListItem < AbstractBlock
71
92
  #
72
93
  # Returns nothing
73
94
  def fold_first(continuation_connects_first_block = false, content_adjacent = false)
74
- if (first_block = @blocks[0]) && first_block.is_a?(Block) &&
95
+ if (first_block = @blocks[0]) && Block === first_block &&
75
96
  ((first_block.context == :paragraph && !continuation_connects_first_block) ||
76
97
  ((content_adjacent || !continuation_connects_first_block) && first_block.context == :literal &&
77
98
  first_block.option?('listparagraph')))
@@ -26,6 +26,20 @@ class Parser
26
26
 
27
27
  BlockMatchData = Struct.new :context, :masq, :tip, :terminator
28
28
 
29
+ # Regexp for replacing tab character
30
+ TabRx = /\t/
31
+
32
+ # Regexp for leading tab indentation
33
+ TabIndentRx = /^\t+/
34
+
35
+ StartOfBlockProc = lambda {|l| ((l.start_with? '[') && BlockAttributeLineRx =~ l) || (is_delimited_block? l) }
36
+
37
+ StartOfListProc = lambda {|l| AnyListRx =~ l }
38
+
39
+ StartOfBlockOrListProc = lambda {|l| (is_delimited_block? l) || ((l.start_with? '[') && BlockAttributeLineRx =~ l) || AnyListRx =~ l }
40
+
41
+ NoOp = nil
42
+
29
43
  # Public: Make sure the Parser object doesn't get initialized.
30
44
  #
31
45
  # Raises RuntimeError if this constructor is invoked.
@@ -101,6 +115,9 @@ class Parser
101
115
  end
102
116
  # default to compat-mode if document uses atx-style doctitle
103
117
  document.set_attribute 'compat-mode', '' unless single_line
118
+ if (separator = block_attributes.delete('separator'))
119
+ document.set_attribute('title-separator', separator)
120
+ end
104
121
  document.header.source_location = source_location if source_location
105
122
  document.attributes['doctitle'] = section_title = doctitle
106
123
  # QUESTION: should the id assignment on Document be encapsulated in the Document class?
@@ -138,6 +155,9 @@ class Parser
138
155
  document.attributes['manvolnum'] = m[2].strip
139
156
  else
140
157
  warn %(asciidoctor: ERROR: #{reader.prev_line_info}: malformed manpage title)
158
+ # provide sensible fallbacks
159
+ document.attributes['mantitle'] = document.attributes['doctitle']
160
+ document.attributes['manvolnum'] = '1'
141
161
  end
142
162
 
143
163
  reader.skip_blank_lines
@@ -417,7 +437,7 @@ class Parser
417
437
  block_extensions = block_macro_extensions = false
418
438
  end
419
439
  #parent_context = parent.is_a?(Block) ? parent.context : nil
420
- in_list = (parent.is_a? List)
440
+ in_list = ListItem === parent
421
441
  block = nil
422
442
  style = nil
423
443
  explicit_style = nil
@@ -536,7 +556,7 @@ class Parser
536
556
  # end
537
557
  # end
538
558
  # document.register(:images, target)
539
- # attributes['alt'] ||= ::File.basename(target, ::File.extname(target)).tr('_-', ' ')
559
+ # attributes['alt'] ||= Helpers.basename(target, true).tr('_-', ' ')
540
560
  # # QUESTION should video or audio have an auto-numbered caption?
541
561
  # block.assign_caption attributes.delete('caption'), 'figure'
542
562
  #end
@@ -579,7 +599,8 @@ class Parser
579
599
  attributes['style'] = 'arabic'
580
600
  reader.unshift_line this_line
581
601
  expected_index = 1
582
- begin
602
+ # NOTE skip the match on the first time through as we've already done it (emulates begin...while)
603
+ while match || (reader.has_more_lines? && (match = CalloutListRx.match(reader.peek_line)))
583
604
  # might want to move this check to a validate method
584
605
  if match[1].to_i != expected_index
585
606
  # FIXME this lineno - 2 hack means we need a proper look-behind cursor
@@ -597,7 +618,8 @@ class Parser
597
618
  warn %(asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: no callouts refer to list item #{block.items.size})
598
619
  end
599
620
  end
600
- end while reader.has_more_lines? && (match = CalloutListRx.match(reader.peek_line))
621
+ match = nil
622
+ end
601
623
 
602
624
  document.callouts.next_list
603
625
  break
@@ -682,19 +704,9 @@ class Parser
682
704
  if style != 'normal' && LiteralParagraphRx =~ this_line
683
705
  # So we need to actually include this one in the read_lines group
684
706
  reader.unshift_line this_line
685
- lines = reader.read_lines_until(
686
- :break_on_blank_lines => true,
687
- :break_on_list_continuation => true,
688
- :preserve_last_line => true) {|line|
689
- # a preceding blank line (skipped > 0) indicates we are in a list continuation
690
- # and therefore we should not break at a list item
691
- # (this won't stop breaking on item of same level since we've already parsed them out)
692
- # QUESTION can we turn this block into a lambda or function call?
693
- (break_at_list && AnyListRx =~ line) ||
694
- (Compliance.block_terminates_paragraph && (is_delimited_block?(line) || BlockAttributeLineRx =~ line))
695
- }
707
+ lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => text_only
696
708
 
697
- reset_block_indent! lines
709
+ adjust_indentation! lines
698
710
 
699
711
  block = Block.new(parent, :literal, :content_model => :verbatim, :source => lines, :attributes => attributes)
700
712
  # a literal gets special meaning inside of a definition list
@@ -704,18 +716,7 @@ class Parser
704
716
  # a paragraph is contiguous nonblank/noncontinuation lines
705
717
  else
706
718
  reader.unshift_line this_line
707
- lines = reader.read_lines_until(
708
- :break_on_blank_lines => true,
709
- :break_on_list_continuation => true,
710
- :preserve_last_line => true,
711
- :skip_line_comments => true) {|line|
712
- # a preceding blank line (skipped > 0) indicates we are in a list continuation
713
- # and therefore we should not break at a list item
714
- # (this won't stop breaking on item of same level since we've already parsed them out)
715
- # QUESTION can we turn this block into a lambda or function call?
716
- (break_at_list && AnyListRx =~ line) ||
717
- (Compliance.block_terminates_paragraph && (is_delimited_block?(line) || BlockAttributeLineRx =~ line))
718
- }
719
+ lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => true
719
720
 
720
721
  # NOTE we need this logic because we've asked the reader to skip
721
722
  # line comments, which may leave us w/ an empty buffer if those
@@ -759,8 +760,7 @@ class Parser
759
760
  # TODO could assume a floating title when inside a block context
760
761
  # FIXME Reader needs to be created w/ line info
761
762
  block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes)
762
- elsif !text_only && lines.size > 1 && first_line.start_with?('"') &&
763
- lines[-1].start_with?('-- ') && lines[-2].end_with?('"')
763
+ elsif !text_only && (blockquote? lines, first_line)
764
764
  lines[0] = first_line[1..-1]
765
765
  attribution, citetitle = lines.pop[3..-1].split(', ', 2)
766
766
  lines.pop while lines[-1].empty?
@@ -771,16 +771,10 @@ class Parser
771
771
  attributes['citetitle'] = citetitle if citetitle
772
772
  block = Block.new(parent, :quote, :content_model => :simple, :source => lines, :attributes => attributes)
773
773
  else
774
- # if [normal] is used over an indented paragraph, unindent it
775
- if style == 'normal' && ((first_char = lines[0].chr) == ' ' || first_char == TAB)
776
- first_line = lines[0]
777
- first_line_shifted = first_line.lstrip
778
- indent = line_length(first_line) - line_length(first_line_shifted)
779
- lines[0] = first_line_shifted
780
- # QUESTION should we fix the rest of the lines, since in XML output it's insignificant?
781
- lines.size.times do |i|
782
- lines[i] = lines[i][indent..-1] if i > 0
783
- end
774
+ # if [normal] is used over an indented paragraph, shift content to left margin
775
+ if style == 'normal'
776
+ # QUESTION do we even need to shift since whitespace is normalized by XML in this case?
777
+ adjust_indentation! lines
784
778
  end
785
779
 
786
780
  block = Block.new(parent, :paragraph, :content_model => :simple, :source => lines, :attributes => attributes)
@@ -814,21 +808,27 @@ class Parser
814
808
  when :listing, :fenced_code, :source
815
809
  if block_context == :fenced_code
816
810
  style = attributes['style'] = 'source'
817
- language, linenums = this_line[3..-1].split(',', 2)
818
- if language && !(language = language.strip).empty?
811
+ language, linenums = this_line[3..-1].tr(' ', '').split(',', 2)
812
+ if !language.nil_or_empty?
819
813
  attributes['language'] = language
820
- attributes['linenums'] = '' if linenums && !linenums.strip.empty?
814
+ attributes['linenums'] = '' unless linenums.nil_or_empty?
821
815
  elsif (default_language = document.attributes['source-language'])
822
816
  attributes['language'] = default_language
823
817
  end
818
+ if !attributes.key?('indent') && document.attributes.key?('source-indent')
819
+ attributes['indent'] = document.attributes['source-indent']
820
+ end
824
821
  terminator = terminator[0..2]
825
822
  elsif block_context == :source
826
823
  AttributeList.rekey(attributes, [nil, 'language', 'linenums'])
827
- unless attributes.has_key? 'language'
824
+ unless attributes.key? 'language'
828
825
  if (default_language = document.attributes['source-language'])
829
826
  attributes['language'] = default_language
830
827
  end
831
828
  end
829
+ if !attributes.key?('indent') && document.attributes.key?('source-indent')
830
+ attributes['indent'] = document.attributes['source-indent']
831
+ end
832
832
  end
833
833
  block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
834
834
 
@@ -905,8 +905,8 @@ class Parser
905
905
  if block.context == :image
906
906
  resolved_target = attributes['target']
907
907
  block.document.register(:images, resolved_target)
908
- attributes['alt'] ||= ::File.basename(resolved_target, ::File.extname(resolved_target)).tr('_-', ' ')
909
- attributes['alt'] = block.sub_specialcharacters attributes['alt']
908
+ attributes['alt'] ||= Helpers.basename(resolved_target, true).tr('_-', ' ')
909
+ attributes['alt'] = block.sub_specialchars attributes['alt']
910
910
  block.assign_caption attributes.delete('caption'), 'figure'
911
911
  if (scaledwidth = attributes['scaledwidth'])
912
912
  # append % to scaledwidth if ends in number (no units present)
@@ -947,6 +947,21 @@ class Parser
947
947
  block
948
948
  end
949
949
 
950
+ def self.blockquote? lines, first_line = nil
951
+ lines.size > 1 && ((first_line || lines[0]).start_with? '"') &&
952
+ (lines[-1].start_with? '-- ') && (lines[-2].end_with? '"')
953
+ end
954
+
955
+ def self.read_paragraph_lines reader, break_at_list, opts = {}
956
+ opts[:break_on_blank_lines] = true
957
+ opts[:break_on_list_continuation] = true
958
+ opts[:preserve_last_line] = true
959
+ break_condition = (break_at_list ?
960
+ (Compliance.block_terminates_paragraph ? StartOfBlockOrListProc : StartOfListProc) :
961
+ (Compliance.block_terminates_paragraph ? StartOfBlockProc : NoOp))
962
+ reader.read_lines_until opts, &break_condition
963
+ end
964
+
950
965
  # Public: Determines whether this line is the start of any of the delimited blocks
951
966
  #
952
967
  # returns the match data if this line is the first line of a delimited block or nil if not
@@ -1035,14 +1050,7 @@ class Parser
1035
1050
  lines = reader.read_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true)
1036
1051
  else
1037
1052
  content_model = :simple if content_model == :compound
1038
- lines = reader.read_lines_until(
1039
- :break_on_blank_lines => true,
1040
- :break_on_list_continuation => true,
1041
- :preserve_last_line => true,
1042
- :skip_line_comments => true,
1043
- :skip_processing => skip_processing) {|line|
1044
- Compliance.block_terminates_paragraph && (is_delimited_block?(line) || BlockAttributeLineRx =~ line)
1045
- }
1053
+ lines = read_paragraph_lines reader, false, :skip_line_comments => true, :skip_processing => true
1046
1054
  # QUESTION check for empty lines after grabbing lines for simple content model?
1047
1055
  end
1048
1056
  block_reader = nil
@@ -1065,8 +1073,12 @@ class Parser
1065
1073
  return lines
1066
1074
  end
1067
1075
 
1068
- if content_model == :verbatim && (indent = attributes['indent'])
1069
- reset_block_indent! lines, indent.to_i
1076
+ if content_model == :verbatim
1077
+ if (indent = attributes['indent'])
1078
+ adjust_indentation! lines, indent, (attributes['tabsize'] || parent.document.attributes['tabsize'])
1079
+ elsif (tab_size = (attributes['tabsize'] || parent.document.attributes['tabsize']).to_i) > 0
1080
+ adjust_indentation! lines, nil, tab_size
1081
+ end
1070
1082
  end
1071
1083
 
1072
1084
  if (extension = options[:extension])
@@ -1238,7 +1250,8 @@ class Parser
1238
1250
  # that uses the same delimiter (::, :::, :::: or ;;)
1239
1251
  sibling_pattern = DefinitionListSiblingRx[match[2]]
1240
1252
 
1241
- begin
1253
+ # NOTE skip the match on the first time through as we've already done it (emulates begin...while)
1254
+ while match || (reader.has_more_lines? && (match = sibling_pattern.match(reader.peek_line)))
1242
1255
  term, item = next_list_item(reader, list_block, match, sibling_pattern)
1243
1256
  if previous_pair && !previous_pair[-1]
1244
1257
  previous_pair.pop
@@ -1248,7 +1261,8 @@ class Parser
1248
1261
  # FIXME this misses the automatic parent assignment
1249
1262
  list_block.items << (previous_pair = [[term], item])
1250
1263
  end
1251
- end while reader.has_more_lines? && (match = sibling_pattern.match(reader.peek_line))
1264
+ match = nil
1265
+ end
1252
1266
 
1253
1267
  list_block
1254
1268
  end
@@ -1333,8 +1347,9 @@ class Parser
1333
1347
  # about sections) since the reader is confined within the boundaries of a
1334
1348
  # list
1335
1349
  while list_item_reader.has_more_lines?
1336
- new_block = next_block(list_item_reader, list_block, {}, options)
1337
- list_item << new_block if new_block
1350
+ if (new_block = next_block(list_item_reader, list_item, {}, options))
1351
+ list_item << new_block
1352
+ end
1338
1353
  end
1339
1354
 
1340
1355
  list_item.fold_first(continuation_connects_first_block, content_adjacent)
@@ -1821,9 +1836,16 @@ class Parser
1821
1836
  if reader.has_more_lines? && !reader.next_line_empty?
1822
1837
  rev_line = reader.read_line
1823
1838
  if (match = RevisionInfoLineRx.match(rev_line))
1824
- rev_metadata['revdate'] = match[2].strip
1825
- rev_metadata['revnumber'] = match[1].rstrip unless match[1].nil?
1826
- rev_metadata['revremark'] = match[3].rstrip unless match[3].nil?
1839
+ rev_metadata['revnumber'] = match[1].rstrip if match[1]
1840
+ unless (component = match[2].strip) == ''
1841
+ # version must begin with 'v' if date is absent
1842
+ if !match[1] && (component.start_with? 'v')
1843
+ rev_metadata['revnumber'] = component[1..-1]
1844
+ else
1845
+ rev_metadata['revdate'] = component
1846
+ end
1847
+ end
1848
+ rev_metadata['revremark'] = match[3].rstrip if match[3]
1827
1849
  else
1828
1850
  # throw it back
1829
1851
  reader.unshift_line rev_line
@@ -1917,7 +1939,7 @@ class Parser
1917
1939
 
1918
1940
  segments = nil
1919
1941
  if names_only
1920
- # splitting on ' ' will collapse repeating spaces
1942
+ # splitting on ' ' with limit will collapse repeating spaces
1921
1943
  segments = author_entry.split(' ', 3)
1922
1944
  elsif (match = AuthorInfoLineRx.match(author_entry))
1923
1945
  segments = match.to_a
@@ -2254,11 +2276,11 @@ class Parser
2254
2276
  table.assign_caption attributes.delete('caption')
2255
2277
  end
2256
2278
 
2257
- if attributes.has_key? 'cols'
2279
+ if attributes['cols'].nil_or_empty?
2280
+ explicit_col_specs = false
2281
+ else
2258
2282
  table.create_columns(parse_col_specs(attributes['cols']))
2259
2283
  explicit_col_specs = true
2260
- else
2261
- explicit_col_specs = false
2262
2284
  end
2263
2285
 
2264
2286
  skipped = table_reader.skip_blank_lines
@@ -2303,7 +2325,13 @@ class Parser
2303
2325
  end
2304
2326
  else
2305
2327
  if m.pre_match.end_with? '\\'
2306
- line = parser_ctx.skip_matched_delimiter(m, true)
2328
+ # skip over escaped delimiter
2329
+ # handle special case when end of line is reached (see issue #1306)
2330
+ if (line = parser_ctx.skip_matched_delimiter(m, true)).empty?
2331
+ parser_ctx.buffer = %(#{parser_ctx.buffer}#{EOL})
2332
+ parser_ctx.keep_cell_open
2333
+ break
2334
+ end
2307
2335
  next
2308
2336
  end
2309
2337
  end
@@ -2380,9 +2408,12 @@ class Parser
2380
2408
  end
2381
2409
 
2382
2410
  specs = []
2383
- records.split(',').each {|record|
2411
+ # NOTE -1 argument ensures we don't drop empty records
2412
+ records.split(',', -1).each {|record|
2413
+ if record == ''
2414
+ specs << { 'width' => 1 }
2384
2415
  # TODO might want to use scan rather than this mega-regexp
2385
- if (m = ColumnSpecRx.match(record))
2416
+ elsif (m = ColumnSpecRx.match(record))
2386
2417
  spec = {}
2387
2418
  if m[2]
2388
2419
  # make this an operation
@@ -2397,18 +2428,20 @@ class Parser
2397
2428
 
2398
2429
  # to_i permits us to support percentage width by stripping the %
2399
2430
  # NOTE this is slightly out of compliance w/ AsciiDoc, but makes way more sense
2400
- spec['width'] = !m[3].nil? ? m[3].to_i : 1
2431
+ spec['width'] = (m[3] ? m[3].to_i : 1)
2401
2432
 
2402
2433
  # make this an operation
2403
2434
  if m[4] && Table::TEXT_STYLES.has_key?(m[4])
2404
2435
  spec['style'] = Table::TEXT_STYLES[m[4]]
2405
2436
  end
2406
2437
 
2407
- repeat = !m[1].nil? ? m[1].to_i : 1
2408
-
2409
- 1.upto(repeat) {
2410
- specs << spec.dup
2411
- }
2438
+ if m[1]
2439
+ 1.upto(m[1].to_i) {
2440
+ specs << spec.dup
2441
+ }
2442
+ else
2443
+ specs << spec
2444
+ end
2412
2445
  end
2413
2446
  }
2414
2447
  specs
@@ -2591,18 +2624,14 @@ class Parser
2591
2624
  end
2592
2625
  end
2593
2626
 
2594
- # Remove the indentation (block offset) shared by all the lines, then
2595
- # indent the lines by the specified amount if specified
2627
+ # Remove the block indentation (the leading whitespace equal to the amount of
2628
+ # leading whitespace of the least indented line), then replace tabs with
2629
+ # spaces (using proper tab expansion logic) and, finally, indent the lines by
2630
+ # the amount specified.
2596
2631
  #
2597
- # Trim the leading whitespace (indentation) equivalent to the length
2598
- # of the indent on the least indented line. If the indent argument
2599
- # is specified, indent the lines by this many spaces (columns).
2600
- #
2601
- # The purpose of this method is to shift a block of text to
2602
- # align to the left margin, while still preserving the relative
2603
- # indentation between lines
2632
+ # This method preserves the relative indentation of the lines.
2604
2633
  #
2605
- # lines - the Array of String lines to process
2634
+ # lines - the Array of String lines to process (no trailing endlines)
2606
2635
  # indent - the integer number of spaces to add to the beginning
2607
2636
  # of each line; if this value is nil, the existing
2608
2637
  # space is preserved (optional, default: 0)
@@ -2615,55 +2644,83 @@ class Parser
2615
2644
  # end
2616
2645
  # EOS
2617
2646
  #
2618
- # source.split("\n")
2647
+ # source.split "\n"
2619
2648
  # # => [" def names", " @names.split ' '", " end"]
2620
2649
  #
2621
- # Parser.reset_block_indent(source.split "\n")
2622
- # # => ["def names", " @names.split ' '", "end"]
2623
- #
2624
- # puts Parser.reset_block_indent(source.split "\n") * "\n"
2650
+ # puts Parser.adjust_indentation!(source.split "\n") * "\n"
2625
2651
  # # => def names
2626
2652
  # # => @names.split ' '
2627
2653
  # # => end
2628
2654
  #
2629
- # returns the Array of String lines with block offset removed
2655
+ # returns Nothing
2630
2656
  #--
2631
- # FIXME refactor gsub matchers into compiled regex
2632
- def self.reset_block_indent!(lines, indent = 0)
2633
- return if !indent || lines.empty?
2634
-
2635
- tab_detected = false
2636
- # TODO make tab size configurable
2637
- tab_expansion = ' '
2638
- # strip leading block indent
2639
- offsets = lines.map do |line|
2640
- # break if the first char is non-whitespace
2641
- break [] unless line.chr.lstrip.empty?
2642
- if line.include? TAB
2643
- tab_detected = true
2644
- line = line.gsub(TAB_PATTERN, tab_expansion)
2645
- end
2646
- if (flush_line = line.lstrip).empty?
2647
- nil
2648
- elsif (offset = line.length - flush_line.length) == 0
2649
- break []
2650
- else
2651
- offset
2657
+ # QUESTION should indent be called margin?
2658
+ def self.adjust_indentation! lines, indent = 0, tab_size = 0
2659
+ return if lines.empty?
2660
+
2661
+ # expand tabs if a tab is detected unless tab_size is nil
2662
+ if (tab_size = tab_size.to_i) > 0 && (lines.join.include? TAB)
2663
+ #if (tab_size = tab_size.to_i) > 0 && (lines.index {|line| line.include? TAB })
2664
+ full_tab_space = ' ' * tab_size
2665
+ lines.map! do |line|
2666
+ next line if line.empty?
2667
+
2668
+ if line.start_with? TAB
2669
+ line.sub!(TabIndentRx) {|tabs| full_tab_space * tabs.length }
2670
+ end
2671
+
2672
+ if line.include? TAB
2673
+ # keeps track of how many spaces were added to adjust offset in match data
2674
+ spaces_added = 0
2675
+ line.gsub!(TabRx) {
2676
+ # calculate how many spaces this tab represents, then replace tab with spaces
2677
+ if (offset = ($~.begin 0) + spaces_added) % tab_size == 0
2678
+ spaces_added += (tab_size - 1)
2679
+ full_tab_space
2680
+ else
2681
+ unless (spaces = tab_size - offset % tab_size) == 1
2682
+ spaces_added += (spaces - 1)
2683
+ end
2684
+ ' ' * spaces
2685
+ end
2686
+ }
2687
+ else
2688
+ line
2689
+ end
2652
2690
  end
2653
2691
  end
2654
-
2655
- unless offsets.empty? || (offsets = offsets.compact).empty?
2656
- if (offset = offsets.min) > 0
2657
- lines.map! {|line|
2658
- line = line.gsub(TAB_PATTERN, tab_expansion) if tab_detected
2659
- line[offset..-1].to_s
2660
- }
2692
+
2693
+ # skip adjustment of gutter if indent is -1
2694
+ return unless indent && (indent = indent.to_i) > -1
2695
+
2696
+ # determine width of gutter
2697
+ gutter_width = nil
2698
+ lines.each do |line|
2699
+ next if line.empty?
2700
+ # NOTE this logic assumes no whitespace-only lines
2701
+ if (line_indent = line.length - line.lstrip.length) == 0
2702
+ gutter_width = nil
2703
+ break
2704
+ else
2705
+ unless gutter_width && line_indent > gutter_width
2706
+ gutter_width = line_indent
2707
+ end
2661
2708
  end
2662
2709
  end
2663
2710
 
2664
- if indent > 0
2711
+ # remove gutter then apply new indent if specified
2712
+ # NOTE gutter_width is > 0 if not nil
2713
+ if indent == 0
2714
+ if gutter_width
2715
+ lines.map! {|line| line.empty? ? line : line[gutter_width..-1] }
2716
+ end
2717
+ else
2665
2718
  padding = ' ' * indent
2666
- lines.map! {|line| %(#{padding}#{line}) }
2719
+ if gutter_width
2720
+ lines.map! {|line| line.empty? ? line : padding + line[gutter_width..-1] }
2721
+ else
2722
+ lines.map! {|line| line.empty? ? line : padding + line }
2723
+ end
2667
2724
  end
2668
2725
 
2669
2726
  nil