asciidoctor 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +10 -0
  3. data/Guardfile +18 -0
  4. data/LICENSE +1 -1
  5. data/README.adoc +65 -21
  6. data/Rakefile +10 -0
  7. data/asciidoctor.gemspec +17 -35
  8. data/compat/asciidoc.conf +130 -13
  9. data/lib/asciidoctor.rb +107 -87
  10. data/lib/asciidoctor/abstract_block.rb +6 -2
  11. data/lib/asciidoctor/abstract_node.rb +21 -13
  12. data/lib/asciidoctor/attribute_list.rb +2 -5
  13. data/{stylesheets/asciidoctor.css → lib/asciidoctor/backends/_stylesheets.rb} +96 -46
  14. data/lib/asciidoctor/backends/base_template.rb +9 -4
  15. data/lib/asciidoctor/backends/docbook45.rb +246 -138
  16. data/lib/asciidoctor/backends/html5.rb +580 -381
  17. data/lib/asciidoctor/block.rb +2 -50
  18. data/lib/asciidoctor/cli/options.rb +9 -8
  19. data/lib/asciidoctor/document.rb +35 -45
  20. data/lib/asciidoctor/helpers.rb +10 -0
  21. data/lib/asciidoctor/lexer.rb +456 -148
  22. data/lib/asciidoctor/list_item.rb +0 -21
  23. data/lib/asciidoctor/path_resolver.rb +18 -12
  24. data/lib/asciidoctor/reader.rb +71 -26
  25. data/lib/asciidoctor/renderer.rb +2 -19
  26. data/lib/asciidoctor/section.rb +0 -1
  27. data/lib/asciidoctor/substituters.rb +150 -36
  28. data/lib/asciidoctor/table.rb +30 -24
  29. data/lib/asciidoctor/version.rb +1 -1
  30. data/man/asciidoctor.1 +22 -16
  31. data/man/asciidoctor.ad +24 -16
  32. data/test/attributes_test.rb +50 -0
  33. data/test/blocks_test.rb +660 -9
  34. data/test/document_test.rb +191 -14
  35. data/test/fixtures/encoding.asciidoc +8 -0
  36. data/test/invoker_test.rb +47 -0
  37. data/test/lexer_test.rb +172 -0
  38. data/test/links_test.rb +28 -0
  39. data/test/lists_test.rb +172 -13
  40. data/test/options_test.rb +29 -2
  41. data/test/paragraphs_test.rb +105 -47
  42. data/test/paths_test.rb +3 -3
  43. data/test/reader_test.rb +46 -0
  44. data/test/sections_test.rb +365 -12
  45. data/test/substitutions_test.rb +127 -11
  46. data/test/tables_test.rb +81 -14
  47. data/test/test_helper.rb +18 -7
  48. data/test/text_test.rb +17 -5
  49. metadata +9 -36
@@ -55,27 +55,6 @@ class ListItem < AbstractBlock
55
55
  nil
56
56
  end
57
57
 
58
- def splain(parent_level = 0)
59
- parent_level += 1
60
- Debug.puts_indented(parent_level, "List Item anchor: #{@anchor}") unless @anchor.nil?
61
- Debug.puts_indented(parent_level, "Text: #{@text}") unless @text.nil?
62
-
63
- Debug.puts_indented(parent_level, "Blocks: #{@blocks.count}")
64
-
65
- if @blocks.any?
66
- Debug.puts_indented(parent_level, "Blocks content (#{@blocks.count}):")
67
- @blocks.each_with_index do |block, i|
68
- Debug.puts_indented(parent_level, "v" * (60 - parent_level*2))
69
- Debug.puts_indented(parent_level, "Block ##{i} is a #{block.class}")
70
- Debug.puts_indented(parent_level, "Name is #{block.title rescue 'n/a'}")
71
- Debug.puts_indented(parent_level, "=" * 40)
72
- block.splain(parent_level) if block.respond_to? :splain
73
- Debug.puts_indented(parent_level, "^" * (60 - parent_level*2))
74
- end
75
- end
76
- nil
77
- end
78
-
79
58
  def to_s
