asciidoctor 1.5.8 → 2.0.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +11 -0
  3. data/CHANGELOG.adoc +628 -45
  4. data/LICENSE +2 -1
  5. data/README-de.adoc +28 -38
  6. data/README-fr.adoc +30 -43
  7. data/README-jp.adoc +255 -201
  8. data/README-zh_CN.adoc +40 -44
  9. data/README.adoc +170 -143
  10. data/asciidoctor.gemspec +22 -34
  11. data/bin/asciidoctor +5 -4
  12. data/data/locale/attributes-ar.adoc +4 -3
  13. data/data/locale/attributes-be.adoc +23 -0
  14. data/data/locale/attributes-bg.adoc +4 -3
  15. data/data/locale/attributes-ca.adoc +6 -5
  16. data/data/locale/attributes-cs.adoc +4 -3
  17. data/data/locale/attributes-da.adoc +6 -5
  18. data/data/locale/attributes-de.adoc +6 -5
  19. data/data/locale/attributes-en.adoc +4 -4
  20. data/data/locale/attributes-es.adoc +6 -5
  21. data/data/locale/attributes-fa.adoc +4 -3
  22. data/data/locale/attributes-fi.adoc +4 -3
  23. data/data/locale/attributes-fr.adoc +8 -7
  24. data/data/locale/attributes-hu.adoc +4 -3
  25. data/data/locale/attributes-id.adoc +4 -3
  26. data/data/locale/attributes-it.adoc +6 -5
  27. data/data/locale/attributes-ja.adoc +4 -3
  28. data/data/locale/{attributes-kr.adoc → attributes-ko.adoc} +4 -3
  29. data/data/locale/attributes-nb.adoc +4 -3
  30. data/data/locale/attributes-nl.adoc +6 -5
  31. data/data/locale/attributes-nn.adoc +4 -3
  32. data/data/locale/attributes-pl.adoc +8 -7
  33. data/data/locale/attributes-pt.adoc +6 -5
  34. data/data/locale/attributes-pt_BR.adoc +6 -5
  35. data/data/locale/attributes-ro.adoc +4 -3
  36. data/data/locale/attributes-ru.adoc +6 -5
  37. data/data/locale/attributes-sr.adoc +4 -4
  38. data/data/locale/attributes-sr_Latn.adoc +4 -4
  39. data/data/locale/attributes-sv.adoc +4 -4
  40. data/data/locale/attributes-th.adoc +23 -0
  41. data/data/locale/attributes-tr.adoc +4 -3
  42. data/data/locale/attributes-uk.adoc +6 -5
  43. data/data/locale/attributes-vi.adoc +23 -0
  44. data/data/locale/attributes-zh_CN.adoc +4 -3
  45. data/data/locale/attributes-zh_TW.adoc +4 -3
  46. data/data/reference/syntax.adoc +296 -0
  47. data/data/stylesheets/asciidoctor-default.css +120 -114
  48. data/data/stylesheets/coderay-asciidoctor.css +15 -17
  49. data/lib/asciidoctor/abstract_block.rb +146 -140
  50. data/lib/asciidoctor/abstract_node.rb +152 -170
  51. data/lib/asciidoctor/attribute_list.rb +77 -89
  52. data/lib/asciidoctor/block.rb +29 -28
  53. data/lib/asciidoctor/callouts.rb +4 -2
  54. data/lib/asciidoctor/cli/invoker.rb +20 -24
  55. data/lib/asciidoctor/cli/options.rb +107 -96
  56. data/lib/asciidoctor/cli.rb +3 -2
  57. data/lib/asciidoctor/convert.rb +199 -0
  58. data/lib/asciidoctor/converter/composite.rb +40 -48
  59. data/lib/asciidoctor/converter/docbook5.rb +627 -644
  60. data/lib/asciidoctor/converter/html5.rb +1053 -951
  61. data/lib/asciidoctor/converter/manpage.rb +581 -532
  62. data/lib/asciidoctor/converter/template.rb +232 -271
  63. data/lib/asciidoctor/converter.rb +370 -185
  64. data/lib/asciidoctor/core_ext/float/truncate.rb +20 -0
  65. data/lib/asciidoctor/core_ext/hash/merge.rb +8 -0
  66. data/lib/asciidoctor/core_ext/match_data/names.rb +7 -0
  67. data/lib/asciidoctor/core_ext/nil_or_empty.rb +1 -0
  68. data/lib/asciidoctor/core_ext/regexp/is_match.rb +4 -2
  69. data/lib/asciidoctor/core_ext.rb +8 -17
  70. data/lib/asciidoctor/document.rb +503 -461
  71. data/lib/asciidoctor/extensions.rb +127 -174
  72. data/lib/asciidoctor/helpers.rb +184 -107
  73. data/lib/asciidoctor/inline.rb +9 -12
  74. data/lib/asciidoctor/list.rb +11 -29
  75. data/lib/asciidoctor/load.rb +119 -0
  76. data/lib/asciidoctor/logging.rb +22 -17
  77. data/lib/asciidoctor/parser.rb +673 -719
  78. data/lib/asciidoctor/path_resolver.rb +48 -33
  79. data/lib/asciidoctor/reader.rb +383 -338
  80. data/lib/asciidoctor/rouge_ext.rb +39 -0
  81. data/lib/asciidoctor/rx.rb +723 -0
  82. data/lib/asciidoctor/section.rb +17 -16
  83. data/lib/asciidoctor/stylesheets.rb +19 -37
  84. data/lib/asciidoctor/substitutors.rb +926 -1022
  85. data/lib/asciidoctor/syntax_highlighter/coderay.rb +88 -0
  86. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +34 -0
  87. data/lib/asciidoctor/syntax_highlighter/html_pipeline.rb +10 -0
  88. data/lib/asciidoctor/syntax_highlighter/prettify.rb +30 -0
  89. data/lib/asciidoctor/syntax_highlighter/pygments.rb +157 -0
  90. data/lib/asciidoctor/syntax_highlighter/rouge.rb +143 -0
  91. data/lib/asciidoctor/syntax_highlighter.rb +253 -0
  92. data/lib/asciidoctor/table.rb +152 -114
  93. data/lib/asciidoctor/timings.rb +7 -5
  94. data/lib/asciidoctor/version.rb +2 -1
  95. data/lib/asciidoctor/writer.rb +30 -0
  96. data/lib/asciidoctor.rb +266 -1340
  97. data/man/asciidoctor.1 +49 -47
  98. data/man/asciidoctor.adoc +54 -45
  99. metadata +50 -245
  100. data/CONTRIBUTING.adoc +0 -185
  101. data/Gemfile +0 -60
  102. data/Rakefile +0 -129
  103. data/bin/asciidoctor-safe +0 -15
  104. data/features/open_block.feature +0 -92
  105. data/features/pass_block.feature +0 -66
  106. data/features/step_definitions.rb +0 -49
  107. data/features/text_formatting.feature +0 -57
  108. data/features/xref.feature +0 -1039
  109. data/lib/asciidoctor/converter/base.rb +0 -59
  110. data/lib/asciidoctor/converter/docbook45.rb +0 -93
  111. data/lib/asciidoctor/converter/factory.rb +0 -226
  112. data/lib/asciidoctor/core_ext/1.8.7/base64/strict_encode64.rb +0 -6
  113. data/lib/asciidoctor/core_ext/1.8.7/concurrent/hash.rb +0 -5
  114. data/lib/asciidoctor/core_ext/1.8.7/hash/key.rb +0 -4
  115. data/lib/asciidoctor/core_ext/1.8.7/io/binread.rb +0 -6
  116. data/lib/asciidoctor/core_ext/1.8.7/io/write.rb +0 -5
  117. data/lib/asciidoctor/core_ext/1.8.7/string/chr.rb +0 -6
  118. data/lib/asciidoctor/core_ext/1.8.7/string/limit_bytesize.rb +0 -29
  119. data/lib/asciidoctor/core_ext/1.8.7/symbol/empty.rb +0 -6
  120. data/lib/asciidoctor/core_ext/1.8.7/symbol/length.rb +0 -6
  121. data/lib/asciidoctor/core_ext/string/limit_bytesize.rb +0 -10
  122. data/test/api_test.rb +0 -1240
  123. data/test/attribute_list_test.rb +0 -242
  124. data/test/attributes_test.rb +0 -1623
  125. data/test/blocks_test.rb +0 -3870
  126. data/test/converter_test.rb +0 -470
  127. data/test/document_test.rb +0 -1853
  128. data/test/extensions_test.rb +0 -1560
  129. data/test/fixtures/asciidoc_index.txt +0 -521
  130. data/test/fixtures/basic-docinfo-footer.html +0 -6
  131. data/test/fixtures/basic-docinfo-footer.xml +0 -8
  132. data/test/fixtures/basic-docinfo.html +0 -1
  133. data/test/fixtures/basic-docinfo.xml +0 -4
  134. data/test/fixtures/basic.asciidoc +0 -5
  135. data/test/fixtures/chapter-a.adoc +0 -3
  136. data/test/fixtures/child-include.adoc +0 -5
  137. data/test/fixtures/circle.svg +0 -9
  138. data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +0 -6
  139. data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +0 -6
  140. data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +0 -3
  141. data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +0 -5
  142. data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +0 -1
  143. data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +0 -6
  144. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +0 -3
  145. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +0 -5
  146. data/test/fixtures/custom-docinfodir/basic-docinfo.html +0 -1
  147. data/test/fixtures/custom-docinfodir/docinfo.html +0 -1
  148. data/test/fixtures/docinfo-footer.html +0 -1
  149. data/test/fixtures/docinfo-footer.xml +0 -9
  150. data/test/fixtures/docinfo.html +0 -1
  151. data/test/fixtures/docinfo.xml +0 -3
  152. data/test/fixtures/doctime-localtime.adoc +0 -2
  153. data/test/fixtures/dot.gif +0 -0
  154. data/test/fixtures/encoding.asciidoc +0 -13
  155. data/test/fixtures/file-with-missing-include.adoc +0 -1
  156. data/test/fixtures/grandchild-include.adoc +0 -3
  157. data/test/fixtures/hello-asciidoctor.pdf +0 -69
  158. data/test/fixtures/include-file.asciidoc +0 -24
  159. data/test/fixtures/include-file.jsx +0 -8
  160. data/test/fixtures/include-file.ml +0 -3
  161. data/test/fixtures/include-file.xml +0 -5
  162. data/test/fixtures/lists.adoc +0 -96
  163. data/test/fixtures/master.adoc +0 -5
  164. data/test/fixtures/mismatched-end-tag.adoc +0 -7
  165. data/test/fixtures/other-chapters.adoc +0 -11
  166. data/test/fixtures/outer-include.adoc +0 -5
  167. data/test/fixtures/parent-include-restricted.adoc +0 -5
  168. data/test/fixtures/parent-include.adoc +0 -5
  169. data/test/fixtures/sample.asciidoc +0 -30
  170. data/test/fixtures/section-a.adoc +0 -4
  171. data/test/fixtures/stylesheets/custom.css +0 -3
  172. data/test/fixtures/subdir/index.adoc +0 -3
  173. data/test/fixtures/subdir/inner-include.adoc +0 -3
  174. data/test/fixtures/subdir/middle-include.adoc +0 -5
  175. data/test/fixtures/subs-docinfo.html +0 -2
  176. data/test/fixtures/subs.adoc +0 -6
  177. data/test/fixtures/tagged-class-enclosed.rb +0 -25
  178. data/test/fixtures/tagged-class.rb +0 -23
  179. data/test/fixtures/tip.gif +0 -0
  180. data/test/fixtures/unclosed-tag.adoc +0 -3
  181. data/test/fixtures/unexpected-end-tag.adoc +0 -4
  182. data/test/invoker_test.rb +0 -745
  183. data/test/links_test.rb +0 -855
  184. data/test/lists_test.rb +0 -5151
  185. data/test/logger_test.rb +0 -211
  186. data/test/manpage_test.rb +0 -660
  187. data/test/options_test.rb +0 -262
  188. data/test/paragraphs_test.rb +0 -562
  189. data/test/parser_test.rb +0 -742
  190. data/test/paths_test.rb +0 -395
  191. data/test/preamble_test.rb +0 -173
  192. data/test/reader_test.rb +0 -2161
  193. data/test/sections_test.rb +0 -3575
  194. data/test/substitutions_test.rb +0 -2066
  195. data/test/tables_test.rb +0 -2036
  196. data/test/test_helper.rb +0 -447
  197. data/test/text_test.rb +0 -309
@@ -1,7 +1,7 @@
1
- # encoding: UTF-8
1
+ # frozen_string_literal: true
2
2
  module Asciidoctor
3
3
  # Public: Methods to perform substitutions on lines of AsciiDoc text. This module
4
- # is intented to be mixed-in to Section and Block to provide operations for performing
4
+ # is intended to be mixed-in to Section and Block to provide operations for performing
5
5
  # the necessary substitutions.
6
6
  module Substitutors
7
7
  SpecialCharsRx = /[<&>]/
@@ -12,55 +12,45 @@ module Substitutors
12
12
 
13
13
  (BASIC_SUBS = [:specialcharacters]).freeze
14
14
  (HEADER_SUBS = [:specialcharacters, :attributes]).freeze
15
+ (NO_SUBS = []).freeze
15
16
  (NORMAL_SUBS = [:specialcharacters, :quotes, :attributes, :replacements, :macros, :post_replacements]).freeze
16
- (NONE_SUBS = []).freeze
17
- (TITLE_SUBS = [:specialcharacters, :quotes, :replacements, :macros, :attributes, :post_replacements]).freeze
18
17
  (REFTEXT_SUBS = [:specialcharacters, :quotes, :replacements]).freeze
19
18
  (VERBATIM_SUBS = [:specialcharacters, :callouts]).freeze
20
19
 
21
20
  SUB_GROUPS = {
22
- :none => NONE_SUBS,
23
- :normal => NORMAL_SUBS,
24
- :verbatim => VERBATIM_SUBS,
25
- :specialchars => BASIC_SUBS
21
+ none: NO_SUBS,
22
+ normal: NORMAL_SUBS,
23
+ verbatim: VERBATIM_SUBS,
24
+ specialchars: BASIC_SUBS,
26
25
  }
27
26
 
28
27
  SUB_HINTS = {
29
- :a => :attributes,
30
- :m => :macros,
31
- :n => :normal,
32
- :p => :post_replacements,
33
- :q => :quotes,
34
- :r => :replacements,
35
- :c => :specialcharacters,
36
- :v => :verbatim
28
+ a: :attributes,
29
+ m: :macros,
30
+ n: :normal,
31
+ p: :post_replacements,
32
+ q: :quotes,
33
+ r: :replacements,
34
+ c: :specialcharacters,
35
+ v: :verbatim,
37
36
  }
38
37
 
39
38
  SUB_OPTIONS = {
40
- :block => SUB_GROUPS.keys + NORMAL_SUBS + [:callouts],
41
- :inline => SUB_GROUPS.keys + NORMAL_SUBS
39
+ block: SUB_GROUPS.keys + NORMAL_SUBS + [:callouts],
40
+ inline: SUB_GROUPS.keys + NORMAL_SUBS,
42
41
  }
43
42
 
44
- SUB_HIGHLIGHT = ['coderay', 'pygments']
43
+ CAN = ?\u0018
44
+ DEL = ?\u007f
45
45
 
