asciidoctor 1.5.5 → 1.5.6

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +216 -1
  3. data/CONTRIBUTING.adoc +2 -2
  4. data/Gemfile +20 -1
  5. data/LICENSE.adoc +1 -1
  6. data/README-fr.adoc +4 -3
  7. data/README-jp.adoc +11 -10
  8. data/README-zh_CN.adoc +4 -3
  9. data/README.adoc +17 -202
  10. data/Rakefile +41 -25
  11. data/asciidoctor.gemspec +9 -10
  12. data/data/locale/attributes.adoc +216 -34
  13. data/data/stylesheets/asciidoctor-default.css +23 -16
  14. data/features/step_definitions.rb +15 -19
  15. data/features/xref.feature +584 -20
  16. data/lib/asciidoctor.rb +292 -278
  17. data/lib/asciidoctor/abstract_block.rb +155 -94
  18. data/lib/asciidoctor/abstract_node.rb +108 -94
  19. data/lib/asciidoctor/attribute_list.rb +30 -22
  20. data/lib/asciidoctor/block.rb +7 -7
  21. data/lib/asciidoctor/cli/invoker.rb +47 -34
  22. data/lib/asciidoctor/cli/options.rb +22 -11
  23. data/lib/asciidoctor/converter.rb +3 -3
  24. data/lib/asciidoctor/converter/base.rb +2 -2
  25. data/lib/asciidoctor/converter/composite.rb +1 -1
  26. data/lib/asciidoctor/converter/docbook45.rb +2 -2
  27. data/lib/asciidoctor/converter/docbook5.rb +132 -87
  28. data/lib/asciidoctor/converter/factory.rb +0 -1
  29. data/lib/asciidoctor/converter/html5.rb +116 -98
  30. data/lib/asciidoctor/converter/manpage.rb +51 -52
  31. data/lib/asciidoctor/converter/template.rb +47 -36
  32. data/lib/asciidoctor/core_ext.rb +8 -2
  33. data/lib/asciidoctor/core_ext/1.8.7/hash/key.rb +4 -0
  34. data/lib/asciidoctor/core_ext/1.8.7/io/binread.rb +6 -0
  35. data/lib/asciidoctor/core_ext/1.8.7/io/write.rb +5 -0
  36. data/lib/asciidoctor/core_ext/1.8.7/string/chr.rb +1 -1
  37. data/lib/asciidoctor/core_ext/1.8.7/string/{limit.rb → limit_bytesize.rb} +7 -6
  38. data/lib/asciidoctor/core_ext/1.8.7/symbol/empty.rb +6 -0
  39. data/lib/asciidoctor/core_ext/1.8.7/symbol/length.rb +1 -1
  40. data/lib/asciidoctor/core_ext/nil_or_empty.rb +5 -5
  41. data/lib/asciidoctor/core_ext/regexp/is_match.rb +3 -0
  42. data/lib/asciidoctor/core_ext/string/{limit.rb → limit_bytesize.rb} +2 -2
  43. data/lib/asciidoctor/document.rb +216 -213
  44. data/lib/asciidoctor/extensions.rb +318 -185
  45. data/lib/asciidoctor/helpers.rb +35 -35
  46. data/lib/asciidoctor/inline.rb +32 -1
  47. data/lib/asciidoctor/list.rb +22 -6
  48. data/lib/asciidoctor/parser.rb +1008 -1038
  49. data/lib/asciidoctor/path_resolver.rb +46 -50
  50. data/lib/asciidoctor/reader.rb +275 -251
  51. data/lib/asciidoctor/section.rb +86 -58
  52. data/lib/asciidoctor/stylesheets.rb +6 -6
  53. data/lib/asciidoctor/substitutors.rb +567 -649
  54. data/lib/asciidoctor/table.rb +163 -108
  55. data/lib/asciidoctor/version.rb +1 -1
  56. data/man/asciidoctor.1 +18 -16
  57. data/man/asciidoctor.adoc +15 -13
  58. data/test/attributes_test.rb +138 -22
  59. data/test/blocks_test.rb +377 -97
  60. data/test/converter_test.rb +13 -0
  61. data/test/document_test.rb +244 -34
  62. data/test/extensions_test.rb +409 -42
  63. data/test/fixtures/asciidoc_index.txt +521 -0
  64. data/test/fixtures/basic-docinfo-footer.html +6 -0
  65. data/test/fixtures/basic-docinfo-footer.xml +8 -0
  66. data/test/fixtures/basic-docinfo.html +1 -0
  67. data/test/fixtures/basic-docinfo.xml +4 -0
  68. data/test/fixtures/basic.asciidoc +5 -0
  69. data/test/fixtures/chapter-a.adoc +3 -0
  70. data/test/fixtures/child-include.adoc +5 -0
  71. data/test/fixtures/circle.svg +9 -0
  72. data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +6 -0
  73. data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
  74. data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
  75. data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
  76. data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
  77. data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
  78. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
  79. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
  80. data/test/fixtures/custom-docinfodir/basic-docinfo.html +1 -0
  81. data/test/fixtures/custom-docinfodir/docinfo.html +1 -0
  82. data/test/fixtures/docinfo-footer.html +1 -0
  83. data/test/fixtures/docinfo-footer.xml +9 -0
  84. data/test/fixtures/docinfo.html +1 -0
  85. data/test/fixtures/docinfo.xml +3 -0
  86. data/test/fixtures/dot.gif +0 -0
  87. data/test/fixtures/encoding.asciidoc +13 -0
  88. data/test/fixtures/grandchild-include.adoc +3 -0
  89. data/test/fixtures/hello-asciidoctor.pdf +69 -0
  90. data/test/fixtures/include-file.asciidoc +24 -0
  91. data/test/fixtures/include-file.ml +3 -0
  92. data/test/fixtures/include-file.xml +5 -0
  93. data/test/fixtures/master.adoc +5 -0
  94. data/test/fixtures/mismatched-end-tag.adoc +7 -0
  95. data/test/fixtures/parent-include-restricted.adoc +5 -0
  96. data/test/fixtures/parent-include.adoc +5 -0
  97. data/test/fixtures/sample.asciidoc +26 -0
  98. data/test/fixtures/stylesheets/custom.css +3 -0
  99. data/test/fixtures/subs-docinfo.html +2 -0
  100. data/test/fixtures/subs.adoc +7 -0
  101. data/test/fixtures/tagged-class-enclosed.rb +26 -0
  102. data/test/fixtures/tagged-class.rb +23 -0
  103. data/test/fixtures/tip.gif +0 -0
  104. data/test/invoker_test.rb +82 -4
  105. data/test/links_test.rb +312 -37
  106. data/test/lists_test.rb +204 -25
  107. data/test/manpage_test.rb +191 -4
  108. data/test/options_test.rb +18 -1
  109. data/test/paragraphs_test.rb +32 -7
  110. data/test/parser_test.rb +150 -30
  111. data/test/paths_test.rb +47 -13
  112. data/test/preamble_test.rb +1 -1
  113. data/test/reader_test.rb +366 -126
  114. data/test/sections_test.rb +203 -56
  115. data/test/substitutions_test.rb +339 -131
  116. data/test/tables_test.rb +315 -15
  117. data/test/test_helper.rb +400 -0
  118. data/test/text_test.rb +5 -5
  119. metadata +110 -22