80
59
  "#{super.to_s} - #@context [text:#@text, blocks:#{(@blocks || []).size}]"
81
60
  end
@@ -76,6 +76,12 @@ module Asciidoctor
76
76
  # resolver.system_path('../../../css', '../../..', '/path/to/docs')
77
77
  # => '/path/to/docs/css'
78
78
  #
79
+ # resolver.system_path('..', 'C:\\data\\docs\\assets', 'C:\\data\\docs')
80
+ # => 'C:/data/docs'
81
+ #
82
+ # resolver.system_path('..\\..\\css', 'C:\\data\\docs\\assets', 'C:\\data\\docs')
83
+ # => 'C:/data/docs/css'
84
+ #
79
85
  # begin
80
86
  # resolver.system_path('../../../css', '../../..', '/path/to/docs', :recover => false)
81
87
  # rescue SecurityError => e
@@ -98,14 +104,13 @@ class PathResolver
98
104
  DOT_DOT = '..'
99
105
  SLASH = '/'
100
106
  BACKSLASH = '\\'
101
- PARTITION_RE = /\/+/
102
107
  WIN_ROOT_RE = /^[[:alpha:]]:(?:\\|\/)/
103
108
 
104
109
  attr_accessor :file_separator
105
110
  attr_accessor :working_dir
106
111
 
107
112
  # Public: Construct a new instance of PathResolver, optionally specifying the
108
- # path separator (to override the system default) and the working directory
113
+ # file separator (to override the system default) and the working directory
109
114
  # (to override the present working directory). The working directory will be
110
115
  # expanded to an absolute path inside the constructor.
111
116
  #
@@ -114,7 +119,7 @@ class PathResolver
114
119
  # working_dir - the String working directory (optional, default: Dir.pwd)
115
120
  #
116
121
  def initialize(file_separator = nil, working_dir = nil)
117
- @file_separator = file_separator.nil? ? File::SEPARATOR : file_separator
122
+ @file_separator = file_separator.nil? ? (File::ALT_SEPARATOR || File::SEPARATOR) : file_separator
118
123
  if working_dir.nil?
119
124
  @working_dir = File.expand_path(Dir.pwd)
120
125
  else
@@ -170,7 +175,7 @@ class PathResolver
170
175
  # returns a String path with any parent or self references resolved.
171
176
  def expand_path(path)
172
177
  path_segments, path_root, _ = partition_path(path)
173
- join_path(path_segments, path_root)
178
+ join_path path_segments, path_root
174
179
  end
175
180
 
176
181
  # Public: Partition the path into path segments and remove any empty segments
@@ -186,7 +191,7 @@ class PathResolver
186
191
  def partition_path(path, web_path = false)
187
192
  posix_path = posixfy path
188
193
  is_root = web_path ? is_web_root?(posix_path) : is_root?(posix_path)
189
- path_segments = posix_path.split(PARTITION_RE)
194
+ path_segments = posix_path.tr_s(SLASH, SLASH).split(SLASH)
190
195
  # capture relative root
191
196
  root = path_segments.first == DOT ? DOT : nil
192
197
  path_segments.delete(DOT)
@@ -196,20 +201,21 @@ class PathResolver
196
201
  [path_segments, root, posix_path]
197
202
  end
198
203
 
199
- # Public: Join the segments using the file separator specified in the
200
- # constructor. Use the root, if specified, to construct an absolute path.
201
- # Otherwise join the segments as a relative path.
204
+ # Public: Join the segments using the posix file separator (since Ruby knows
205
+ # how to work with paths specified this way, regardless of OS). Use the root,
206
+ # if specified, to construct an absolute path. Otherwise join the segments as
207
+ # a relative path.
202
208
  #
203
209
  # segments - a String Array of path segments
204
210
  # root - a String path root (optional, default: nil)