46
- if ::RUBY_MIN_VERSION_1_9
47
- CAN = %(\u0018)
48
- DEL = %(\u007f)
46
+ # Delimiters and matchers for the passthrough placeholder
47
+ # See http://www.aivosto.com/vbtips/control-characters.html#listabout for characters to use
49
48
 
50
- # Delimiters and matchers for the passthrough placeholder
51
- # See http://www.aivosto.com/vbtips/control-characters.html#listabout for characters to use
49
+ # SPA, start of guarded protected area (\u0096)
50
+ PASS_START = ?\u0096
52
51
 
53
- # SPA, start of guarded protected area (\u0096)
54
- PASS_START = %(\u0096)
55
-
56
- # EPA, end of guarded protected area (\u0097)
57
- PASS_END = %(\u0097)
58
- else
59
- CAN = 24.chr
60
- DEL = 127.chr
61
- PASS_START = 150.chr
62
- PASS_END = 151.chr
63
- end
52
+ # EPA, end of guarded protected area (\u0097)
53
+ PASS_END = ?\u0097
64
54
 
65
55
  # match passthrough slot
66
56
  PassSlotRx = /#{PASS_START}(\d+)#{PASS_END}/
@@ -76,14 +66,6 @@ module Substitutors
76
66
 
77
67
  PLUS = '+'
78
68
 
79
- PygmentsWrapperDivRx = %r(<div class="pyhl">(.*)</div>)m
80
- # NOTE handles all permutations of <pre> wrapper
81
- # NOTE trailing whitespace appears when pygments-linenums-mode=table; <pre> has style attribute when pygments-css=inline
82
- PygmentsWrapperPreRx = %r(<pre\b[^>]*?>(.*?)</pre>\s*)m
83
-
84
- # Internal: A String Array of passthough (unprocessed) text captured from this block
85
- attr_reader :passthroughs
86
-
87
69
  # Public: Apply the specified substitutions to the text.
88
70
  #
89
71
  # text - The String or String Array of text to process; must not be nil.
@@ -93,14 +75,17 @@ module Substitutors
93
75
  def apply_subs text, subs = NORMAL_SUBS
94
76
  return text if text.empty? || !subs
95
77
 
96
- if (multiline = ::Array === text)
97
- #text = text.size > 1 ? (text.join LF) : text[0]
78
+ if (is_multiline = ::Array === text)
98
79
  text = text[1] ? (text.join LF) : text[0]
99
80
  end
100
81
 
101
- if (has_passthroughs = subs.include? :macros)
82
+ if subs.include? :macros
102
83
  text = extract_passthroughs text
103
- has_passthroughs = false if @passthroughs.empty?
84
+ unless @passthroughs.empty?
85
+ passthrus = @passthroughs
86
+ # NOTE placeholders can move around, so we can only clear in the outermost substitution call
87
+ @passthroughs_locked ||= (clear_passthrus = true)
88
+ end
104
89
  end
105
90
 
106
91
  subs.each do |type|