@@ -13,14 +13,16 @@ class AbstractBlock < AbstractNode
13
13
  # Public: Set the Integer level of this Section or the Section level in which this Block resides
14
14
  attr_accessor :level
15
15
 
16
- # Public: Set the String block title.
17
- attr_writer :title
18
-
19
16
  # Public: Get/Set the String style (block type qualifier) for this block.
20
17
  attr_accessor :style
21
18
 
22
- # Public: Get/Set the caption for this block
23
- attr_accessor :caption
19
+ # Public: Set the caption for this block
20
+ attr_writer :caption
21
+
22
+ # Public: Get/Set the number of this block (if section, relative to parent, otherwise absolute)
23
+ # Only assigned to section if automatic section numbering is enabled
24
+ # Only assigned to formal block (block with title) if corresponding caption attribute is present
25
+ attr_accessor :number
24
26
 
25
27
  # Public: Gets/Sets the location in the AsciiDoc source where this block begins
26
28
  attr_accessor :source_location
@@ -28,21 +30,18 @@ class AbstractBlock < AbstractNode
28
30
  def initialize parent, context, opts = {}
29
31
  super
30
32
  @content_model = :compound
31
- @subs = []
32
- @default_subs = nil
33
33
  @blocks = []
34
- @id = nil
35
- @title = nil
36
- @caption = nil
37
- @style = nil
38
- @level = if context == :document
39
- 0
34
+ @subs = []
35
+ @id = @title = @title_converted = @caption = @number = @style = @default_subs = @source_location = nil
36
+ if context == :document
37
+ @level = 0
40
38
  elsif parent && context != :section
41
- parent.level
39
+ @level = parent.level
40
+ else
41
+ @level = nil
42
42
  end
43
43
  @next_section_index = 0
44
44
  @next_section_number = 1
45
- @source_location = nil
46
45
  end
47
46
 
48
47
  def block?
@@ -72,12 +71,12 @@ class AbstractBlock < AbstractNode
72
71
  end
73
72
 
74
73
  # Alias render to convert to maintain backwards compatibility
75
- alias :render :convert
74
+ alias render convert
76
75
 
77
76
  # Public: Get the converted result of the child blocks by converting the
78
77
  # children appropriate to content model that this block supports.
79
78
  def content
80
- @blocks.map {|b| b.convert } * EOL
79
+ @blocks.map {|b| b.convert } * LF
81
80
  end
82
81
 