205
211
  #
206
- # returns a String path formed by joining the segments and prepending
207
- # the root, if specified
212
+ # returns a String path formed by joining the segments using the posix file
213
+ # separator and prepending the root, if specified
208
214
  def join_path(segments, root = nil)
209
215
  if root
210
- "#{root}#{@file_separator}#{segments * @file_separator}"
216
+ "#{root}#{SLASH}#{segments * SLASH}"
211
217
  else
212
- segments * @file_separator
218
+ segments * SLASH
213
219
  end
214
220
  end
215
221
 
@@ -425,10 +425,15 @@ class Reader
425
425
  #
426
426
  # returns a Boolean indicating whether the line under the cursor has changed.
427
427
  def preprocess_include(target, raw_attributes)
428
+ target = @document.sub_attributes target
429
+ if target.empty?
430
+ advance
431
+ @next_line_preprocessed = false
432
+ false
428
433
  # if running in SafeMode::SECURE or greater, don't process this directive
429
434
  # however, be friendly and at least make it a link to the source document
430
- if @document.safe >= SafeMode::SECURE
431
- @lines[0] = "link:#{target}[#{target}]"
435
+ elsif @document.safe >= SafeMode::SECURE
436
+ @lines[0] = "link:#{target}[#{target}]\n"
432
437
  @next_line_preprocessed = true
433
438
  false
434
439
  # assume that if a block is given, the developer wants
@@ -437,7 +442,7 @@ class Reader
437
442
  elsif @include_block
438
443
  advance
439
444
  # FIXME this borks line numbers
440
- @lines.unshift(*@include_block.call(target).map {|l| "#{l.rstrip}\n"})
445
+ @lines.unshift(*normalize_include_data(@include_block.call(target)))
441
446
  # FIXME currently we're not checking the upper bound of the include depth
442
447
  elsif @document.attributes.fetch('include-depth', 0).to_i > 0
443
448
  advance
@@ -448,48 +453,48 @@ class Reader
448
453
  return true
449
454
  end
450
455
 
451
- lines = nil
456
+ inc_lines = nil
452
457
  tags = nil
458
+ attributes = {}
453
459
  if !raw_attributes.empty?
454
460
  attributes = AttributeList.new(raw_attributes).parse
455
461
  if attributes.has_key? 'lines'
456
- lines = []
457
- attributes['lines'].split(REGEXP[:scsv_csv_delim]).each do |linedef|
462
+ inc_lines = []
463
+ attributes['lines'].split(REGEXP[:ssv_or_csv_delim]).each do |linedef|
458
464
  if linedef.include?('..')
459
465
  from, to = linedef.split('..').map(&:to_i)
460
466
  if to == -1
461
- lines << from
462
- lines << 1.0/0.0
467
+ inc_lines << from
468
+ inc_lines << 1.0/0.0
463
469
  else
464
- lines.concat Range.new(from, to).to_a
470
+ inc_lines.concat Range.new(from, to).to_a
465
471
  end
466
472
  else
467
- lines << linedef.to_i
473
+ inc_lines << linedef.to_i
468
474
  end
469
475
  end
470
- lines = lines.sort.uniq
471
- #lines.push lines.shift if lines.first == -1
476
+ inc_lines = inc_lines.sort.uniq
472
477
  elsif attributes.has_key? 'tags'
473
- tags = attributes['tags'].split(REGEXP[:scsv_csv_delim]).uniq
478
+ tags = attributes['tags'].split(REGEXP[:ssv_or_csv_delim]).uniq
474
479
  end
475
480
  end
476
- if !lines.nil?
477
- if !lines.empty?
481
+ if !inc_lines.nil?
482
+ if !inc_lines.empty?
478
483
  selected = []
479
484
  f = File.new(include_file)
480
485
  f.each_line do |l|
481
- take = lines.first
486
+ take = inc_lines.first
482
487
  if take.is_a?(Float) && take.infinite?
