asciidoctor 1.5.7.1 → 1.5.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +95 -5
  3. data/Gemfile +23 -13
  4. data/README-de.adoc +482 -0
  5. data/README-fr.adoc +128 -119
  6. data/README-jp.adoc +2 -3
  7. data/README-zh_CN.adoc +2 -3
  8. data/README.adoc +131 -106
  9. data/asciidoctor.gemspec +9 -7
  10. data/data/locale/attributes-ar.adoc +1 -1
  11. data/data/locale/attributes-bg.adoc +1 -1
  12. data/data/locale/attributes-ca.adoc +1 -1
  13. data/data/locale/attributes-cs.adoc +1 -1
  14. data/data/locale/attributes-da.adoc +1 -1
  15. data/data/locale/attributes-de.adoc +1 -1
  16. data/data/locale/attributes-en.adoc +1 -1
  17. data/data/locale/attributes-es.adoc +1 -1
  18. data/data/locale/attributes-fa.adoc +1 -1
  19. data/data/locale/attributes-fi.adoc +1 -1
  20. data/data/locale/attributes-fr.adoc +1 -1
  21. data/data/locale/attributes-hu.adoc +1 -1
  22. data/data/locale/attributes-id.adoc +1 -1
  23. data/data/locale/attributes-it.adoc +1 -1
  24. data/data/locale/attributes-ja.adoc +1 -1
  25. data/data/locale/attributes-kr.adoc +1 -1
  26. data/data/locale/attributes-nb.adoc +1 -1
  27. data/data/locale/attributes-nl.adoc +1 -1
  28. data/data/locale/attributes-nn.adoc +1 -1
  29. data/data/locale/attributes-pl.adoc +1 -1
  30. data/data/locale/attributes-pt.adoc +1 -1
  31. data/data/locale/attributes-pt_BR.adoc +1 -1
  32. data/data/locale/attributes-ro.adoc +1 -1
  33. data/data/locale/attributes-ru.adoc +1 -1
  34. data/data/locale/attributes-sr.adoc +5 -4
  35. data/data/locale/attributes-sr_Latn.adoc +5 -4
  36. data/data/locale/attributes-sv.adoc +23 -0
  37. data/data/locale/attributes-tr.adoc +1 -1
  38. data/data/locale/attributes-uk.adoc +1 -1
  39. data/data/locale/attributes-zh_CN.adoc +1 -1
  40. data/data/locale/attributes-zh_TW.adoc +1 -1
  41. data/data/stylesheets/asciidoctor-default.css +23 -23
  42. data/lib/asciidoctor.rb +110 -104
  43. data/lib/asciidoctor/abstract_block.rb +55 -32
  44. data/lib/asciidoctor/abstract_node.rb +32 -17
  45. data/lib/asciidoctor/attribute_list.rb +8 -7
  46. data/lib/asciidoctor/block.rb +5 -7
  47. data/lib/asciidoctor/cli/options.rb +5 -9
  48. data/lib/asciidoctor/converter.rb +2 -2
  49. data/lib/asciidoctor/converter/docbook45.rb +7 -20
  50. data/lib/asciidoctor/converter/docbook5.rb +36 -37
  51. data/lib/asciidoctor/converter/factory.rb +10 -8
  52. data/lib/asciidoctor/converter/html5.rb +90 -65
  53. data/lib/asciidoctor/converter/manpage.rb +72 -62
  54. data/lib/asciidoctor/converter/template.rb +8 -6
  55. data/lib/asciidoctor/core_ext/1.8.7/concurrent/hash.rb +5 -0
  56. data/lib/asciidoctor/document.rb +62 -10
  57. data/lib/asciidoctor/extensions.rb +74 -16
  58. data/lib/asciidoctor/helpers.rb +11 -14
  59. data/lib/asciidoctor/list.rb +2 -2
  60. data/lib/asciidoctor/parser.rb +223 -195
  61. data/lib/asciidoctor/path_resolver.rb +15 -7
  62. data/lib/asciidoctor/reader.rb +65 -36
  63. data/lib/asciidoctor/section.rb +6 -4
  64. data/lib/asciidoctor/substitutors.rb +170 -149
  65. data/lib/asciidoctor/table.rb +16 -8
  66. data/lib/asciidoctor/version.rb +1 -1
  67. data/man/asciidoctor.1 +6 -5
  68. data/man/asciidoctor.adoc +3 -2
  69. data/test/api_test.rb +236 -0
  70. data/test/attribute_list_test.rb +242 -0
  71. data/test/attributes_test.rb +65 -52
  72. data/test/blocks_test.rb +408 -260
  73. data/test/converter_test.rb +7 -7
  74. data/test/document_test.rb +60 -54
  75. data/test/extensions_test.rb +218 -32
  76. data/test/fixtures/doctime-localtime.adoc +2 -0
  77. data/test/fixtures/section-a.adoc +4 -0
  78. data/test/fixtures/subs.adoc +0 -1
  79. data/test/invoker_test.rb +56 -18
  80. data/test/links_test.rb +105 -81
  81. data/test/lists_test.rb +636 -265
  82. data/test/logger_test.rb +1 -1
  83. data/test/manpage_test.rb +140 -3
  84. data/test/paragraphs_test.rb +42 -42
  85. data/test/parser_test.rb +63 -183
  86. data/test/paths_test.rb +21 -4
  87. data/test/preamble_test.rb +9 -9
  88. data/test/reader_test.rb +78 -28
  89. data/test/sections_test.rb +273 -151
  90. data/test/substitutions_test.rb +53 -19
  91. data/test/tables_test.rb +286 -163
  92. data/test/test_helper.rb +4 -3
  93. data/test/text_test.rb +65 -65
  94. metadata +16 -21
@@ -109,7 +109,7 @@ class PathResolver
109
109
  SLASH = '/'
110
110
  BACKSLASH = '\\'
111
111
  DOUBLE_SLASH = '//'
112
- WindowsRootRx = /^[a-zA-Z]:(?:\\|\/)/
112
+ WindowsRootRx = /^(?:[a-zA-Z]:)?[\\\/]/
113
113
 
114
114
  attr_accessor :file_separator
115
115
  attr_accessor :working_dir
@@ -132,11 +132,11 @@ class PathResolver
132
132
 
133
133
  # Public: Check whether the specified path is an absolute path.
134
134
  #
135
- # This operation considers both posix paths and Windows paths. It does not
136
- # consider URIs.
135
+ # This operation considers both posix paths and Windows paths. The path does
136
+ # not have to be posixified beforehand. This operation does not handle URIs.
137
137
  #
138
- # Unix absolute paths and UNC paths both start with slash. Windows roots can
139
- # start with a drive letter.
138
+ # Unix absolute paths start with a slash. UNC paths can start with a slash or
139
+ # backslash. Windows roots can start with a drive letter.
140
140
  #
141
141
  # path - the String path to check
142
142
  #
@@ -214,7 +214,15 @@ class PathResolver
214
214
  #
215
215
  # Return the [String] relative path of the specified path calculated from the base directory.
216
216
  def relative_path path, base
217
- (root? path) && (offset = descends_from? path, base) ? (path.slice offset, path.length) : path
217
+ if root? path
218
+ if (offset = descends_from? path, base)
219
+ path.slice offset, path.length
220
+ else
221
+ (Pathname.new path).relative_path_from(Pathname.new base).to_s
222
+ end
223
+ else
224
+ path
225
+ end
218
226
  end
219
227
 
220
228
  # Public: Normalize path by converting any backslashes to forward slashes
@@ -313,7 +321,7 @@ class PathResolver
313
321
  # returns a String path formed by joining the segments using the posix file
314
322
  # separator and prepending the root, if specified
315
323
  def join_path segments, root = nil