83
82
  # Public: Get the source file where this block started
@@ -101,10 +100,11 @@ class AbstractBlock < AbstractNode
101
100
  @subs.include? name
102
101
  end
103
102
 
104
- # Public: A convenience method that indicates whether the title instance
105
- # variable is blank (nil or empty)
103
+ # Public: A convenience method that checks whether the title of this block is defined.
104
+ #
105
+ # Returns a [Boolean] indicating whether this block has a title.
106
106
  def title?
107
- !@title.nil_or_empty?
107
+ @title ? true : false
108
108
  end
109
109
 
110
110
  # Public: Get the String title of this Block with title substitions applied
@@ -119,16 +119,28 @@ class AbstractBlock < AbstractNode
119
119
  # block.title
120
120
  # => "Foo 3^ # :: Bar(1)"
121
121
  #
122
- # Returns the String title of this Block
122
+ # Returns the converted String title for this Block, or nil if the source title is falsy
123
123
  def title
124
- # prevent substitutions from being applied multiple times
125
- if defined?(@subbed_title)
126
- @subbed_title
127
- elsif @title
128
- @subbed_title = apply_title_subs(@title)
129
- else
130
- @title
131
- end
124
+ # prevent substitutions from being applied to title multiple times
125
+ @title_converted ? @converted_title : (@converted_title = (@title_converted = true) && @title && (apply_title_subs @title))
126
+ end
127
+
128
+ # Public: Set the String block title.
129
+ #
130
+ # Returns the new String title assigned to this Block
131
+ def title= val
132
+ @title, @title_converted = val, nil
133
+ end
134
+
135
+ # Gets the caption for this block.
136
+ #
137
+ # This method routes the deprecated use of the caption method on an
138
+ # admonition block to the textlabel attribute.
139
+ #
140
+ # Returns the [String] caption for this block (or the value of the textlabel
141
+ # attribute if this is an admonition block).
142
+ def caption
143
+ @context == :admonition ? @attributes['textlabel'] : @caption
132
144
  end
133
145
 
134
146
  # Public: Convenience method that returns the interpreted title of the Block
@@ -139,12 +151,70 @@ class AbstractBlock < AbstractNode
139
151
  # two values. If the Block does not have a caption, the interpreted title is
140
152
  # returned.
141
153
  #
142
- # Returns the String title prefixed with the caption, or just the title if no
143
- # caption is set
154
+ # Returns the converted String title prefixed with the caption, or just the
155
+ # converted String title if no caption is set
144
156
  def captioned_title