483
- selected.push("#{l.rstrip}\n")
488
+ selected.push l
484
489
  else
485
490
  if f.lineno == take
486
- selected.push("#{l.rstrip}\n")
487
- lines.shift
491
+ selected.push l
492
+ inc_lines.shift
488
493
  end
489
- break if lines.empty?
494
+ break if inc_lines.empty?
490
495
  end
491
496
  end
492
- @lines.unshift(*selected) unless selected.empty?
497
+ @lines.unshift(*normalize_include_data(selected, attributes['indent'])) unless selected.empty?
493
498
  end
494
499
  elsif !tags.nil?
495
500
  if !tags.empty?
@@ -497,11 +502,12 @@ class Reader
497
502
  active_tag = nil
498
503
  f = File.new(include_file)
499
504
  f.each_line do |l|
505
+ l.force_encoding(::Encoding::UTF_8) if ::Asciidoctor::FORCE_ENCODING
500
506
  if !active_tag.nil?
501
507
  if l.include?("end::#{active_tag}[]")
502
508
  active_tag = nil
503
509
  else
504
- selected.push("#{l.rstrip}\n")
510
+ selected.push "#{l.rstrip}\n"
505
511
  end
506
512
  else
507
513
  tags.each do |tag|
@@ -512,10 +518,11 @@ class Reader
512
518
  end
513
519
  end
514
520
  end
515
- @lines.unshift(*selected) unless selected.empty?
521
+ #@lines.unshift(*selected) unless selected.empty?
522
+ @lines.unshift(*normalize_include_data(selected, attributes['indent'])) unless selected.empty?
516
523
  end
517
524
  else
518
- @lines.unshift(*File.readlines(include_file).map {|l| "#{l.rstrip}\n"})
525
+ @lines.unshift(*normalize_include_data(File.readlines(include_file), attributes['indent']))
519
526
  end
520
527
  true
521
528
  else
@@ -675,7 +682,7 @@ class Reader
675
682
  end
676
683
 
677
684
  if val.include? '{'
678
- val = @document.sub_attributes(val)
685
+ val = @document.sub_attributes val
679
686
  end
680
687
 
681
688
  if type != :s
@@ -695,6 +702,39 @@ class Reader
695
702
  val
696
703
  end
697
704
 
705
+ # Private: Normalize raw input read from an include directive
706
+ #
707
+ # This method strips whitespace from the end of every line of
708
+ # the source data and appends a LF (i.e., Unix endline). This
709
+ # whitespace substitution is very important to how Asciidoctor
710
+ # works.
711
+ #
712
+ # Any leading or trailing blank lines are also removed. (DISABLED)
713
+ #
714
+ # data - A String Array of input data to be normalized
715
+ #
716
+ # returns the processed lines
717
+ #-
718
+ # FIXME this shares too much in common w/ normalize_data; combine
719
+ # in a shared function
720
+ def normalize_include_data(data, indent = nil)
721
+ if ::Asciidoctor::FORCE_ENCODING
722
+ result = data.map {|line| "#{line.rstrip.force_encoding(::Encoding::UTF_8)}\n" }
723
+ else
724
+ result = data.map {|line| "#{line.rstrip}\n" }
725
+ end
726
+
727
+ unless indent.nil?
728
+ Lexer.reset_block_indent! result, indent.to_i
729
+ end
730
+
731
+ result
732
+
733
+ #data.shift while !data.first.nil? && data.first.chomp.empty?
734
+ #data.pop while !data.last.nil? && data.last.chomp.empty?
735
+ #data
736
+ end
737
+
698
738
  # Private: Normalize raw input, used for the outermost Reader.
699
739
  #
700
740
  # This method strips whitespace from the end of every line of
@@ -712,7 +752,12 @@ class Reader
712
752
  def normalize_data(data)
713
753
  # normalize line ending to LF (purging occurrences of CRLF)
714
754
  # this rstrip is *very* important to how Asciidoctor works
