asciidoctor 0.1.2 → 0.1.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 (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}]"