145
157
  %(#{@caption}#{title})
146
158
  end
147
159
 
160
+ # Public: Returns the converted alt text for this block image.
161
+ #
162
+ # Returns the [String] value of the alt attribute with XML special character
163
+ # and replacement substitutions applied.
164
+ def alt
165
+ if (text = @attributes['alt'])
166
+ if text == @attributes['default-alt']
167
+ sub_specialchars text
168
+ else
169
+ text = sub_specialchars text
170
+ (ReplaceableTextRx.match? text) ? (sub_replacements text) : text
171
+ end
172
+ end
173
+ end
174
+
175
+ # Public: Generate cross reference text (xreftext) that can be used to refer
176
+ # to this block.
177
+ #
178
+ # Use the explicit reftext for this block, if specified, retrieved from the
179
+ # {#reftext} method. Otherwise, if this is a section or captioned block (a
180
+ # block with both a title and caption), generate the xreftext according to
181
+ # the value of the xrefstyle argument (e.g., full, short). This logic may
182
+ # leverage the {Substitutors#sub_quotes} method to apply formatting to the
183
+ # text. If this is not a captioned block, return the title, if present, or
184
+ # nil otherwise.
185
+ #
186
+ # xrefstyle - An optional String that specifies the style to use to format
187
+ # the xreftext ('full', 'short', or 'basic') (default: nil).
188
+ #
189
+ # Returns the generated [String] xreftext used to refer to this block or
190
+ # nothing if there isn't sufficient information to generate one.
191
+ def xreftext xrefstyle = nil
192
+ if (val = reftext) && !val.empty?
193
+ val
194
+ # NOTE xrefstyle only applies to blocks with a title and a caption or number
195
+ elsif xrefstyle && @title && @caption
196
+ case xrefstyle
197
+ when 'full'
198
+ quoted_title = sprintf sub_quotes(@document.compat_mode ? %q(``%s'') : '"`%s`"'), title
199
+ if @number && (prefix = @document.attributes[@context == :image ? 'figure-caption' : %(#{@context}-caption)])
200
+ %(#{prefix} #{@number}, #{quoted_title})
201
+ else
202
+ %(#{@caption.chomp '. '}, #{quoted_title})
203
+ end
204
+ when 'short'
205
+ if @number && (prefix = @document.attributes[@context == :image ? 'figure-caption' : %(#{@context}-caption)])
206
+ %(#{prefix} #{@number})
207
+ else
208
+ @caption.chomp '. '
209
+ end
210
+ else # 'basic'
211
+ title
212
+ end
213
+ else
214
+ title
215
+ end
216
+ end
217
+
148
218
  # Public: Determine whether this Block contains block content
149
219
  #
150
220
  # Returns A Boolean indicating whether this Block has block content
@@ -176,7 +246,7 @@ class AbstractBlock < AbstractNode
176
246
  end
177
247
 
178
248
  # NOTE append alias required for adapting to a Java API
179
- alias :append :<<
249
+ alias append <<
180
250
 
181
251
  # Public: Get the Array of child Section objects
182
252
  #
@@ -307,7 +377,13 @@ class AbstractBlock < AbstractNode
307
377
  end
308
378
  result
309
379
  end
310
- alias :query :find_by
380
+ alias query find_by
381
+
382
+ # Move to the next adjacent block in document order. If the current block is the last
383
+ # item in a list, this method will return the following sibling of the list block.
384
+ def next_adjacent_block
385
+ (sib = (p = parent).blocks[(p.blocks.find_index self) + 1]) ? sib : p.next_adjacent_block unless @context == :document
386
+ end
311
387
 
312
388
  # Public: Remove a substitution from this block
313
389
  #
@@ -319,39 +395,53 @@ class AbstractBlock < AbstractNode
319
395
  nil
320
396
  end
321
397
 
322
- # Public: Generate a caption and assign it to this block if one
323
- # is not already assigned.
398
+ # Public: Generate and assign caption to block if not already assigned.
324
399
  #
325
- # If the block has a title and a caption prefix is available
326
- # for this block, then build a caption from this information,
327
- # assign it a number and store it to the caption attribute on
328
- # the block.
400
+ # If the block has a title and a caption prefix is available for this block,
401
+ # then build a caption from this information, assign it a number and store it
402
+ # to the caption attribute on the block.
329
403
  #
330
- # If an explicit caption has been specified on this block, then
331
- # do nothing.
404
+ # If a caption has already been assigned to this block, do nothing.
332
405
  #
333
- # key - The prefix of the caption and counter attribute names.
334
- # If not provided, the name of the context for this block
335
- # is used. (default: nil).
406
+ # The parts of a complete caption are: <prefix> <number>. <title>
407
+ # This partial caption represents the part the precedes the title.
336
408
  #
337
- # Returns nothing
338
- def assign_caption(caption = nil, key = nil)
339
- return unless title? || !@caption
340
-
341
- if caption
342
- @caption = caption
343
- else
344
- if (value = @document.attributes['caption'])
345
- @caption = value
346
- elsif title?
347
- key ||= @context.to_s
348
- caption_key = "#{key}-caption"
349
- if (caption_title = @document.attributes[caption_key])
350
- caption_num = @document.counter_increment("#{key}-number", self)
351
- @caption = "#{caption_title} #{caption_num}. "
352
- end
409
+ # value - The explicit String caption to assign to this block (default: nil).
410
+ # key - The String prefix for the caption and counter attribute names.
411
+ # If not provided, the name of the context for this block is used.
412
+ # (default: nil)
413
+ #
414
+ # Returns nothing.
415
+ def assign_caption value = nil, key = nil
416
+ unless @caption || !@title || (@caption = value || @document.attributes['caption'])
417
+ if (prefix = @document.attributes[%(#{key ||= @context}-caption)])
418
+ @caption = %(#{prefix} #{@number = @document.increment_and_store_counter "#{key}-number", self}. )
419
+ nil
353
420
  end
354
421
  end
422
+ end
423
+
424
+ # Internal: Assign the next index (0-based) and number (1-based) to the section
425
+ #
426
+ # Assign to the specified section the next index and, if the section is
427
+ # numbered, number within this block (its parent).
428
+ #
429
+ # Returns nothing
430
+ def enumerate_section section
431
+ @next_section_index = (section.index = @next_section_index) + 1
432
+ if (sectname = section.sectname) == 'appendix'
433
+ section.number = @document.counter 'appendix-number', 'A'
434
+ if (caption = @document.attributes['appendix-caption'])
435
+ section.caption = %(#{caption} #{section.number}: )
436
+ else
437
+ section.caption = %(#{section.number}. )
438
+ end
439
+ # NOTE currently chapters in a book doctype are sequential even for multi-part books (see #979)
440
+ elsif sectname == 'chapter'
441
+ section.number = @document.counter 'chapter-number', 1
442
+ else
443
+ @next_section_number = (section.number = @next_section_number) + 1
444
+ end if section.numbered
355
445
  nil
356
446
  end
357
447
 
@@ -366,35 +456,6 @@ class AbstractBlock < AbstractNode
366
456
  ORDERED_LIST_KEYWORDS[list_type || @style]
367
457
  end
368
458
 
369
- # Internal: Assign the next index (0-based) to this section
370
- #
371
- # Assign the next index of this section within the parent
372
- # Block (in document order)
373
- #
374
- # Returns nothing
375
- def assign_index(section)
376
- section.index = @next_section_index
377
- @next_section_index += 1
378
-
379
- if section.sectname == 'appendix'
380
- appendix_number = @document.counter 'appendix-number', 'A'
381
- section.number = appendix_number if section.numbered
382
- if (caption = @document.attr 'appendix-caption', '').empty?
383
- section.caption = %(#{appendix_number}. )
384
- else
385
- section.caption = %(#{caption} #{appendix_number}: )
386
- end
387
- elsif section.numbered
388
- # chapters in a book doctype should be sequential even when divided into parts
389
- if (section.level == 1 || (section.level == 0 && section.special)) && @document.doctype == 'book'
390
- section.number = @document.counter('chapter-number', 1)
391
- else
392
- section.number = @next_section_number
393
- @next_section_number += 1
394
- end
395
- end
396
- end
397
-
398
459
  # Internal: Reassign the section indexes
399
460
  #
400
461
  # Walk the descendents of the current Document or Section
@@ -403,17 +464,17 @@ class AbstractBlock < AbstractNode
403
464
  #
404
465
  # IMPORTANT You must invoke this method on a node after removing
405
466
  # child sections or else the internal counters will be off.
406
- #
467
+ #
407
468
  # Returns nothing
408
469
  def reindex_sections
409
470
  @next_section_index = 0
410
- @next_section_number = 0
411
- @blocks.each {|block|
471
+ @next_section_number = 1
472
+ @blocks.each do |block|
412
473
  if block.context == :section
413
- assign_index(block)
474
+ enumerate_section block
414
475
  block.reindex_sections
415
476
  end
416
- }
477
+ end
417
478
  end
418
479
  end
419
480
  end
@@ -26,20 +26,17 @@ class AbstractNode
26
26
  attr_reader :attributes
27
27
 
28
28
  def initialize parent, context, opts = {}
29
- # document is a special case, should refer to itself
30
29
  if context == :document
31
- @document = parent
30
+ # document is a special case, should refer to itself
31
+ @document, @parent = self, nil
32
32
  else
33
33
  if parent
34
- @parent = parent
35
- @document = parent.document
34
+ @document, @parent = parent.document, parent
36
35
  else
37
- @parent = nil
38
- @document = nil
36
+ @document = @parent = nil
39
37
  end
40
38
  end
41
- @context = context
42
- @node_name = context.to_s
39
+ @node_name = (@context = context).to_s
43
40
  # QUESTION are we correct in duplicating the attributes (seems to be just as fast)
44
41
  @attributes = (opts.key? :attributes) ? opts[:attributes].dup : {}
45
42
  @passthroughs = {}
@@ -51,8 +48,7 @@ class AbstractNode
51
48
  #
52
49
  # Returns nothing
53
50
  def parent=(parent)
54
- @parent = parent
55
- @document = parent.document
51
+ @parent, @document = parent, parent.document
56
52
  nil
57
53
  end
58
54
 
@@ -82,21 +78,17 @@ class AbstractNode
82
78
  # Document node and return the value of the attribute if found. Otherwise,
83
79
  # return the default value, which defaults to nil.
84
80
  #
85
- # name - the String or Symbol name of the attribute to lookup
86
- # default_value - the Object value to return if the attribute is not found (default: nil)
87
- # inherit - a Boolean indicating whether to check for the attribute on the
88
- # AsciiDoctor::Document if not found on this node (default: false)
81
+ # name - the String or Symbol name of the attribute to lookup
82
+ # default_val - the Object value to return if the attribute is not found (default: nil)
83
+ # inherit - a Boolean indicating whether to check for the attribute on the
84
+ # AsciiDoctor::Document if not found on this node (default: false)
89
85
  #
90
86
  # return the value of the attribute or the default value if the attribute
91
87
  # is not found in the attributes of this node or the document node
92
- def attr(name, default_value = nil, inherit = true)
93
- name = name.to_s if ::Symbol === name
94
- inherit = false if self == @document
95
- if inherit
96
- @attributes[name] || @document.attributes[name] || default_value
97
- else
98
- @attributes[name] || default_value
99
- end
88
+ def attr name, default_val = nil, inherit = true
89
+ name = name.to_s
90
+ # NOTE if @parent is set, it means @document is also set
91
+ @attributes[name] || (inherit && @parent ? @document.attributes[name] || default_val : default_val)
100
92
  end
101
93
 
102
94
  # Public: Check if the attribute is defined, optionally performing a
@@ -108,35 +100,33 @@ class AbstractNode
108
100
  # comparison value is specified (not nil), return whether the two values match.
109
101
  # Otherwise, return whether the attribute was found.
110
102
  #
111
- # name - the String or Symbol name of the attribute to lookup
112
- # expect - the expected Object value of the attribute (default: nil)
113
- # inherit - a Boolean indicating whether to check for the attribute on the
114
- # AsciiDoctor::Document if not found on this node (default: false)
103
+ # name - the String or Symbol name of the attribute to lookup
104
+ # expect_val - the expected Object value of the attribute (default: nil)
105
+ # inherit - a Boolean indicating whether to check for the attribute on the
106
+ # AsciiDoctor::Document if not found on this node (default: false)
115
107
  #
116
108
  # return a Boolean indicating whether the attribute exists and, if a
117
109
  # comparison value is specified, whether the value of the attribute matches
118
110
  # the comparison value
119
- def attr?(name, expect = nil, inherit = true)
120
- name = name.to_s if ::Symbol === name
121
- inherit = false if self == @document
122
- if expect.nil?
123
- @attributes.has_key?(name) || (inherit && @document.attributes.has_key?(name))
124
- elsif inherit
125
- expect == (@attributes[name] || @document.attributes[name])
111
+ def attr? name, expect_val = nil, inherit = true
112
+ name = name.to_s
113
+ # NOTE if @parent is set, it means @document is also set
114
+ if expect_val.nil?
115
+ (@attributes.key? name) || (inherit && @parent && (@document.attributes.key? name))
126
116
  else
127
- expect == @attributes[name]
117
+ expect_val == (@attributes[name] || (inherit && @parent ? @document.attributes[name] : nil))
128
118
  end
129
119
  end
130
120
 
131
121
  # Public: Assign the value to the attribute name for the current node.
132
122
  #
133
123
  # name - The String attribute name to assign
134
- # value - The Object value to assign to the attribute
124
+ # value - The Object value to assign to the attribute (default: '')
135
125
  # overwrite - A Boolean indicating whether to assign the attribute
136
126
  # if currently present in the attributes Hash (default: true)
137
127
  #
138
128
  # Returns a [Boolean] indicating whether the assignment was performed
139
- def set_attr name, value, overwrite = true
129
+ def set_attr name, value = '', overwrite = true
140
130
  if overwrite == false && (@attributes.key? name)
141
131
  false
142
132
  else
@@ -145,14 +135,23 @@ class AbstractNode
145
135
  end
146
136
  end
147
137
 
138
+ # Public: Remove the attribute from the current node.
139
+ #
140
+ # name - The String attribute name to remove
141
+ #
142
+ # Returns the previous [String] value, or nil if the attribute was not present.
143
+ def remove_attr name
144
+ @attributes.delete name
145
+ end
146
+
148
147
  # TODO document me
149
148
  def set_option(name)
150
- if @attributes.has_key? 'options'
151
- @attributes['options'] = "#{@attributes['options']},#{name}"
149
+ if @attributes.key? 'options'
150
+ @attributes['options'] = %(#{@attributes['options']},#{name})
152
151
  else
153
152
  @attributes['options'] = name
154
153
  end
155
- @attributes["#{name}-option"] = ''
154
+ @attributes[%(#{name}-option)] = ''
156
155
  end
157
156
 
158
157
  # Public: A convenience method to check if the specified option attribute is
@@ -165,7 +164,7 @@ class AbstractNode
165
164
  #
166
165
  # return a Boolean indicating whether the option has been specified
167
166
  def option?(name)
168
- @attributes.has_key? %(#{name}-option)
167
+ @attributes.key? %(#{name}-option)
169
168
  end
170
169
 
171
170
  # Public: Update the attributes of this node with the new values in
@@ -189,11 +188,11 @@ class AbstractNode
189
188
  end
190
189
 
191
190
  # Public: A convenience method that checks if the role attribute is specified
192
- def role?(expect = nil)
193
- if expect
194
- expect == (@attributes['role'] || @document.attributes['role'])
191
+ def role? expect_val = nil
192
+ if expect_val
193
+ expect_val == (@attributes['role'] || @document.attributes['role'])
195
194
  else
196
- @attributes.has_key?('role') || @document.attributes.has_key?('role')
195
+ @attributes.key?('role') || @document.attributes.key?('role')
197
196
  end
198
197
  end
199
198
 
@@ -205,45 +204,59 @@ class AbstractNode
205
204
  # Public: A convenience method that checks if the specified role is present
206
205
  # in the list of roles on this node
207
206
  def has_role?(name)
208
- if (val = (@attributes['role'] || @document.attributes['role']))
209
- val.split(' ').include?(name)
210
- else
211
- false
212
- end
207
+ # NOTE center + include? is faster than split + include?
208
+ (val = @attributes['role'] || @document.attributes['role']).nil_or_empty? ? false : %( #{val} ).include?(%( #{name} ))
213
209
  end
214
210
 
215
211
  # Public: A convenience method that returns the role names as an Array
212
+ #
213
+ # Returns the role names as an Array or an empty Array if the role attribute is absent.
216
214
  def roles
217
- if (val = (@attributes['role'] || @document.attributes['role']))
218
- val.split(' ')
219
- else
220
- []
221
- end
215
+ (val = @attributes['role'] || @document.attributes['role']).nil_or_empty? ? [] : val.split
222
216
  end
223
217
 
224
218
  # Public: A convenience method that adds the given role directly to this node
219
+ #
220
+ # Returns a Boolean indicating whether the role was added.
225
221
  def add_role(name)
226
- unless (roles = (@attributes['role'] || '').split(' ')).include? name
227
- @attributes['role'] = roles.push(name) * ' '
222
+ if (val = @attributes['role']).nil_or_empty?
223
+ @attributes['role'] = name
224
+ true
225
+ # NOTE center + include? is faster than split + include?
226
+ elsif %( #{val} ).include?(%( #{name} ))
227
+ false
228
+ else
229
+ @attributes['role'] = %(#{val} #{name})
230
+ true
228
231
  end
229
232
  end
230
233
 
231
234
  # Public: A convenience method that removes the given role directly from this node
235
+ #
236
+ # Returns a Boolean indicating whether the role was removed.
232
237
  def remove_role(name)
233
- if (roles = (@attributes['role'] || '').split(' ')).include? name
234
- roles.delete name
235
- @attributes['role'] = roles * ' '
238
+ if (val = @attributes['role']).nil_or_empty?
239
+ false
240
+ elsif (val = val.split).delete name
241
+ if val.empty?
242
+ @attributes.delete('role')
243
+ else
244
+ @attributes['role'] = val * ' '
245
+ end
246
+ true
247
+ else
248
+ false
236
249
  end
237
250
  end
238
251
 
239
- # Public: A convenience method that checks if the reftext attribute is specified
252
+ # Public: A convenience method that checks if the reftext attribute is defined.
240
253
  def reftext?
241
- @attributes.has_key?('reftext') || @document.attributes.has_key?('reftext')
254
+ @attributes.key? 'reftext'
242
255
  end
243
256
 
244
- # Public: A convenience method that returns the value of the reftext attribute
257
+ # Public: A convenience method that returns the value of the reftext attribute with substitutions applied.
245
258
  def reftext
246
- @attributes['reftext'] || @document.attributes['reftext']
259
+ (val = @attributes['reftext']) ? (apply_reftext_subs val) : nil
247
260
  end
248
261
 
249
262
  # Public: Construct a reference or data URI to an icon image for the
@@ -315,12 +328,12 @@ class AbstractNode
315
328
  #
316
329
  # Returns A String reference or data URI for the target image
317
330
  def image_uri(target_image, asset_dir_key = 'imagesdir')
318
- if (doc = @document).safe < SafeMode::SECURE && doc.attr?('data-uri')
319
- if (Helpers.uriish? target_image) ||
320
- (asset_dir_key && (images_base = doc.attr(asset_dir_key)) && (Helpers.uriish? images_base) &&
321
- (target_image = normalize_web_path(target_image, images_base, false)))
322
- if doc.attr?('allow-uri-read')
323
- generate_data_uri_from_uri target_image, doc.attr?('cache-uri')
331
+ if (doc = @document).safe < SafeMode::SECURE && (doc.attr? 'data-uri')
332
+ if ((Helpers.uriish? target_image) && (target_image = uri_encode_spaces target_image)) ||
333
+ (asset_dir_key && (images_base = doc.attr asset_dir_key) && (Helpers.uriish? images_base) &&
334
+ (target_image = normalize_web_path target_image, images_base, false))
335
+ if doc.attr? 'allow-uri-read'
336
+ generate_data_uri_from_uri target_image, (doc.attr? 'cache-uri')
324
337
  else
325
338
  target_image
326
339
  end
@@ -328,7 +341,7 @@ class AbstractNode
328
341
  generate_data_uri target_image, asset_dir_key
329
342
  end
330
343
  else
331
- normalize_web_path target_image, (asset_dir_key ? doc.attr(asset_dir_key) : nil)
344
+ normalize_web_path target_image, (asset_dir_key ? (doc.attr asset_dir_key) : nil)
332
345
  end
333
346
  end
334
347
 
@@ -356,20 +369,13 @@ class AbstractNode
356
369
 
357
370
  unless ::File.readable? image_path
358
371
  warn %(asciidoctor: WARNING: image to embed not found or not readable: #{image_path})
359
- # must enclose string following return in " for Opal
360
- return "data:#{mimetype}:base64,"
372
+ return %(data:#{mimetype};base64,)
361
373
  # uncomment to return 1 pixel white dot instead
362
374
  #return 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
363
375
  end
364
376
 
365
- bindata = nil
366
- if ::IO.respond_to? :binread
367
- bindata = ::IO.binread(image_path)
368
- else
369
- bindata = ::File.open(image_path, 'rb') {|file| file.read }
370
- end
371
377
  # NOTE base64 is autoloaded by reference to ::Base64
372
- %(data:#{mimetype};base64,#{::Base64.encode64(bindata).delete EOL})
378
+ %(data:#{mimetype};base64,#{::Base64.encode64(::IO.binread image_path).delete LF})
373
379
  end
374
380
 
375
381
  # Public: Read the image data from the specified URI and generate a data URI
@@ -396,12 +402,12 @@ class AbstractNode
396
402
 
397
403
  begin
398
404
  mimetype = nil
399
- bindata = open(image_uri, 'rb') {|file|
400
- mimetype = file.content_type
401
- file.read
405
+ bindata = open(image_uri, 'rb') {|fd|
406
+ mimetype = fd.content_type
407
+ fd.read
402
408
  }
403
409
  # NOTE base64 is autoloaded by reference to ::Base64
404
- %(data:#{mimetype};base64,#{::Base64.encode64(bindata).delete EOL})
410
+ %(data:#{mimetype};base64,#{::Base64.encode64(bindata).delete LF})
405
411
  rescue
406
412
  warn %(asciidoctor: WARNING: could not retrieve image data from URI: #{image_uri})
407
413
  image_uri
@@ -436,7 +442,7 @@ class AbstractNode
436
442
  Helpers.require_library 'open-uri/cached', 'open-uri-cached' if doc.attr? 'cache-uri'
437
443
  begin
438
444
  data = ::OpenURI.open_uri(target) {|fd| fd.read }
439
- data = (Helpers.normalize_lines_from_string data) * EOL if opts[:normalize]
445
+ data = (Helpers.normalize_lines_from_string data) * LF if opts[:normalize]
440
446
  rescue
441
447
  warn %(asciidoctor: WARNING: could not retrieve contents of #{opts[:label] || 'asset'} at URI: #{target}) if opts.fetch :warn_on_failure, true
442
448
  data = nil
@@ -447,7 +453,7 @@ class AbstractNode
447
453
  end
448
454
  else
449
455
  target = normalize_system_path target, opts[:start], nil, :target_name => (opts[:label] || 'asset')
450
- data = read_asset target, :normalize => opts[:normalize], :warn_on_failure => (opts.fetch :warn_on_failure, true)
456
+ data = read_asset target, :normalize => opts[:normalize], :warn_on_failure => (opts.fetch :warn_on_failure, true), :label => opts[:label]
451
457
  end
452
458
  data
453
459
  end
@@ -465,25 +471,24 @@ class AbstractNode
465
471
  #
466
472
  # Returns the [String] content of the file at the specified path, or nil
467
473
  # if the file does not exist.
468
- def read_asset(path, opts = {})
474
+ def read_asset path, opts = {}
469
475
  # remap opts for backwards compatibility
470
476
  opts = { :warn_on_failure => (opts != false) } unless ::Hash === opts
471
477
  if ::File.readable? path
472
478
  if opts[:normalize]
473
- Helpers.normalize_lines_from_string(::IO.read(path)) * EOL
479
+ Helpers.normalize_lines_from_string(::IO.read path) * LF
474
480
  else
475
481
  # QUESTION should we chomp or rstrip content?
476
- ::IO.read(path)
482
+ ::IO.read path
477
483
  end
478
- else
479
- warn %(asciidoctor: WARNING: file does not exist or cannot be read: #{path}) if opts[:warn_on_failure]
480
- nil
484
+ elsif opts[:warn_on_failure]
485
+ warn %(asciidoctor: WARNING: #{(attr 'docfile') || '<stdin>'}: #{opts[:label] || 'file'} does not exist or cannot be read: #{path})
481
486
  end
482
487
  end
483
488
 
484
- # Public: Normalize the web page using the PathResolver.
489
+ # Public: Normalize the web path using the PathResolver.
485
490
  #
486
- # See {PathResolver#web_path} for details.
491
+ # See {PathResolver#web_path} for details about path resolution and encoding.
487
492
  #
488
493
  # target - the String target path
489
494
  # start - the String start (i.e, parent) path (optional, default: nil)
@@ -492,12 +497,21 @@ class AbstractNode
492
497
  # Returns the resolved [String] path
493
498
  def normalize_web_path(target, start = nil, preserve_uri_target = true)
494
499
  if preserve_uri_target && (Helpers.uriish? target)
495
- target
500
+ uri_encode_spaces target
496
501
  else
497
502
  (@path_resolver ||= PathResolver.new).web_path target, start
498
503
  end
499
504
  end
500
505
 
506
+ # Internal: URI encode spaces in a String
507
+ #
508
+ # str - the String to encode
509
+ #
510
+ # Returns the String with all spaces replaced with %20.
511
+ def uri_encode_spaces str
512
+ (str.include? ' ') ? (str.gsub ' ', '%20') : str
513
+ end
514
+
501
515
  # Public: Resolve and normalize a secure path from the target and start paths
502
516
  # using the PathResolver.
503
517
  #