715
- @lines = data.map {|line| "#{line.rstrip}\n" }
755
+
756
+ if ::Asciidoctor::FORCE_ENCODING
757
+ @lines = data.map {|line| "#{line.rstrip.force_encoding(::Encoding::UTF_8)}\n" }
758
+ else
759
+ @lines = data.map {|line| "#{line.rstrip}\n" }
760
+ end
716
761
 
717
762
  @lines.shift && @lineno += 1 while !@lines.first.nil? && @lines.first.chomp.empty?
718
763
  @lines.pop while !@lines.last.nil? && @lines.last.chomp.empty?
@@ -55,18 +55,11 @@ class Renderer
55
55
  :slim => { :disable_escape => true, :sort_attrs => false, :pretty => false }
56
56
  }
57
57
 
58
- if backend == 'html5'
58
+ # workaround until we have a proper way to configure
59
+ if {'html5' => true, 'dzslides' => true, 'deckjs' => true, 'revealjs' => true}.has_key? backend
59
60
  view_opts[:haml][:format] = view_opts[:slim][:format] = :html5
60
61
  end
61
62
 
62
- Debug.debug {
63
- msg = []
64
- msg << "Views going in are like so:"
65
- msg << @views.map {|k, v| "#{k}: #{v}"}
66
- msg << '=' * 60
67
- msg * "\n"
68
- }
69
-
70
63
  slim_loaded = false
71
64
  helpers = nil
72
65
 
@@ -91,14 +84,6 @@ class Renderer
91
84
  end
92
85
 
93
86
  require helpers unless helpers.nil?
94
-
95
- Debug.debug {
96
- msg = []
97
- msg << "Views going in are like so:"
98
- msg << @views.map {|k, v| "#{k}: #{v}"}
99
- msg << '=' * 60
100
- msg * "\n"
101
- }
102
87
  end
103
88
  end
104
89
 
@@ -110,8 +95,6 @@ class Renderer
110
95
  def render(view, object, locals = {})
111
96
  if !@views.has_key? view
112
97
  raise "Couldn't find a view in @views for #{view}"
113
- else
114
- Debug.debug { "View for #{view} is #{@views[view]}, object is #{object}" }
115
98
  end
116
99
 
117
100
  @views[view].render(object, locals)
@@ -90,7 +90,6 @@ class Section < AbstractBlock
90
90
  # Public: Get the rendered String content for this Section and all its child
91
91
  # Blocks.
92
92
  def render
93
- Debug.debug { "Now rendering section for #{self}" }
94
93
  @document.playback_attributes @attributes
95
94
  renderer.render('section', self)
96
95
  end
@@ -40,8 +40,9 @@ module Substituters
40
40
  multiline = lines.is_a?(Array)
41
41
  text = multiline ? lines.join : lines
42
42
 
43
- passthroughs = subs.include?(:macros)
44
- text = extract_passthroughs(text) if passthroughs
43
+ if (has_passthroughs = subs.include?(:macros))
44
+ text = extract_passthroughs(text)
45
+ end
45
46
 
46
47
  subs.each {|type|
47
48
  case type
@@ -63,7 +64,7 @@ module Substituters
63
64
  puts "asciidoctor: WARNING: unknown substitution type #{type}"
64
65
  end
65
66
  }
66
- text = restore_passthroughs(text) if passthroughs
67
+ text = restore_passthroughs(text) if has_passthroughs
67
68
 
68
69
  multiline ? text.lines.entries : text
69
70
  end
@@ -165,7 +166,7 @@ module Substituters
165
166
  # TODO move unescaping closing square bracket to an operation
166
167
  @passthroughs << {:text => m[2] || m[4].gsub('\]', ']'), :subs => subs}
167
168
  index = @passthroughs.size - 1
168
- "\x0#{index}\x0"
169
+ "\e#{index}\e"
169
170
  } unless !(result.include?('+++') || result.include?('$$') || result.include?('pass:'))