@@ -125,9 +110,16 @@ module Substitutors
125
110
  logger.warn %(unknown substitution type #{type})
126
111
  end
127
112
  end
128
- text = restore_passthroughs text if has_passthroughs
129
113
 
130
- multiline ? (text.split LF, -1) : text
114
+ if passthrus
115
+ text = restore_passthroughs text
116
+ if clear_passthrus
117
+ passthrus.clear
118
+ @passthroughs_locked = nil
119
+ end
120
+ end
121
+
122
+ is_multiline ? (text.split LF, -1) : text
131
123
  end
132
124
 
133
125
  # Public: Apply normal substitutions.
@@ -138,316 +130,69 @@ module Substitutors
138
130
  #
139
131
  # Returns the String with normal substitutions applied.
140
132
  def apply_normal_subs text
141
- apply_subs text
142
- end
143
-
144
- # Public: Apply substitutions for titles.
145
- #
146
- # title - The String title to process
147
- #
148
- # returns - A String with title substitutions performed
149
- def apply_title_subs(title)
150
- apply_subs title, TITLE_SUBS
151
- end
152
-
153
- # Public: Apply substitutions for reftext.
154
- #
155
- # text - The String to process
156
- #
157
- # Returns a String with all substitutions from the reftext substitution group applied
158
- def apply_reftext_subs text
159
- apply_subs text, REFTEXT_SUBS
133
+ apply_subs text, NORMAL_SUBS
160
134
  end
161
135
 
162
136
  # Public: Apply substitutions for header metadata and attribute assignments
163
137
  #
164
138
  # text - String containing the text process
165
139
  #
166
- # returns - A String with header substitutions performed
167
- def apply_header_subs(text)
140
+ # Returns A String with header substitutions performed
141
+ def apply_header_subs text
168
142
  apply_subs text, HEADER_SUBS
169
143
  end
170
144
 
171
- # Internal: Extract the passthrough text from the document for reinsertion after processing.
145
+ # Public: Apply substitutions for titles.
172
146
  #
173
- # text - The String from which to extract passthrough fragements
147
+ # title - The String title to process
174
148
  #
175
- # returns - The text with the passthrough region substituted with placeholders
176
- def extract_passthroughs(text)
177
- compat_mode = @document.compat_mode
178
- passes = @passthroughs
179
- text = text.gsub(InlinePassMacroRx) {
180
- # alias match for Ruby 1.8.7 compat
181
- m = $~
182
- preceding = nil
183
-
184
- if (boundary = m[4]) # $$, ++, or +++
185
- # skip ++ in compat mode, handled as normal quoted text
186
- if compat_mode && boundary == '++'
187
- next m[2] ?
188
- %(#{m[1]}[#{m[2]}]#{m[3]}++#{extract_passthroughs m[5]}++) :
189
- %(#{m[1]}#{m[3]}++#{extract_passthroughs m[5]}++)
190
- end
191
-
192
- attributes = m[2]
193
- escape_count = m[3].length
194
- content = m[5]
195
- old_behavior = false
196
-
197
- if attributes
198
- if escape_count > 0
199
- # NOTE we don't look for nested unconstrained pass macros
200
- next %(#{m[1]}[#{attributes}]#{RS * (escape_count - 1)}#{boundary}#{m[5]}#{boundary})
201
- elsif m[1] == RS
202
- preceding = %([#{attributes}])
203
- attributes = nil
204
- else
205
- if boundary == '++' && (attributes.end_with? 'x-')
206
- old_behavior = true
207
- attributes = attributes.slice 0, attributes.length - 2
208
- end
209
- attributes = parse_quoted_text_attributes attributes
210
- end
211
- elsif escape_count > 0
212
- # NOTE we don't look for nested unconstrained pass macros
213
- next %(#{RS * (escape_count - 1)}#{boundary}#{m[5]}#{boundary})
214
- end
215
- subs = (boundary == '+++' ? [] : BASIC_SUBS)
216
-
217
- pass_key = passes.size
218
- if attributes
219
- if old_behavior
220
- passes[pass_key] = {:text => content, :subs => NORMAL_SUBS, :type => :monospaced, :attributes => attributes}
221
- else
222
- passes[pass_key] = {:text => content, :subs => subs, :type => :unquoted, :attributes => attributes}
223
- end
224
- else
225
- passes[pass_key] = {:text => content, :subs => subs}
226
- end
227
- else # pass:[]
228
- if m[6] == RS
229
- # NOTE we don't look for nested pass:[] macros
230
- next m[0].slice 1, m[0].length
231
- end
232
-
233
- passes[pass_key = passes.size] = {:text => (unescape_brackets m[8]), :subs => (m[7] ? (resolve_pass_subs m[7]) : nil)}
234
- end
235
-
236
- %(#{preceding}#{PASS_START}#{pass_key}#{PASS_END})
237
- } if (text.include? '++') || (text.include? '$$') || (text.include? 'ss:')
238
-
239
- pass_inline_char1, pass_inline_char2, pass_inline_rx = InlinePassRx[compat_mode]
240
- text = text.gsub(pass_inline_rx) {
241
- # alias match for Ruby 1.8.7 compat
242
- m = $~
243
- preceding = m[1]
244
- attributes = m[2]
245
- escape_mark = RS if (quoted_text = m[3]).start_with? RS
246
- format_mark = m[4]
247
- content = m[5]
248
-
249
- if compat_mode
250
- old_behavior = true
251
- else
252
- if (old_behavior = (attributes && (attributes.end_with? 'x-')))
253
- attributes = attributes.slice 0, attributes.length - 2
254
- end
255
- end
256
-
257
- if attributes
258
- if format_mark == '`' && !old_behavior
259
- # extract nested single-plus passthrough; otherwise return unprocessed
260
- next (extract_inner_passthrough content, %(#{preceding}[#{attributes}]#{escape_mark}), attributes)
261
- end
149
+ # Returns A String with title substitutions performed
150
+ alias apply_title_subs apply_subs
262
151
 
263
- if escape_mark
264
- # honor the escape of the formatting mark
265
- next %(#{preceding}[#{attributes}]#{quoted_text.slice 1, quoted_text.length})
266
- elsif preceding == RS
267
- # honor the escape of the attributes
268
- preceding = %([#{attributes}])
269
- attributes = nil
270
- else
271
- attributes = parse_quoted_text_attributes attributes
272
- end
273
- elsif format_mark == '`' && !old_behavior
274
- # extract nested single-plus passthrough; otherwise return unprocessed
275
- next (extract_inner_passthrough content, %(#{preceding}#{escape_mark}))
276
- elsif escape_mark
277
- # honor the escape of the formatting mark
278
- next %(#{preceding}#{quoted_text.slice 1, quoted_text.length})
279
- end
280
-
281
- pass_key = passes.size
282
- if compat_mode
283
- passes[pass_key] = {:text => content, :subs => BASIC_SUBS, :attributes => attributes, :type => :monospaced}
284
- elsif attributes
285
- if old_behavior
286
- subs = (format_mark == '`' ? BASIC_SUBS : NORMAL_SUBS)
287
- passes[pass_key] = {:text => content, :subs => subs, :attributes => attributes, :type => :monospaced}
288
- else
289
- passes[pass_key] = {:text => content, :subs => BASIC_SUBS, :attributes => attributes, :type => :unquoted}
290
- end
291
- else
292
- passes[pass_key] = {:text => content, :subs => BASIC_SUBS}
293
- end
294
-
295
- %(#{preceding}#{PASS_START}#{pass_key}#{PASS_END})
296
- } if (text.include? pass_inline_char1) || (pass_inline_char2 && (text.include? pass_inline_char2))
297
-
298
- # NOTE we need to do the stem in a subsequent step to allow it to be escaped by the former
299
- text = text.gsub(InlineStemMacroRx) {
300
- # alias match for Ruby 1.8.7 compat
301
- m = $~
302
- # honor the escape
303
- if $&.start_with? RS
304
- next m[0].slice 1, m[0].length
305
- end
306
-
307
- if (type = m[1].to_sym) == :stem
308
- type = STEM_TYPE_ALIASES[@document.attributes['stem']].to_sym
309
- end
310
- content = unescape_brackets m[3]
311
- subs = m[2] ? (resolve_pass_subs m[2]) : ((@document.basebackend? 'html') ? BASIC_SUBS : nil)
312
- passes[pass_key = passes.size] = {:text => content, :subs => subs, :type => type}
313
- %(#{PASS_START}#{pass_key}#{PASS_END})
314
- } if (text.include? ':') && ((text.include? 'stem:') || (text.include? 'math:'))
315
-
316
- text
317
- end
318
-
319
- def extract_inner_passthrough text, pre, attributes = nil
320
- if (text.end_with? '+') && (text.start_with? '+', '\+') && SinglePlusInlinePassRx =~ text
321
- if $1
322
- %(#{pre}`+#{$2}+`)
323
- else
324
- @passthroughs[pass_key = @passthroughs.size] = attributes ?
325
- { :text => $2, :subs => BASIC_SUBS, :attributes => attributes, :type => :unquoted } :
326
- { :text => $2, :subs => BASIC_SUBS }
327
- %(#{pre}`#{PASS_START}#{pass_key}#{PASS_END}`)
328
- end
329
- else
330
- %(#{pre}`#{text}`)
331
- end
332
- end
333
-
334
- # Internal: Restore the passthrough text by reinserting into the placeholder positions
152
+ # Public: Apply substitutions for reftext.
335
153
  #
336
- # text - The String text into which to restore the passthrough text
337
- # outer - A Boolean indicating whether we are in the outer call (default: true)
154
+ # text - The String to process
338
155
  #
339
- # returns The String text with the passthrough text restored
340
- def restore_passthroughs text, outer = true
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
346
-
347
- text.gsub(PassSlotRx) {
348
- # NOTE we can't remove entry from map because placeholder may have been duplicated by other substitutions
349
- pass = passes[$1.to_i]
350
- subbed_text = apply_subs(pass[:text], pass[:subs])
351
- if (type = pass[:type])
352
- subbed_text = Inline.new(self, :quoted, subbed_text, :type => type, :attributes => pass[:attributes]).convert
353
- end
354
- subbed_text.include?(PASS_START) ? restore_passthroughs(subbed_text, false) : subbed_text
355
- }
356
- ensure
357
- # free memory if in outer call...we don't need these anymore
358
- passes.clear if outer
359
- end
360
-
361
- if RUBY_ENGINE == 'opal'
362
- def sub_quotes text
363
- if QuotedTextSniffRx[compat = @document.compat_mode].match? text
364
- QUOTE_SUBS[compat].each do |type, scope, pattern|
365
- text = text.gsub(pattern) { convert_quoted_text $~, type, scope }
366
- end
367
- end
368
- text
369
- end
370
-
371
- def sub_replacements text
372
- if ReplaceableTextRx.match? text
373
- REPLACEMENTS.each do |pattern, replacement, restore|
374
- text = text.gsub(pattern) { do_replacement $~, replacement, restore }
375
- end
376
- end
377
- text
378
- end
379
- else
380
- # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc)
381
- #
382
- # text - The String text to process
383
- #
384
- # returns The converted String text
385
- def sub_quotes text
386
- if QuotedTextSniffRx[compat = @document.compat_mode].match? text
387
- # NOTE interpolation is faster than String#dup
388
- text = %(#{text})
389
- QUOTE_SUBS[compat].each do |type, scope, pattern|
390
- # NOTE using gsub! here as an MRI Ruby optimization
391
- text.gsub!(pattern) { convert_quoted_text $~, type, scope }
392
- end
393
- end
394
- text
395
- end
396
-
397
- # Public: Substitute replacement characters (e.g., copyright, trademark, etc)
398
- #
399
- # text - The String text to process
400
- #
401
- # returns The String text with the replacement characters substituted
402
- def sub_replacements text
403
- if ReplaceableTextRx.match? text
404
- # NOTE interpolation is faster than String#dup
405
- text = %(#{text})
406
- REPLACEMENTS.each do |pattern, replacement, restore|
407
- # NOTE Using gsub! as optimization
408
- text.gsub!(pattern) { do_replacement $~, replacement, restore }
409
- end
410
- end
411
- text
412
- end
156
+ # Returns a String with all substitutions from the reftext substitution group applied
157
+ def apply_reftext_subs text
158
+ apply_subs text, REFTEXT_SUBS
413
159
  end
414
160
 
415
161
  # Public: Substitute special characters (i.e., encode XML)
416
162
  #
417
- # The special characters <, &, and > get replaced with &lt;,
418
- # &amp;, and &gt;, respectively.
163
+ # The special characters <, &, and > get replaced with &lt;, &amp;, and &gt;, respectively.
419
164
  #
420
165
  # text - The String text to process.
421
166
  #
422
- # returns The String text with special characters replaced.
423
- if ::RUBY_MIN_VERSION_1_9
167
+ # Returns The String text with special characters replaced.
168
+ if RUBY_ENGINE == 'opal'
424
169
  def sub_specialchars text
425
- (text.include? '<') || (text.include? '&') || (text.include? '>') ? (text.gsub SpecialCharsRx, SpecialCharsTr) : text
170
+ (text.include? ?>) || (text.include? ?&) || (text.include? ?<) ? (text.gsub SpecialCharsRx, SpecialCharsTr) : text
426
171
  end
427
172
  else
173
+ CGI = ::CGI
428
174
  def sub_specialchars text
429
- (text.include? '<') || (text.include? '&') || (text.include? '>') ? (text.gsub(SpecialCharsRx) { SpecialCharsTr[$&] }) : text
175
+ if (text.include? ?>) || (text.include? ?&) || (text.include? ?<)
176
+ (text.include? ?') || (text.include? ?") ? (text.gsub SpecialCharsRx, SpecialCharsTr) : (CGI.escape_html text)
177
+ else
178
+ text
179
+ end
430
180
  end
431
181
  end
432
182
  alias sub_specialcharacters sub_specialchars
433
183
 
434
- # Internal: Substitute replacement text for matched location
184
+ # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc.)
435
185
  #
436
- # returns The String text with the replacement characters substituted
437
- def do_replacement m, replacement, restore
438
- if (captured = m[0]).include? RS
439
- # we have to use sub since we aren't sure it's the first char
440
- captured.sub RS, ''
441
- else
442
- case restore
443
- when :none
444
- replacement
445
- when :bounding
446
- %(#{m[1]}#{replacement}#{m[2]})
447
- else # :leading
448
- %(#{m[1]}#{replacement})
186
+ # text - The String text to process
187
+ #
188
+ # returns The converted [String] text
189
+ def sub_quotes text
190
+ if QuotedTextSniffRx[compat = @document.compat_mode].match? text
191
+ QUOTE_SUBS[compat].each do |type, scope, pattern|
192
+ text = text.gsub(pattern) { convert_quoted_text $~, type, scope }
449
193
  end
450
194
  end
195
+ text
451
196
  end
452
197
 
453
198
  # Public: Substitutes attribute references in the specified text
@@ -459,12 +204,13 @@ module Substitutors
459
204
  #
460
205
  # text - The String text to process
461
206
  # opts - A Hash of options to control processing: (default: {})
462
- # * :attribute_missing controls how to handle a missing attribute
207
+ # * :attribute_missing controls how to handle a missing attribute (see Compliance.attribute_missing for values)
208
+ # * :drop_line_severity the severity level at which to log a dropped line (:info or :ignore)
463
209
  #
464
210
  # Returns the [String] text with the attribute references replaced with resolved values
465
211
  def sub_attributes text, opts = {}
466
212
  doc_attrs = @document.attributes
467
- drop = drop_line = drop_empty_line = attribute_undefined = attribute_missing = nil
213
+ drop = drop_line = drop_line_severity = drop_empty_line = attribute_undefined = attribute_missing = nil
468
214
  text = text.gsub AttributeReferenceRx do
469
215
  # escaped attribute, return unescaped
470
216
  if $1 == RS || $4 == RS
@@ -474,7 +220,7 @@ module Substitutors
474
220
  when 'set'
475
221
  _, value = Parser.store_attribute args[0], args[1] || '', @document
476
222
  # NOTE since this is an assignment, only drop-line applies here (skip and drop imply the same result)
477
- if value || (attribute_undefined ||= doc_attrs['attribute-undefined'] || Compliance.attribute_undefined) != 'drop-line'
223
+ if value || (attribute_undefined ||= (doc_attrs['attribute-undefined'] || Compliance.attribute_undefined)) != 'drop-line'
478
224
  drop = drop_empty_line = DEL
479
225
  else
480
226
  drop = drop_line = CAN
@@ -490,11 +236,15 @@ module Substitutors
490
236
  elsif (value = INTRINSIC_ATTRIBUTES[key])
491
237
  value
492
238
  else
493
- case (attribute_missing ||= opts[:attribute_missing] || doc_attrs['attribute-missing'] || Compliance.attribute_missing)
239
+ case (attribute_missing ||= (opts[:attribute_missing] || doc_attrs['attribute-missing'] || Compliance.attribute_missing))
494
240
  when 'drop'
495
241
  drop = drop_empty_line = DEL
496
242
  when 'drop-line'
497
- logger.warn %(dropping line containing reference to missing attribute: #{key})
243
+ if (drop_line_severity ||= (opts[:drop_line_severity] || :info)) == :info
244
+ logger.info { %(dropping line containing reference to missing attribute: #{key}) }
245
+ #elsif drop_line_severity == :warn
246
+ # logger.warn %(dropping line containing reference to missing attribute: #{key})
247
+ end
498
248
  drop = drop_line = CAN
499
249
  when 'warn'
500
250
  logger.warn %(skipping reference to missing attribute: #{key})
@@ -508,7 +258,7 @@ module Substitutors
508
258
  if drop
509
259
  # drop lines from text
510
260
  if drop_empty_line
511
- lines = (text.tr_s DEL, DEL).split LF, -1
261
+ lines = (text.squeeze DEL).split LF, -1
512
262
  if drop_line
513
263
  (lines.reject {|line| line == DEL || line == CAN || (line.start_with? CAN) || (line.include? CAN) }.join LF).delete DEL
514
264
  else
@@ -524,6 +274,18 @@ module Substitutors
524
274
  end
525
275
  end
526
276
 
277
+ # Public: Substitute replacement characters (e.g., copyright, trademark, etc.)
278
+ #
279
+ # text - The String text to process
280
+ #
281
+ # returns The [String] text with the replacement characters substituted
282
+ def sub_replacements text
283
+ REPLACEMENTS.each do |pattern, replacement, restore|
284
+ text = text.gsub(pattern) { do_replacement $~, replacement, restore }
285
+ end if ReplaceableTextRx.match? text
286
+ text
287
+ end
288
+
527
289
  # Public: Substitute inline macros (e.g., links, images, etc)
528
290
  #
529
291
  # Replace inline macros, which may span multiple lines, in the provided text
@@ -531,19 +293,61 @@ module Substitutors
531
293
  # source - The String text to process
532
294
  #
533
295
  # returns The converted String text
534
- def sub_macros(text)
296
+ def sub_macros text
535
297
  #return text if text.nil_or_empty?
536
298
  # some look ahead assertions to cut unnecessary regex calls
537
- found = {}
538
- found_square_bracket = found[:square_bracket] = (text.include? '[')
299
+ found_square_bracket = text.include? '['
539
300
  found_colon = text.include? ':'
540
- found_macroish = found[:macroish] = found_square_bracket && found_colon
301
+ found_macroish = found_square_bracket && found_colon
541
302
  found_macroish_short = found_macroish && (text.include? ':[')
542
303
  doc_attrs = (doc = @document).attributes
543
304
 
305
+ # TODO allow position of substitution to be controlled (before or after other macros)
306
+ # TODO this handling needs some cleanup
307
+ if (extensions = doc.extensions) && extensions.inline_macros? # && found_macroish
308
+ extensions.inline_macros.each do |extension|
309
+ text = text.gsub extension.instance.regexp do
310
+ # honor the escape
311
+ next $&.slice 1, $&.length if (match = $&).start_with? RS
312
+ if $~.names.empty?
313
+ target, content = $1, $2
314
+ else
315
+ target, content = ($~[:target] rescue nil), ($~[:content] rescue nil)
316
+ end
317
+ attributes = (default_attrs = (ext_config = extension.config)[:default_attrs]) ? default_attrs.merge : {}
318
+ if content
319
+ if content.empty?
320
+ attributes['text'] = content unless ext_config[:content_model] == :attributes
321
+ else
322
+ content = normalize_text content, true, true
323
+ # QUESTION should we store the unparsed attrlist in the attrlist key?
324
+ if ext_config[:content_model] == :attributes
325
+ parse_attributes content, ext_config[:positional_attrs] || ext_config[:pos_attrs] || [], into: attributes
326
+ else
327
+ attributes['text'] = content
328
+ end
329
+ end
330
+ # NOTE for convenience, map content (unparsed attrlist) to target when format is short
331
+ target ||= ext_config[:format] == :short ? content : target
332
+ end
333
+ if Inline === (replacement = extension.process_method[self, target, attributes])
334
+ if (inline_subs = replacement.attributes.delete 'subs') && (inline_subs = expand_subs inline_subs, 'custom inline macro')
335
+ replacement.text = apply_subs replacement.text, inline_subs
336
+ end
337
+ replacement.convert
338
+ elsif replacement
339
+ logger.info { %(expected substitution value for custom inline macro to be of type Inline; got #{replacement.class}: #{match}) }
340
+ replacement
341
+ else
342
+ ''
343
+ end
344
+ end
345
+ end
346
+ end
347
+
544
348
  if doc_attrs.key? 'experimental'
545
349
  if found_macroish_short && ((text.include? 'kbd:') || (text.include? 'btn:'))
546
- text = text.gsub(InlineKbdBtnMacroRx) {
350
+ text = text.gsub InlineKbdBtnMacroRx do
547
351
  # honor the escape
548
352
  if $1
549
353
  $&.slice 1, $&.length
@@ -557,32 +361,27 @@ module Substitutors
557
361
  # NOTE handle special case where keys ends with delimiter (e.g., Ctrl++ or Ctrl,,)
558
362
  if keys.end_with? delim
559
363
  keys = (keys.chop.split delim, -1).map {|key| key.strip }
560
- keys[-1] = %(#{keys[-1]}#{delim})
364
+ keys[-1] += delim
561
365
  else
562
366
  keys = keys.split(delim).map {|key| key.strip }
563
367
  end
564
368
  else
565
369
  keys = [keys]
566
370
  end
567
- (Inline.new self, :kbd, nil, :attributes => { 'keys' => keys }).convert
371
+ (Inline.new self, :kbd, nil, attributes: { 'keys' => keys }).convert
568
372
  else # $2 == 'btn'
569
- (Inline.new self, :button, (unescape_bracketed_text $3)).convert
373
+ (Inline.new self, :button, (normalize_text $3, true, true)).convert
570
374
  end
571
- }
375
+ end
572
376
  end
573
377
 
574
378
  if found_macroish && (text.include? 'menu:')
575
- text = text.gsub(InlineMenuMacroRx) {
576
- # alias match for Ruby 1.8.7 compat
577
- m = $~
379
+ text = text.gsub InlineMenuMacroRx do
578
380
  # honor the escape
579
- if $&.start_with? RS
580
- next m[0].slice 1, m[0].length
581
- end
582
-
583
- menu, items = m[1], m[2]
381
+ next $&.slice 1, $&.length if $&.start_with? RS
584
382
 
585
- if items
383
+ menu = $1
384
+ if (items = $2)
586
385
  items = items.gsub ESC_R_SB, R_SB if items.include? R_SB
587
386
  if (delim = items.include?('&gt;') ? '&gt;' : (items.include?(',') ? ',' : nil))
588
387
  submenus = items.split(delim).map {|it| it.strip }
@@ -594,87 +393,42 @@ module Substitutors
594
393
  submenus, menuitem = [], nil
595
394
  end
596
395
 
597
- Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).convert
598
- }
396
+ Inline.new(self, :menu, nil, attributes: { 'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem }).convert
397
+ end
599
398
  end
600
399
 
601
400
  if (text.include? '"') && (text.include? '&gt;')
602
- text = text.gsub(InlineMenuRx) {
603
- # alias match for Ruby 1.8.7 compat
604
- m = $~
401
+ text = text.gsub InlineMenuRx do
605
402
  # honor the escape
606
- if $&.start_with? RS
607
- next m[0].slice 1, m[0].length
608
- end
609
-
610
- input = m[1]
403
+ next $&.slice 1, $&.length if $&.start_with? RS
611
404
 
612
- menu, *submenus = input.split('&gt;').map {|it| it.strip }
405
+ menu, *submenus = $1.split('&gt;').map {|it| it.strip }
613
406
  menuitem = submenus.pop
614
- Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).convert
615
- }
616
- end
617
- end
618
-
619
- # FIXME this location is somewhat arbitrary, probably need to be able to control ordering
620
- # TODO this handling needs some cleanup
621
- if (extensions = doc.extensions) && extensions.inline_macros? # && found_macroish
622
- extensions.inline_macros.each do |extension|
623
- text = text.gsub(extension.instance.regexp) {
624
- # alias match for Ruby 1.8.7 compat
625
- m = $~
626
- # honor the escape
627
- if $&.start_with? RS
628
- next m[0].slice 1, m[0].length
629
- end
630
-
631
- if (m.names rescue []).empty?
632
- target, content, extconf = m[1], m[2], extension.config
633
- else
634
- target, content, extconf = (m[:target] rescue nil), (m[:content] rescue nil), extension.config
635
- end
636
- attributes = (attributes = extconf[:default_attrs]) ? attributes.dup : {}
637
- if content.nil_or_empty?
638
- attributes['text'] = content if content && extconf[:content_model] != :attributes
639
- else
640
- content = unescape_bracketed_text content
641
- if extconf[:content_model] == :attributes
642
- # QUESTION should we store the text in the _text key?
643
- # NOTE bracked text has already been escaped
644
- parse_attributes content, extconf[:pos_attrs] || [], :into => attributes
645
- else
646
- attributes['text'] = content
647
- end
648
- end
649
- # NOTE use content if target is not set (short form only); deprecated - remove in 1.6.0
650
- replacement = extension.process_method[self, target || content, attributes]
651
- Inline === replacement ? replacement.convert : replacement
652
- }
407
+ Inline.new(self, :menu, nil, attributes: { 'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem }).convert
408
+ end
653
409
  end
654
410
  end
655
411
 
656
412
  if found_macroish && ((text.include? 'image:') || (text.include? 'icon:'))
657
413
  # image:filename.png[Alt Text]
658
- text = text.gsub(InlineImageMacroRx) {
659
- # alias match for Ruby 1.8.7 compat
660
- m = $~
414
+ text = text.gsub InlineImageMacroRx do
661
415
  # honor the escape
662
- if (captured = $&).start_with? RS
663
- next captured.slice 1, captured.length
664
- elsif captured.start_with? 'icon:'
416
+ if $&.start_with? RS
417
+ next $&.slice 1, $&.length
418
+ elsif $&.start_with? 'icon:'
665
419
  type, posattrs = 'icon', ['size']
666
420
  else
667
421
  type, posattrs = 'image', ['alt', 'width', 'height']
668
422
  end
669
- if (target = m[1]).include? ATTR_REF_HEAD
670
- # TODO remove this special case once titles use normal substitution order
671
- target = sub_attributes target
423
+ target = $1
424
+ attrs = parse_attributes $2, posattrs, unescape_input: true
425
+ unless type == 'icon'
426
+ doc.register :images, target
427
+ attrs['imagesdir'] = doc_attrs['imagesdir']
672
428
  end
673
- attrs = parse_attributes m[2], posattrs, :unescape_input => true
674
- doc.register :images, [target, (attrs['imagesdir'] = doc_attrs['imagesdir'])] unless type == 'icon'
675
429
  attrs['alt'] ||= (attrs['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
676
- Inline.new(self, :image, nil, :type => type, :target => target, :attributes => attrs).convert
677
- }
430
+ Inline.new(self, :image, nil, type: type, target: target, attributes: attrs).convert
431
+ end
678
432
  end
679
433
 
680
434
  if ((text.include? '((') && (text.include? '))')) || (found_macroish_short && (text.include? 'dexterm'))
@@ -682,151 +436,183 @@ module Substitutors
682
436
  # indexterm:[Tigers,Big cats]
683
437
  # ((Tigers))
684
438
  # indexterm2:[Tigers]
685
- text = text.gsub(InlineIndextermMacroRx) {
686
- captured = $&
439
+ text = text.gsub InlineIndextermMacroRx do
687
440
  case $1
688
441
  when 'indexterm'
689
- text = $2
690
442
  # honor the escape
691
- if captured.start_with? RS
692
- next captured.slice 1, captured.length
693
- end
443
+ next $&.slice 1, $&.length if $&.start_with? RS
444
+
694
445
  # indexterm:[Tigers,Big cats]
695
- terms = split_simple_csv normalize_string text, true
696
- doc.register :indexterms, terms
697
- (Inline.new self, :indexterm, nil, :attributes => { 'terms' => terms }).convert
446
+ if (attrlist = normalize_text $2, true, true).include? '='
447
+ if (primary = (attrs = (AttributeList.new attrlist, self).parse)[1])
448
+ attrs['terms'] = [primary]
449
+ if (see_also = attrs['see-also'])
450
+ attrs['see-also'] = (see_also.include? ',') ? (see_also.split ',').map {|it| it.lstrip } : [see_also]
451
+ end
452
+ else
453
+ attrs = { 'terms' => attrlist }
454
+ end
455
+ else
456
+ attrs = { 'terms' => (split_simple_csv attrlist) }
457
+ end
458
+ (Inline.new self, :indexterm, nil, attributes: attrs).convert
698
459
  when 'indexterm2'
699
- text = $2
700
460
  # honor the escape
701
- if captured.start_with? RS
702
- next captured.slice 1, captured.length
703
- end
461
+ next $&.slice 1, $&.length if $&.start_with? RS
462
+
704
463
  # indexterm2:[Tigers]
705
- term = normalize_string text, true
706
- doc.register :indexterms, [term]
707
- (Inline.new self, :indexterm, term, :type => :visible).convert
464
+ if (term = normalize_text $2, true, true).include? '='
465
+ term = (attrs = (AttributeList.new term, self).parse)[1] || (attrs = nil) || term
466
+ if attrs && (see_also = attrs['see-also'])
467
+ attrs['see-also'] = (see_also.include? ',') ? (see_also.split ',').map {|it| it.lstrip } : [see_also]
468
+ end
469
+ end
470
+ (Inline.new self, :indexterm, term, attributes: attrs, type: :visible).convert
708
471
  else
709
- text = $3
472
+ encl_text = $3
710
473
  # honor the escape
711
- if captured.start_with? RS
474
+ if $&.start_with? RS
712
475
  # escape concealed index term, but process nested flow index term
713
- if (text.start_with? '(') && (text.end_with? ')')
714
- text = text.slice 1, text.length - 2
476
+ if (encl_text.start_with? '(') && (encl_text.end_with? ')')
477
+ encl_text = encl_text.slice 1, encl_text.length - 2
715
478
  visible, before, after = true, '(', ')'
716
479
  else
717
- next captured.slice 1, captured.length
480
+ next $&.slice 1, $&.length
718
481
  end
719
482
  else
720
483
  visible = true
721
- if text.start_with? '('
722
- if text.end_with? ')'
723
- text, visible = (text.slice 1, text.length - 2), false
484
+ if encl_text.start_with? '('
485
+ if encl_text.end_with? ')'
486
+ encl_text, visible = (encl_text.slice 1, encl_text.length - 2), false
724
487
  else
725
- text, before, after = (text.slice 1, text.length), '(', ''
488
+ encl_text, before, after = (encl_text.slice 1, encl_text.length), '(', ''
726
489
  end
727
- elsif text.end_with? ')'
728
- text, before, after = (text.slice 0, text.length - 1), '', ')'
490
+ elsif encl_text.end_with? ')'
491
+ encl_text, before, after = encl_text.chop, '', ')'
729
492
  end
730
493
  end
731
494
  if visible
732
495
  # ((Tigers))
733
- term = normalize_string text
734
- doc.register :indexterms, [term]
735
- subbed_term = (Inline.new self, :indexterm, term, :type => :visible).convert
496
+ if (term = normalize_text encl_text, true).include? ';&'
497
+ if term.include? ' &gt;&gt; '
498
+ term, _, see = term.partition ' &gt;&gt; '
499
+ attrs = { 'see' => see }
500
+ elsif term.include? ' &amp;&gt; '
501
+ term, *see_also = term.split ' &amp;&gt; '
502
+ attrs = { 'see-also' => see_also }
503
+ end
504
+ end
505
+ subbed_term = (Inline.new self, :indexterm, term, attributes: attrs, type: :visible).convert
736
506
  else
737
507
  # (((Tigers,Big cats)))
738
- terms = split_simple_csv(normalize_string text)
739
- doc.register :indexterms, terms
740
- subbed_term = (Inline.new self, :indexterm, nil, :attributes => { 'terms' => terms }).convert
508
+ attrs = {}
509
+ if (terms = normalize_text encl_text, true).include? ';&'
510
+ if terms.include? ' &gt;&gt; '
511
+ terms, _, see = terms.partition ' &gt;&gt; '
512
+ attrs['see'] = see
513
+ elsif terms.include? ' &amp;&gt; '
514
+ terms, *see_also = terms.split ' &amp;&gt; '
515
+ attrs['see-also'] = see_also
516
+ end
517
+ end
518
+ attrs['terms'] = split_simple_csv terms
519
+ subbed_term = (Inline.new self, :indexterm, nil, attributes: attrs).convert
741
520
  end
742
521
  before ? %(#{before}#{subbed_term}#{after}) : subbed_term
743
522
  end
744
- }
523
+ end
745
524
  end
746
525
 
747
526
  if found_colon && (text.include? '://')
748
527
  # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>)
749
- text = text.gsub(InlineLinkRx) {
750
- # alias match for Ruby 1.8.7 compat
751
- m = $~
752
- # honor the escape
528
+ text = text.gsub InlineLinkRx do
753
529
  if (target = $2).start_with? RS
754
- next %(#{m[1]}#{target.slice 1, target.length}#{m[3]})
530
+ # honor the escape
531
+ next %(#{$1}#{target.slice 1, target.length}#{$4})
755
532
  end
756
- # NOTE if text is non-nil, then we've matched a formal macro (i.e., trailing square brackets)
757
- prefix, text, suffix = m[1], (macro = m[3]) || '', ''
758
- if prefix == 'link:'
759
- if macro
760
- prefix = ''
761
- else
762
- # invalid macro syntax (link: prefix w/o trailing square brackets)
763
- # we probably shouldn't even get here...our regex is doing too much
764
- next m[0]
533
+
534
+ prefix, suffix = $1, ''
535
+ # NOTE if $4 is set, we're looking at a formal macro (e.g., https://example.org[])
536
+ if $4
537
+ prefix = '' if prefix == 'link:'
538
+ link_text = nil if (link_text = $4).empty?
539
+ else
540
+ # invalid macro syntax (link: prefix w/o trailing square brackets or enclosed in double quotes)
541
+ # FIXME we probably shouldn't even get here when the link: prefix is present; the regex is doing too much
542
+ case prefix
543
+ when 'link:', ?", ?'
544
+ next $&
765
545
  end
766
- end
767
- unless macro || UriTerminatorRx !~ target
768
- case $&
769
- when ')'
770
- # strip trailing )
546
+ case $3
547
+ when ')', '?', '!'
771
548
  target = target.chop
772
- suffix = ')'
549
+ if (suffix = $3) == ')' && (target.end_with? '.', '?', '!')
550
+ suffix = target[-1] + suffix
551
+ target = target.chop
552
+ end
553
+ # NOTE handle case when modified target is a URI scheme (e.g., http://)
554
+ next $& if target.end_with? '://'
773
555
  when ';'
774
- # strip <> around URI
775
- if prefix.start_with?('&lt;') && target.end_with?('&gt;')
556
+ if (prefix.start_with? '&lt;') && (target.end_with? '&gt;')
557
+ # move surrounding <> out of URL
776
558
  prefix = prefix.slice 4, prefix.length
777
559
  target = target.slice 0, target.length - 4
778
- # strip trailing ;
779
- # check for trailing );
780
- elsif (target = target.chop).end_with?(')')
560
+ elsif (target = target.chop).end_with? ')'
561
+ # move trailing ); out of URL
781
562
  target = target.chop
782
563
  suffix = ');'
783
564
  else
565
+ # move trailing ; out of URL
784
566
  suffix = ';'
785
567
  end
568
+ # NOTE handle case when modified target is a URI scheme (e.g., http://)
569
+ next $& if target.end_with? '://'
786
570
  when ':'
787
- # strip trailing :
788
- # check for trailing ):
789
- if (target = target.chop).end_with?(')')
571
+ if (target = target.chop).end_with? ')'
572
+ # move trailing ): out of URL
790
573
  target = target.chop
791
574
  suffix = '):'
792
575
  else
576
+ # move trailing : out of URL
793
577
  suffix = ':'
794
578
  end
579
+ # NOTE handle case when modified target is a URI scheme (e.g., http://)
580
+ next $& if target.end_with? '://'
795
581
  end
796
- # NOTE handle case when remaining target is a URI scheme (e.g., http://)
797
- return m[0] if target.end_with? '://'
798
582
  end
799
583
 
800
- attrs, link_opts = nil, { :type => :link }
801
- unless text.empty?
802
- text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
803
- if !doc.compat_mode && (text.include? '=')
804
- text = (attrs = (AttributeList.new text, self).parse)[1] || ''
805
- link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
584
+ attrs, link_opts = nil, { type: :link }
585
+
586
+ if link_text
587
+ new_link_text = link_text = link_text.gsub ESC_R_SB, R_SB if link_text.include? R_SB
588
+ if !doc.compat_mode && (link_text.include? '=')
589
+ # NOTE if an equals sign (=) is present, extract attributes from link text
590
+ link_text, attrs = extract_attributes_from_text link_text, ''
591
+ new_link_text = link_text
592
+ link_opts[:id] = attrs['id']
806
593
  end
807
594
 
808
- # TODO enable in Asciidoctor 1.6.x
809
- # support pipe-separated text and title
810
- #unless attrs && (attrs.key? 'title')
811
- # if text.include? '|'
812
- # attrs ||= {}
813
- # text, attrs['title'] = text.split '|', 2
814
- # end
815
- #end
816
-
817
- if text.end_with? '^'
818
- text = text.chop
595
+ if link_text.end_with? '^'
596
+ new_link_text = link_text = link_text.chop
819
597
  if attrs
820
598
  attrs['window'] ||= '_blank'
821
599
  else
822
600
  attrs = { 'window' => '_blank' }
823
601
  end
824
602
  end
825
- end
826
603
 
827
- if text.empty?
604
+ if new_link_text && new_link_text.empty?
605
+ # NOTE it's not possible for the URI scheme to be bare in this case
606
+ link_text = (doc_attrs.key? 'hide-uri-scheme') ? (target.sub UriSniffRx, '') : target
607
+ bare = true
608
+ end
609
+ else
828
610
  # NOTE it's not possible for the URI scheme to be bare in this case
829
- text = (doc_attrs.key? 'hide-uri-scheme') ? (target.sub UriSniffRx, '') : target
611
+ link_text = (doc_attrs.key? 'hide-uri-scheme') ? (target.sub UriSniffRx, '') : target
612
+ bare = true
613
+ end
614
+
615
+ if bare
830
616
  if attrs
831
617
  attrs['role'] = (attrs.key? 'role') ? %(bare #{attrs['role']}) : 'bare'
832
618
  else
@@ -836,51 +622,45 @@ module Substitutors
836
622
 
837
623
  doc.register :links, (link_opts[:target] = target)
838
624
  link_opts[:attributes] = attrs if attrs
839
- %(#{prefix}#{Inline.new(self, :anchor, text, link_opts).convert}#{suffix})
840
- }
625
+ %(#{prefix}#{(Inline.new self, :anchor, link_text, link_opts).convert}#{suffix})
626
+ end
841
627
  end
842
628
 
843
- if found_macroish && ((text.include? 'link:') || (text.include? 'mailto:'))
629
+ if found_macroish && ((text.include? 'link:') || (text.include? 'ilto:'))
844
630
  # inline link macros, link:target[text]
845
- text = text.gsub(InlineLinkMacroRx) {
846
- # alias match for Ruby 1.8.7 compat
847
- m = $~
631
+ text = text.gsub InlineLinkMacroRx do
848
632
  # honor the escape
849
633
  if $&.start_with? RS
850
- next m[0].slice 1, m[0].length
634
+ next $&.slice 1, $&.length
635
+ elsif (mailto = $1)
636
+ target = 'mailto:' + (mailto_text = $2)
637
+ else
638
+ target = $2
851
639
  end
852
- target = (mailto = m[1]) ? %(mailto:#{m[2]}) : m[2]
853
- attrs, link_opts = nil, { :type => :link }
854
- unless (text = m[3]).empty?
855
- text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
640
+ attrs, link_opts = nil, { type: :link }
641
+ unless (link_text = $3).empty?
642
+ link_text = link_text.gsub ESC_R_SB, R_SB if link_text.include? R_SB
856
643
  if mailto
857
- if !doc.compat_mode && (text.include? ',')
858
- text = (attrs = (AttributeList.new text, self).parse)[1] || ''
859
- link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
644
+ if !doc.compat_mode && (link_text.include? ',')
645
+ # NOTE if a comma (,) is present, extract attributes from link text
646
+ link_text, attrs = extract_attributes_from_text link_text, ''
647
+ link_opts[:id] = attrs['id']
860
648
  if attrs.key? 2
861
649
  if attrs.key? 3
862
- target = %(#{target}?subject=#{Helpers.uri_encode attrs[2]}&amp;body=#{Helpers.uri_encode attrs[3]})
650
+ target = %(#{target}?subject=#{Helpers.encode_uri_component attrs[2]}&amp;body=#{Helpers.encode_uri_component attrs[3]})
863
651
  else
864
- target = %(#{target}?subject=#{Helpers.uri_encode attrs[2]})
652
+ target = %(#{target}?subject=#{Helpers.encode_uri_component attrs[2]})
865
653
  end
866
654
  end
867
655
  end
868
- elsif !doc.compat_mode && (text.include? '=')
869
- text = (attrs = (AttributeList.new text, self).parse)[1] || ''
870
- link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
656
+ elsif !doc.compat_mode && (link_text.include? '=')
657
+ # NOTE if an equals sign (=) is present, extract attributes from link text
658
+ link_text, attrs = extract_attributes_from_text link_text, ''
659
+ link_opts[:id] = attrs['id']
871
660
  end
872
661
 
873
- # TODO enable in Asciidoctor 1.6.x
874
- # support pipe-separated text and title
875
- #unless attrs && (attrs.key? 'title')
876
- # if text.include? '|'
877
- # attrs ||= {}
878
- # text, attrs['title'] = text.split '|', 2
879
- # end
880
- #end
881
-
882
- if text.end_with? '^'
883
- text = text.chop
662
+ if link_text.end_with? '^'
663
+ link_text = link_text.chop
884
664
  if attrs
885
665
  attrs['window'] ||= '_blank'
886
666
  else
@@ -889,17 +669,17 @@ module Substitutors
889
669
  end
890
670
  end
891
671
 
892
- if text.empty?
672
+ if link_text.empty?
893
673
  # mailto is a special case, already processed
894
674
  if mailto
895
- text = m[2]
675
+ link_text = mailto_text
896
676
  else
897
677
  if doc_attrs.key? 'hide-uri-scheme'
898
- if (text = target.sub UriSniffRx, '').empty?
899
- text = target
678
+ if (link_text = target.sub UriSniffRx, '').empty?
679
+ link_text = target
900
680
  end
901
681
  else
902
- text = target
682
+ link_text = target
903
683
  end
904
684
  if attrs
905
685
  attrs['role'] = (attrs.key? 'role') ? %(bare #{attrs['role']}) : 'bare'
@@ -912,122 +692,64 @@ module Substitutors
912
692
  # QUESTION should a mailto be registered as an e-mail address?
913
693
  doc.register :links, (link_opts[:target] = target)
914
694
  link_opts[:attributes] = attrs if attrs
915
- Inline.new(self, :anchor, text, link_opts).convert
916
- }
695
+ Inline.new(self, :anchor, link_text, link_opts).convert
696
+ end
917
697
  end
918
698
 
919
699
  if text.include? '@'
920
- text = text.gsub(InlineEmailRx) {
921
- address, tip = $&, $1
922
- if tip
923
- next (tip == RS ? (address.slice 1, address.length) : address)
924
- end
700
+ text = text.gsub InlineEmailRx do
701
+ # honor the escape
702
+ next $1 == RS ? ($&.slice 1, $&.length) : $& if $1
925
703
 
926
- target = %(mailto:#{address})
704
+ target = 'mailto:' + (address = $&)
927
705
  # QUESTION should this be registered as an e-mail address?
928
706
  doc.register(:links, target)
929
707
 
930
- Inline.new(self, :anchor, address, :type => :link, :target => target).convert
931
- }
708
+ Inline.new(self, :anchor, address, type: :link, target: target).convert
709
+ end
932
710
  end
933
711
 
934
- if found_macroish && (text.include? 'tnote')
935
- text = text.gsub(InlineFootnoteMacroRx) {
936
- # alias match for Ruby 1.8.7 compat
937
- m = $~
712
+ if found_square_bracket && @context == :list_item && @parent.style == 'bibliography'
713
+ text = text.sub(InlineBiblioAnchorRx) { (Inline.new self, :anchor, $2, type: :bibref, id: $1).convert }
714
+ end
715
+
716
+ if (found_square_bracket && text.include?('[[')) || (found_macroish && text.include?('or:'))
717
+ text = text.gsub InlineAnchorRx do
938
718
  # honor the escape
939
- if $&.start_with? RS
940
- next m[0].slice 1, m[0].length
941
- end
942
- if m[1] # footnoteref (legacy)
943
- id, text = (m[3] || '').split(',', 2)
719
+ next $&.slice 1, $&.length if $1
720
+
721
+ # NOTE reftext is only relevant for DocBook output; used as value of xreflabel attribute
722
+ if (id = $2)
723
+ reftext = $3
944
724
  else
945
- id, text = m[2], m[3]
725
+ id = $4
726
+ if (reftext = $5) && (reftext.include? R_SB)
727
+ reftext = reftext.gsub ESC_R_SB, R_SB
728
+ end
946
729
  end
947
- if id
948
- if text
949
- # REVIEW it's a dirty job, but somebody's gotta do it
950
- text = restore_passthroughs(sub_inline_xrefs(sub_inline_anchors(normalize_string text, true)), false)
951
- index = doc.counter('footnote-number')
952
- doc.register(:footnotes, Document::Footnote.new(index, id, text))
953
- type, target = :ref, nil
954
- else
955
- if (footnote = doc.footnotes.find {|candidate| candidate.id == id })
956
- index, text = footnote.index, footnote.text
957
- else
958
- logger.warn %(invalid footnote reference: #{id})
959
- index, text = nil, id
960
- end
961
- type, target, id = :xref, id, nil
962
- end
963
- elsif text
964
- # REVIEW it's a dirty job, but somebody's gotta do it
965
- text = restore_passthroughs(sub_inline_xrefs(sub_inline_anchors(normalize_string text, true)), false)
966
- index = doc.counter('footnote-number')
967
- doc.register(:footnotes, Document::Footnote.new(index, id, text))
968
- type = target = nil
969
- else
970
- next m[0]
971
- end
972
- Inline.new(self, :footnote, text, :attributes => {'index' => index}, :id => id, :target => target, :type => type).convert
973
- }
974
- end
975
-
976
- sub_inline_xrefs(sub_inline_anchors(text, found), found)
977
- end
978
-
979
- # Internal: Substitute normal and bibliographic anchors
980
- def sub_inline_anchors(text, found = nil)
981
- if @context == :list_item && @parent.style == 'bibliography'
982
- text = text.sub(InlineBiblioAnchorRx) {
983
- # NOTE target property on :bibref is deprecated
984
- Inline.new(self, :anchor, %([#{$2 || $1}]), :type => :bibref, :id => $1, :target => $1).convert
985
- }
730
+ Inline.new(self, :anchor, reftext, type: :ref, id: id).convert
731
+ end
986
732
  end
987
733
 
988
- if ((!found || found[:square_bracket]) && text.include?('[[')) ||
989
- ((!found || found[:macroish]) && text.include?('or:'))
990
- text = text.gsub(InlineAnchorRx) {
734
+ #if (text.include? ';&l') || (found_macroish && (text.include? 'xref:'))
735
+ if ((text.include? '&') && (text.include? ';&l')) || (found_macroish && (text.include? 'xref:'))
736
+ text = text.gsub InlineXrefMacroRx do
991
737
  # honor the escape
992
- next $&.slice 1, $&.length if $1
993
- # NOTE reftext is only relevant for DocBook output; used as value of xreflabel attribute
994
- if (id = $2)
995
- reftext = $3
996
- else
997
- id = $4
998
- if (reftext = $5) && (reftext.include? R_SB)
999
- reftext = reftext.gsub ESC_R_SB, R_SB
1000
- end
1001
- end
1002
- # NOTE target property on :ref is deprecated
1003
- Inline.new(self, :anchor, reftext, :type => :ref, :id => id, :target => id).convert
1004
- }
1005
- end
1006
-
1007
- text
1008
- end
738
+ next $&.slice 1, $&.length if $&.start_with? RS
1009
739
 
1010
- # Internal: Substitute cross reference links
1011
- def sub_inline_xrefs(content, found = nil)
1012
- if ((found ? found[:macroish] : (content.include? '[')) && (content.include? 'xref:')) || ((content.include? '&') && (content.include? 'lt;&'))
1013
- content = content.gsub(InlineXrefMacroRx) {
1014
- # alias match for Ruby 1.8.7 compat
1015
- m = $~
1016
- # honor the escape
1017
- if $&.start_with? RS
1018
- next m[0].slice 1, m[0].length
1019
- end
1020
- attrs, doc = {}, @document
1021
- if (refid = m[1])
1022
- refid, text = refid.split ',', 2
1023
- text = text.lstrip if text
740
+ attrs = {}
741
+ if (refid = $1)
742
+ if refid.include? ','
743
+ refid, _, link_text = refid.partition ','
744
+ link_text = nil if (link_text = link_text.lstrip).empty?
745
+ end
1024
746
  else
1025
747
  macro = true
1026
- refid = m[2]
1027
- if (text = m[3])
1028
- text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
1029
- # NOTE if an equal sign (=) is present, parse text as attributes
1030
- text = ((AttributeList.new text, self).parse_into attrs)[1] if !doc.compat_mode && (text.include? '=')
748
+ refid = $2
749
+ if (link_text = $3)
750
+ link_text = link_text.gsub ESC_R_SB, R_SB if link_text.include? R_SB
751
+ # NOTE if an equals sign (=) is present, extract attributes from link text
752
+ link_text, attrs = extract_attributes_from_text link_text if !doc.compat_mode && (link_text.include? '=')
1031
753
  end
1032
754
  end
1033
755
 
@@ -1035,21 +757,33 @@ module Substitutors
1035
757
  fragment = refid
1036
758
  elsif (hash_idx = refid.index '#')
1037
759
  if hash_idx > 0
1038
- if (fragment_len = refid.length - hash_idx - 1) > 0
760
+ if (fragment_len = refid.length - 1 - hash_idx) > 0
1039
761
  path, fragment = (refid.slice 0, hash_idx), (refid.slice hash_idx + 1, fragment_len)
1040
762
  else
1041
- path = refid.slice 0, hash_idx
763
+ path = refid.chop
1042
764
  end
1043
- if (ext = ::File.extname path).empty?
765
+ if macro
766
+ if path.end_with? '.adoc'
767
+ src2src = path = path.slice 0, path.length - 5
768
+ elsif !(Helpers.extname? path)
769
+ src2src = path
770
+ end
771
+ elsif path.end_with?(*ASCIIDOC_EXTENSIONS.keys)
772
+ src2src = path = path.slice 0, (path.rindex '.')
773
+ else
1044
774
  src2src = path
1045
- elsif ASCIIDOC_EXTENSIONS[ext]
1046
- src2src = (path = path.slice 0, path.length - ext.length)
1047
775
  end
1048
776
  else
1049
777
  target, fragment = refid, (refid.slice 1, refid.length)
1050
778
  end
1051
- elsif macro && (refid.end_with? '.adoc')
1052
- src2src = (path = refid.slice 0, refid.length - 5)
779
+ elsif macro
780
+ if refid.end_with? '.adoc'
781
+ src2src = path = refid.slice 0, refid.length - 5
782
+ elsif Helpers.extname? refid
783
+ path = refid
784
+ else
785
+ fragment = refid
786
+ end
1053
787
  else
1054
788
  fragment = refid
1055
789
  end
@@ -1057,19 +791,19 @@ module Substitutors
1057
791
  # handles: #id
1058
792
  if target
1059
793
  refid = fragment
1060
- logger.warn %(invalid reference: #{refid}) if $VERBOSE && !(doc.catalog[:ids].key? refid)
794
+ logger.info %(possible invalid reference: #{refid}) if logger.info? && !doc.catalog[:refs][refid]
1061
795
  elsif path
1062
796
  # handles: path#, path#id, path.adoc#, path.adoc#id, or path.adoc (xref macro only)
1063
797
  # the referenced path is the current document, or its contents have been included in the current document
1064
798
  if src2src && (doc.attributes['docname'] == path || doc.catalog[:includes][path])
1065
799
  if fragment
1066
800
  refid, path, target = fragment, nil, %(##{fragment})
1067
- logger.warn %(invalid reference: #{refid}) if $VERBOSE && !(doc.catalog[:ids].key? refid)
801
+ logger.info %(possible invalid reference: #{refid}) if logger.info? && !doc.catalog[:refs][refid]
1068
802
  else
1069
803
  refid, path, target = nil, nil, '#'
1070
804
  end
1071
805
  else
1072
- refid, path = path, %(#{doc.attributes['relfileprefix']}#{path}#{src2src ? (doc.attributes.fetch 'relfilesuffix', doc.outfilesuffix) : ''})
806
+ refid, path = path, %(#{doc.attributes['relfileprefix'] || ''}#{path}#{src2src ? (doc.attributes.fetch 'relfilesuffix', doc.outfilesuffix) : ''})
1073
807
  if fragment
1074
808
  refid, target = %(#{refid}##{fragment}), %(#{path}##{fragment})
1075
809
  else
@@ -1079,43 +813,70 @@ module Substitutors
1079
813
  # handles: id (in compat mode or when natural xrefs are disabled)
1080
814
  elsif doc.compat_mode || !Compliance.natural_xrefs
1081
815
  refid, target = fragment, %(##{fragment})
1082
- logger.warn %(invalid reference: #{refid}) if $VERBOSE && !(doc.catalog[:ids].key? refid)
816
+ logger.info %(possible invalid reference: #{refid}) if logger.info? && !doc.catalog[:refs][refid]
1083
817
  # handles: id
1084
- elsif doc.catalog[:ids].key? fragment
818
+ elsif doc.catalog[:refs][fragment]
1085
819
  refid, target = fragment, %(##{fragment})
1086
820
  # handles: Node Title or Reference Text
1087
821
  # do reverse lookup on fragment if not a known ID and resembles reftext (contains a space or uppercase char)
1088
- elsif (refid = doc.catalog[:ids].key fragment) && ((fragment.include? ' ') || fragment.downcase != fragment)
822
+ elsif ((fragment.include? ' ') || fragment.downcase != fragment) && (refid = doc.resolve_id fragment)
1089
823
  fragment, target = refid, %(##{refid})
1090
824
  else
1091
825
  refid, target = fragment, %(##{fragment})
1092
- logger.warn %(invalid reference: #{refid}) if $VERBOSE
826
+ logger.info %(possible invalid reference: #{refid}) if logger.info?
1093
827
  end
1094
- attrs['path'], attrs['fragment'], attrs['refid'] = path, fragment, refid
1095
- Inline.new(self, :anchor, text, :type => :xref, :target => target, :attributes => attrs).convert
1096
- }
828
+ attrs['path'] = path
829
+ attrs['fragment'] = fragment
830
+ attrs['refid'] = refid
831
+ Inline.new(self, :anchor, link_text, type: :xref, target: target, attributes: attrs).convert
832
+ end
1097
833
  end
1098
834
 
1099
- content
1100
- end
835
+ if found_macroish && (text.include? 'tnote')
836
+ text = text.gsub InlineFootnoteMacroRx do
837
+ # honor the escape
838
+ next $&.slice 1, $&.length if $&.start_with? RS
1101
839
 
1102
- # Public: Substitute callout source references
1103
- #
1104
- # text - The String text to process
1105
- #
1106
- # Returns the converted String text
1107
- def sub_callouts(text)
1108
- callout_rx = (attr? 'line-comment') ? CalloutSourceRxMap[attr 'line-comment'] : CalloutSourceRx
1109
- autonum = 0
1110
- text.gsub(callout_rx) {
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
840
+ # footnoteref
841
+ if $1
842
+ if $3
843
+ id, content = $3.split ',', 2
844
+ logger.warn %(found deprecated footnoteref macro: #{$&}; use footnote macro with target instead) unless doc.compat_mode
845
+ else
846
+ next $&
847
+ end
848
+ # footnote
849
+ else
850
+ id = $2
851
+ content = $3
852
+ end
853
+
854
+ if id
855
+ if (footnote = doc.footnotes.find {|candidate| candidate.id == id })
856
+ index, content = footnote.index, footnote.text
857
+ type, target, id = :xref, id, nil
858
+ elsif content
859
+ content = restore_passthroughs(normalize_text content, true, true)
860
+ index = doc.counter('footnote-number')
861
+ doc.register(:footnotes, Document::Footnote.new(index, id, content))
862
+ type, target = :ref, nil
863
+ else
864
+ logger.warn %(invalid footnote reference: #{id})
865
+ type, target, content, id = :xref, id, id, nil
866
+ end
867
+ elsif content
868
+ content = restore_passthroughs(normalize_text content, true, true)
869
+ index = doc.counter('footnote-number')
870
+ doc.register(:footnotes, Document::Footnote.new(index, id, content))
871
+ type = target = nil
872
+ else
873
+ next $&
874
+ end
875
+ Inline.new(self, :footnote, content, attributes: { 'index' => index }, id: id, target: target, type: type).convert
1117
876
  end
1118
- }
877
+ end
878
+
879
+ text
1119
880
  end
1120
881
 
1121
882
  # Public: Substitute post replacements
@@ -1123,216 +884,286 @@ module Substitutors
1123
884
  # text - The String text to process
1124
885
  #
1125
886
  # Returns the converted String text
1126
- def sub_post_replacements(text)
1127
- if (@document.attributes.key? 'hardbreaks') || (@attributes.key? 'hardbreaks-option')
887
+ def sub_post_replacements text
888
+ #if attr? 'hardbreaks-option', nil, true
889
+ if @attributes['hardbreaks-option'] || @document.attributes['hardbreaks-option']
1128
890
  lines = text.split LF, -1
1129
891
  return text if lines.size < 2
1130
892
  last = lines.pop
1131
- (lines.map {|line|
1132
- Inline.new(self, :break, (line.end_with? HARD_LINE_BREAK) ? (line.slice 0, line.length - 2) : line, :type => :line).convert
1133
- } << last).join LF
893
+ (lines.map do |line|
894
+ Inline.new(self, :break, (line.end_with? HARD_LINE_BREAK) ? (line.slice 0, line.length - 2) : line, type: :line).convert
895
+ end << last).join LF
1134
896
  elsif (text.include? PLUS) && (text.include? HARD_LINE_BREAK)
1135
- text.gsub(HardLineBreakRx) { Inline.new(self, :break, $1, :type => :line).convert }
897
+ text.gsub(HardLineBreakRx) { Inline.new(self, :break, $1, type: :line).convert }
1136
898
  else
1137
899
  text
1138
900
  end
1139
901
  end
1140
902
 
1141
- # Internal: Convert a quoted text region
903
+ # Public: Apply verbatim substitutions on source (for use when highlighting is disabled).
1142
904
  #
1143
- # match - The MatchData for the quoted text region
1144
- # type - The quoting type (single, double, strong, emphasis, monospaced, etc)
1145
- # scope - The scope of the quoting (constrained or unconstrained)
905
+ # source - the source code String on which to apply verbatim substitutions
906
+ # process_callouts - a Boolean flag indicating whether callout marks should be substituted
1146
907
  #
1147
- # Returns The converted String text for the quoted text region
1148
- def convert_quoted_text(match, type, scope)
1149
- if match[0].start_with? RS
1150
- if scope == :constrained && (attrs = match[2])
1151
- unescaped_attrs = %([#{attrs}])
1152
- else
1153
- return match[0].slice 1, match[0].length
1154
- end
1155
- end
908
+ # Returns the substituted source
909
+ def sub_source source, process_callouts
910
+ process_callouts ? sub_callouts(sub_specialchars source) : (sub_specialchars source)
911
+ end
1156
912
 
1157
- if scope == :constrained
1158
- if unescaped_attrs
1159
- %(#{unescaped_attrs}#{Inline.new(self, :quoted, match[3], :type => type).convert})
913
+ # Public: Substitute callout source references
914
+ #
915
+ # text - The String text to process
916
+ #
917
+ # Returns the converted String text
918
+ def sub_callouts text
919
+ callout_rx = (attr? 'line-comment') ? CalloutSourceRxMap[attr 'line-comment'] : CalloutSourceRx
920
+ autonum = 0
921
+ text.gsub callout_rx do
922
+ # honor the escape
923
+ if $2
924
+ # use sub since it might be behind a line comment
925
+ $&.sub RS, ''
1160
926
  else
1161
- if (attrlist = match[2])
1162
- id = (attributes = parse_quoted_text_attributes attrlist).delete 'id'
1163
- type = :unquoted if type == :mark
1164
- end
1165
- %(#{match[1]}#{Inline.new(self, :quoted, match[3], :type => type, :id => id, :attributes => attributes).convert})
927
+ Inline.new(self, :callout, $4 == '.' ? (autonum += 1).to_s : $4, id: @document.callouts.read_next_id, attributes: { 'guard' => $1 || ($3 == '--' ? ['<!--', '-->'] : nil) }).convert
1166
928
  end
1167
- else
1168
- if (attrlist = match[1])
1169
- id = (attributes = parse_quoted_text_attributes attrlist).delete 'id'
1170
- type = :unquoted if type == :mark
1171
- end
1172
- Inline.new(self, :quoted, match[2], :type => type, :id => id, :attributes => attributes).convert
1173
929
  end
1174
930
  end
1175
931
 
1176
- # Internal: Parse the attributes that are defined on quoted (aka formatted) text
932
+ # Public: Highlight (i.e., colorize) the source code during conversion using a syntax highlighter, if activated by the
933
+ # source-highlighter document attribute. Otherwise return the text with verbatim substitutions applied.
1177
934
  #
1178
- # str - A non-nil String of unprocessed attributes;
1179
- # space-separated roles (e.g., role1 role2) or the id/role shorthand syntax (e.g., #idname.role)
935
+ # If the process_callouts argument is true, this method will extract the callout marks from the source before passing
936
+ # it to the syntax highlighter, then subsequently restore those callout marks to the highlighted source so the callout
937
+ # marks don't confuse the syntax highlighter.
1180
938
  #
1181
- # Returns a Hash of attributes (role and id only)
1182
- def parse_quoted_text_attributes str
1183
- # NOTE attributes are typically resolved after quoted text, so substitute eagerly
1184
- str = sub_attributes str if str.include? ATTR_REF_HEAD
1185
- # for compliance, only consider first positional attribute
1186
- str = str.slice 0, (str.index ',') if str.include? ','
939
+ # source - the source code String to syntax highlight
940
+ # process_callouts - a Boolean flag indicating whether callout marks should be located and substituted
941
+ #
942
+ # Returns the highlighted source code, if a syntax highlighter is defined on the document, otherwise the source with
943
+ # verbatim substitutions applied
944
+ def highlight_source source, process_callouts
945
+ # NOTE the call to highlight? is a defensive check since, normally, we wouldn't arrive here unless it returns true
946
+ return sub_source source, process_callouts unless (syntax_hl = @document.syntax_highlighter) && syntax_hl.highlight?
1187
947
 
1188
- if (str = str.strip).empty?
1189
- {}
1190
- elsif (str.start_with? '.', '#') && Compliance.shorthand_property_syntax
1191
- segments = str.split('#', 2)
948
+ source, callout_marks = extract_callouts source if process_callouts
1192
949
 
1193
- if segments.size > 1
1194
- id, *more_roles = segments[1].split('.')
1195
- else
1196
- id = nil
1197
- more_roles = []
1198
- end
950
+ doc_attrs = @document.attributes
951
+ syntax_hl_name = syntax_hl.name
952
+ if (linenums_mode = (attr? 'linenums') ? (doc_attrs[%(#{syntax_hl_name}-linenums-mode)] || :table).to_sym : nil) &&
953
+ (start_line_number = (attr 'start', 1).to_i) < 1
954
+ start_line_number = 1
955
+ end
956
+ highlight_lines = resolve_lines_to_highlight source, (attr 'highlight'), start_line_number if attr? 'highlight'
1199
957
 
1200
- roles = segments[0].empty? ? [] : segments[0].split('.')
1201
- if roles.size > 1
1202
- roles.shift
1203
- end
958
+ highlighted, source_offset = syntax_hl.highlight self, source, (attr 'language'),
959
+ callouts: callout_marks,
960
+ css_mode: (doc_attrs[%(#{syntax_hl_name}-css)] || :class).to_sym,
961
+ highlight_lines: highlight_lines,
962
+ number_lines: linenums_mode,
963
+ start_line_number: start_line_number,
964
+ style: doc_attrs[%(#{syntax_hl_name}-style)]
1204
965
 
1205
- if more_roles.size > 0
1206
- roles.concat more_roles
1207
- end
966
+ # fix passthrough placeholders that got caught up in syntax highlighting
967
+ highlighted = highlighted.gsub HighlightedPassSlotRx, %(#{PASS_START}\\1#{PASS_END}) unless @passthroughs.empty?
1208
968
 
1209
- attrs = {}
1210
- attrs['id'] = id if id
1211
- attrs['role'] = roles.join ' ' unless roles.empty?
1212
- attrs
1213
- else
1214
- {'role' => str}
1215
- end
969
+ # NOTE highlight method may have depleted callouts
970
+ callout_marks.nil_or_empty? ? highlighted : (restore_callouts highlighted, callout_marks, source_offset)
1216
971
  end
1217
972
 
1218
- # Internal: Parse attributes in name or name=value format from a comma-separated String
973
+ # Public: Resolve the line numbers in the specified source to highlight from the provided spec.
1219
974
  #
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).
975
+ # e.g., highlight="1-5, !2, 10" or highlight=1-5;!2,10
1230
976
  #
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]
1236
- # substitutions are only performed on attribute values if block is not nil
1237
- block = self if opts[:sub_result]
1238
- if (into = opts[:into])
1239
- AttributeList.new(attrlist, block).parse_into(into, posattrs)
1240
- else
1241
- AttributeList.new(attrlist, block).parse(posattrs)
977
+ # source - The String source.
978
+ # spec - The lines specifier (e.g., "1-5, !2, 10" or "1..5;!2;10")
979
+ # start - The line number of the first line (optional, default: false)
980
+ #
981
+ # Returns an [Array] of unique, sorted line numbers.
982
+ def resolve_lines_to_highlight source, spec, start = nil
983
+ lines = []
984
+ spec = spec.delete ' ' if spec.include? ' '
985
+ ((spec.include? ',') ? (spec.split ',') : (spec.split ';')).map do |entry|
986
+ if entry.start_with? '!'
987
+ entry = entry.slice 1, entry.length
988
+ negate = true
989
+ end
990
+ if (delim = (entry.include? '..') ? '..' : ((entry.include? '-') ? '-' : nil))
991
+ from, _, to = entry.partition delim
992
+ to = (source.count LF) + 1 if to.empty? || (to = to.to_i) < 0
993
+ if negate
994
+ lines -= (from.to_i..to).to_a
995
+ else
996
+ lines |= (from.to_i..to).to_a
997
+ end
998
+ elsif negate
999
+ lines.delete entry.to_i
1000
+ elsif !lines.include?(line = entry.to_i)
1001
+ lines << line
1002
+ end
1242
1003
  end
1004
+ # If the start attribute is defined, then the lines to highlight specified by the provided spec should be relative to the start value.
1005
+ unless (shift = start ? start - 1 : 0) == 0
1006
+ lines = lines.map {|it| it - shift }
1007
+ end
1008
+ lines.sort
1243
1009
  end
1244
1010
 
1245
- # Expand all groups in the subs list and return. If no subs are resolve, return nil.
1011
+ # Public: Extract the passthrough text from the document for reinsertion after processing.
1246
1012
  #
1247
- # subs - The substitutions to expand; can be a Symbol, Symbol Array or nil
1013
+ # text - The String from which to extract passthrough fragments
1248
1014
  #
1249
- # Returns a Symbol Array of substitutions to pass to apply_subs or nil if no substitutions were resolved.
1250
- def expand_subs subs
1251
- if ::Symbol === subs
1252
- unless subs == :none
1253
- SUB_GROUPS[subs] || [subs]
1254
- end
1255
- else
1256
- expanded_subs = []
1257
- subs.each do |key|
1258
- unless key == :none
1259
- if (sub_group = SUB_GROUPS[key])
1260
- expanded_subs += sub_group
1015
+ # Returns the String text with passthrough regions substituted with placeholders
1016
+ def extract_passthroughs text
1017
+ compat_mode = @document.compat_mode
1018
+ passthrus = @passthroughs
1019
+ text = text.gsub InlinePassMacroRx do
1020
+ if (boundary = $4) # $$, ++, or +++
1021
+ # skip ++ in compat mode, handled as normal quoted text
1022
+ next %(#{$2 ? "#{$1}[#{$2}]#{$3}" : "#{$1}#{$3}"}++#{extract_passthroughs $5}++) if compat_mode && boundary == '++'
1023
+
1024
+ if (attrlist = $2)
1025
+ if (escape_count = $3.length) > 0
1026
+ # NOTE we don't look for nested unconstrained pass macros
1027
+ next %(#{$1}[#{attrlist}]#{RS * (escape_count - 1)}#{boundary}#{$5}#{boundary})
1028
+ elsif $1 == RS
1029
+ preceding = %([#{attrlist}])
1261
1030
  else
1262
- expanded_subs << key
1031
+ if boundary == '++' && (attrlist.end_with? 'x-')
1032
+ old_behavior = true
1033
+ attrlist = attrlist.slice 0, attrlist.length - 2
1034
+ end
1035
+ attributes = parse_quoted_text_attributes attrlist
1263
1036
  end
1037
+ elsif (escape_count = $3.length) > 0
1038
+ # NOTE we don't look for nested unconstrained pass macros
1039
+ next %(#{RS * (escape_count - 1)}#{boundary}#{$5}#{boundary})
1040
+ end
1041
+ subs = (boundary == '+++' ? [] : BASIC_SUBS)
1042
+
1043
+ if attributes
1044
+ if old_behavior
1045
+ passthrus[passthru_key = passthrus.size] = { text: $5, subs: NORMAL_SUBS, type: :monospaced, attributes: attributes }
1046
+ else
1047
+ passthrus[passthru_key = passthrus.size] = { text: $5, subs: subs, type: :unquoted, attributes: attributes }
1048
+ end
1049
+ else
1050
+ passthrus[passthru_key = passthrus.size] = { text: $5, subs: subs }
1051
+ end
1052
+ else # pass:[]
1053
+ # NOTE we don't look for nested pass:[] macros
1054
+ # honor the escape
1055
+ next $&.slice 1, $&.length if $6 == RS
1056
+ if (subs = $7)
1057
+ passthrus[passthru_key = passthrus.size] = { text: (normalize_text $8, nil, true), subs: (resolve_pass_subs subs) }
1058
+ else
1059
+ passthrus[passthru_key = passthrus.size] = { text: (normalize_text $8, nil, true) }
1264
1060
  end
1265
1061
  end
1266
1062
 
1267
- expanded_subs.empty? ? nil : expanded_subs
1268
- end
1269
- end
1063
+ %(#{preceding || ''}#{PASS_START}#{passthru_key}#{PASS_END})
1064
+ end if (text.include? '++') || (text.include? '$$') || (text.include? 'ss:')
1270
1065
 
1271
- # Internal: Strip bounding whitespace, fold endlines and unescape closing
1272
- # square brackets from text extracted from brackets
1273
- def unescape_bracketed_text text
1274
- if (text = text.strip.tr LF, ' ').include? R_SB
1275
- text = text.gsub ESC_R_SB, R_SB
1276
- end unless text.empty?
1277
- text
1278
- end
1066
+ pass_inline_char1, pass_inline_char2, pass_inline_rx = InlinePassRx[compat_mode]
1067
+ text = text.gsub pass_inline_rx do
1068
+ preceding = $1
1069
+ attrlist = $2
1070
+ escape_mark = RS if (quoted_text = $3).start_with? RS
1071
+ format_mark = $4
1072
+ content = $5
1279
1073
 
1280
- # Internal: Strip bounding whitespace and fold endlines
1281
- def normalize_string str, unescape_brackets = false
1282
- unless str.empty?
1283
- str = str.strip.tr LF, ' '
1284
- str = str.gsub ESC_R_SB, R_SB if unescape_brackets && (str.include? R_SB)
1285
- end
1286
- str
1287
- end
1074
+ if compat_mode
1075
+ old_behavior = true
1076
+ elsif (old_behavior = attrlist && (attrlist.end_with? 'x-'))
1077
+ attrlist = attrlist.slice 0, attrlist.length - 2
1078
+ end
1288
1079
 
1289
- # Internal: Unescape closing square brackets.
1290
- # Intended for text extracted from square brackets.
1291
- def unescape_brackets str
1292
- if str.include? RS
1293
- str = str.gsub ESC_R_SB, R_SB
1294
- end unless str.empty?
1295
- str
1296
- end
1080
+ if attrlist
1081
+ if format_mark == '`' && !old_behavior
1082
+ next extract_inner_passthrough content, %(#{preceding}[#{attrlist}]#{escape_mark})
1083
+ elsif escape_mark
1084
+ # honor the escape of the formatting mark
1085
+ next %(#{preceding}[#{attrlist}]#{quoted_text.slice 1, quoted_text.length})
1086
+ elsif preceding == RS
1087
+ # honor the escape of the attributes
1088
+ preceding = %([#{attrlist}])
1089
+ else
1090
+ attributes = parse_quoted_text_attributes attrlist
1091
+ end
1092
+ elsif format_mark == '`' && !old_behavior
1093
+ next extract_inner_passthrough content, %(#{preceding}#{escape_mark})
1094
+ elsif escape_mark
1095
+ # honor the escape of the formatting mark
1096
+ next %(#{preceding}#{quoted_text.slice 1, quoted_text.length})
1097
+ end
1297
1098
 
1298
- # Internal: Split text formatted as CSV with support
1299
- # for double-quoted values (in which commas are ignored)
1300
- def split_simple_csv str
1301
- if str.empty?
1302
- values = []
1303
- elsif str.include? '"'
1304
- values = []
1305
- current = []
1306
- quote_open = false
1307
- str.each_char do |c|
1308
- case c
1309
- when ','
1310
- if quote_open
1311
- current << c
1312
- else
1313
- values << current.join.strip
1314
- current = []
1315
- end
1316
- when '"'
1317
- quote_open = !quote_open
1099
+ if compat_mode
1100
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: BASIC_SUBS, attributes: attributes, type: :monospaced }
1101
+ elsif attributes
1102
+ if old_behavior
1103
+ subs = (format_mark == '`' ? BASIC_SUBS : NORMAL_SUBS)
1104
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: subs, attributes: attributes, type: :monospaced }
1318
1105
  else
1319
- current << c
1106
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: BASIC_SUBS, attributes: attributes, type: :unquoted }
1320
1107
  end
1108
+ else
1109
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: BASIC_SUBS }
1321
1110
  end
1322
1111
 
1323
- values << current.join.strip
1324
- else
1325
- values = str.split(',').map {|it| it.strip }
1326
- end
1112
+ %(#{preceding}#{PASS_START}#{passthru_key}#{PASS_END})
1113
+ end if (text.include? pass_inline_char1) || (pass_inline_char2 && (text.include? pass_inline_char2))
1114
+
1115
+ # NOTE we need to do the stem in a subsequent step to allow it to be escaped by the former
1116
+ text = text.gsub InlineStemMacroRx do
1117
+ # honor the escape
1118
+ next $&.slice 1, $&.length if $&.start_with? RS
1327
1119
 
1328
- values
1120
+ if (type = $1.to_sym) == :stem
1121
+ type = STEM_TYPE_ALIASES[@document.attributes['stem']].to_sym
1122
+ end
1123
+ subs = $2
1124
+ content = normalize_text $3, nil, true
1125
+ # NOTE drop enclosing $ signs around latexmath for backwards compatibility with AsciiDoc.py
1126
+ content = content.slice 1, content.length - 2 if type == :latexmath && (content.start_with? '$') && (content.end_with? '$')
1127
+ subs = subs ? (resolve_pass_subs subs) : ((@document.basebackend? 'html') ? BASIC_SUBS : nil)
1128
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: subs, type: type }
1129
+ %(#{PASS_START}#{passthru_key}#{PASS_END})
1130
+ end if (text.include? ':') && ((text.include? 'stem:') || (text.include? 'math:'))
1131
+
1132
+ text
1133
+ end
1134
+
1135
+ # Public: Restore the passthrough text by reinserting into the placeholder positions
1136
+ #
1137
+ # text - The String text into which to restore the passthrough text
1138
+ #
1139
+ # returns The String text with the passthrough text restored
1140
+ def restore_passthroughs text
1141
+ passthrus = @passthroughs
1142
+ text.gsub PassSlotRx do
1143
+ if (pass = passthrus[$1.to_i])
1144
+ subbed_text = apply_subs(pass[:text], pass[:subs])
1145
+ if (type = pass[:type])
1146
+ if (attributes = pass[:attributes])
1147
+ id = attributes['id']
1148
+ end
1149
+ subbed_text = Inline.new(self, :quoted, subbed_text, type: type, id: id, attributes: attributes).convert
1150
+ end
1151
+ subbed_text.include?(PASS_START) ? restore_passthroughs(subbed_text) : subbed_text
1152
+ else
1153
+ logger.error %(unresolved passthrough detected: #{text})
1154
+ '??pass??'
1155
+ end
1156
+ end
1329
1157
  end
1330
1158
 
1331
- # Internal: Resolve the list of comma-delimited subs against the possible options.
1159
+ # Public: Resolve the list of comma-delimited subs against the possible options.
1332
1160
  #
1333
- # subs - A comma-delimited String of substitution aliases
1161
+ # subs - The comma-delimited String of substitution names or aliases.
1162
+ # type - A Symbol representing the context for which the subs are being resolved (default: :block).
1163
+ # defaults - An Array of substitutions to start with when computing incremental substitutions (default: nil).
1164
+ # subject - The String to use in log messages to communicate the subject for which subs are being resolved (default: nil)
1334
1165
  #
1335
- # returns An Array of Symbols representing the substitution operation or nothing if no subs are found.
1166
+ # Returns An Array of Symbols representing the substitution operation or nothing if no subs are found.
1336
1167
  def resolve_subs subs, type = :block, defaults = nil, subject = nil
1337
1168
  return if subs.nil_or_empty?
1338
1169
  # QUESTION should we store candidates as a Set instead of an Array?
@@ -1386,7 +1217,7 @@ module Substitutors
1386
1217
  end
1387
1218
  end
1388
1219
  return unless candidates
1389
- # weed out invalid options and remove duplicates (order is preserved; first occurence wins)
1220
+ # weed out invalid options and remove duplicates (order is preserved; first occurrence wins)
1390
1221
  resolved = candidates & SUB_OPTIONS[type]
1391
1222
  unless (candidates - resolved).empty?
1392
1223
  invalid = candidates - resolved
@@ -1395,254 +1226,327 @@ module Substitutors
1395
1226
  resolved
1396
1227
  end
1397
1228
 
1229
+ # Public: Call resolve_subs for the :block type.
1398
1230
  def resolve_block_subs subs, defaults, subject
1399
1231
  resolve_subs subs, :block, defaults, subject
1400
1232
  end
1401
1233
 
1234
+ # Public: Call resolve_subs for the :inline type with the subject set as passthrough macro.
1402
1235
  def resolve_pass_subs subs
1403
1236
  resolve_subs subs, :inline, nil, 'passthrough macro'
1404
1237
  end
1405
1238
 
1406
- # Public: Highlight the source code if a source highlighter is defined
1407
- # on the document, otherwise return the text unprocessed
1239
+ # Public: Expand all groups in the subs list and return. If no subs are resolved, return nil.
1408
1240
  #
1409
- # Callout marks are stripped from the source prior to passing it to the
1410
- # highlighter, then later restored in converted form, so they are not
1411
- # incorrectly processed by the source highlighter.
1241
+ # subs - The substitutions to expand; can be a Symbol, Symbol Array, or String
1242
+ # subject - The String to use in log messages to communicate the subject for which subs are being resolved (default: nil)
1412
1243
  #
1413
- # source - the source code String to highlight
1414
- # process_callouts - a Boolean flag indicating whether callout marks should be substituted
1415
- #
1416
- # returns the highlighted source code, if a source highlighter is defined
1417
- # on the document, otherwise the source with verbatim substituions applied
1418
- def highlight_source source, process_callouts, highlighter = nil
1419
- case (highlighter ||= @document.attributes['source-highlighter'])
1420
- when 'coderay'
1421
- unless (highlighter_loaded = defined? ::CodeRay) ||
1422
- (defined? @@coderay_unavailable) || @document.attributes['coderay-unavailable']
1423
- if (Helpers.require_library 'coderay', true, :warn).nil?
1424
- # prevent further attempts to load CodeRay in this process
1425
- @@coderay_unavailable = true
1426
- else
1427
- highlighter_loaded = true
1244
+ # Returns a Symbol Array of substitutions to pass to apply_subs or nil if no substitutions were resolved.
1245
+ def expand_subs subs, subject = nil
1246
+ case subs
1247
+ when ::Symbol
1248
+ subs == :none ? nil : SUB_GROUPS[subs] || [subs]
1249
+ when ::Array
1250
+ expanded_subs = []
1251
+ subs.each do |key|
1252
+ unless key == :none
1253
+ if (sub_group = SUB_GROUPS[key])
1254
+ expanded_subs += sub_group
1255
+ else
1256
+ expanded_subs << key
1257
+ end
1428
1258
  end
1429
1259
  end
1430
- when 'pygments'
1431
- unless (highlighter_loaded = defined? ::Pygments) ||
1432
- (defined? @@pygments_unavailable) || @document.attributes['pygments-unavailable']
1433
- if (Helpers.require_library 'pygments', 'pygments.rb', :warn).nil?
1434
- # prevent further attempts to load Pygments in this process
1435
- @@pygments_unavailable = true
1436
- else
1437
- highlighter_loaded = true
1438
- end
1260
+ expanded_subs.empty? ? nil : expanded_subs
1261
+ else
1262
+ resolve_subs subs, :inline, nil, subject
1263
+ end
1264
+ end
1265
+
1266
+ # Internal: Commit the requested substitutions to this block.
1267
+ #
1268
+ # Looks for an attribute named "subs". If present, resolves substitutions
1269
+ # from the value of that attribute and assigns them to the subs property on
1270
+ # this block. Otherwise, uses the substitutions assigned to the default_subs
1271
+ # property, if specified, or selects a default set of substitutions based on
1272
+ # the content model of the block.
1273
+ #
1274
+ # Returns nothing
1275
+ def commit_subs
1276
+ unless (default_subs = @default_subs)
1277
+ case @content_model
1278
+ when :simple
1279
+ default_subs = NORMAL_SUBS
1280
+ when :verbatim
1281
+ # NOTE :literal with listparagraph-option gets folded into text of list item later
1282
+ default_subs = @context == :verse ? NORMAL_SUBS : VERBATIM_SUBS
1283
+ when :raw
1284
+ # TODO make pass subs a compliance setting; AsciiDoc.py performs :attributes and :macros on a pass block
1285
+ default_subs = @context == :stem ? BASIC_SUBS : NO_SUBS
1286
+ else
1287
+ return @subs
1439
1288
  end
1289
+ end
1290
+
1291
+ if (custom_subs = @attributes['subs'])
1292
+ @subs = (resolve_block_subs custom_subs, default_subs, @context) || []
1440
1293
  else
1441
- # unknown highlighting library (something is misconfigured if we arrive here)
1442
- highlighter_loaded = false
1294
+ @subs = default_subs.drop 0
1295
+ end
1296
+
1297
+ # QUESION delegate this logic to a method?
1298
+ if @context == :listing && @style == 'source' && (syntax_hl = @document.syntax_highlighter) &&
1299
+ syntax_hl.highlight? && (idx = @subs.index :specialcharacters)
1300
+ @subs[idx] = :highlight
1443
1301
  end
1444
1302
 
1445
- return sub_source source, process_callouts unless highlighter_loaded
1303
+ nil
1304
+ end
1446
1305
 
1447
- lineno = 0
1448
- callout_on_last = false
1449
- if process_callouts
1450
- callout_marks = {}
1451
- last = -1
1452
- callout_rx = (attr? 'line-comment') ? CalloutExtractRxMap[attr 'line-comment'] : CalloutExtractRx
1453
- # extract callout marks, indexed by line number
1454
- source = source.split(LF, -1).map {|line|
1455
- lineno = lineno + 1
1456
- line.gsub(callout_rx) {
1457
- # honor the escape
1458
- if $2
1459
- # use sub since it might be behind a line comment
1460
- $&.sub(RS, '')
1461
- else
1462
- (callout_marks[lineno] ||= []) << [$1, $4]
1463
- last = lineno
1464
- nil
1465
- end
1466
- }
1467
- }.join LF
1468
- callout_on_last = (last == lineno)
1469
- callout_marks = nil if callout_marks.empty?
1306
+ # Internal: Parse attributes in name or name=value format from a comma-separated String
1307
+ #
1308
+ # attrlist - A comma-separated String list of attributes in name or name=value format.
1309
+ # posattrs - An Array of positional attribute names (default: []).
1310
+ # opts - A Hash of options to control how the string is parsed (default: {}):
1311
+ # :into - The Hash to parse the attributes into (optional, default: false).
1312
+ # :sub_input - A Boolean that indicates whether to substitute attributes prior to
1313
+ # parsing (optional, default: false).
1314
+ # :sub_result - A Boolean that indicates whether to apply substitutions
1315
+ # single-quoted attribute values (optional, default: true).
1316
+ # :unescape_input - A Boolean that indicates whether to unescape square brackets prior
1317
+ # to parsing (optional, default: false).
1318
+ #
1319
+ # Returns an empty Hash if attrlist is nil or empty, otherwise a Hash of parsed attributes.
1320
+ def parse_attributes attrlist, posattrs = [], opts = {}
1321
+ return {} if attrlist ? attrlist.empty? : true
1322
+ attrlist = normalize_text attrlist, true, true if opts[:unescape_input]
1323
+ attrlist = @document.sub_attributes attrlist if opts[:sub_input] && (attrlist.include? ATTR_REF_HEAD)
1324
+ # substitutions are only performed on attribute values if block is not nil
1325
+ block = self if opts[:sub_result]
1326
+ if (into = opts[:into])
1327
+ AttributeList.new(attrlist, block).parse_into(into, posattrs)
1470
1328
  else
1471
- callout_marks = nil
1329
+ AttributeList.new(attrlist, block).parse(posattrs)
1472
1330
  end
1331
+ end
1473
1332
 
1474
- linenums_mode = nil
1475
- highlight_lines = nil
1333
+ private
1476
1334
 
1477
- case highlighter
1478
- when 'coderay'
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
1481
- if attr? 'highlight', nil, false
1482
- highlight_lines = resolve_lines_to_highlight source, (attr 'highlight', nil, false)
1483
- end
1484
- end
1485
- result = ::CodeRay::Duo[attr('language', :text, false).to_sym, :html, {
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
1493
- when 'pygments'
1494
- lexer = ::Pygments::Lexer.find_by_alias(attr 'language', 'text', false) || ::Pygments::Lexer.find_by_mimetype('text/plain')
1495
- opts = { :cssclass => 'pyhl', :classprefix => 'tok-', :nobackground => true, :stripnl => false }
1496
- opts[:startinline] = !(option? 'mixed') if lexer.name == 'PHP'
1497
- unless (@document.attributes['pygments-css'] || 'class') == 'class'
1498
- opts[:noclasses] = true
1499
- opts[:style] = (@document.attributes['pygments-style'] || Stylesheets::DEFAULT_PYGMENTS_STYLE)
1500
- end
1501
- if attr? 'highlight', nil, false
1502
- unless (highlight_lines = resolve_lines_to_highlight source, (attr 'highlight', nil, false)).empty?
1503
- opts[:hl_lines] = highlight_lines.join ' '
1504
- end
1505
- end
1506
- # NOTE highlight can return nil if something goes wrong; fallback to source if this happens
1507
- # TODO we could add the line numbers in ourselves instead of having to strip out the junk
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'
1510
- linenums_mode = :table
1511
- if (result = lexer.highlight source, :options => opts)
1512
- result = (result.sub PygmentsWrapperDivRx, '\1').gsub PygmentsWrapperPreRx, '\1'
1335
+ # This method is used in cases when the attrlist can be mixed with the text of a macro.
1336
+ # If no attributes are detected aside from the first positional attribute, and the first positional
1337
+ # attribute matches the attrlist, then the original text is returned.
1338
+ def extract_attributes_from_text text, default_text = nil
1339
+ attrlist = (text.include? LF) ? (text.tr LF, ' ') : text
1340
+ if (resolved_text = (attrs = (AttributeList.new attrlist, self).parse)[1])
1341
+ # NOTE if resolved text remains unchanged, clear attributes and return unparsed text
1342
+ resolved_text == attrlist ? [text, attrs.clear] : [resolved_text, attrs]
1343
+ else
1344
+ [default_text, attrs]
1345
+ end
1346
+ end
1347
+
1348
+ # Internal: Extract the callout numbers from the source to prepare it for syntax highlighting.
1349
+ def extract_callouts source
1350
+ callout_marks = {}
1351
+ autonum = lineno = 0
1352
+ last_lineno = nil
1353
+ callout_rx = (attr? 'line-comment') ? CalloutExtractRxMap[attr 'line-comment'] : CalloutExtractRx
1354
+ # extract callout marks, indexed by line number
1355
+ source = (source.split LF, -1).map do |line|
1356
+ lineno += 1
1357
+ line.gsub callout_rx do
1358
+ # honor the escape
1359
+ if $2
1360
+ # use sub since it might be behind a line comment
1361
+ $&.sub RS, ''
1513
1362
  else
1514
- result = sub_specialchars source
1363
+ (callout_marks[lineno] ||= []) << [$1 || ($3 == '--' ? ['<!--', '-->'] : nil), $4 == '.' ? (autonum += 1).to_s : $4]
1364
+ last_lineno = lineno
1365
+ ''
1515
1366
  end
1516
- elsif (result = lexer.highlight source, :options => opts)
1517
- if PygmentsWrapperPreRx =~ result
1518
- result = $1
1519
- end
1520
- else
1521
- result = sub_specialchars source
1522
1367
  end
1368
+ end.join LF
1369
+ if last_lineno
1370
+ source = %(#{source}#{LF}) if last_lineno == lineno
1371
+ else
1372
+ callout_marks = nil
1523
1373
  end
1374
+ [source, callout_marks]
1375
+ end
1524
1376
 
1525
- # fix passthrough placeholders that got caught up in syntax highlighting
1526
- result = result.gsub HighlightedPassSlotRx, %(#{PASS_START}\\1#{PASS_END}) unless @passthroughs.empty?
1527
-
1528
- if process_callouts && callout_marks
1529
- lineno = 0
1530
- autonum = 0
1531
- reached_code = linenums_mode != :table
1532
- result.split(LF, -1).map {|line|
1533
- unless reached_code
1534
- next line unless line.include?('<td class="code">')
1535
- reached_code = true
1536
- end
1537
- lineno += 1
1538
- if (conums = callout_marks.delete(lineno))
1539
- tail = nil
1540
- if callout_on_last && callout_marks.empty? && linenums_mode == :table
1541
- if highlighter == 'coderay' && (pos = line.index '</pre>')
1542
- line, tail = (line.slice 0, pos), (line.slice pos, line.length)
1543
- elsif highlighter == 'pygments' && (pos = line.start_with? '</td>')
1544
- line, tail = '', line
1545
- end
1546
- end
1547
- if conums.size == 1
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})
1550
- else
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 ' '
1552
- %(#{line}#{conums_markup}#{tail})
1553
- end
1377
+ # Internal: Restore the callout numbers to the highlighted source.
1378
+ def restore_callouts source, callout_marks, source_offset = nil
1379
+ if source_offset
1380
+ preamble = source.slice 0, source_offset
1381
+ source = source.slice source_offset, source.length
1382
+ else
1383
+ preamble = ''
1384
+ end
1385
+ lineno = 0
1386
+ preamble + ((source.split LF, -1).map do |line|
1387
+ if (conums = callout_marks.delete lineno += 1)
1388
+ if conums.size == 1
1389
+ guard, numeral = conums[0]
1390
+ %(#{line}#{Inline.new(self, :callout, numeral, id: @document.callouts.read_next_id, attributes: { 'guard' => guard }).convert})
1554
1391
  else
1555
- line
1392
+ %(#{line}#{conums.map do |guard_it, numeral_it|
1393
+ Inline.new(self, :callout, numeral_it, id: @document.callouts.read_next_id, attributes: { 'guard' => guard_it }).convert
1394
+ end.join ' '})
1556
1395
  end
1557
- }.join LF
1396
+ else
1397
+ line
1398
+ end
1399
+ end.join LF)
1400
+ end
1401
+
1402
+ # Internal: Extract nested single-plus passthrough; otherwise return unprocessed
1403
+ def extract_inner_passthrough text, pre
1404
+ if (text.end_with? '+') && (text.start_with? '+', '\+') && SinglePlusInlinePassRx =~ text
1405
+ if $1
1406
+ %(#{pre}`+#{$2}+`)
1407
+ else
1408
+ @passthroughs[passthru_key = @passthroughs.size] = { text: $2, subs: BASIC_SUBS }
1409
+ %(#{pre}`#{PASS_START}#{passthru_key}#{PASS_END}`)
1410
+ end
1558
1411
  else
1559
- result
1412
+ %(#{pre}`#{text}`)
1560
1413
  end
1561
1414
  end
1562
1415
 
1563
- # e.g., highlight="1-5, !2, 10" or highlight=1-5;!2,10
1564
- def resolve_lines_to_highlight source, spec
1565
- lines = []
1566
- spec = spec.delete ' ' if spec.include? ' '
1567
- ((spec.include? ',') ? (spec.split ',') : (spec.split ';')).map do |entry|
1568
- negate = false
1569
- if entry.start_with? '!'
1570
- entry = entry.slice 1, entry.length
1571
- negate = true
1416
+ # Internal: Convert a quoted text region
1417
+ #
1418
+ # match - The MatchData for the quoted text region
1419
+ # type - The quoting type (single, double, strong, emphasis, monospaced, etc)
1420
+ # scope - The scope of the quoting (constrained or unconstrained)
1421
+ #
1422
+ # Returns The converted String text for the quoted text region
1423
+ def convert_quoted_text match, type, scope
1424
+ if match[0].start_with? RS
1425
+ if scope == :constrained && (attrs = match[2])
1426
+ unescaped_attrs = %([#{attrs}])
1427
+ else
1428
+ return match[0].slice 1, match[0].length
1572
1429
  end
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
1577
- if negate
1578
- lines -= line_nums
1579
- else
1580
- lines.concat line_nums
1581
- end
1430
+ end
1431
+
1432
+ if scope == :constrained
1433
+ if unescaped_attrs
1434
+ %(#{unescaped_attrs}#{Inline.new(self, :quoted, match[3], type: type).convert})
1582
1435
  else
1583
- if negate
1584
- lines.delete entry.to_i
1585
- else
1586
- lines << entry.to_i
1436
+ if (attrlist = match[2])
1437
+ id = (attributes = parse_quoted_text_attributes attrlist)['id']
1438
+ type = :unquoted if type == :mark
1587
1439
  end
1440
+ %(#{match[1]}#{Inline.new(self, :quoted, match[3], type: type, id: id, attributes: attributes).convert})
1588
1441
  end
1442
+ else
1443
+ if (attrlist = match[1])
1444
+ id = (attributes = parse_quoted_text_attributes attrlist)['id']
1445
+ type = :unquoted if type == :mark
1446
+ end
1447
+ Inline.new(self, :quoted, match[2], type: type, id: id, attributes: attributes).convert
1589
1448
  end
1590
- lines.sort.uniq
1591
1449
  end
1592
1450
 
1593
- # Public: Apply verbatim substitutions on source (for use when highlighting is disabled).
1594
- #
1595
- # source - the source code String on which to apply verbatim substitutions
1596
- # process_callouts - a Boolean flag indicating whether callout marks should be substituted
1451
+ # Internal: Substitute replacement text for matched location
1597
1452
  #
1598
- # returns the substituted source
1599
- def sub_source source, process_callouts
1600
- process_callouts ? sub_callouts(sub_specialchars source) : (sub_specialchars source)
1453
+ # returns The String text with the replacement characters substituted
1454
+ def do_replacement m, replacement, restore
1455
+ if (captured = m[0]).include? RS
1456
+ # we have to use sub since we aren't sure it's the first char
1457
+ captured.sub RS, ''
1458
+ else
1459
+ case restore
1460
+ when :none
1461
+ replacement
1462
+ when :bounding
1463
+ m[1] + replacement + m[2]
1464
+ else # :leading
1465
+ m[1] + replacement
1466
+ end
1467
+ end
1601
1468
  end
1602
1469
 
1603
- # Internal: Lock-in the substitutions for this block
1470
+ # Internal: Inserts text into a formatted text enclosure; used by xreftext
1471
+ alias sub_placeholder sprintf unless RUBY_ENGINE == 'opal'
1472
+
1473
+ # Internal: Parse the attributes that are defined on quoted (aka formatted) text
1604
1474
  #
1605
- # Looks for an attribute named "subs". If present, resolves substitutions
1606
- # from the value of that attribute and assigns them to the subs property on
1607
- # this block. Otherwise, uses the substitutions assigned to the default_subs
1608
- # property, if specified, or selects a default set of substitutions based on
1609
- # the content model of the block.
1475
+ # str - A non-nil String of unprocessed attributes;
1476
+ # space-separated roles (e.g., role1 role2) or the id/role shorthand syntax (e.g., #idname.role)
1610
1477
  #
1611
- # Returns The Array of resolved substitutions now assigned to this block
1612
- def lock_in_subs
1613
- unless (default_subs = @default_subs)
1614
- case @content_model
1615
- when :simple
1616
- default_subs = NORMAL_SUBS
1617
- when :verbatim
1618
- if @context == :listing || (@context == :literal && !(option? 'listparagraph'))
1619
- default_subs = VERBATIM_SUBS
1620
- elsif @context == :verse
1621
- default_subs = NORMAL_SUBS
1478
+ # Returns a Hash of attributes (role and id only)
1479
+ def parse_quoted_text_attributes str
1480
+ # NOTE attributes are typically resolved after quoted text, so substitute eagerly
1481
+ str = sub_attributes str if str.include? ATTR_REF_HEAD
1482
+ # for compliance, only consider first positional attribute (very unlikely)
1483
+ str = str.slice 0, (str.index ',') if str.include? ','
1484
+ if (str = str.strip).empty?
1485
+ {}
1486
+ elsif (str.start_with? '.', '#') && Compliance.shorthand_property_syntax
1487
+ before, _, after = str.partition '#'
1488
+ attrs = {}
1489
+ if after.empty?
1490
+ attrs['role'] = (before.tr '.', ' ').lstrip if before.length > 1
1491
+ else
1492
+ id, _, roles = after.partition '.'
1493
+ attrs['id'] = id unless id.empty?
1494
+ if roles.empty?
1495
+ attrs['role'] = (before.tr '.', ' ').lstrip if before.length > 1
1496
+ elsif before.length > 1
1497
+ attrs['role'] = ((before + '.' + roles).tr '.', ' ').lstrip
1622
1498
  else
1623
- default_subs = BASIC_SUBS
1499
+ attrs['role'] = roles.tr '.', ' '
1624
1500
  end
1625
- when :raw
1626
- # TODO make pass subs a compliance setting; AsciiDoc Python performs :attributes and :macros on a pass block
1627
- default_subs = @context == :stem ? BASIC_SUBS : NONE_SUBS
1628
- else
1629
- return @subs
1630
1501
  end
1631
- end
1632
-
1633
- if (custom_subs = @attributes['subs'])
1634
- @subs = (resolve_block_subs custom_subs, default_subs, @context) || []
1502
+ attrs
1635
1503
  else
1636
- @subs = default_subs.drop 0
1504
+ { 'role' => str }
1637
1505
  end
1506
+ end
1638
1507
 
1639
- # QUESION delegate this logic to a method?
1640
- if @context == :listing && @style == 'source' && (@attributes.key? 'language') && (@document.basebackend? 'html') &&
1641
- (SUB_HIGHLIGHT.include? @document.attributes['source-highlighter']) && (idx = @subs.index :specialcharacters)
1642
- @subs[idx] = :highlight
1508
+ # Internal: Normalize text to prepare it for parsing.
1509
+ #
1510
+ # If normalize_whitespace is true, strip surrounding whitespace and fold newlines. If unescape_closing_square_bracket
1511
+ # is set, unescape any escaped closing square brackets.
1512
+ #
1513
+ # Returns the normalized text String
1514
+ def normalize_text text, normalize_whitespace = nil, unescape_closing_square_brackets = nil
1515
+ unless text.empty?
1516
+ text = text.strip.tr LF, ' ' if normalize_whitespace
1517
+ text = text.gsub ESC_R_SB, R_SB if unescape_closing_square_brackets && (text.include? R_SB)
1643
1518
  end
1519
+ text
1520
+ end
1644
1521
 
1645
- @subs
1522
+ # Internal: Split text formatted as CSV with support
1523
+ # for double-quoted values (in which commas are ignored)
1524
+ def split_simple_csv str
1525
+ if str.empty?
1526
+ []
1527
+ elsif str.include? '"'
1528
+ values = []
1529
+ accum = ''
1530
+ quote_open = nil
1531
+ str.each_char do |c|
1532
+ case c
1533
+ when ','
1534
+ if quote_open
1535
+ accum += c
1536
+ else
1537
+ values << accum.strip
1538
+ accum = ''
1539
+ end
1540
+ when '"'
1541
+ quote_open = !quote_open
1542
+ else
1543
+ accum += c
1544
+ end
1545
+ end
1546
+ values << accum.strip
1547
+ else
1548
+ str.split(',').map {|it| it.strip }
1549
+ end
1646
1550
  end
1647
1551
  end
1648
1552
  end