316
- root ? %(#{root}#{segments * SLASH}) : segments * SLASH
324
+ root ? %(#{root}#{segments.join SLASH}) : (segments.join SLASH)
317
325
  end
318
326
 
319
327
  # Public: Securely resolve a system path
@@ -60,12 +60,13 @@ class Reader
60
60
  @lineno = cursor.lineno || 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
61
61
  end
62
62
  @lines = data ? (prepare_lines data, opts) : []
63
- @source_lines = @lines.dup
63
+ @source_lines = @lines.drop 0
64
64
  @mark = nil
65
65
  @look_ahead = 0
66
66
  @process_lines = true
67
67
  @unescape_next_line = false
68
68
  @unterminated = nil
69
+ @saved = nil
69
70
  end
70
71
 
71
72
  # Internal: Prepare the lines from the provided data
@@ -91,7 +92,7 @@ class Reader
91
92
  elsif opts[:normalize]
92
93
  Helpers.normalize_lines_array data
93
94
  else
94
- data.dup
95
+ data.drop 0
95
96
  end
96
97
  end
97
98
 
@@ -167,7 +168,7 @@ class Reader
167
168
  # Returns nothing if there is no more data.
168
169
  def peek_line direct = false
169
170
  if direct || @look_ahead > 0
170
- @unescape_next_line ? @lines[0][1..-1] : @lines[0]
171
+ @unescape_next_line ? ((line = @lines[0]).slice 1, line.length) : @lines[0]
171
172
  elsif @lines.empty?
172
173
  @look_ahead = 0
173
174
  nil
@@ -245,7 +246,7 @@ class Reader
245
246
  #
246
247
  # Returns the lines read joined as a String
247
248
  def read
248
- read_lines * LF
249
+ read_lines.join LF
249
250
  end
250
251
 
251
252
  # Public: Advance to the next line by discarding the line at the front of the stack
@@ -556,17 +557,39 @@ class Reader
556
557
  #
557
558
  # Returns A copy of the String Array of lines remaining in this Reader
558
559
  def lines
559
- @lines.dup
560
+ @lines.drop 0
560
561
  end
561
562
 
562
563
  # Public: Get a copy of the remaining lines managed by this Reader joined as a String
563
564
  def string
564
- @lines * LF
565
+ @lines.join LF
565
566
  end
566
567
 
567
568
  # Public: Get the source lines for this Reader joined as a String
568
569
  def source
569
- @source_lines * LF
570
+ @source_lines.join LF
571
+ end
572
+
573
+ def save
574
+ accum = {}
575
+ instance_variables.each do |name|
576
+ accum[name] = ::Array === (val = instance_variable_get name) ? val.dup : val unless name == :@saved || name == :@source_lines
577
+ end
578
+ @saved = accum
579
+ nil
580
+ end
581
+
582
+ def restore_save
583
+ if @saved
584
+ @saved.each do |name, val|
585
+ instance_variable_set name, val
586
+ end
587
+ @saved = nil
588
+ end
589
+ end
590
+
591
+ def discard_save
592
+ @saved = nil
570
593
  end
571
594
 
572
595
  # Public: Get a summary of this Reader.
@@ -602,7 +625,7 @@ class PreprocessorReader < Reader
602
625
  # QUESTION should this work for AsciiDoc table cell content? Currently it does not.
603
626
  if @document && @document.attributes['skip-front-matter']
604
627
  if (front_matter = skip_front_matter! result)
605
- @document.attributes['front-matter'] = front_matter * LF
628
+ @document.attributes['front-matter'] = front_matter.join LF
606
629
  end
607
630
  end
608
631
 
@@ -633,7 +656,7 @@ class PreprocessorReader < Reader
633
656
  if $1 == '\\'
634
657
  @unescape_next_line = true
635
658
  @look_ahead += 1
636
- line[1..-1]
659
+ line.slice 1, line.length
637
660
  elsif preprocess_conditional_directive $2, $3, $4, $5
638
661
  # move the pointer past the conditional line
639
662
  shift
@@ -653,7 +676,7 @@ class PreprocessorReader < Reader
653
676
  if $1 == '\\'
654
677
  @unescape_next_line = true
655
678
  @look_ahead += 1
656
- line[1..-1]
679
+ line.slice 1, line.length
657
680
  # QUESTION should we strip whitespace from raw attributes in Substitutors#parse_attributes? (check perf)
658
681
  elsif preprocess_include_directive $2, $3
659
682
  # peek again since the content has changed
@@ -842,21 +865,22 @@ class PreprocessorReader < Reader
842
865
  # Returns a [Boolean] indicating whether the line under the cursor was changed. To skip over the
843
866
  # directive, call shift and return true.
844
867
  def preprocess_include_directive target, attrlist
868
+ doc = @document
845
869
  if ((expanded_target = target).include? ATTR_REF_HEAD) &&
846
- (expanded_target = @document.sub_attributes target, :attribute_missing => 'drop-line').empty?
870
+ (expanded_target = doc.sub_attributes target, :attribute_missing => 'drop-line').empty?
847
871
  shift
848
- if (@document.attributes['attribute-missing'] || Compliance.attribute_missing) == 'skip'
872
+ if (doc.attributes['attribute-missing'] || Compliance.attribute_missing) == 'skip'
849
873
  unshift %(Unresolved directive in #{@path} - include::#{target}[#{attrlist}])
850
874
  end
851
875
  true
852
876
  elsif include_processors? && (ext = @include_processor_extensions.find {|candidate| candidate.instance.handles? expanded_target })
853
877
  shift
854
878
  # FIXME parse attributes only if requested by extension
855
- ext.process_method[@document, self, expanded_target, attrlist ? (AttributeList.new attrlist).parse : {}]
879
+ ext.process_method[doc, self, expanded_target, (doc.parse_attributes attrlist, [], :sub_input => true)]
856
880
  true
857
881
  # if running in SafeMode::SECURE or greater, don't process this directive
858
882
  # however, be friendly and at least make it a link to the source document
859
- elsif @document.safe >= SafeMode::SECURE
883
+ elsif doc.safe >= SafeMode::SECURE
860
884
  # FIXME we don't want to use a link macro if we are in a verbatim context
861
885
  replace_next_line %(link:#{expanded_target}[])
862
886
  elsif (abs_maxdepth = @maxdepth[:abs]) > 0
@@ -865,30 +889,30 @@ class PreprocessorReader < Reader
865
889
  return
866
890
  end
867
891
 
868
- parsed_attributes = attrlist ? (AttributeList.new attrlist).parse : {}
869
- inc_path, target_type, relpath = resolve_include_path expanded_target, attrlist, parsed_attributes
892
+ parsed_attrs = doc.parse_attributes attrlist, [], :sub_input => true
893
+ inc_path, target_type, relpath = resolve_include_path expanded_target, attrlist, parsed_attrs
870
894
  return inc_path unless target_type
871
895
 
872
896
  inc_linenos = inc_tags = nil
873
897
  if attrlist
874
- if parsed_attributes.key? 'lines'
898
+ if parsed_attrs.key? 'lines'
875
899
  inc_linenos = []
876
- parsed_attributes['lines'].split(DataDelimiterRx).each do |linedef|
877
- if linedef.include?('..')
878
- from, to = linedef.split('..', 2).map {|it| it.to_i }
879
- inc_linenos += to < 0 ? [from, 1.0/0.0] : ::Range.new(from, to).to_a
900
+ (split_delimited_value parsed_attrs['lines']).each do |linedef|
901
+ if linedef.include? '..'
902
+ from, to = linedef.split '..', 2
903
+ inc_linenos += (to.empty? || (to = to.to_i) < 0) ? [from.to_i, 1.0/0.0] : ::Range.new(from.to_i, to).to_a
880
904
  else
881
905
  inc_linenos << linedef.to_i
882
906
  end
883
907
  end
884
908
  inc_linenos = inc_linenos.empty? ? nil : inc_linenos.sort.uniq
885
- elsif parsed_attributes.key? 'tag'
886
- unless (tag = parsed_attributes['tag']).empty? || tag == '!'
909
+ elsif parsed_attrs.key? 'tag'
910
+ unless (tag = parsed_attrs['tag']).empty? || tag == '!'
887
911
  inc_tags = (tag.start_with? '!') ? { (tag.slice 1, tag.length) => false } : { tag => true }
888
912
  end
889
- elsif parsed_attributes.key? 'tags'
913
+ elsif parsed_attrs.key? 'tags'
890
914
  inc_tags = {}
891
- parsed_attributes['tags'].split(DataDelimiterRx).each do |tagdef|
915
+ (split_delimited_value parsed_attrs['tags']).each do |tagdef|
892
916
  if tagdef.start_with? '!'
893
917
  inc_tags[tagdef.slice 1, tagdef.length] = false
894
918
  else
@@ -928,8 +952,8 @@ class PreprocessorReader < Reader
928
952
  shift
929
953
  # FIXME not accounting for skipped lines in reader line numbering
930
954
  if inc_offset
931
- parsed_attributes['partial-option'] = true
932
- push_include inc_lines, inc_path, relpath, inc_offset, parsed_attributes
955
+ parsed_attrs['partial-option'] = true
956
+ push_include inc_lines, inc_path, relpath, inc_offset, parsed_attrs
933
957
  end
934
958
  elsif inc_tags
935
959
  inc_lines, inc_offset, inc_lineno, tag_stack, tags_used, active_tag = [], nil, 0, [], ::Set.new, nil
@@ -995,16 +1019,16 @@ class PreprocessorReader < Reader
995
1019
  end
996
1020
  shift
997
1021
  if inc_offset
998
- parsed_attributes['partial-option'] = true unless base_select && wildcard && inc_tags.empty?
1022
+ parsed_attrs['partial-option'] = true unless base_select && wildcard && inc_tags.empty?
999
1023
  # FIXME not accounting for skipped lines in reader line numbering
1000
- push_include inc_lines, inc_path, relpath, inc_offset, parsed_attributes
1024
+ push_include inc_lines, inc_path, relpath, inc_offset, parsed_attrs
1001
1025
  end
1002
1026
  else
1003
1027
  begin
1004
1028
  # NOTE read content first so that we only advance cursor if IO operation succeeds
1005
- inc_content = target_type == :file ? (::IO.binread inc_path) : open(inc_path, 'rb') {|f| f.read }
1029
+ inc_content = target_type == :file ? ::File.open(inc_path, 'rb') {|f| f.read } : open(inc_path, 'rb') {|f| f.read }
1006
1030
  shift
1007
- push_include inc_content, inc_path, relpath, 1, parsed_attributes
1031
+ push_include inc_content, inc_path, relpath, 1, parsed_attrs
1008
1032
  rescue
1009
1033
  logger.error message_with_context %(include #{target_type} not readable: #{inc_path}), :source_location => cursor
1010
1034
  return replace_next_line %(Unresolved directive in #{@path} - include::#{expanded_target}[#{attrlist}])
@@ -1182,19 +1206,24 @@ class PreprocessorReader < Reader
1182
1206
  def shift
1183
1207
  if @unescape_next_line
1184
1208
  @unescape_next_line = false
1185
- super[1..-1]
1209
+ (line = super).slice 1, line.length
1186
1210
  else
1187
1211
  super
1188
1212
  end
1189
1213
  end
1190
1214
 
1215
+ # Private: Split delimited value on comma (if found), otherwise semi-colon
1216
+ def split_delimited_value val
1217
+ (val.include? ',') ? (val.split ',') : (val.split ';')
1218
+ end
1219
+
1191
1220
  # Private: Ignore front-matter, commonly used in static site generators
1192
1221
  def skip_front_matter! data, increment_linenos = true
1193
1222
  front_matter = nil
1194
1223
  if data[0] == '---'
1195
- original_data = data.dup
1196
- front_matter = []
1224
+ original_data = data.drop 0
1197
1225
  data.shift
1226
+ front_matter = []
1198
1227
  @lineno += 1 if increment_linenos
1199
1228
  while !data.empty? && data[0] != '---'
1200
1229
  front_matter << data.shift
@@ -1248,7 +1277,7 @@ class PreprocessorReader < Reader
1248
1277
  if ((val.start_with? '"') && (val.end_with? '"')) ||
1249
1278
  ((val.start_with? '\'') && (val.end_with? '\''))
1250
1279
  quoted = true
1251
- val = val[1...-1]
1280
+ val = val.slice 1, (val.length - 1)
1252
1281
  else
1253
1282
  quoted = false
1254
1283
  end
@@ -1289,7 +1318,7 @@ class PreprocessorReader < Reader
1289
1318
  end
1290
1319
 
1291
1320
  def to_s
1292
- %(#<#{self.class}@#{object_id} {path: #{@path.inspect}, line #: #{@lineno}, include depth: #{@include_stack.size}, include stack: [#{@include_stack.map {|inc| inc.to_s } * ', '}]}>)
1321
+ %(#<#{self.class}@#{object_id} {path: #{@path.inspect}, line #: #{@lineno}, include depth: #{@include_stack.size}, include stack: [#{@include_stack.map {|inc| inc.to_s }.join ', '}]}>)
1293
1322
  end
1294
1323
  end
1295
1324
  end
@@ -30,7 +30,8 @@ class Section < AbstractBlock
30
30
  # Public: Get/Set the flag to indicate whether this is a special section or a child of one
31
31
  attr_accessor :special
32
32
 
33
- # Public: Get the state of the numbered attribute at this section (need to preserve for creating TOC)
33
+ # Public: Get/Set the flag to indicate whether this section should be numbered.
34
+ # The sectnum method should only be called if this flag is true.
34
35
  attr_accessor :numbered
35
36
 
36
37
  # Public: Get the caption for this section (only relevant for appendices)
@@ -111,11 +112,12 @@ class Section < AbstractBlock
111
112
  def sectnum(delimiter = '.', append = nil)
112
113
  append ||= (append == false ? '' : delimiter)
113
114
  if @level == 1
114
- %(#{@number}#{append})
115
+ %(#{@numeral}#{append})
115
116
  elsif @level > 1
116
- Section === @parent ? %(#{@parent.sectnum(delimiter)}#{@number}#{append}) : %(#{@number}#{append})
117
+ Section === @parent ? %(#{@parent.sectnum(delimiter, delimiter)}#{@numeral}#{append}) : %(#{@numeral}#{append})
117
118
  else # @level == 0
118
- %(#{Helpers.int_to_roman @number}#{append})
119
+ # NOTE coerce @numeral to int just in case not set; can happen if section nesting is out of sequence
120
+ %(#{Helpers.int_to_roman @numeral.to_i}#{append})
119
121
  end
120
122
  end
121
123
 
@@ -175,6 +175,7 @@ module Substitutors
175
175
  # returns - The text with the passthrough region substituted with placeholders
176
176
  def extract_passthroughs(text)
177
177
  compat_mode = @document.compat_mode
178
+ passes = @passthroughs
178
179
  text = text.gsub(InlinePassMacroRx) {
179
180
  # alias match for Ruby 1.8.7 compat
180
181
  m = $~
@@ -203,9 +204,9 @@ module Substitutors
203
204
  else
204
205
  if boundary == '++' && (attributes.end_with? 'x-')
205
206
  old_behavior = true
206
- attributes = attributes[0...-2]
207
+ attributes = attributes.slice 0, attributes.length - 2
207
208
  end
208
- attributes = parse_attributes attributes
209
+ attributes = parse_quoted_text_attributes attributes
209
210
  end
210
211
  elsif escape_count > 0
211
212
  # NOTE we don't look for nested unconstrained pass macros
@@ -213,23 +214,23 @@ module Substitutors
213
214
  end
214
215
  subs = (boundary == '+++' ? [] : BASIC_SUBS)
215
216
 
216
- pass_key = @passthroughs.size
217
+ pass_key = passes.size
217
218
  if attributes
218
219
  if old_behavior
219
- @passthroughs[pass_key] = {:text => content, :subs => NORMAL_SUBS, :type => :monospaced, :attributes => attributes}
220
+ passes[pass_key] = {:text => content, :subs => NORMAL_SUBS, :type => :monospaced, :attributes => attributes}
220
221
  else
221
- @passthroughs[pass_key] = {:text => content, :subs => subs, :type => :unquoted, :attributes => attributes}
222
+ passes[pass_key] = {:text => content, :subs => subs, :type => :unquoted, :attributes => attributes}
222
223
  end
223
224
  else
224
- @passthroughs[pass_key] = {:text => content, :subs => subs}
225
+ passes[pass_key] = {:text => content, :subs => subs}
225
226
  end
226
227
  else # pass:[]
227
228
  if m[6] == RS
228
229
  # NOTE we don't look for nested pass:[] macros
229
- next m[0][1..-1]
230
+ next m[0].slice 1, m[0].length
230
231
  end
231
232
 
232
- @passthroughs[pass_key = @passthroughs.size] = {:text => (unescape_brackets m[8]), :subs => (m[7] ? (resolve_pass_subs m[7]) : nil)}
233
+ passes[pass_key = passes.size] = {:text => (unescape_brackets m[8]), :subs => (m[7] ? (resolve_pass_subs m[7]) : nil)}
233
234
  end
234
235
 
235
236
  %(#{preceding}#{PASS_START}#{pass_key}#{PASS_END})
@@ -241,7 +242,7 @@ module Substitutors
241
242
  m = $~
242
243
  preceding = m[1]
243
244
  attributes = m[2]
244
- escape_mark = RS if m[3].start_with? RS
245
+ escape_mark = RS if (quoted_text = m[3]).start_with? RS
245
246
  format_mark = m[4]
246
247
  content = m[5]
247
248
 
@@ -249,7 +250,7 @@ module Substitutors
249
250
  old_behavior = true
250
251
  else
251
252
  if (old_behavior = (attributes && (attributes.end_with? 'x-')))
252
- attributes = attributes[0...-2]
253
+ attributes = attributes.slice 0, attributes.length - 2
253
254
  end
254
255
  end
255
256
 
@@ -261,34 +262,34 @@ module Substitutors
261
262
 
262
263
  if escape_mark
263
264
  # honor the escape of the formatting mark
264
- next %(#{preceding}[#{attributes}]#{m[3][1..-1]})
265
+ next %(#{preceding}[#{attributes}]#{quoted_text.slice 1, quoted_text.length})
265
266
  elsif preceding == RS
266
267
  # honor the escape of the attributes
267
268
  preceding = %([#{attributes}])
268
269
  attributes = nil
269
270
  else
270
- attributes = parse_attributes attributes
271
+ attributes = parse_quoted_text_attributes attributes
271
272
  end
272
273
  elsif format_mark == '`' && !old_behavior
273
274
  # extract nested single-plus passthrough; otherwise return unprocessed
274
275
  next (extract_inner_passthrough content, %(#{preceding}#{escape_mark}))
275
276
  elsif escape_mark
276
277
  # honor the escape of the formatting mark
277
- next %(#{preceding}#{m[3][1..-1]})
278
+ next %(#{preceding}#{quoted_text.slice 1, quoted_text.length})
278
279
  end
279
280
 
280
- pass_key = @passthroughs.size
281
+ pass_key = passes.size
281
282
  if compat_mode
282
- @passthroughs[pass_key] = {:text => content, :subs => BASIC_SUBS, :attributes => attributes, :type => :monospaced}
283
+ passes[pass_key] = {:text => content, :subs => BASIC_SUBS, :attributes => attributes, :type => :monospaced}
283
284
  elsif attributes
284
285
  if old_behavior
285
286
  subs = (format_mark == '`' ? BASIC_SUBS : NORMAL_SUBS)
286
- @passthroughs[pass_key] = {:text => content, :subs => subs, :attributes => attributes, :type => :monospaced}
287
+ passes[pass_key] = {:text => content, :subs => subs, :attributes => attributes, :type => :monospaced}
287
288
  else
288
- @passthroughs[pass_key] = {:text => content, :subs => BASIC_SUBS, :attributes => attributes, :type => :unquoted}
289
+ passes[pass_key] = {:text => content, :subs => BASIC_SUBS, :attributes => attributes, :type => :unquoted}
289
290
  end
290
291
  else
291
- @passthroughs[pass_key] = {:text => content, :subs => BASIC_SUBS}
292
+ passes[pass_key] = {:text => content, :subs => BASIC_SUBS}
292
293
  end
293
294
 
294
295
  %(#{preceding}#{PASS_START}#{pass_key}#{PASS_END})
@@ -299,8 +300,8 @@ module Substitutors
299
300
  # alias match for Ruby 1.8.7 compat
300
301
  m = $~
301
302
  # honor the escape
302
- if m[0].start_with? RS
303
- next m[0][1..-1]
303
+ if $&.start_with? RS
304
+ next m[0].slice 1, m[0].length
304
305
  end
305
306
 
306
307
  if (type = m[1].to_sym) == :stem
@@ -308,7 +309,7 @@ module Substitutors
308
309
  end
309
310
  content = unescape_brackets m[3]
310
311
  subs = m[2] ? (resolve_pass_subs m[2]) : ((@document.basebackend? 'html') ? BASIC_SUBS : nil)
311
- @passthroughs[pass_key = @passthroughs.size] = {:text => content, :subs => subs, :type => type}
312
+ passes[pass_key = passes.size] = {:text => content, :subs => subs, :type => type}
312
313
  %(#{PASS_START}#{pass_key}#{PASS_END})
313
314
  } if (text.include? ':') && ((text.include? 'stem:') || (text.include? 'math:'))
314
315
 
@@ -337,13 +338,15 @@ module Substitutors
337
338
  #
338
339
  # returns The String text with the passthrough text restored
339
340
  def restore_passthroughs text, outer = true
340
- if outer && (@passthroughs.empty? || !text.include?(PASS_START))
341
- return text
342
- end
341
+ passes = @passthroughs
342
+ # passthroughs may have been eagerly restored (e.g., footnotes)
343
+ #if outer && (passes.empty? || !text.include?(PASS_START))
344
+ # return text
345
+ #end
343
346
 
344
347
  text.gsub(PassSlotRx) {
345
348
  # NOTE we can't remove entry from map because placeholder may have been duplicated by other substitutions
346
- pass = @passthroughs[$1.to_i]
349
+ pass = passes[$1.to_i]
347
350
  subbed_text = apply_subs(pass[:text], pass[:subs])
348
351
  if (type = pass[:type])
349
352
  subbed_text = Inline.new(self, :quoted, subbed_text, :type => type, :attributes => pass[:attributes]).convert
@@ -352,10 +355,9 @@ module Substitutors
352
355
  }
353
356
  ensure
354
357
  # free memory if in outer call...we don't need these anymore
355
- @passthroughs.clear if outer
358
+ passes.clear if outer
356
359
  end
357
360
 
358
-
359
361
  if RUBY_ENGINE == 'opal'
360
362
  def sub_quotes text
361
363
  if QuotedTextSniffRx[compat = @document.compat_mode].match? text
@@ -463,7 +465,7 @@ module Substitutors
463
465
  def sub_attributes text, opts = {}
464
466
  doc_attrs = @document.attributes
465
467
  drop = drop_line = drop_empty_line = attribute_undefined = attribute_missing = nil
466
- result = text.gsub AttributeReferenceRx do
468
+ text = text.gsub AttributeReferenceRx do
467
469
  # escaped attribute, return unescaped
468
470
  if $1 == RS || $4 == RS
469
471
  %({#{$2}})
@@ -504,21 +506,21 @@ module Substitutors
504
506
  end
505
507
 
506
508
  if drop
507
- # drop lines from result
509
+ # drop lines from text
508
510
  if drop_empty_line
509
- lines = (result.tr_s DEL, DEL).split LF, -1
511
+ lines = (text.tr_s DEL, DEL).split LF, -1
510
512
  if drop_line
511
513
  (lines.reject {|line| line == DEL || line == CAN || (line.start_with? CAN) || (line.include? CAN) }.join LF).delete DEL
512
514
  else
513
515
  (lines.reject {|line| line == DEL }.join LF).delete DEL
514
516
  end
515
- elsif result.include? LF
516
- (result.split LF, -1).reject {|line| line == CAN || (line.start_with? CAN) || (line.include? CAN) }.join LF
517
+ elsif text.include? LF
518
+ (text.split LF, -1).reject {|line| line == CAN || (line.start_with? CAN) || (line.include? CAN) }.join LF
517
519
  else
518
520
  ''
519
521
  end
520
522
  else
521
- result
523
+ text
522
524
  end
523
525
  end
524
526
 
@@ -529,20 +531,19 @@ module Substitutors
529
531
  # source - The String text to process
530
532
  #
531
533
  # returns The converted String text
532
- def sub_macros(source)
533
- #return source if source.nil_or_empty?
534
+ def sub_macros(text)
535
+ #return text if text.nil_or_empty?
534
536
  # some look ahead assertions to cut unnecessary regex calls
535
537
  found = {}
536
- found_square_bracket = found[:square_bracket] = (source.include? '[')
537
- found_colon = source.include? ':'
538
+ found_square_bracket = found[:square_bracket] = (text.include? '[')
539
+ found_colon = text.include? ':'
538
540
  found_macroish = found[:macroish] = found_square_bracket && found_colon
539
- found_macroish_short = found_macroish && (source.include? ':[')
541
+ found_macroish_short = found_macroish && (text.include? ':[')
540
542
  doc_attrs = (doc = @document).attributes
541
- result = source
542
543
 
543
544
  if doc_attrs.key? 'experimental'
544
- if found_macroish_short && ((result.include? 'kbd:') || (result.include? 'btn:'))
545
- result = result.gsub(InlineKbdBtnMacroRx) {
545
+ if found_macroish_short && ((text.include? 'kbd:') || (text.include? 'btn:'))
546
+ text = text.gsub(InlineKbdBtnMacroRx) {
546
547
  # honor the escape
547
548
  if $1
548
549
  $&.slice 1, $&.length
@@ -570,13 +571,13 @@ module Substitutors
570
571
  }
571
572
  end
572
573
 
573
- if found_macroish && (result.include? 'menu:')
574
- result = result.gsub(InlineMenuMacroRx) {
574
+ if found_macroish && (text.include? 'menu:')
575
+ text = text.gsub(InlineMenuMacroRx) {
575
576
  # alias match for Ruby 1.8.7 compat
576
577
  m = $~
577
578
  # honor the escape
578
- if (captured = m[0]).start_with? RS
579
- next captured[1..-1]
579
+ if $&.start_with? RS
580
+ next m[0].slice 1, m[0].length
580
581
  end
581
582
 
582
583
  menu, items = m[1], m[2]
@@ -597,13 +598,13 @@ module Substitutors
597
598
  }
598
599
  end
599
600
 
600
- if (result.include? '"') && (result.include? '&gt;')
601
- result = result.gsub(InlineMenuRx) {
601
+ if (text.include? '"') && (text.include? '&gt;')
602
+ text = text.gsub(InlineMenuRx) {
602
603
  # alias match for Ruby 1.8.7 compat
603
604
  m = $~
604
605
  # honor the escape
605
- if (captured = m[0]).start_with? RS
606
- next captured[1..-1]
606
+ if $&.start_with? RS
607
+ next m[0].slice 1, m[0].length
607
608
  end
608
609
 
609
610
  input = m[1]
@@ -619,12 +620,12 @@ module Substitutors
619
620
  # TODO this handling needs some cleanup
620
621
  if (extensions = doc.extensions) && extensions.inline_macros? # && found_macroish
621
622
  extensions.inline_macros.each do |extension|
622
- result = result.gsub(extension.instance.regexp) {
623
+ text = text.gsub(extension.instance.regexp) {
623
624
  # alias match for Ruby 1.8.7 compat
624
625
  m = $~
625
626
  # honor the escape
626
- if m[0].start_with? RS
627
- next m[0][1..-1]
627
+ if $&.start_with? RS
628
+ next m[0].slice 1, m[0].length
628
629
  end
629
630
 
630
631
  if (m.names rescue []).empty?
@@ -639,8 +640,8 @@ module Substitutors
639
640
  content = unescape_bracketed_text content
640
641
  if extconf[:content_model] == :attributes
641
642
  # QUESTION should we store the text in the _text key?
642
- # QUESTION why is the sub_result option false? why isn't the unescape_input option true?
643
- parse_attributes content, extconf[:pos_attrs] || [], :sub_result => false, :into => attributes
643
+ # NOTE bracked text has already been escaped
644
+ parse_attributes content, extconf[:pos_attrs] || [], :into => attributes
644
645
  else
645
646
  attributes['text'] = content
646
647
  end
@@ -652,17 +653,15 @@ module Substitutors
652
653
  end
653
654
  end
654
655
 
655
- if found_macroish && ((result.include? 'image:') || (result.include? 'icon:'))
656
+ if found_macroish && ((text.include? 'image:') || (text.include? 'icon:'))
656
657
  # image:filename.png[Alt Text]
657
- result = result.gsub(InlineImageMacroRx) {
658
+ text = text.gsub(InlineImageMacroRx) {
658
659
  # alias match for Ruby 1.8.7 compat
659
660
  m = $~
660
661
  # honor the escape
661
662
  if (captured = $&).start_with? RS
662
- next captured[1..-1]
663
- end
664
-
665
- if captured.start_with? 'icon:'
663
+ next captured.slice 1, captured.length
664
+ elsif captured.start_with? 'icon:'
666
665
  type, posattrs = 'icon', ['size']
667
666
  else
668
667
  type, posattrs = 'image', ['alt', 'width', 'height']
@@ -671,25 +670,26 @@ module Substitutors
671
670
  # TODO remove this special case once titles use normal substitution order
672
671
  target = sub_attributes target
673
672
  end
674
- doc.register(:images, target) unless type == 'icon'
675
- attrs = parse_attributes(m[2], posattrs, :unescape_input => true)
673
+ attrs = parse_attributes m[2], posattrs, :unescape_input => true
674
+ doc.register :images, [target, (attrs['imagesdir'] = doc_attrs['imagesdir'])] unless type == 'icon'
676
675
  attrs['alt'] ||= (attrs['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
677
676
  Inline.new(self, :image, nil, :type => type, :target => target, :attributes => attrs).convert
678
677
  }
679
678
  end
680
679
 
681
- if ((result.include? '((') && (result.include? '))')) || (found_macroish_short && (result.include? 'dexterm'))
680
+ if ((text.include? '((') && (text.include? '))')) || (found_macroish_short && (text.include? 'dexterm'))
682
681
  # (((Tigers,Big cats)))
683
682
  # indexterm:[Tigers,Big cats]
684
683
  # ((Tigers))
685
684
  # indexterm2:[Tigers]
686
- result = result.gsub(InlineIndextermMacroRx) {
685
+ text = text.gsub(InlineIndextermMacroRx) {
686
+ captured = $&
687
687
  case $1
688
688
  when 'indexterm'
689
689
  text = $2
690
690
  # honor the escape
691
- if (m0 = $&).start_with? RS
692
- next m0.slice 1, m0.length
691
+ if captured.start_with? RS
692
+ next captured.slice 1, captured.length
693
693
  end
694
694
  # indexterm:[Tigers,Big cats]
695
695
  terms = split_simple_csv normalize_string text, true
@@ -698,8 +698,8 @@ module Substitutors
698
698
  when 'indexterm2'
699
699
  text = $2
700
700
  # honor the escape
701
- if (m0 = $&).start_with? RS
702
- next m0.slice 1, m0.length
701
+ if captured.start_with? RS
702
+ next captured.slice 1, captured.length
703
703
  end
704
704
  # indexterm2:[Tigers]
705
705
  term = normalize_string text, true
@@ -708,13 +708,13 @@ module Substitutors
708
708
  else
709
709
  text = $3
710
710
  # honor the escape
711
- if (m0 = $&).start_with? RS
711
+ if captured.start_with? RS
712
712
  # escape concealed index term, but process nested flow index term
713
713
  if (text.start_with? '(') && (text.end_with? ')')
714
714
  text = text.slice 1, text.length - 2
715
715
  visible, before, after = true, '(', ')'
716
716
  else
717
- next m0.slice 1, m0.length
717
+ next captured.slice 1, captured.length
718
718
  end
719
719
  else
720
720
  visible = true
@@ -732,29 +732,29 @@ module Substitutors
732
732
  # ((Tigers))
733
733
  term = normalize_string text
734
734
  doc.register :indexterms, [term]
735
- result = (Inline.new self, :indexterm, term, :type => :visible).convert
735
+ subbed_term = (Inline.new self, :indexterm, term, :type => :visible).convert
736
736
  else
737
737
  # (((Tigers,Big cats)))
738
738
  terms = split_simple_csv(normalize_string text)
739
739
  doc.register :indexterms, terms
740
- result = (Inline.new self, :indexterm, nil, :attributes => { 'terms' => terms }).convert
740
+ subbed_term = (Inline.new self, :indexterm, nil, :attributes => { 'terms' => terms }).convert
741
741
  end
742
- before ? %(#{before}#{result}#{after}) : result
742
+ before ? %(#{before}#{subbed_term}#{after}) : subbed_term
743
743
  end
744
744
  }
745
745
  end
746
746
 
747
- if found_colon && (result.include? '://')
747
+ if found_colon && (text.include? '://')
748
748
  # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>)
749
- result = result.gsub(InlineLinkRx) {
749
+ text = text.gsub(InlineLinkRx) {
750
750
  # alias match for Ruby 1.8.7 compat
751
751
  m = $~
752
752
  # honor the escape
753
- if m[2].start_with? RS
754
- next %(#{m[1]}#{m[2][1..-1]}#{m[3]})
753
+ if (target = $2).start_with? RS
754
+ next %(#{m[1]}#{target.slice 1, target.length}#{m[3]})
755
755
  end
756
756
  # NOTE if text is non-nil, then we've matched a formal macro (i.e., trailing square brackets)
757
- prefix, target, text, suffix = m[1], m[2], (macro = m[3]) || '', ''
757
+ prefix, text, suffix = m[1], (macro = m[3]) || '', ''
758
758
  if prefix == 'link:'
759
759
  if macro
760
760
  prefix = ''
@@ -773,8 +773,8 @@ module Substitutors
773
773
  when ';'
774
774
  # strip <> around URI
775
775
  if prefix.start_with?('&lt;') && target.end_with?('&gt;')
776
- prefix = prefix[4..-1]
777
- target = target[0...-4]
776
+ prefix = prefix.slice 4, prefix.length
777
+ target = target.slice 0, target.length - 4
778
778
  # strip trailing ;
779
779
  # check for trailing );
780
780
  elsif (target = target.chop).end_with?(')')
@@ -825,6 +825,7 @@ module Substitutors
825
825
  end
826
826
 
827
827
  if text.empty?
828
+ # NOTE it's not possible for the URI scheme to be bare in this case
828
829
  text = (doc_attrs.key? 'hide-uri-scheme') ? (target.sub UriSniffRx, '') : target
829
830
  if attrs
830
831
  attrs['role'] = (attrs.key? 'role') ? %(bare #{attrs['role']}) : 'bare'
@@ -839,14 +840,14 @@ module Substitutors
839
840
  }
840
841
  end
841
842
 
842
- if found_macroish && ((result.include? 'link:') || (result.include? 'mailto:'))
843
+ if found_macroish && ((text.include? 'link:') || (text.include? 'mailto:'))
843
844
  # inline link macros, link:target[text]
844
- result = result.gsub(InlineLinkMacroRx) {
845
+ text = text.gsub(InlineLinkMacroRx) {
845
846
  # alias match for Ruby 1.8.7 compat
846
847
  m = $~
847
848
  # honor the escape
848
- if m[0].start_with? RS
849
- next m[0][1..-1]
849
+ if $&.start_with? RS
850
+ next m[0].slice 1, m[0].length
850
851
  end
851
852
  target = (mailto = m[1]) ? %(mailto:#{m[2]}) : m[2]
852
853
  attrs, link_opts = nil, { :type => :link }
@@ -893,7 +894,13 @@ module Substitutors
893
894
  if mailto
894
895
  text = m[2]
895
896
  else
896
- text = (doc_attrs.key? 'hide-uri-scheme') ? (target.sub UriSniffRx, '') : target
897
+ if doc_attrs.key? 'hide-uri-scheme'
898
+ if (text = target.sub UriSniffRx, '').empty?
899
+ text = target
900
+ end
901
+ else
902
+ text = target
903
+ end
897
904
  if attrs
898
905
  attrs['role'] = (attrs.key? 'role') ? %(bare #{attrs['role']}) : 'bare'
899
906
  else
@@ -909,11 +916,11 @@ module Substitutors
909
916
  }
910
917
  end
911
918
 
912
- if result.include? '@'
913
- result = result.gsub(InlineEmailRx) {
919
+ if text.include? '@'
920
+ text = text.gsub(InlineEmailRx) {
914
921
  address, tip = $&, $1
915
922
  if tip
916
- next (tip == RS ? address[1..-1] : address)
923
+ next (tip == RS ? (address.slice 1, address.length) : address)
917
924
  end
918
925
 
919
926
  target = %(mailto:#{address})
@@ -924,13 +931,13 @@ module Substitutors
924
931
  }
925
932
  end
926
933
 
927
- if found_macroish && (result.include? 'tnote')
928
- result = result.gsub(InlineFootnoteMacroRx) {
934
+ if found_macroish && (text.include? 'tnote')
935
+ text = text.gsub(InlineFootnoteMacroRx) {
929
936
  # alias match for Ruby 1.8.7 compat
930
937
  m = $~
931
938
  # honor the escape
932
- if m[0].start_with? RS
933
- next m[0][1..-1]
939
+ if $&.start_with? RS
940
+ next m[0].slice 1, m[0].length
934
941
  end
935
942
  if m[1] # footnoteref (legacy)
936
943
  id, text = (m[3] || '').split(',', 2)
@@ -966,7 +973,7 @@ module Substitutors
966
973
  }
967
974
  end
968
975
 
969
- sub_inline_xrefs(sub_inline_anchors(result, found), found)
976
+ sub_inline_xrefs(sub_inline_anchors(text, found), found)
970
977
  end
971
978
 
972
979
  # Internal: Substitute normal and bibliographic anchors
@@ -1007,8 +1014,8 @@ module Substitutors
1007
1014
  # alias match for Ruby 1.8.7 compat
1008
1015
  m = $~
1009
1016
  # honor the escape
1010
- if m[0].start_with? RS
1011
- next m[0][1..-1]
1017
+ if $&.start_with? RS
1018
+ next m[0].slice 1, m[0].length
1012
1019
  end
1013
1020
  attrs, doc = {}, @document
1014
1021
  if (refid = m[1])
@@ -1098,14 +1105,16 @@ module Substitutors
1098
1105
  #
1099
1106
  # Returns the converted String text
1100
1107
  def sub_callouts(text)
1101
- # FIXME cache this dynamic regex
1102
- callout_rx = (attr? 'line-comment') ? /(?:#{::Regexp.escape(attr 'line-comment')} )?#{CalloutSourceRxt}/ : CalloutSourceRx
1108
+ callout_rx = (attr? 'line-comment') ? CalloutSourceRxMap[attr 'line-comment'] : CalloutSourceRx
1109
+ autonum = 0
1103
1110
  text.gsub(callout_rx) {
1104
- if $1
1105
- # we have to use sub since we aren't sure it's the first char
1106
- next $&.sub(RS, '')
1111
+ # honor the escape
1112
+ if $2
1113
+ # use sub since it might be behind a line comment
1114
+ $&.sub(RS, '')
1115
+ else
1116
+ Inline.new(self, :callout, $4 == '.' ? (autonum += 1).to_s : $4, :id => @document.callouts.read_next_id, :attributes => { 'guard' => $1 }).convert
1107
1117
  end
1108
- Inline.new(self, :callout, $3, :id => @document.callouts.read_next_id).convert
1109
1118
  }
1110
1119
  end
1111
1120
 
@@ -1121,7 +1130,7 @@ module Substitutors
1121
1130
  last = lines.pop
1122
1131
  (lines.map {|line|
1123
1132
  Inline.new(self, :break, (line.end_with? HARD_LINE_BREAK) ? (line.slice 0, line.length - 2) : line, :type => :line).convert
1124
- } << last) * LF
1133
+ } << last).join LF
1125
1134
  elsif (text.include? PLUS) && (text.include? HARD_LINE_BREAK)
1126
1135
  text.gsub(HardLineBreakRx) { Inline.new(self, :break, $1, :type => :line).convert }
1127
1136
  else
@@ -1141,7 +1150,7 @@ module Substitutors
1141
1150
  if scope == :constrained && (attrs = match[2])
1142
1151
  unescaped_attrs = %([#{attrs}])
1143
1152
  else
1144
- return match[0][1..-1]
1153
+ return match[0].slice 1, match[0].length
1145
1154
  end
1146
1155
  end
1147
1156
 
@@ -1172,7 +1181,7 @@ module Substitutors
1172
1181
  # Returns a Hash of attributes (role and id only)
1173
1182
  def parse_quoted_text_attributes str
1174
1183
  # NOTE attributes are typically resolved after quoted text, so substitute eagerly
1175
- str = sub_attributes str, :multiline => true if str.include? ATTR_REF_HEAD
1184
+ str = sub_attributes str if str.include? ATTR_REF_HEAD
1176
1185
  # for compliance, only consider first positional attribute
1177
1186
  str = str.slice 0, (str.index ',') if str.include? ','
1178
1187
 
@@ -1199,30 +1208,37 @@ module Substitutors
1199
1208
 
1200
1209
  attrs = {}
1201
1210
  attrs['id'] = id if id
1202
- attrs['role'] = roles * ' ' unless roles.empty?
1211
+ attrs['role'] = roles.join ' ' unless roles.empty?
1203
1212
  attrs
1204
1213
  else
1205
1214
  {'role' => str}
1206
1215
  end
1207
1216
  end
1208
1217
 
1209
- # Internal: Parse the attributes in the attribute line
1218
+ # Internal: Parse attributes in name or name=value format from a comma-separated String
1210
1219
  #
1211
- # attrline - A String of unprocessed attributes (key/value pairs)
1212
- # posattrs - The keys for positional attributes
1220
+ # attrlist - A comma-separated String list of attributes in name or name=value format.
1221
+ # posattrs - An Array of positional attribute names (default: []).
1222
+ # opts - A Hash of options to control how the string is parsed (default: {}):
1223
+ # :into - The Hash to parse the attributes into (optional, default: false).
1224
+ # :sub_input - A Boolean that indicates whether to substitute attributes prior to
1225
+ # parsing (optional, default: false).
1226
+ # :sub_result - A Boolean that indicates whether to apply substitutions
1227
+ # single-quoted attribute values (optional, default: true).
1228
+ # :unescape_input - A Boolean that indicates whether to unescape square brackets prior
1229
+ # to parsing (optional, default: false).
1213
1230
  #
1214
- # returns nil if attrline is nil, an empty Hash if attrline is empty, otherwise a Hash of parsed attributes
1215
- def parse_attributes(attrline, posattrs = ['role'], opts = {})
1216
- return unless attrline
1217
- return {} if attrline.empty?
1218
- attrline = @document.sub_attributes attrline if opts[:sub_input] && (attrline.include? ATTR_REF_HEAD)
1219
- attrline = unescape_bracketed_text attrline if opts[:unescape_input]
1231
+ # Returns an empty Hash if attrlist is nil or empty, otherwise a Hash of parsed attributes.
1232
+ def parse_attributes attrlist, posattrs = [], opts = {}
1233
+ return {} unless attrlist && !attrlist.empty?
1234
+ attrlist = @document.sub_attributes attrlist if opts[:sub_input] && (attrlist.include? ATTR_REF_HEAD)
1235
+ attrlist = unescape_bracketed_text attrlist if opts[:unescape_input]
1220
1236
  # substitutions are only performed on attribute values if block is not nil
1221
- block = opts.fetch(:sub_result, true) ? self : nil
1237
+ block = self if opts[:sub_result]
1222
1238
  if (into = opts[:into])
1223
- AttributeList.new(attrline, block).parse_into(into, posattrs)
1239
+ AttributeList.new(attrlist, block).parse_into(into, posattrs)
1224
1240
  else
1225
- AttributeList.new(attrline, block).parse(posattrs)
1241
+ AttributeList.new(attrlist, block).parse(posattrs)
1226
1242
  end
1227
1243
  end
1228
1244
 
@@ -1328,10 +1344,10 @@ module Substitutors
1328
1344
  if modifiers_present
1329
1345
  if (first = key.chr) == '+'
1330
1346
  modifier_operation = :append
1331
- key = key[1..-1]
1347
+ key = key.slice 1, key.length
1332
1348
  elsif first == '-'
1333
1349
  modifier_operation = :remove
1334
- key = key[1..-1]
1350
+ key = key.slice 1, key.length
1335
1351
  elsif key.end_with? '+'
1336
1352
  modifier_operation = :prepend
1337
1353
  key = key.chop
@@ -1355,7 +1371,7 @@ module Substitutors
1355
1371
  end
1356
1372
 
1357
1373
  if modifier_operation
1358
- candidates ||= (defaults ? defaults.dup : [])
1374
+ candidates ||= (defaults ? (defaults.drop 0) : [])
1359
1375
  case modifier_operation
1360
1376
  when :append
1361
1377
  candidates += resolved_keys
@@ -1374,7 +1390,7 @@ module Substitutors
1374
1390
  resolved = candidates & SUB_OPTIONS[type]
1375
1391
  unless (candidates - resolved).empty?
1376
1392
  invalid = candidates - resolved
1377
- logger.warn %(invalid substitution type#{invalid.size > 1 ? 's' : ''}#{subject ? ' for ' : ''}#{subject}: #{invalid * ', '})
1393
+ logger.warn %(invalid substitution type#{invalid.size > 1 ? 's' : ''}#{subject ? ' for ' : ''}#{subject}: #{invalid.join ', '})
1378
1394
  end
1379
1395
  resolved
1380
1396
  end
@@ -1433,25 +1449,22 @@ module Substitutors
1433
1449
  if process_callouts
1434
1450
  callout_marks = {}
1435
1451
  last = -1
1436
- # FIXME cache this dynamic regex
1437
- callout_rx = (attr? 'line-comment') ? /(?:#{::Regexp.escape(attr 'line-comment')} )?#{CalloutExtractRxt}/ : CalloutExtractRx
1452
+ callout_rx = (attr? 'line-comment') ? CalloutExtractRxMap[attr 'line-comment'] : CalloutExtractRx
1438
1453
  # extract callout marks, indexed by line number
1439
1454
  source = source.split(LF, -1).map {|line|
1440
1455
  lineno = lineno + 1
1441
1456
  line.gsub(callout_rx) {
1442
- # alias match for Ruby 1.8.7 compat
1443
- m = $~
1444
1457
  # honor the escape
1445
- if m[1] == RS
1446
- # we have to use sub since we aren't sure it's the first char
1447
- m[0].sub RS, ''
1458
+ if $2
1459
+ # use sub since it might be behind a line comment
1460
+ $&.sub(RS, '')
1448
1461
  else
1449
- (callout_marks[lineno] ||= []) << m[3]
1462
+ (callout_marks[lineno] ||= []) << [$1, $4]
1450
1463
  last = lineno
1451
1464
  nil
1452
1465
  end
1453
1466
  }
1454
- } * LF
1467
+ }.join LF
1455
1468
  callout_on_last = (last == lineno)
1456
1469
  callout_marks = nil if callout_marks.empty?
1457
1470
  else
@@ -1464,16 +1477,19 @@ module Substitutors
1464
1477
  case highlighter
1465
1478
  when 'coderay'
1466
1479
  if (linenums_mode = (attr? 'linenums', nil, false) ? (@document.attributes['coderay-linenums-mode'] || :table).to_sym : nil)
1480
+ start = 1 if (start = (attr 'start', nil, 1).to_i) < 1
1467
1481
  if attr? 'highlight', nil, false
1468
- highlight_lines = resolve_highlight_lines(attr 'highlight', nil, false)
1482
+ highlight_lines = resolve_lines_to_highlight source, (attr 'highlight', nil, false)
1469
1483
  end
1470
1484
  end
1471
1485
  result = ::CodeRay::Duo[attr('language', :text, false).to_sym, :html, {
1472
- :css => (@document.attributes['coderay-css'] || :class).to_sym,
1473
- :line_numbers => linenums_mode,
1474
- :line_number_anchors => false,
1475
- :highlight_lines => highlight_lines,
1476
- :bold_every => false}].highlight source
1486
+ :css => (@document.attributes['coderay-css'] || :class).to_sym,
1487
+ :line_numbers => linenums_mode,
1488
+ :line_number_start => start,
1489
+ :line_number_anchors => false,
1490
+ :highlight_lines => highlight_lines,
1491
+ :bold_every => false
1492
+ }].highlight source
1477
1493
  when 'pygments'
1478
1494
  lexer = ::Pygments::Lexer.find_by_alias(attr 'language', 'text', false) || ::Pygments::Lexer.find_by_mimetype('text/plain')
1479
1495
  opts = { :cssclass => 'pyhl', :classprefix => 'tok-', :nobackground => true, :stripnl => false }
@@ -1483,13 +1499,14 @@ module Substitutors
1483
1499
  opts[:style] = (@document.attributes['pygments-style'] || Stylesheets::DEFAULT_PYGMENTS_STYLE)
1484
1500
  end
1485
1501
  if attr? 'highlight', nil, false
1486
- unless (highlight_lines = resolve_highlight_lines(attr 'highlight', nil, false)).empty?
1487
- opts[:hl_lines] = highlight_lines * ' '
1502
+ unless (highlight_lines = resolve_lines_to_highlight source, (attr 'highlight', nil, false)).empty?
1503
+ opts[:hl_lines] = highlight_lines.join ' '
1488
1504
  end
1489
1505
  end
1490
1506
  # NOTE highlight can return nil if something goes wrong; fallback to source if this happens
1491
1507
  # TODO we could add the line numbers in ourselves instead of having to strip out the junk
1492
- if (attr? 'linenums', nil, false) && (opts[:linenos] = @document.attributes['pygments-linenums-mode'] || 'table') == 'table'
1508
+ if (attr? 'linenums', nil, false) && (opts[:linenostart] = (start = attr 'start', 1, false).to_i < 1 ? 1 : start) &&
1509
+ (opts[:linenos] = @document.attributes['pygments-linenums-mode'] || 'table') == 'table'
1493
1510
  linenums_mode = :table
1494
1511
  if (result = lexer.highlight source, :options => opts)
1495
1512
  result = (result.sub PygmentsWrapperDivRx, '\1').gsub PygmentsWrapperPreRx, '\1'
@@ -1510,6 +1527,7 @@ module Substitutors
1510
1527
 
1511
1528
  if process_callouts && callout_marks
1512
1529
  lineno = 0
1530
+ autonum = 0
1513
1531
  reached_code = linenums_mode != :table
1514
1532
  result.split(LF, -1).map {|line|
1515
1533
  unless reached_code
@@ -1527,32 +1545,35 @@ module Substitutors
1527
1545
  end
1528
1546
  end
1529
1547
  if conums.size == 1
1530
- %(#{line}#{Inline.new(self, :callout, conums[0], :id => @document.callouts.read_next_id).convert}#{tail})
1548
+ guard, conum = conums[0]
1549
+ %(#{line}#{Inline.new(self, :callout, conum == '.' ? (autonum += 1).to_s : conum, :id => @document.callouts.read_next_id, :attributes => { 'guard' => guard }).convert}#{tail})
1531
1550
  else
1532
- conums_markup = conums.map {|conum| Inline.new(self, :callout, conum, :id => @document.callouts.read_next_id).convert } * ' '
1551
+ conums_markup = conums.map {|guard_it, conum_it| Inline.new(self, :callout, conum_it == '.' ? (autonum += 1).to_s : conum_it, :id => @document.callouts.read_next_id, :attributes => { 'guard' => guard_it }).convert }.join ' '
1533
1552
  %(#{line}#{conums_markup}#{tail})
1534
1553
  end
1535
1554
  else
1536
1555
  line
1537
1556
  end
1538
- } * LF
1557
+ }.join LF
1539
1558
  else
1540
1559
  result
1541
1560
  end
1542
1561
  end
1543
1562
 
1544
1563
  # e.g., highlight="1-5, !2, 10" or highlight=1-5;!2,10
1545
- def resolve_highlight_lines spec
1564
+ def resolve_lines_to_highlight source, spec
1546
1565
  lines = []
1547
- ((spec.include? ' ') ? (spec.delete ' ') : spec).split(DataDelimiterRx).map do |entry|
1566
+ spec = spec.delete ' ' if spec.include? ' '
1567
+ ((spec.include? ',') ? (spec.split ',') : (spec.split ';')).map do |entry|
1548
1568
  negate = false
1549
1569
  if entry.start_with? '!'
1550
- entry = entry[1..-1]
1570
+ entry = entry.slice 1, entry.length
1551
1571
  negate = true
1552
1572
  end
1553
- if entry.include? '-'
1554
- s, e = entry.split '-', 2
1555
- line_nums = (s.to_i..e.to_i).to_a
1573
+ if (delim = (entry.include? '..') ? '..' : ((entry.include? '-') ? '-' : nil))
1574
+ from, to = entry.split delim, 2
1575
+ to = (source.count LF) + 1 if to.empty? || (to = to.to_i) < 0
1576
+ line_nums = (::Range.new from.to_i, to).to_a
1556
1577
  if negate
1557
1578
  lines -= line_nums
1558
1579
  else
@@ -1612,7 +1633,7 @@ module Substitutors
1612
1633
  if (custom_subs = @attributes['subs'])
1613
1634
  @subs = (resolve_block_subs custom_subs, default_subs, @context) || []
1614
1635
  else
1615
- @subs = default_subs.dup
1636
+ @subs = default_subs.drop 0
1616
1637
  end
1617
1638
 
1618
1639
  # QUESION delegate this logic to a method?