170
171
 
171
172
  result.gsub!(REGEXP[:pass_lit]) {
@@ -179,7 +180,7 @@ module Substituters
179
180
 
180
181
  @passthroughs << {:text => m[3], :subs => [:specialcharacters], :literal => true}
181
182
  index = @passthroughs.size - 1
182
- "#{m[1]}\x0#{index}\x0"
183
+ "#{m[1]}\e#{index}\e"
183
184
  } unless !result.include?('`')
184
185
 
185
186
  result
@@ -191,7 +192,7 @@ module Substituters
191
192
  #
192
193
  # returns The String text with the passthrough text restored
193
194
  def restore_passthroughs(text)
194
- return text if @passthroughs.nil? || @passthroughs.empty? || !text.include?("\x0")
195
+ return text if @passthroughs.nil? || @passthroughs.empty? || !text.include?("\e")
195
196
 
196
197
  text.gsub(REGEXP[:pass_placeholder]) {
197
198
  pass = @passthroughs[$1.to_i];
@@ -275,41 +276,55 @@ module Substituters
275
276
  def sub_attributes(data)
276
277
  return data if data.nil? || data.empty?
277
278
 
279
+ string_data = data.is_a? String
278
280
  # normalizes data type to an array (string becomes single-element array)
279
- lines = Array(data)
281
+ lines = string_data ? [data] : data
280
282
 
281
- result = lines.map {|line|
283
+ result = []
284
+ lines.each {|line|
282
285
  reject = false
283
- subject = line.dup
284
- subject.gsub!(REGEXP[:attr_ref]) {
286
+ line = line.gsub(REGEXP[:attr_ref]) {
285
287
  # alias match for Ruby 1.8.7 compat
286
288
  m = $~
287
- key = m[2].downcase
288
- # escaped attribute
289
- if !$1.empty? || !$3.empty?
290
- "{#$2}"
291
- elsif m[2].start_with?('counter:')
292
- args = m[2].split(':')
293
- @document.counter(args[1], args[2])
294
- elsif m[2].start_with?('counter2:')
295
- args = m[2].split(':')
296
- @document.counter(args[1], args[2])
297
- ''
298
- elsif document.attributes.has_key? key
289
+ # escaped attribute, return unescaped
290
+ if !m[1].nil? || !m[4].nil?
291
+ "{#{m[2]}}"
292
+ elsif (directive = m[3])
293
+ offset = directive.length + 1
294
+ expr = m[2][offset..-1]
295
+ case directive
296
+ when 'set'
297
+ args = expr.split(':')
298
+ _, value = Lexer::store_attribute(args[0], args[1] || '', @document)
299
+ if value.nil?
300
+ reject = true
301
+ break '{undefined}'
302
+ end
303
+ ''
304
+ when 'counter', 'counter2'
305
+ args = expr.split(':')
306
+ val = @document.counter(args[0], args[1])
307
+ directive == 'counter2' ? '' : val
308
+ else
309
+ # if we get here, our attr_ref regex is too loose
310
+ puts "asciidoctor: WARNING: illegal attribute directive: #{m[2]}"
311
+ ''
312
+ end
313
+ elsif (key = m[2].downcase) && @document.attributes.has_key?(key)
299
314
  @document.attributes[key]
300
315
  elsif INTRINSICS.has_key? key
301
316
  INTRINSICS[key]
302
317
  else
303
- Debug.debug { "Missing attribute: #{m[2]}, line marked for removal" }
318
+ Debug.debug { "Missing attribute: #{key}, line marked for removal" }
304
319
  reject = true
305
320
  break '{undefined}'
306
321
  end
307
- } if subject.include?('{')
322
+ } if line.include? '{'
308
323
 
309
- !reject ? subject : nil
310
- }.compact
324
+ result << line unless reject
325
+ }
311
326
 
312
- data.is_a?(String) ? result.join : result
327
+ string_data ? result.join : result
313
328
  end
314
329
 
315
330
  # Public: Substitute inline macros (e.g., links, images, etc)
@@ -333,7 +348,90 @@ module Substituters
333
348
  found[:macroish] = (found[:square_bracket] && found[:colon])
334
349
  found[:macroish_short_form] = (found[:square_bracket] && found[:colon] && result.include?(':['))
335
350
  found[:uri] = (found[:colon] && result.include?('://'))
336
- link_attrs = @document.attributes.has_key?('linkattrs')
351
+ use_link_attrs = @document.attributes.has_key?('linkattrs')
352
+ experimental = @document.attributes.has_key?('experimental')
353
+
354
+ if experimental
355
+ if found[:macroish_short_form] && (result.include?('kbd:') || result.include?('btn:'))
356
+ result.gsub!(REGEXP[:kbd_btn_macro]) {
357
+ # alias match for Ruby 1.8.7 compat
358
+ m = $~
359
+ # honor the escape
360
+ if (captured = m[0]).start_with? '\\'
361
+ next captured[1..-1]
362
+ end
363
+
364
+ if captured.start_with?('kbd')
365
+ keys = unescape_bracketed_text m[1]
366
+
367
+ if keys == '+'
368
+ keys = ['+']
369
+ else
370
+ # need to use closure to work around lack of negative lookbehind
371
+ keys = keys.split(REGEXP[:kbd_delim]).inject([]) {|c, key|
372
+ if key.end_with?('++')
373
+ c << key[0..-3].strip
374
+ c << '+'
375
+ else
376
+ c << key.strip
377
+ end
378
+ c
379
+ }
380
+ end
381
+ Inline.new(self, :kbd, nil, :attributes => {'keys' => keys}).render
382
+ elsif captured.start_with?('btn')
383
+ label = unescape_bracketed_text m[1]
384
+ Inline.new(self, :button, label).render
385
+ end
386
+ }
387
+ end
388
+
389
+ if found[:macroish] && result.include?('menu:')
390
+ result.gsub!(REGEXP[:menu_macro]) {
391
+ # alias match for Ruby 1.8.7 compat
392
+ m = $~
393
+ # honor the escape
394
+ if (captured = m[0]).start_with? '\\'
395
+ next captured[1..-1]
396
+ end
397
+
398
+ menu = m[1]
399
+ items = m[2]
400
+
401
+ if items.nil?
402
+ submenus = []
403
+ menuitem = nil
404
+ else
405
+ if (delim = items.include?('&gt;') ? '&gt;' : (items.include?(',') ? ',' : nil))
406
+ submenus = items.split(delim).map(&:strip)
407
+ menuitem = submenus.pop
408
+ else
409
+ submenus = []
410
+ menuitem = items.rstrip
411
+ end
412
+ end
413
+
414
+ Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).render
415
+ }
416
+ end
417
+
418
+ if result.include?('"') && result.include?('&gt;')
419
+ result.gsub!(REGEXP[:menu_inline_macro]) {
420
+ # alias match for Ruby 1.8.7 compat
421
+ m = $~
422
+ # honor the escape
423
+ if (captured = m[0]).start_with? '\\'
424
+ next captured[1..-1]
425
+ end
426
+
427
+ input = m[1]
428
+
429
+ menu, *submenus = input.split('&gt;').map(&:strip)
430
+ menuitem = submenus.pop
431
+ Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).render
432
+ }
433
+ end
434
+ end
337
435
 
338
436
  if found[:macroish] && result.include?('image:')
339
437
  # image:filename.png[Alt Text]
@@ -365,7 +463,7 @@ module Substituters
365
463
  next m[0][1..-1]
366
464
  end
367
465
 
368
- terms = unescape_bracketed_text(m[1] || m[2]).split(REGEXP[:csv_delimiter])
466
+ terms = unescape_bracketed_text(m[1] || m[2]).split(',').map(&:strip)
369
467
  document.register(:indexterms, [*terms])
370
468
  Inline.new(self, :indexterm, text, :attributes => {'terms' => terms}).render
371
469
  }
@@ -409,18 +507,27 @@ module Substituters
409
507
  elsif prefix.start_with?('(') && target.end_with?(')')
410
508
  target = target[0..-2]
411
509
  suffix = ')'
510
+ elsif target.end_with?('):')
511
+ target = target[0..-3]
512
+ suffix = '):'
412
513
  end
413
514
  @document.register(:links, target)
414
515
 
415
516
  attrs = nil
416
517
  #text = !m[3].nil? ? sub_attributes(m[3].gsub('\]', ']')) : ''
417
518
  if !m[3].to_s.empty?
418
- if link_attrs && (m[3].start_with?('"') || m[3].include?(','))
419
- attrs = parse_attributes(sub_attributes(m[3].gsub('\]', ']')))
519
+ if use_link_attrs && (m[3].start_with?('"') || m[3].include?(','))
520
+ attrs = parse_attributes(sub_attributes(m[3].gsub('\]', ']')), [])
420
521
  text = attrs[1]
421
522
  else
422
523
  text = sub_attributes(m[3].gsub('\]', ']'))
423
524
  end
525
+
526
+ if text.end_with? '^'
527
+ text = text.chop
528
+ attrs ||= {}
529
+ attrs['window'] = '_blank' unless attrs.has_key?('window')
530
+ end
424
531
  else
425
532
  text = ''
426
533
  end
@@ -444,8 +551,8 @@ module Substituters
444
551
 
445
552
  attrs = nil
446
553
  #text = sub_attributes(m[2].gsub('\]', ']'))
447
- if link_attrs && (m[2].start_with?('"') || m[2].include?(','))
448
- attrs = parse_attributes(sub_attributes(m[2].gsub('\]', ']')))
554
+ if use_link_attrs && (m[2].start_with?('"') || m[2].include?(','))
555
+ attrs = parse_attributes(sub_attributes(m[2].gsub('\]', ']')), [])
449
556
  text = attrs[1]
450
557
  if mailto
451
558
  if attrs.has_key? 2
@@ -459,6 +566,13 @@ module Substituters
459
566
  else
460
567
  text = sub_attributes(m[2].gsub('\]', ']'))
461
568
  end
569
+
570
+ if text.end_with? '^'
571
+ text = text.chop
572
+ attrs ||= {}
573
+ attrs['window'] = '_blank' unless attrs.has_key?('window')
574
+ end
575
+
462
576
  # QUESTION should a mailto be registered as an e-mail address?
463
577
  @document.register(:links, target)
464
578
 
@@ -503,7 +617,7 @@ module Substituters
503
617
  type = nil
504
618
  target = nil
505
619
  else
506
- id, text = m[2].split(REGEXP[:csv_delimiter], 2)
620
+ id, text = m[2].split(',', 2).map(&:strip)
507
621
  if !text.nil?
508
622
  # hmmmm
509
623
  text = restore_passthroughs(text)
@@ -533,7 +647,7 @@ module Substituters
533
647
  next m[0][1..-1]
534
648
  end
535
649
  if !m[1].nil?
536
- id, reftext = m[1].split(REGEXP[:csv_delimiter], 2)
650
+ id, reftext = m[1].split(',', 2).map(&:strip)
537
651
  id.sub!(REGEXP[:dbl_quoted], '\2')
538
652
  reftext.sub!(REGEXP[:m_dbl_quoted], '\2') unless reftext.nil?
539
653
  else
@@ -565,7 +679,7 @@ module Substituters
565
679
  if m[0].start_with? '\\'
566
680
  next m[0][1..-1]
567
681
  end
568
- id, reftext = m[1].split(REGEXP[:csv_delimiter])
682
+ id, reftext = m[1].split(',').map(&:strip)
569
683
  id.sub!(REGEXP[:dbl_quoted], '\2')
570
684
  if reftext.nil?
571
685
  reftext = "[#{id}]"