asciidoctor 2.0.0.rc.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -101,7 +101,7 @@ class Converter::Html5Converter < Converter::Base
101
101
  result << %(<meta name="application-name" content="#{node.attr 'app-name'}"#{slash}>) if node.attr? 'app-name'
102
102
  result << %(<meta name="description" content="#{node.attr 'description'}"#{slash}>) if node.attr? 'description'
103
103
  result << %(<meta name="keywords" content="#{node.attr 'keywords'}"#{slash}>) if node.attr? 'keywords'
104
- result << %(<meta name="author" content="#{((authors = node.attr 'authors').include? '<') ? (authors.gsub XmlSanitizeRx, '') : authors}"#{slash}>) if node.attr? 'authors'
104
+ result << %(<meta name="author" content="#{((authors = node.sub_replacements node.attr 'authors').include? '<') ? (authors.gsub XmlSanitizeRx, '') : authors}"#{slash}>) if node.attr? 'authors'
105
105
  result << %(<meta name="copyright" content="#{node.attr 'copyright'}"#{slash}>) if node.attr? 'copyright'
106
106
  if node.attr? 'favicon'
107
107
  if (icon_href = node.attr 'favicon').empty?
@@ -184,7 +184,7 @@ class Converter::Html5Converter < Converter::Base
184
184
  details = []
185
185
  idx = 1
186
186
  node.authors.each do |author|
187
- details << %(<span id="author#{idx > 1 ? idx : ''}" class="author">#{author.name}</span>#{br})
187
+ details << %(<span id="author#{idx > 1 ? idx : ''}" class="author">#{node.sub_replacements author.name}</span>#{br})
188
188
  details << %(<span id="email#{idx > 1 ? idx : ''}" class="email">#{node.sub_macros author.email}</span>#{br}) if author.email
189
189
  idx += 1
190
190
  end
@@ -143,7 +143,9 @@ module Extensions
143
143
  else
144
144
  sect.numbered = true if opts.fetch :numbered, (book && (doc.attr? 'partnums'))
145
145
  end
146
- unless (id = attrs.delete 'id') == false
146
+ if (id = attrs['id']) == false
147
+ attrs.delete 'id'
148
+ else
147
149
  sect.id = attrs['id'] = id || ((doc.attr? 'sectids') ? (Section.generate_id sect.title, doc) : nil)
148
150
  end
149
151
  sect.update_attributes attrs
@@ -211,12 +213,25 @@ module Extensions
211
213
  # QUESTION is parse_content the right method name? should we wrap in open block automatically?
212
214
  def parse_content parent, content, attributes = nil
213
215
  reader = Reader === content ? content : (Reader.new content)
214
- while ((block = Parser.next_block reader, parent, (attributes ? attributes.merge : {})) && parent << block) ||
215
- reader.has_more_lines?
216
- end
216
+ Parser.parse_blocks reader, parent, attributes
217
217
  parent
218
218
  end
219
219
 
220
+ # Public: Parses the attrlist String into a Hash of attributes
221
+ #
222
+ # block - the current AbstractBlock or the parent AbstractBlock if there is no current block (used for applying subs)
223
+ # attrlist - the list of attributes as a String
224
+ # opts - an optional Hash of options to control processing:
225
+ # :positional_attributes - an Array of attribute names to map positional arguments to (optional, default: false)
226
+ # :sub_attributes - enables attribute substitution on the attrlist argument (optional, default: false)
227
+ #
228
+ # Returns a Hash of parsed attributes
229
+ def parse_attributes block, attrlist, opts = {}
230
+ return {} if attrlist ? attrlist.empty? : true
231
+ attrlist = block.sub_attributes attrlist if opts[:sub_attributes] && (attrlist.include? ATTR_REF_HEAD)
232
+ (AttributeList.new attrlist).parse (opts[:positional_attributes] || [])
233
+ end
234
+
220
235
  # TODO fill out remaining methods
221
236
  [
222
237
  [:create_paragraph, :create_block, :paragraph],
@@ -294,16 +309,17 @@ module Extensions
294
309
  alias parse_content_as content_model
295
310
 
296
311
  def positional_attributes *value
297
- option :pos_attrs, value.flatten
312
+ option :positional_attrs, value.flatten
298
313
  end
299
314
  alias name_positional_attributes positional_attributes
300
315
  # NOTE positional_attrs alias is deprecated
301
316
  alias positional_attrs positional_attributes
302
317
 
303
- def default_attrs value
318
+ def default_attributes value
304
319
  option :default_attrs, value
305
320
  end
306
- alias default_attributes default_attrs
321
+ # NOTE default_attrs alias is deprecated
322
+ alias default_attr default_attributes
307
323
 
308
324
  def resolve_attributes *args
309
325
  # NOTE assume true as default value; rewrap single-argument string or symbol
@@ -312,7 +328,7 @@ module Extensions
312
328
  end unless args.size > 1
313
329
  case args
314
330
  when true
315
- option :pos_attrs, []
331
+ option :positional_attrs, []
316
332
  option :default_attrs, {}
317
333
  when ::Array
318
334
  names, defaults = [], {}
@@ -333,7 +349,7 @@ module Extensions
333
349
  names << arg
334
350
  end
335
351
  end
336
- option :pos_attrs, names.compact
352
+ option :positional_attrs, names.compact
337
353
  option :default_attrs, defaults
338
354
  when ::Hash
339
355
  names, defaults = [], {}
@@ -345,7 +361,7 @@ module Extensions
345
361
  end
346
362
  defaults[name] = val if val
347
363
  end
348
- option :pos_attrs, names.compact
364
+ option :positional_attrs, names.compact
349
365
  option :default_attrs, defaults
350
366
  else
351
367
  raise ::ArgumentError, %(unsupported attributes specification for macro: #{args.inspect})
@@ -507,7 +523,7 @@ module Extensions
507
523
  # * :named - The name of the block (required: true)
508
524
  # * :contexts - The blocks contexts on which this style can be used (default: [:paragraph, :open]
509
525
  # * :content_model - The structure of the content supported in this block (default: :compound)
510
- # * :pos_attrs - A list of attribute names used to map positional attributes (default: nil)
526
+ # * :positional_attrs - A list of attribute names used to map positional attributes (default: nil)
511
527
  # * :default_attrs - A hash of attribute names and values used to seed the attributes hash (default: nil)
512
528
  # * ...
513
529
  #
@@ -41,6 +41,8 @@ class Parser
41
41
 
42
42
  NoOp = nil
43
43
 
44
+ AuthorKeys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
45
+
44
46
  # Internal: A Hash mapping horizontal alignment abbreviations to alignments
45
47
  # that can be applied to a table cell (or to all cells in a column)
46
48
  TableCellHorzAlignments = {
@@ -631,7 +633,7 @@ class Parser
631
633
  end
632
634
  end
633
635
  if extension.config[:content_model] == :attributes
634
- document.parse_attributes content, extension.config[:pos_attrs] || [], sub_input: true, into: attributes if content
636
+ document.parse_attributes content, extension.config[:positional_attrs] || [], sub_input: true, into: attributes if content
635
637
  else
636
638
  attributes['text'] = content || ''
637
639
  end
@@ -786,40 +788,27 @@ class Parser
786
788
 
787
789
  # either delimited block or styled paragraph
788
790
  unless block
789
- # abstract and partintro should be handled by open block
790
- # FIXME kind of hackish...need to sort out how to generalize this
791
- block_context = :open if block_context == :abstract || block_context == :partintro
792
-
793
791
  case block_context
794
- when :admonition
795
- attributes['name'] = admonition_name = style.downcase
796
- attributes['textlabel'] = (attributes.delete 'caption') || doc_attrs[%(#{admonition_name}-caption)]
797
- block = build_block(block_context, :compound, terminator, parent, reader, attributes)
798
-
799
- when :comment
800
- build_block(block_context, :skip, terminator, parent, reader, attributes)
801
- attributes.clear
802
- return
803
-
804
- when :example
805
- block = build_block(block_context, :compound, terminator, parent, reader, attributes)
806
-
807
- when :listing, :literal
808
- block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
809
-
810
- when :source
811
- AttributeList.rekey attributes, [nil, 'language', 'linenums']
812
- if doc_attrs.key? 'source-language'
813
- attributes['language'] = doc_attrs['source-language']
814
- end unless attributes.key? 'language'
815
- if attributes['linenums-option'] || doc_attrs['source-linenums-option']
816
- attributes['linenums'] = ''
817
- end unless attributes.key? 'linenums'
818
- if doc_attrs.key? 'source-indent'
819
- attributes['indent'] = doc_attrs['source-indent']
820
- end unless attributes.key? 'indent'
792
+ when :listing, :source
793
+ if block_context == :source || (!attributes[1] && (language = attributes[2] || doc_attrs['source-language']))
794
+ if language
795
+ attributes['style'] = 'source'
796
+ attributes['language'] = language
797
+ AttributeList.rekey attributes, [nil, nil, 'linenums']
798
+ else
799
+ AttributeList.rekey attributes, [nil, 'language', 'linenums']
800
+ if doc_attrs.key? 'source-language'
801
+ attributes['language'] = doc_attrs['source-language']
802
+ end unless attributes.key? 'language'
803
+ end
804
+ if attributes['linenums-option'] || doc_attrs['source-linenums-option']
805
+ attributes['linenums'] = ''
806
+ end unless attributes.key? 'linenums'
807
+ if doc_attrs.key? 'source-indent'
808
+ attributes['indent'] = doc_attrs['source-indent']
809
+ end unless attributes.key? 'indent'
810
+ end
821
811
  block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
822
-
823
812
  when :fenced_code
824
813
  attributes['style'] = 'source'
825
814
  if (ll = this_line.length) > 3
@@ -847,17 +836,6 @@ class Parser
847
836
  end unless attributes.key? 'indent'
848
837
  terminator = terminator.slice 0, 3
849
838
  block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
850
-
851
- when :pass
852
- block = build_block(block_context, :raw, terminator, parent, reader, attributes)
853
-
854
- when :stem, :latexmath, :asciimath
855
- attributes['style'] = STEM_TYPE_ALIASES[attributes[2] || doc_attrs['stem']] if block_context == :stem
856
- block = build_block(:stem, :raw, terminator, parent, reader, attributes)
857
-
858
- when :open, :sidebar
859
- block = build_block(block_context, :compound, terminator, parent, reader, attributes)
860
-
861
839
  when :table
862
840
  block_cursor = reader.cursor
863
841
  block_reader = Reader.new reader.read_lines_until(terminator: terminator, skip_line_comments: true, context: :table, cursor: :at_mark), block_cursor
@@ -867,16 +845,35 @@ class Parser
867
845
  attributes['format'] ||= (terminator.start_with? ',') ? 'csv' : 'dsv'
868
846
  end
869
847
  block = parse_table(block_reader, parent, attributes)
870
-
848
+ when :sidebar
849
+ block = build_block(block_context, :compound, terminator, parent, reader, attributes)
850
+ when :admonition
851
+ attributes['name'] = admonition_name = style.downcase
852
+ attributes['textlabel'] = (attributes.delete 'caption') || doc_attrs[%(#{admonition_name}-caption)]
853
+ block = build_block(block_context, :compound, terminator, parent, reader, attributes)
854
+ when :open, :abstract, :partintro
855
+ block = build_block(:open, :compound, terminator, parent, reader, attributes)
856
+ when :literal
857
+ block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
858
+ when :example
859
+ block = build_block(block_context, :compound, terminator, parent, reader, attributes)
871
860
  when :quote, :verse
872
861
  AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
873
862
  block = build_block(block_context, (block_context == :verse ? :verbatim : :compound), terminator, parent, reader, attributes)
874
-
863
+ when :stem, :latexmath, :asciimath
864
+ attributes['style'] = STEM_TYPE_ALIASES[attributes[2] || doc_attrs['stem']] if block_context == :stem
865
+ block = build_block(:stem, :raw, terminator, parent, reader, attributes)
866
+ when :pass
867
+ block = build_block(block_context, :raw, terminator, parent, reader, attributes)
868
+ when :comment
869
+ build_block(block_context, :skip, terminator, parent, reader, attributes)
870
+ attributes.clear
871
+ return
875
872
  else
876
- if block_extensions && (extension = extensions.registered_for_block?(block_context, cloaked_context))
873
+ if block_extensions && (extension = extensions.registered_for_block? block_context, cloaked_context)
877
874
  unless (content_model = extension.config[:content_model]) == :skip
878
- unless (pos_attrs = extension.config[:pos_attrs] || []).empty?
879
- AttributeList.rekey(attributes, [nil] + pos_attrs)
875
+ unless (positional_attrs = extension.config[:positional_attrs] || []).empty?
876
+ AttributeList.rekey(attributes, [nil] + positional_attrs)
880
877
  end
881
878
  if (default_attrs = extension.config[:default_attrs])
882
879
  default_attrs.each {|k, v| attributes[k] ||= v }
@@ -884,8 +881,7 @@ class Parser
884
881
  # QUESTION should we clone the extension for each cloaked context and set in config?
885
882
  attributes['cloaked-context'] = cloaked_context
886
883
  end
887
- block = build_block block_context, content_model, terminator, parent, reader, attributes, extension: extension
888
- unless block
884
+ unless (block = build_block block_context, content_model, terminator, parent, reader, attributes, extension: extension)
889
885
  attributes.clear
890
886
  return
891
887
  end
@@ -909,7 +905,7 @@ class Parser
909
905
  end
910
906
  end
911
907
  # FIXME remove the need for this update!
912
- block.attributes.update(attributes) unless attributes.empty?
908
+ block.update_attributes attributes unless attributes.empty?
913
909
  block.commit_subs
914
910
 
915
911
  #if doc_attrs.key? :pending_attribute_entries
@@ -1072,9 +1068,13 @@ class Parser
1072
1068
  # parent - The parent Block to which to attach the parsed blocks
1073
1069
  #
1074
1070
  # Returns nothing.
1075
- def self.parse_blocks(reader, parent)
1076
- while ((block = next_block reader, parent) && parent.blocks << block) || reader.has_more_lines?
1071
+ def self.parse_blocks(reader, parent, attributes = nil)
1072
+ if attributes
1073
+ while ((block = next_block reader, parent, attributes.merge) && parent.blocks << block) || reader.has_more_lines?; end
1074
+ else
1075
+ while ((block = next_block reader, parent) && parent.blocks << block) || reader.has_more_lines?; end
1077
1076
  end
1077
+ nil
1078
1078
  end
1079
1079
 
1080
1080
  # Internal: Parse and construct an ordered or unordered list at the current position of the Reader
@@ -1127,8 +1127,7 @@ class Parser
1127
1127
  # doc - The document to which the node belongs; computed from node if not specified
1128
1128
  #
1129
1129
  # Returns nothing
1130
- def self.catalog_inline_anchor id, reftext, node, location, doc = nil
1131
- doc = node.document unless doc
1130
+ def self.catalog_inline_anchor id, reftext, node, location, doc = node.document
1132
1131
  reftext = doc.sub_attributes reftext if reftext && (reftext.include? ATTR_REF_HEAD)
1133
1132
  unless doc.register :refs, [id, (Inline.new node, :anchor, reftext, type: :ref, id: id)]
1134
1133
  location = location.cursor if Reader === location
@@ -1318,6 +1317,9 @@ class Parser
1318
1317
  end
1319
1318
  else # :colist
1320
1319
  list_item.marker = sibling_trait
1320
+ if item_text.start_with?('[[') && LeadingInlineAnchorRx =~ item_text
1321
+ catalog_inline_anchor $1, $2, list_item, reader
1322
+ end
1321
1323
  end
1322
1324
  end
1323
1325
 
@@ -1789,9 +1791,8 @@ class Parser
1789
1791
  if document
1790
1792
  # apply header subs and assign to document
1791
1793
  author_metadata.each do |key, val|
1792
- unless doc_attrs.key? key
1793
- doc_attrs[key] = ::String === val ? (document.apply_header_subs val) : val
1794
- end
1794
+ # NOTE the attributes substitution only applies for the email record
1795
+ doc_attrs[key] = ::String === val ? (document.apply_header_subs val) : val unless doc_attrs.key? key
1795
1796
  end
1796
1797
 
1797
1798
  implicit_author = doc_attrs['author']
@@ -1915,20 +1916,13 @@ class Parser
1915
1916
  def self.process_authors author_line, names_only = false, multiple = true
1916
1917
  author_metadata = {}
1917
1918
  author_idx = 0
1918
- keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
1919
- author_entries = multiple ? (author_line.split ';').map {|it| it.strip } : [*author_line]
1920
- author_entries.each do |author_entry|
1919
+ (multiple && (author_line.include? ';') ? (author_line.split AuthorDelimiterRx) : [*author_line]).each do |author_entry|
1921
1920
  next if author_entry.empty?
1922
- author_idx += 1
1923
1921
  key_map = {}
1924
- if author_idx == 1
1925
- keys.each do |key|
1926
- key_map[key.to_sym] = key
1927
- end
1922
+ if (author_idx += 1) == 1
1923
+ AuthorKeys.each {|key| key_map[key.to_sym] = key }
1928
1924
  else
1929
- keys.each do |key|
1930
- key_map[key.to_sym] = %(#{key}_#{author_idx})
1931
- end
1925
+ AuthorKeys.each {|key| key_map[key.to_sym] = %(#{key}_#{author_idx}) }
1932
1926
  end
1933
1927
 
1934
1928
  if names_only # when parsing an attribute value
@@ -1972,9 +1966,7 @@ class Parser
1972
1966
  else
1973
1967
  # only assign the _1 attributes once we see the second author
1974
1968
  if author_idx == 2
1975
- keys.each do |key|
1976
- author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.key? key
1977
- end
1969
+ AuthorKeys.each {|key| author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.key? key }
1978
1970
  end
1979
1971
  author_metadata['authors'] = %(#{author_metadata['authors']}, #{author_metadata[key_map[:author]]})
1980
1972
  end
@@ -80,12 +80,11 @@ module Substitutors
80
80
  end
81
81
 
82
82
  if subs.include? :macros
83
- text, passthrus = extract_passthroughs text
84
- if passthrus.empty?
85
- passthrus = nil
86
- else
83
+ text = extract_passthroughs text
84
+ unless @passthroughs.empty?
85
+ passthrus = @passthroughs
87
86
  # NOTE placeholders can move around, so we can only clear in the outermost substitution call
88
- @passthroughs_locked ||= (reset_passthrus = true)
87
+ @passthroughs_locked ||= (clear_passthrus = true)
89
88
  end
90
89
  end
91
90
 
@@ -114,7 +113,7 @@ module Substitutors
114
113
 
115
114
  if passthrus
116
115
  text = restore_passthroughs text
117
- if reset_passthrus
116
+ if clear_passthrus
118
117
  passthrus.clear
119
118
  @passthroughs_locked = nil
120
119
  end
@@ -134,11 +133,20 @@ module Substitutors
134
133
  apply_subs text, NORMAL_SUBS
135
134
  end
136
135
 
136
+ # Public: Apply substitutions for header metadata and attribute assignments
137
+ #
138
+ # text - String containing the text process
139
+ #
140
+ # Returns A String with header substitutions performed
141
+ def apply_header_subs text
142
+ apply_subs text, HEADER_SUBS
143
+ end
144
+
137
145
  # Public: Apply substitutions for titles.
138
146
  #
139
147
  # title - The String title to process
140
148
  #
141
- # returns - A String with title substitutions performed
149
+ # Returns A String with title substitutions performed
142
150
  alias apply_title_subs apply_subs
143
151
 
144
152
  # Public: Apply substitutions for reftext.
@@ -150,223 +158,13 @@ module Substitutors
150
158
  apply_subs text, REFTEXT_SUBS
151
159
  end
152
160
 
153
- # Public: Apply substitutions for header metadata and attribute assignments
154
- #
155
- # text - String containing the text process
156
- #
157
- # returns - A String with header substitutions performed
158
- def apply_header_subs(text)
159
- apply_subs text, HEADER_SUBS
160
- end
161
-
162
- # Internal: Extract the passthrough text from the document for reinsertion after processing.
163
- #
164
- # text - The String from which to extract passthrough fragements
165
- #
166
- # returns - A tuple of the String text with passthrough regions substituted with placeholders and the passthroughs Hash
167
- def extract_passthroughs(text)
168
- compat_mode = @document.compat_mode
169
- passthrus = @passthroughs
170
- text = text.gsub InlinePassMacroRx do
171
- preceding = ''
172
-
173
- if (boundary = $4) # $$, ++, or +++
174
- # skip ++ in compat mode, handled as normal quoted text
175
- if compat_mode && boundary == '++'
176
- content, _ = extract_passthroughs $5
177
- next $2 ? %(#{$1}[#{$2}]#{$3}++#{content}++) : %(#{$1}#{$3}++#{content}++)
178
- end
179
-
180
- attributes = $2
181
- escape_count = $3.length
182
- content = $5
183
- old_behavior = false
184
-
185
- if attributes
186
- if escape_count > 0
187
- # NOTE we don't look for nested unconstrained pass macros
188
- next %(#{$1}[#{attributes}]#{RS * (escape_count - 1)}#{boundary}#{$5}#{boundary})
189
- elsif $1 == RS
190
- preceding = %([#{attributes}])
191
- attributes = nil
192
- else
193
- if boundary == '++' && (attributes.end_with? 'x-')
194
- old_behavior = true
195
- attributes = attributes.slice 0, attributes.length - 2
196
- end
197
- attributes = parse_quoted_text_attributes attributes
198
- end
199
- elsif escape_count > 0
200
- # NOTE we don't look for nested unconstrained pass macros
201
- next %(#{RS * (escape_count - 1)}#{boundary}#{$5}#{boundary})
202
- end
203
- subs = (boundary == '+++' ? [] : BASIC_SUBS)
204
-
205
- if attributes
206
- if old_behavior
207
- passthrus[passthru_key = passthrus.size] = { text: content, subs: NORMAL_SUBS, type: :monospaced, attributes: attributes }
208
- else
209
- passthrus[passthru_key = passthrus.size] = { text: content, subs: subs, type: :unquoted, attributes: attributes }
210
- end
211
- else
212
- passthrus[passthru_key = passthrus.size] = { text: content, subs: subs }
213
- end
214
- else # pass:[]
215
- # NOTE we don't look for nested pass:[] macros
216
- # honor the escape
217
- next $&.slice 1, $&.length if $6 == RS
218
- passthrus[passthru_key = passthrus.size] = { text: (unescape_brackets $8), subs: ($7 ? (resolve_pass_subs $7) : nil) }
219
- end
220
-
221
- %(#{preceding}#{PASS_START}#{passthru_key}#{PASS_END})
222
- end if (text.include? '++') || (text.include? '$$') || (text.include? 'ss:')
223
-
224
- pass_inline_char1, pass_inline_char2, pass_inline_rx = InlinePassRx[compat_mode]
225
- text = text.gsub pass_inline_rx do
226
- preceding = $1
227
- attributes = $2
228
- escape_mark = RS if (quoted_text = $3).start_with? RS
229
- format_mark = $4
230
- content = $5
231
-
232
- if compat_mode
233
- old_behavior = true
234
- else
235
- if (old_behavior = attributes && (attributes.end_with? 'x-'))
236
- attributes = attributes.slice 0, attributes.length - 2
237
- end
238
- end
239
-
240
- if attributes
241
- if format_mark == '`' && !old_behavior
242
- next extract_inner_passthrough content, %(#{preceding}[#{attributes}]#{escape_mark}), attributes
243
- elsif escape_mark
244
- # honor the escape of the formatting mark
245
- next %(#{preceding}[#{attributes}]#{quoted_text.slice 1, quoted_text.length})
246
- elsif preceding == RS
247
- # honor the escape of the attributes
248
- preceding = %([#{attributes}])
249
- attributes = nil
250
- else
251
- attributes = parse_quoted_text_attributes attributes
252
- end
253
- elsif format_mark == '`' && !old_behavior
254
- next extract_inner_passthrough content, %(#{preceding}#{escape_mark})
255
- elsif escape_mark
256
- # honor the escape of the formatting mark
257
- next %(#{preceding}#{quoted_text.slice 1, quoted_text.length})
258
- end
259
-
260
- if compat_mode
261
- passthrus[passthru_key = passthrus.size] = { text: content, subs: BASIC_SUBS, attributes: attributes, type: :monospaced }
262
- elsif attributes
263
- if old_behavior
264
- subs = (format_mark == '`' ? BASIC_SUBS : NORMAL_SUBS)
265
- passthrus[passthru_key = passthrus.size] = { text: content, subs: subs, attributes: attributes, type: :monospaced }
266
- else
267
- passthrus[passthru_key = passthrus.size] = { text: content, subs: BASIC_SUBS, attributes: attributes, type: :unquoted }
268
- end
269
- else
270
- passthrus[passthru_key = passthrus.size] = { text: content, subs: BASIC_SUBS }
271
- end
272
-
273
- %(#{preceding}#{PASS_START}#{passthru_key}#{PASS_END})
274
- end if (text.include? pass_inline_char1) || (pass_inline_char2 && (text.include? pass_inline_char2))
275
-
276
- # NOTE we need to do the stem in a subsequent step to allow it to be escaped by the former
277
- text = text.gsub InlineStemMacroRx do
278
- # honor the escape
279
- next $&.slice 1, $&.length if $&.start_with? RS
280
-
281
- if (type = $1.to_sym) == :stem
282
- type = STEM_TYPE_ALIASES[@document.attributes['stem']].to_sym
283
- end
284
- content = unescape_brackets $3
285
- # NOTE for backwards compatibility with AsciiDoc Python, drop enclosing $ signs around latexmath
286
- content = content.slice 1, content.length - 2 if type == :latexmath && (content.start_with? '$') && (content.end_with? '$')
287
- subs = $2 ? (resolve_pass_subs $2) : ((@document.basebackend? 'html') ? BASIC_SUBS : nil)
288
- passthrus[passthru_key = passthrus.size] = { text: content, subs: subs, type: type }
289
- %(#{PASS_START}#{passthru_key}#{PASS_END})
290
- end if (text.include? ':') && ((text.include? 'stem:') || (text.include? 'math:'))
291
-
292
- [text, passthrus]
293
- end
294
-
295
- # Internal: Extract nested single-plus passthrough; otherwise return unprocessed
296
- def extract_inner_passthrough text, pre, attributes = nil
297
- if (text.end_with? '+') && (text.start_with? '+', '\+') && SinglePlusInlinePassRx =~ text
298
- if $1
299
- %(#{pre}`+#{$2}+`)
300
- else
301
- @passthroughs[passthru_key = @passthroughs.size] = attributes ?
302
- { text: $2, subs: BASIC_SUBS, attributes: attributes, type: :unquoted } :
303
- { text: $2, subs: BASIC_SUBS }
304
- %(#{pre}`#{PASS_START}#{passthru_key}#{PASS_END}`)
305
- end
306
- else
307
- %(#{pre}`#{text}`)
308
- end
309
- end
310
-
311
- # Internal: Restore the passthrough text by reinserting into the placeholder positions
312
- #
313
- # text - The String text into which to restore the passthrough text
314
- #
315
- # returns The String text with the passthrough text restored
316
- def restore_passthroughs text
317
- passthrus = @passthroughs
318
- text.gsub PassSlotRx do
319
- if (pass = passthrus[$1.to_i])
320
- subbed_text = apply_subs(pass[:text], pass[:subs])
321
- if (type = pass[:type])
322
- if (attributes = pass[:attributes])
323
- id = attributes.delete 'id'
324
- end
325
- subbed_text = Inline.new(self, :quoted, subbed_text, type: type, id: id, attributes: attributes).convert
326
- end
327
- subbed_text.include?(PASS_START) ? restore_passthroughs(subbed_text) : subbed_text
328
- else
329
- logger.error %(unresolved passthrough detected: #{text})
330
- '??pass??'
331
- end
332
- end
333
- end
334
-
335
- # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc.)
336
- #
337
- # text - The String text to process
338
- #
339
- # returns The converted [String] text
340
- def sub_quotes text
341
- if QuotedTextSniffRx[compat = @document.compat_mode].match? text
342
- QUOTE_SUBS[compat].each do |type, scope, pattern|
343
- text = text.gsub(pattern) { convert_quoted_text $~, type, scope }
344
- end
345
- end
346
- text
347
- end
348
-
349
- # Public: Substitute replacement characters (e.g., copyright, trademark, etc.)
350
- #
351
- # text - The String text to process
352
- #
353
- # returns The [String] text with the replacement characters substituted
354
- def sub_replacements text
355
- if ReplaceableTextRx.match? text
356
- REPLACEMENTS.each do |pattern, replacement, restore|
357
- text = text.gsub(pattern) { do_replacement $~, replacement, restore }
358
- end
359
- end
360
- text
361
- end
362
-
363
161
  # Public: Substitute special characters (i.e., encode XML)
364
162
  #
365
163
  # The special characters <, &, and > get replaced with &lt;, &amp;, and &gt;, respectively.
366
164
  #
367
165
  # text - The String text to process.
368
166
  #
369
- # returns The String text with special characters replaced.
167
+ # Returns The String text with special characters replaced.
370
168
  if RUBY_ENGINE == 'opal'
371
169
  def sub_specialchars text
372
170
  (text.include? ?>) || (text.include? ?&) || (text.include? ?<) ? (text.gsub SpecialCharsRx, SpecialCharsTr) : text
@@ -383,23 +181,18 @@ module Substitutors
383
181
  end
384
182
  alias sub_specialcharacters sub_specialchars
385
183
 
386
- # Internal: Substitute replacement text for matched location
184
+ # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc.)
387
185
  #
388
- # returns The String text with the replacement characters substituted
389
- def do_replacement m, replacement, restore
390
- if (captured = m[0]).include? RS
391
- # we have to use sub since we aren't sure it's the first char
392
- captured.sub RS, ''
393
- else
394
- case restore
395
- when :none
396
- replacement
397
- when :bounding
398
- m[1] + replacement + m[2]
399
- else # :leading
400
- 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 }
401
193
  end
402
194
  end
195
+ text
403
196
  end
404
197
 
405
198
  # Public: Substitutes attribute references in the specified text
@@ -411,7 +204,8 @@ module Substitutors
411
204
  #
412
205
  # text - The String text to process
413
206
  # opts - A Hash of options to control processing: (default: {})
414
- # * :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)
415
209
  #
416
210
  # Returns the [String] text with the attribute references replaced with resolved values
417
211
  def sub_attributes text, opts = {}
@@ -480,6 +274,18 @@ module Substitutors
480
274
  end
481
275
  end
482
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
+
483
289
  # Public: Substitute inline macros (e.g., links, images, etc)
484
290
  #
485
291
  # Replace inline macros, which may span multiple lines, in the provided text
@@ -487,7 +293,7 @@ module Substitutors
487
293
  # source - The String text to process
488
294
  #
489
295
  # returns The converted String text
490
- def sub_macros(text)
296
+ def sub_macros text
491
297
  #return text if text.nil_or_empty?
492
298
  # some look ahead assertions to cut unnecessary regex calls
493
299
  found_square_bracket = text.include? '['
@@ -513,10 +319,10 @@ module Substitutors
513
319
  if content.nil_or_empty?
514
320
  attributes['text'] = content if content && extconf[:content_model] != :attributes
515
321
  else
516
- content = unescape_bracketed_text content
322
+ content = normalize_text content, true, true
517
323
  # QUESTION should we store the unparsed attrlist in the attrlist key?
518
324
  if extconf[:content_model] == :attributes
519
- parse_attributes content, extconf[:pos_attrs] || [], into: attributes
325
+ parse_attributes content, extconf[:positional_attrs] || [], into: attributes
520
326
  else
521
327
  attributes['text'] = content
522
328
  end
@@ -563,7 +369,7 @@ module Substitutors
563
369
  end
564
370
  (Inline.new self, :kbd, nil, attributes: { 'keys' => keys }).convert
565
371
  else # $2 == 'btn'
566
- (Inline.new self, :button, (unescape_bracketed_text $3)).convert
372
+ (Inline.new self, :button, (normalize_text $3, true, true)).convert
567
373
  end
568
374
  end
569
375
  end
@@ -613,10 +419,7 @@ module Substitutors
613
419
  else
614
420
  type, posattrs = 'image', ['alt', 'width', 'height']
615
421
  end
616
- if (target = $1).include? ATTR_REF_HEAD
617
- # TODO remove this special case once titles use normal substitution order
618
- target = sub_attributes target
619
- end
422
+ target = $1
620
423
  attrs = parse_attributes $2, posattrs, unescape_input: true
621
424
  doc.register :images, [target, (attrs['imagesdir'] = doc_attrs['imagesdir'])] unless type == 'icon'
622
425
  attrs['alt'] ||= (attrs['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
@@ -636,17 +439,39 @@ module Substitutors
636
439
  next $&.slice 1, $&.length if $&.start_with? RS
637
440
 
638
441
  # indexterm:[Tigers,Big cats]
639
- terms = split_simple_csv normalize_string $2, true
442
+ if (attrlist = normalize_text $2, true, true).include? '='
443
+ if (primary = (attrs = (AttributeList.new attrlist, self).parse)[1])
444
+ attrs['terms'] = terms = [primary]
445
+ if (secondary = attrs[2])
446
+ terms << secondary
447
+ if (tertiary = attrs[3])
448
+ terms << tertiary
449
+ end
450
+ end
451
+ if (see_also = attrs['see-also'])
452
+ attrs['see-also'] = (see_also.include? ',') ? (see_also.split ',').map {|it| it.lstrip } : [see_also]
453
+ end
454
+ else
455
+ attrs = { 'terms' => (terms = attrlist) }
456
+ end
457
+ else
458
+ attrs = { 'terms' => (terms = split_simple_csv attrlist) }
459
+ end
640
460
  #doc.register :indexterms, terms
641
- (Inline.new self, :indexterm, nil, attributes: { 'terms' => terms }).convert
461
+ (Inline.new self, :indexterm, nil, attributes: attrs).convert
642
462
  when 'indexterm2'
643
463
  # honor the escape
644
464
  next $&.slice 1, $&.length if $&.start_with? RS
645
465
 
646
466
  # indexterm2:[Tigers]
647
- term = normalize_string $2, true
467
+ if (term = normalize_text $2, true, true).include? '='
468
+ term = (attrs = (AttributeList.new term, self).parse)[1] || (attrs = nil) || term
469
+ if attrs && (see_also = attrs['see-also'])
470
+ attrs['see-also'] = (see_also.include? ',') ? (see_also.split ',').map {|it| it.lstrip } : [see_also]
471
+ end
472
+ end
648
473
  #doc.register :indexterms, [term]
649
- (Inline.new self, :indexterm, term, type: :visible).convert
474
+ (Inline.new self, :indexterm, term, attributes: attrs, type: :visible).convert
650
475
  else
651
476
  text = $3
652
477
  # honor the escape
@@ -672,14 +497,32 @@ module Substitutors
672
497
  end
673
498
  if visible
674
499
  # ((Tigers))
675
- term = normalize_string text
500
+ if (term = normalize_text text, true).include? ';&'
501
+ if term.include? ' &gt;&gt; '
502
+ term, _, see = term.partition ' &gt;&gt; '
503
+ attrs = { 'see' => see }
504
+ elsif term.include? ' &amp;&gt; '
505
+ term, *see_also = term.split ' &amp;&gt; '
506
+ attrs = { 'see-also' => see_also }
507
+ end
508
+ end
676
509
  #doc.register :indexterms, [term]
677
- subbed_term = (Inline.new self, :indexterm, term, type: :visible).convert
510
+ subbed_term = (Inline.new self, :indexterm, term, attributes: attrs, type: :visible).convert
678
511
  else
679
512
  # (((Tigers,Big cats)))
680
- terms = split_simple_csv(normalize_string text)
513
+ attrs = {}
514
+ if (terms = normalize_text text, true).include? ';&'
515
+ if terms.include? ' &gt;&gt; '
516
+ terms, _, see = terms.partition ' &gt;&gt; '
517
+ attrs['see'] = see
518
+ elsif terms.include? ' &amp;&gt; '
519
+ terms, *see_also = terms.split ' &amp;&gt; '
520
+ attrs['see-also'] = see_also
521
+ end
522
+ end
523
+ attrs['terms'] = terms = split_simple_csv terms
681
524
  #doc.register :indexterms, terms
682
- subbed_term = (Inline.new self, :indexterm, nil, attributes: { 'terms' => terms }).convert
525
+ subbed_term = (Inline.new self, :indexterm, nil, attributes: attrs).convert
683
526
  end
684
527
  before ? %(#{before}#{subbed_term}#{after}) : subbed_term
685
528
  end
@@ -745,18 +588,9 @@ module Substitutors
745
588
  text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
746
589
  if !doc.compat_mode && (text.include? '=')
747
590
  text = (attrs = (AttributeList.new text, self).parse)[1] || ''
748
- link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
591
+ link_opts[:id] = attrs['id']
749
592
  end
750
593
 
751
- # TODO enable in Asciidoctor 1.6.x
752
- # support pipe-separated text and title
753
- #unless attrs && (attrs.key? 'title')
754
- # if text.include? '|'
755
- # attrs ||= {}
756
- # text, _, attrs['title'] = text.partition '|'
757
- # end
758
- #end
759
-
760
594
  if text.end_with? '^'
761
595
  text = text.chop
762
596
  if attrs
@@ -800,7 +634,7 @@ module Substitutors
800
634
  if mailto
801
635
  if !doc.compat_mode && (text.include? ',')
802
636
  text = (attrs = (AttributeList.new text, self).parse)[1] || ''
803
- link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
637
+ link_opts[:id] = attrs['id']
804
638
  if attrs.key? 2
805
639
  if attrs.key? 3
806
640
  target = %(#{target}?subject=#{Helpers.encode_uri_component attrs[2]}&amp;body=#{Helpers.encode_uri_component attrs[3]})
@@ -811,18 +645,9 @@ module Substitutors
811
645
  end
812
646
  elsif !doc.compat_mode && (text.include? '=')
813
647
  text = (attrs = (AttributeList.new text, self).parse)[1] || ''
814
- link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
648
+ link_opts[:id] = attrs['id']
815
649
  end
816
650
 
817
- # TODO enable in Asciidoctor 1.6.x
818
- # support pipe-separated text and title
819
- #unless attrs && (attrs.key? 'title')
820
- # if text.include? '|'
821
- # attrs ||= {}
822
- # text, _, attrs['title'] = text.partition '|'
823
- # end
824
- #end
825
-
826
651
  if text.end_with? '^'
827
652
  text = text.chop
828
653
  if attrs
@@ -1009,7 +834,7 @@ module Substitutors
1009
834
 
1010
835
  if id
1011
836
  if text
1012
- text = restore_passthroughs(normalize_string text, true)
837
+ text = restore_passthroughs(normalize_text text, true, true)
1013
838
  index = doc.counter('footnote-number')
1014
839
  doc.register(:footnotes, Document::Footnote.new(index, id, text))
1015
840
  type, target = :ref, nil
@@ -1023,7 +848,7 @@ module Substitutors
1023
848
  type, target, id = :xref, id, nil
1024
849
  end
1025
850
  elsif text
1026
- text = restore_passthroughs(normalize_string text, true)
851
+ text = restore_passthroughs(normalize_text text, true, true)
1027
852
  index = doc.counter('footnote-number')
1028
853
  doc.register(:footnotes, Document::Footnote.new(index, id, text))
1029
854
  type = target = nil
@@ -1037,31 +862,12 @@ module Substitutors
1037
862
  text
1038
863
  end
1039
864
 
1040
- # Public: Substitute callout source references
865
+ # Public: Substitute post replacements
1041
866
  #
1042
867
  # text - The String text to process
1043
868
  #
1044
869
  # Returns the converted String text
1045
- def sub_callouts(text)
1046
- callout_rx = (attr? 'line-comment') ? CalloutSourceRxMap[attr 'line-comment'] : CalloutSourceRx
1047
- autonum = 0
1048
- text.gsub callout_rx do
1049
- # honor the escape
1050
- if $2
1051
- # use sub since it might be behind a line comment
1052
- $&.sub(RS, '')
1053
- else
1054
- Inline.new(self, :callout, $4 == '.' ? (autonum += 1).to_s : $4, id: @document.callouts.read_next_id, attributes: { 'guard' => $1 }).convert
1055
- end
1056
- end
1057
- end
1058
-
1059
- # Public: Substitute post replacements
1060
- #
1061
- # text - The String text to process
1062
- #
1063
- # Returns the converted String text
1064
- def sub_post_replacements(text)
870
+ def sub_post_replacements text
1065
871
  #if attr? 'hardbreaks-option', nil, true
1066
872
  if @attributes['hardbreaks-option'] || @document.attributes['hardbreaks-option']
1067
873
  lines = text.split LF, -1
@@ -1077,196 +883,271 @@ module Substitutors
1077
883
  end
1078
884
  end
1079
885
 
1080
- # Internal: Convert a quoted text region
886
+ # Public: Apply verbatim substitutions on source (for use when highlighting is disabled).
1081
887
  #
1082
- # match - The MatchData for the quoted text region
1083
- # type - The quoting type (single, double, strong, emphasis, monospaced, etc)
1084
- # scope - The scope of the quoting (constrained or unconstrained)
888
+ # source - the source code String on which to apply verbatim substitutions
889
+ # process_callouts - a Boolean flag indicating whether callout marks should be substituted
1085
890
  #
1086
- # Returns The converted String text for the quoted text region
1087
- def convert_quoted_text(match, type, scope)
1088
- if match[0].start_with? RS
1089
- if scope == :constrained && (attrs = match[2])
1090
- unescaped_attrs = %([#{attrs}])
1091
- else
1092
- return match[0].slice 1, match[0].length
1093
- end
1094
- end
891
+ # Returns the substituted source
892
+ def sub_source source, process_callouts
893
+ process_callouts ? sub_callouts(sub_specialchars source) : (sub_specialchars source)
894
+ end
1095
895
 
1096
- if scope == :constrained
1097
- if unescaped_attrs
1098
- %(#{unescaped_attrs}#{Inline.new(self, :quoted, match[3], type: type).convert})
896
+ # Public: Substitute callout source references
897
+ #
898
+ # text - The String text to process
899
+ #
900
+ # Returns the converted String text
901
+ def sub_callouts text
902
+ callout_rx = (attr? 'line-comment') ? CalloutSourceRxMap[attr 'line-comment'] : CalloutSourceRx
903
+ autonum = 0
904
+ text.gsub callout_rx do
905
+ # honor the escape
906
+ if $2
907
+ # use sub since it might be behind a line comment
908
+ $&.sub(RS, '')
1099
909
  else
1100
- if (attrlist = match[2])
1101
- id = (attributes = parse_quoted_text_attributes attrlist).delete 'id'
1102
- type = :unquoted if type == :mark
1103
- end
1104
- %(#{match[1]}#{Inline.new(self, :quoted, match[3], type: type, id: id, attributes: attributes).convert})
1105
- end
1106
- else
1107
- if (attrlist = match[1])
1108
- id = (attributes = parse_quoted_text_attributes attrlist).delete 'id'
1109
- type = :unquoted if type == :mark
910
+ Inline.new(self, :callout, $4 == '.' ? (autonum += 1).to_s : $4, id: @document.callouts.read_next_id, attributes: { 'guard' => $1 }).convert
1110
911
  end
1111
- Inline.new(self, :quoted, match[2], type: type, id: id, attributes: attributes).convert
1112
912
  end
1113
913
  end
1114
914
 
1115
- # Internal: Parse the attributes that are defined on quoted (aka formatted) text
915
+ # Public: Highlight (i.e., colorize) the source code during conversion using a syntax highlighter, if activated by the
916
+ # source-highlighter document attribute. Otherwise return the text with verbatim substitutions applied.
1116
917
  #
1117
- # str - A non-nil String of unprocessed attributes;
1118
- # space-separated roles (e.g., role1 role2) or the id/role shorthand syntax (e.g., #idname.role)
918
+ # If the process_callouts argument is true, this method will extract the callout marks from the source before passing
919
+ # it to the syntax highlighter, then subsequently restore those callout marks to the highlighted source so the callout
920
+ # marks don't confuse the syntax highlighter.
1119
921
  #
1120
- # Returns a Hash of attributes (role and id only)
1121
- def parse_quoted_text_attributes str
1122
- return {} if (str = str.rstrip).empty?
1123
- # NOTE attributes are typically resolved after quoted text, so substitute eagerly
1124
- str = sub_attributes str if str.include? ATTR_REF_HEAD
1125
- # for compliance, only consider first positional attribute (very unlikely)
1126
- str = str.slice 0, (str.index ',') if str.include? ','
922
+ # source - the source code String to syntax highlight
923
+ # process_callouts - a Boolean flag indicating whether callout marks should be located and substituted
924
+ #
925
+ # Returns the highlighted source code, if a syntax highlighter is defined on the document, otherwise the source with
926
+ # verbatim substituions applied
927
+ def highlight_source source, process_callouts
928
+ # NOTE the call to highlight? is a defensive check since, normally, we wouldn't arrive here unless it returns true
929
+ return sub_source source, process_callouts unless (syntax_hl = @document.syntax_highlighter) && syntax_hl.highlight?
1127
930
 
1128
- if (str.start_with? '.', '#') && Compliance.shorthand_property_syntax
1129
- segments = str.split '#', 2
931
+ source, callout_marks = extract_callouts source if process_callouts
1130
932
 
1131
- if segments.size > 1
1132
- id, *more_roles = segments[1].split('.')
1133
- else
1134
- more_roles = []
1135
- end
933
+ doc_attrs = @document.attributes
934
+ syntax_hl_name = syntax_hl.name
935
+ if (linenums_mode = (attr? 'linenums') ? (doc_attrs[%(#{syntax_hl_name}-linenums-mode)] || :table).to_sym : nil)
936
+ start_line_number = 1 if (start_line_number = (attr 'start', 1).to_i) < 1
937
+ end
938
+ highlight_lines = resolve_lines_to_highlight source, (attr 'highlight') if attr? 'highlight'
1136
939
 
1137
- roles = segments[0].empty? ? [] : segments[0].split('.')
1138
- if roles.size > 1
1139
- roles.shift
1140
- end
940
+ highlighted, source_offset = syntax_hl.highlight self, source, (attr 'language'),
941
+ callouts: callout_marks,
942
+ css_mode: (doc_attrs[%(#{syntax_hl_name}-css)] || :class).to_sym,
943
+ highlight_lines: highlight_lines,
944
+ number_lines: linenums_mode,
945
+ start_line_number: start_line_number,
946
+ style: doc_attrs[%(#{syntax_hl_name}-style)]
1141
947
 
1142
- if more_roles.size > 0
1143
- roles.concat more_roles
1144
- end
948
+ # fix passthrough placeholders that got caught up in syntax highlighting
949
+ highlighted = highlighted.gsub HighlightedPassSlotRx, %(#{PASS_START}\\1#{PASS_END}) unless @passthroughs.empty?
1145
950
 
1146
- attrs = {}
1147
- attrs['id'] = id if id
1148
- attrs['role'] = roles.join ' ' unless roles.empty?
1149
- attrs
1150
- else
1151
- { 'role' => str }
1152
- end
951
+ # NOTE highlight method may have depleted callouts
952
+ callout_marks.nil_or_empty? ? highlighted : (restore_callouts highlighted, callout_marks, source_offset)
1153
953
  end
1154
954
 
1155
- # Internal: Parse attributes in name or name=value format from a comma-separated String
955
+ # Public: Resolve the line numbers in the specified source to highlight from the provided spec.
1156
956
  #
1157
- # attrlist - A comma-separated String list of attributes in name or name=value format.
1158
- # posattrs - An Array of positional attribute names (default: []).
1159
- # opts - A Hash of options to control how the string is parsed (default: {}):
1160
- # :into - The Hash to parse the attributes into (optional, default: false).
1161
- # :sub_input - A Boolean that indicates whether to substitute attributes prior to
1162
- # parsing (optional, default: false).
1163
- # :sub_result - A Boolean that indicates whether to apply substitutions
1164
- # single-quoted attribute values (optional, default: true).
1165
- # :unescape_input - A Boolean that indicates whether to unescape square brackets prior
1166
- # to parsing (optional, default: false).
957
+ # e.g., highlight="1-5, !2, 10" or highlight=1-5;!2,10
1167
958
  #
1168
- # Returns an empty Hash if attrlist is nil or empty, otherwise a Hash of parsed attributes.
1169
- def parse_attributes attrlist, posattrs = [], opts = {}
1170
- return {} unless attrlist && !attrlist.empty?
1171
- attrlist = unescape_bracketed_text attrlist if opts[:unescape_input]
1172
- attrlist = @document.sub_attributes attrlist if opts[:sub_input] && (attrlist.include? ATTR_REF_HEAD)
1173
- # substitutions are only performed on attribute values if block is not nil
1174
- block = self if opts[:sub_result]
1175
- if (into = opts[:into])
1176
- AttributeList.new(attrlist, block).parse_into(into, posattrs)
1177
- else
1178
- AttributeList.new(attrlist, block).parse(posattrs)
959
+ # source - The String source.
960
+ # spec - The lines specifier (e.g., "1-5, !2, 10" or "1..5;!2;10")
961
+ #
962
+ # Returns an [Array] of unique, sorted line numbers.
963
+ def resolve_lines_to_highlight source, spec
964
+ lines = []
965
+ spec = spec.delete ' ' if spec.include? ' '
966
+ ((spec.include? ',') ? (spec.split ',') : (spec.split ';')).map do |entry|
967
+ if entry.start_with? '!'
968
+ entry = entry.slice 1, entry.length
969
+ negate = true
970
+ end
971
+ if (delim = (entry.include? '..') ? '..' : ((entry.include? '-') ? '-' : nil))
972
+ from, delim, to = entry.partition delim
973
+ to = (source.count LF) + 1 if to.empty? || (to = to.to_i) < 0
974
+ line_nums = (from.to_i..to).to_a
975
+ if negate
976
+ lines -= line_nums
977
+ else
978
+ lines.concat line_nums
979
+ end
980
+ else
981
+ if negate
982
+ lines.delete entry.to_i
983
+ else
984
+ lines << entry.to_i
985
+ end
986
+ end
1179
987
  end
988
+ lines.sort.uniq
1180
989
  end
1181
990
 
1182
- # Expand all groups in the subs list and return. If no subs are resolve, return nil.
991
+ # Public: Extract the passthrough text from the document for reinsertion after processing.
1183
992
  #
1184
- # subs - The substitutions to expand; can be a Symbol, Symbol Array or nil
993
+ # text - The String from which to extract passthrough fragements
1185
994
  #
1186
- # Returns a Symbol Array of substitutions to pass to apply_subs or nil if no substitutions were resolved.
1187
- def expand_subs subs
1188
- if ::Symbol === subs
1189
- unless subs == :none
1190
- SUB_GROUPS[subs] || [subs]
1191
- end
1192
- else
1193
- expanded_subs = []
1194
- subs.each do |key|
1195
- unless key == :none
1196
- if (sub_group = SUB_GROUPS[key])
1197
- expanded_subs += sub_group
995
+ # Returns the String text with passthrough regions substituted with placeholders
996
+ def extract_passthroughs text
997
+ compat_mode = @document.compat_mode
998
+ passthrus = @passthroughs
999
+ text = text.gsub InlinePassMacroRx do
1000
+ if (boundary = $4) # $$, ++, or +++
1001
+ # skip ++ in compat mode, handled as normal quoted text
1002
+ if compat_mode && boundary == '++'
1003
+ content = extract_passthroughs $5
1004
+ next $2 ? %(#{$1}[#{$2}]#{$3}++#{content}++) : %(#{$1}#{$3}++#{content}++)
1005
+ end
1006
+
1007
+ attributes = $2
1008
+ escape_count = $3.length
1009
+ content = $5
1010
+
1011
+ if attributes
1012
+ if escape_count > 0
1013
+ # NOTE we don't look for nested unconstrained pass macros
1014
+ next %(#{$1}[#{attributes}]#{RS * (escape_count - 1)}#{boundary}#{$5}#{boundary})
1015
+ elsif $1 == RS
1016
+ preceding = %([#{attributes}])
1017
+ attributes = nil
1198
1018
  else
1199
- expanded_subs << key
1019
+ if boundary == '++' && (attributes.end_with? 'x-')
1020
+ old_behavior = true
1021
+ attributes = attributes.slice 0, attributes.length - 2
1022
+ end
1023
+ attributes = parse_quoted_text_attributes attributes
1200
1024
  end
1025
+ elsif escape_count > 0
1026
+ # NOTE we don't look for nested unconstrained pass macros
1027
+ next %(#{RS * (escape_count - 1)}#{boundary}#{$5}#{boundary})
1201
1028
  end
1029
+ subs = (boundary == '+++' ? [] : BASIC_SUBS)
1030
+
1031
+ if attributes
1032
+ if old_behavior
1033
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: NORMAL_SUBS, type: :monospaced, attributes: attributes }
1034
+ else
1035
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: subs, type: :unquoted, attributes: attributes }
1036
+ end
1037
+ else
1038
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: subs }
1039
+ end
1040
+ else # pass:[]
1041
+ # NOTE we don't look for nested pass:[] macros
1042
+ # honor the escape
1043
+ next $&.slice 1, $&.length if $6 == RS
1044
+ passthrus[passthru_key = passthrus.size] = { text: (normalize_text $8, nil, true), subs: ($7 ? (resolve_pass_subs $7) : nil) }
1202
1045
  end
1203
1046
 
1204
- expanded_subs.empty? ? nil : expanded_subs
1205
- end
1206
- end
1047
+ %(#{preceding || ''}#{PASS_START}#{passthru_key}#{PASS_END})
1048
+ end if (text.include? '++') || (text.include? '$$') || (text.include? 'ss:')
1207
1049
 
1208
- # Internal: Strip bounding whitespace, fold newlines and unescape closing
1209
- # square brackets from text extracted from brackets
1210
- def unescape_bracketed_text text
1211
- if (text = text.strip.tr LF, ' ').include? R_SB
1212
- text = text.gsub ESC_R_SB, R_SB
1213
- end unless text.empty?
1214
- text
1215
- end
1050
+ pass_inline_char1, pass_inline_char2, pass_inline_rx = InlinePassRx[compat_mode]
1051
+ text = text.gsub pass_inline_rx do
1052
+ preceding = $1
1053
+ attributes = $2
1054
+ escape_mark = RS if (quoted_text = $3).start_with? RS
1055
+ format_mark = $4
1056
+ content = $5
1216
1057
 
1217
- # Internal: Strip bounding whitespace and fold newlines
1218
- def normalize_string str, unescape_brackets = false
1219
- unless str.empty?
1220
- str = str.strip.tr LF, ' '
1221
- str = str.gsub ESC_R_SB, R_SB if unescape_brackets && (str.include? R_SB)
1222
- end
1223
- str
1224
- end
1058
+ if compat_mode
1059
+ old_behavior = true
1060
+ elsif (old_behavior = attributes && (attributes.end_with? 'x-'))
1061
+ attributes = attributes.slice 0, attributes.length - 2
1062
+ end
1225
1063
 
1226
- # Internal: Unescape closing square brackets.
1227
- # Intended for text extracted from square brackets.
1228
- def unescape_brackets str
1229
- if str.include? RS
1230
- str = str.gsub ESC_R_SB, R_SB
1231
- end unless str.empty?
1232
- str
1064
+ if attributes
1065
+ if format_mark == '`' && !old_behavior
1066
+ next extract_inner_passthrough content, %(#{preceding}[#{attributes}]#{escape_mark}), attributes
1067
+ elsif escape_mark
1068
+ # honor the escape of the formatting mark
1069
+ next %(#{preceding}[#{attributes}]#{quoted_text.slice 1, quoted_text.length})
1070
+ elsif preceding == RS
1071
+ # honor the escape of the attributes
1072
+ preceding = %([#{attributes}])
1073
+ attributes = nil
1074
+ else
1075
+ attributes = parse_quoted_text_attributes attributes
1076
+ end
1077
+ elsif format_mark == '`' && !old_behavior
1078
+ next extract_inner_passthrough content, %(#{preceding}#{escape_mark})
1079
+ elsif escape_mark
1080
+ # honor the escape of the formatting mark
1081
+ next %(#{preceding}#{quoted_text.slice 1, quoted_text.length})
1082
+ end
1083
+
1084
+ if compat_mode
1085
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: BASIC_SUBS, attributes: attributes, type: :monospaced }
1086
+ elsif attributes
1087
+ if old_behavior
1088
+ subs = (format_mark == '`' ? BASIC_SUBS : NORMAL_SUBS)
1089
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: subs, attributes: attributes, type: :monospaced }
1090
+ else
1091
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: BASIC_SUBS, attributes: attributes, type: :unquoted }
1092
+ end
1093
+ else
1094
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: BASIC_SUBS }
1095
+ end
1096
+
1097
+ %(#{preceding}#{PASS_START}#{passthru_key}#{PASS_END})
1098
+ end if (text.include? pass_inline_char1) || (pass_inline_char2 && (text.include? pass_inline_char2))
1099
+
1100
+ # NOTE we need to do the stem in a subsequent step to allow it to be escaped by the former
1101
+ text = text.gsub InlineStemMacroRx do
1102
+ # honor the escape
1103
+ next $&.slice 1, $&.length if $&.start_with? RS
1104
+
1105
+ if (type = $1.to_sym) == :stem
1106
+ type = STEM_TYPE_ALIASES[@document.attributes['stem']].to_sym
1107
+ end
1108
+ content = normalize_text $3, nil, true
1109
+ # NOTE drop enclosing $ signs around latexmath for backwards compatibility with AsciiDoc Python
1110
+ content = content.slice 1, content.length - 2 if type == :latexmath && (content.start_with? '$') && (content.end_with? '$')
1111
+ subs = $2 ? (resolve_pass_subs $2) : ((@document.basebackend? 'html') ? BASIC_SUBS : nil)
1112
+ passthrus[passthru_key = passthrus.size] = { text: content, subs: subs, type: type }
1113
+ %(#{PASS_START}#{passthru_key}#{PASS_END})
1114
+ end if (text.include? ':') && ((text.include? 'stem:') || (text.include? 'math:'))
1115
+
1116
+ text
1233
1117
  end
1234
1118
 
1235
- # Internal: Split text formatted as CSV with support
1236
- # for double-quoted values (in which commas are ignored)
1237
- def split_simple_csv str
1238
- if str.empty?
1239
- []
1240
- elsif str.include? '"'
1241
- values = []
1242
- accum = ''
1243
- quote_open = false
1244
- str.each_char do |c|
1245
- case c
1246
- when ','
1247
- if quote_open
1248
- accum = accum + c
1249
- else
1250
- values << accum.strip
1251
- accum = ''
1119
+ # Public: Restore the passthrough text by reinserting into the placeholder positions
1120
+ #
1121
+ # text - The String text into which to restore the passthrough text
1122
+ #
1123
+ # returns The String text with the passthrough text restored
1124
+ def restore_passthroughs text
1125
+ passthrus = @passthroughs
1126
+ text.gsub PassSlotRx do
1127
+ if (pass = passthrus[$1.to_i])
1128
+ subbed_text = apply_subs(pass[:text], pass[:subs])
1129
+ if (type = pass[:type])
1130
+ if (attributes = pass[:attributes])
1131
+ id = attributes['id']
1252
1132
  end
1253
- when '"'
1254
- quote_open = !quote_open
1255
- else
1256
- accum = accum + c
1133
+ subbed_text = Inline.new(self, :quoted, subbed_text, type: type, id: id, attributes: attributes).convert
1257
1134
  end
1135
+ subbed_text.include?(PASS_START) ? restore_passthroughs(subbed_text) : subbed_text
1136
+ else
1137
+ logger.error %(unresolved passthrough detected: #{text})
1138
+ '??pass??'
1258
1139
  end
1259
- values << accum.strip
1260
- else
1261
- str.split(',').map {|it| it.strip }
1262
1140
  end
1263
1141
  end
1264
1142
 
1265
- # Internal: Resolve the list of comma-delimited subs against the possible options.
1143
+ # Public: Resolve the list of comma-delimited subs against the possible options.
1266
1144
  #
1267
- # subs - A comma-delimited String of substitution aliases
1145
+ # subs - The comma-delimited String of substitution names or aliases.
1146
+ # type - A Symbol representing the context for which the subs are being resolved (default: :block).
1147
+ # defaults - An Array of substitutions to start with when computing incremental substitutions (default: nil).
1148
+ # subject - The String to use in log messages to communicate the subject for which subs are being resolved (default: nil)
1268
1149
  #
1269
- # returns An Array of Symbols representing the substitution operation or nothing if no subs are found.
1150
+ # Returns An Array of Symbols representing the substitution operation or nothing if no subs are found.
1270
1151
  def resolve_subs subs, type = :block, defaults = nil, subject = nil
1271
1152
  return if subs.nil_or_empty?
1272
1153
  # QUESTION should we store candidates as a Set instead of an Array?
@@ -1315,68 +1196,125 @@ module Substitutors
1315
1196
  candidates -= resolved_keys
1316
1197
  end
1317
1198
  else
1318
- candidates ||= []
1319
- candidates += resolved_keys
1199
+ candidates ||= []
1200
+ candidates += resolved_keys
1201
+ end
1202
+ end
1203
+ return unless candidates
1204
+ # weed out invalid options and remove duplicates (order is preserved; first occurence wins)
1205
+ resolved = candidates & SUB_OPTIONS[type]
1206
+ unless (candidates - resolved).empty?
1207
+ invalid = candidates - resolved
1208
+ logger.warn %(invalid substitution type#{invalid.size > 1 ? 's' : ''}#{subject ? ' for ' : ''}#{subject}: #{invalid.join ', '})
1209
+ end
1210
+ resolved
1211
+ end
1212
+
1213
+ # Public: Call resolve_subs for the :block type.
1214
+ def resolve_block_subs subs, defaults, subject
1215
+ resolve_subs subs, :block, defaults, subject
1216
+ end
1217
+
1218
+ # Public: Call resolve_subs for the :inline type with the subject set as passthrough macro.
1219
+ def resolve_pass_subs subs
1220
+ resolve_subs subs, :inline, nil, 'passthrough macro'
1221
+ end
1222
+
1223
+ # Public: Expand all groups in the subs list and return. If no subs are resolve, return nil.
1224
+ #
1225
+ # subs - The substitutions to expand; can be a Symbol, Symbol Array or nil
1226
+ #
1227
+ # Returns a Symbol Array of substitutions to pass to apply_subs or nil if no substitutions were resolved.
1228
+ def expand_subs subs
1229
+ if ::Symbol === subs
1230
+ unless subs == :none
1231
+ SUB_GROUPS[subs] || [subs]
1232
+ end
1233
+ else
1234
+ expanded_subs = []
1235
+ subs.each do |key|
1236
+ unless key == :none
1237
+ if (sub_group = SUB_GROUPS[key])
1238
+ expanded_subs += sub_group
1239
+ else
1240
+ expanded_subs << key
1241
+ end
1242
+ end
1243
+ end
1244
+
1245
+ expanded_subs.empty? ? nil : expanded_subs
1246
+ end
1247
+ end
1248
+
1249
+ # Internal: Commit the requested substitutions to this block.
1250
+ #
1251
+ # Looks for an attribute named "subs". If present, resolves substitutions
1252
+ # from the value of that attribute and assigns them to the subs property on
1253
+ # this block. Otherwise, uses the substitutions assigned to the default_subs
1254
+ # property, if specified, or selects a default set of substitutions based on
1255
+ # the content model of the block.
1256
+ #
1257
+ # Returns nothing
1258
+ def commit_subs
1259
+ unless (default_subs = @default_subs)
1260
+ case @content_model
1261
+ when :simple
1262
+ default_subs = NORMAL_SUBS
1263
+ when :verbatim
1264
+ # NOTE :literal with listparagraph-option gets folded into text of list item later
1265
+ default_subs = @context == :verse ? NORMAL_SUBS : VERBATIM_SUBS
1266
+ when :raw
1267
+ # TODO make pass subs a compliance setting; AsciiDoc Python performs :attributes and :macros on a pass block
1268
+ default_subs = @context == :stem ? BASIC_SUBS : NO_SUBS
1269
+ else
1270
+ return @subs
1320
1271
  end
1321
1272
  end
1322
- return unless candidates
1323
- # weed out invalid options and remove duplicates (order is preserved; first occurence wins)
1324
- resolved = candidates & SUB_OPTIONS[type]
1325
- unless (candidates - resolved).empty?
1326
- invalid = candidates - resolved
1327
- logger.warn %(invalid substitution type#{invalid.size > 1 ? 's' : ''}#{subject ? ' for ' : ''}#{subject}: #{invalid.join ', '})
1273
+
1274
+ if (custom_subs = @attributes['subs'])
1275
+ @subs = (resolve_block_subs custom_subs, default_subs, @context) || []
1276
+ else
1277
+ @subs = default_subs.drop 0
1328
1278
  end
1329
- resolved
1330
- end
1331
1279
 
1332
- def resolve_block_subs subs, defaults, subject
1333
- resolve_subs subs, :block, defaults, subject
1334
- end
1280
+ # QUESION delegate this logic to a method?
1281
+ if @context == :listing && @style == 'source' && (syntax_hl = @document.syntax_highlighter) &&
1282
+ syntax_hl.highlight? && (idx = @subs.index :specialcharacters)
1283
+ @subs[idx] = :highlight
1284
+ end
1335
1285
 
1336
- def resolve_pass_subs subs
1337
- resolve_subs subs, :inline, nil, 'passthrough macro'
1286
+ nil
1338
1287
  end
1339
1288
 
1340
- # Public: Highlight (i.e., colorize) the source code during conversion using a syntax highlighter, if activated by the
1341
- # source-highlighter document attribute. Otherwise return the text with verbatim substitutions applied.
1342
- #
1343
- # If the process_callouts argument is true, this method will extract the callout marks from the source before passing
1344
- # it to the syntax highlighter, then subsequently restore those callout marks to the highlighted source so the callout
1345
- # marks don't confuse the syntax highlighter.
1289
+ # Internal: Parse attributes in name or name=value format from a comma-separated String
1346
1290
  #
1347
- # source - the source code String to syntax highlight
1348
- # process_callouts - a Boolean flag indicating whether callout marks should be located and substituted
1291
+ # attrlist - A comma-separated String list of attributes in name or name=value format.
1292
+ # posattrs - An Array of positional attribute names (default: []).
1293
+ # opts - A Hash of options to control how the string is parsed (default: {}):
1294
+ # :into - The Hash to parse the attributes into (optional, default: false).
1295
+ # :sub_input - A Boolean that indicates whether to substitute attributes prior to
1296
+ # parsing (optional, default: false).
1297
+ # :sub_result - A Boolean that indicates whether to apply substitutions
1298
+ # single-quoted attribute values (optional, default: true).
1299
+ # :unescape_input - A Boolean that indicates whether to unescape square brackets prior
1300
+ # to parsing (optional, default: false).
1349
1301
  #
1350
- # Returns the highlighted source code, if a syntax highlighter is defined on the document, otherwise the source with
1351
- # verbatim substituions applied
1352
- def highlight_source source, process_callouts
1353
- # NOTE the call to highlight? is a defensive check since, normally, we wouldn't arrive here unless it returns true
1354
- return sub_source source, process_callouts unless (syntax_hl = @document.syntax_highlighter) && syntax_hl.highlight?
1355
-
1356
- source, callout_marks = extract_callouts source if process_callouts
1357
-
1358
- doc_attrs = @document.attributes
1359
- syntax_hl_name = syntax_hl.name
1360
- if (linenums_mode = (attr? 'linenums') ? (doc_attrs[%(#{syntax_hl_name}-linenums-mode)] || :table).to_sym : nil)
1361
- start_line_number = 1 if (start_line_number = (attr 'start', 1).to_i) < 1
1302
+ # Returns an empty Hash if attrlist is nil or empty, otherwise a Hash of parsed attributes.
1303
+ def parse_attributes attrlist, posattrs = [], opts = {}
1304
+ return {} if attrlist ? attrlist.empty? : true
1305
+ attrlist = normalize_text attrlist, true, true if opts[:unescape_input]
1306
+ attrlist = @document.sub_attributes attrlist if opts[:sub_input] && (attrlist.include? ATTR_REF_HEAD)
1307
+ # substitutions are only performed on attribute values if block is not nil
1308
+ block = self if opts[:sub_result]
1309
+ if (into = opts[:into])
1310
+ AttributeList.new(attrlist, block).parse_into(into, posattrs)
1311
+ else
1312
+ AttributeList.new(attrlist, block).parse(posattrs)
1362
1313
  end
1363
- highlight_lines = resolve_lines_to_highlight source, (attr 'highlight') if attr? 'highlight'
1364
-
1365
- highlighted, source_offset = syntax_hl.highlight self, source, (attr 'language'),
1366
- callouts: callout_marks,
1367
- css_mode: (doc_attrs[%(#{syntax_hl_name}-css)] || :class).to_sym,
1368
- highlight_lines: highlight_lines,
1369
- number_lines: linenums_mode,
1370
- start_line_number: start_line_number,
1371
- style: doc_attrs[%(#{syntax_hl_name}-style)]
1372
-
1373
- # fix passthrough placeholders that got caught up in syntax highlighting
1374
- highlighted = highlighted.gsub HighlightedPassSlotRx, %(#{PASS_START}\\1#{PASS_END}) unless @passthroughs.empty?
1375
-
1376
- # NOTE highlight method may have depleted callouts
1377
- callout_marks.nil_or_empty? ? highlighted : (restore_callouts highlighted, callout_marks, source_offset)
1378
1314
  end
1379
1315
 
1316
+ private
1317
+
1380
1318
  # Internal: Extract the callout numbers from the source to prepare it for syntax highlighting.
1381
1319
  def extract_callouts source
1382
1320
  callout_marks = {}
@@ -1431,87 +1369,161 @@ module Substitutors
1431
1369
  end.join LF)
1432
1370
  end
1433
1371
 
1434
- # e.g., highlight="1-5, !2, 10" or highlight=1-5;!2,10
1435
- def resolve_lines_to_highlight source, spec
1436
- lines = []
1437
- spec = spec.delete ' ' if spec.include? ' '
1438
- ((spec.include? ',') ? (spec.split ',') : (spec.split ';')).map do |entry|
1439
- negate = false
1440
- if entry.start_with? '!'
1441
- entry = entry.slice 1, entry.length
1442
- negate = true
1372
+ # Internal: Extract nested single-plus passthrough; otherwise return unprocessed
1373
+ def extract_inner_passthrough text, pre, attributes = nil
1374
+ if (text.end_with? '+') && (text.start_with? '+', '\+') && SinglePlusInlinePassRx =~ text
1375
+ if $1
1376
+ %(#{pre}`+#{$2}+`)
1377
+ else
1378
+ @passthroughs[passthru_key = @passthroughs.size] = attributes ?
1379
+ { text: $2, subs: BASIC_SUBS, attributes: attributes, type: :unquoted } :
1380
+ { text: $2, subs: BASIC_SUBS }
1381
+ %(#{pre}`#{PASS_START}#{passthru_key}#{PASS_END}`)
1443
1382
  end
1444
- if (delim = (entry.include? '..') ? '..' : ((entry.include? '-') ? '-' : nil))
1445
- from, delim, to = entry.partition delim
1446
- to = (source.count LF) + 1 if to.empty? || (to = to.to_i) < 0
1447
- line_nums = (from.to_i..to).to_a
1448
- if negate
1449
- lines -= line_nums
1450
- else
1451
- lines.concat line_nums
1452
- end
1383
+ else
1384
+ %(#{pre}`#{text}`)
1385
+ end
1386
+ end
1387
+
1388
+ # Internal: Convert a quoted text region
1389
+ #
1390
+ # match - The MatchData for the quoted text region
1391
+ # type - The quoting type (single, double, strong, emphasis, monospaced, etc)
1392
+ # scope - The scope of the quoting (constrained or unconstrained)
1393
+ #
1394
+ # Returns The converted String text for the quoted text region
1395
+ def convert_quoted_text match, type, scope
1396
+ if match[0].start_with? RS
1397
+ if scope == :constrained && (attrs = match[2])
1398
+ unescaped_attrs = %([#{attrs}])
1453
1399
  else
1454
- if negate
1455
- lines.delete entry.to_i
1456
- else
1457
- lines << entry.to_i
1400
+ return match[0].slice 1, match[0].length
1401
+ end
1402
+ end
1403
+
1404
+ if scope == :constrained
1405
+ if unescaped_attrs
1406
+ %(#{unescaped_attrs}#{Inline.new(self, :quoted, match[3], type: type).convert})
1407
+ else
1408
+ if (attrlist = match[2])
1409
+ id = (attributes = parse_quoted_text_attributes attrlist)['id']
1410
+ type = :unquoted if type == :mark
1458
1411
  end
1412
+ %(#{match[1]}#{Inline.new(self, :quoted, match[3], type: type, id: id, attributes: attributes).convert})
1413
+ end
1414
+ else
1415
+ if (attrlist = match[1])
1416
+ id = (attributes = parse_quoted_text_attributes attrlist)['id']
1417
+ type = :unquoted if type == :mark
1459
1418
  end
1419
+ Inline.new(self, :quoted, match[2], type: type, id: id, attributes: attributes).convert
1460
1420
  end
1461
- lines.sort.uniq
1462
1421
  end
1463
1422
 
1464
- # Public: Apply verbatim substitutions on source (for use when highlighting is disabled).
1465
- #
1466
- # source - the source code String on which to apply verbatim substitutions
1467
- # process_callouts - a Boolean flag indicating whether callout marks should be substituted
1423
+ # Internal: Substitute replacement text for matched location
1468
1424
  #
1469
- # returns the substituted source
1470
- def sub_source source, process_callouts
1471
- process_callouts ? sub_callouts(sub_specialchars source) : (sub_specialchars source)
1425
+ # returns The String text with the replacement characters substituted
1426
+ def do_replacement m, replacement, restore
1427
+ if (captured = m[0]).include? RS
1428
+ # we have to use sub since we aren't sure it's the first char
1429
+ captured.sub RS, ''
1430
+ else
1431
+ case restore
1432
+ when :none
1433
+ replacement
1434
+ when :bounding
1435
+ m[1] + replacement + m[2]
1436
+ else # :leading
1437
+ m[1] + replacement
1438
+ end
1439
+ end
1472
1440
  end
1473
1441
 
1474
1442
  # Internal: Inserts text into a formatted text enclosure; used by xreftext
1475
1443
  alias sub_placeholder sprintf unless RUBY_ENGINE == 'opal'
1476
1444
 
1477
- # Internal: Commit the requested substitutions to this block.
1445
+ # Internal: Parse the attributes that are defined on quoted (aka formatted) text
1478
1446
  #
1479
- # Looks for an attribute named "subs". If present, resolves substitutions
1480
- # from the value of that attribute and assigns them to the subs property on
1481
- # this block. Otherwise, uses the substitutions assigned to the default_subs
1482
- # property, if specified, or selects a default set of substitutions based on
1483
- # the content model of the block.
1447
+ # str - A non-nil String of unprocessed attributes;
1448
+ # space-separated roles (e.g., role1 role2) or the id/role shorthand syntax (e.g., #idname.role)
1484
1449
  #
1485
- # Returns The Array of resolved substitutions now assigned to this block
1486
- def commit_subs
1487
- unless (default_subs = @default_subs)
1488
- case @content_model
1489
- when :simple
1490
- default_subs = NORMAL_SUBS
1491
- when :verbatim
1492
- # NOTE :literal with listparagraph-option gets folded into text of list item later
1493
- default_subs = @context == :verse ? NORMAL_SUBS : VERBATIM_SUBS
1494
- when :raw
1495
- # TODO make pass subs a compliance setting; AsciiDoc Python performs :attributes and :macros on a pass block
1496
- default_subs = @context == :stem ? BASIC_SUBS : NO_SUBS
1450
+ # Returns a Hash of attributes (role and id only)
1451
+ def parse_quoted_text_attributes str
1452
+ return {} if (str = str.rstrip).empty?
1453
+ # NOTE attributes are typically resolved after quoted text, so substitute eagerly
1454
+ str = sub_attributes str if str.include? ATTR_REF_HEAD
1455
+ # for compliance, only consider first positional attribute (very unlikely)
1456
+ str = str.slice 0, (str.index ',') if str.include? ','
1457
+
1458
+ if (str.start_with? '.', '#') && Compliance.shorthand_property_syntax
1459
+ segments = str.split '#', 2
1460
+
1461
+ if segments.size > 1
1462
+ id, *more_roles = segments[1].split('.')
1497
1463
  else
1498
- return @subs
1464
+ more_roles = []
1499
1465
  end
1500
- end
1501
1466
 
1502
- if (custom_subs = @attributes['subs'])
1503
- @subs = (resolve_block_subs custom_subs, default_subs, @context) || []
1467
+ roles = segments[0].empty? ? [] : segments[0].split('.')
1468
+ if roles.size > 1
1469
+ roles.shift
1470
+ end
1471
+
1472
+ if more_roles.size > 0
1473
+ roles.concat more_roles
1474
+ end
1475
+
1476
+ attrs = {}
1477
+ attrs['id'] = id if id
1478
+ attrs['role'] = roles.join ' ' unless roles.empty?
1479
+ attrs
1504
1480
  else
1505
- @subs = default_subs.drop 0
1481
+ { 'role' => str }
1506
1482
  end
1483
+ end
1507
1484
 
1508
- # QUESION delegate this logic to a method?
1509
- if @context == :listing && @style == 'source' && (syntax_hl = @document.syntax_highlighter) &&
1510
- syntax_hl.highlight? && (idx = @subs.index :specialcharacters)
1511
- @subs[idx] = :highlight
1485
+ # Internal: Normalize text to prepare it for parsing.
1486
+ #
1487
+ # If normalize_whitespace is true, strip surrounding whitespace and fold newlines. If unescape_closing_square_bracket
1488
+ # is set, unescape any escaped closing square brackets.
1489
+ #
1490
+ # Returns the normalized text String
1491
+ def normalize_text text, normalize_whitespace = nil, unescape_closing_square_brackets = nil
1492
+ unless text.empty?
1493
+ text = text.strip.tr LF, ' ' if normalize_whitespace
1494
+ text = text.gsub ESC_R_SB, R_SB if unescape_closing_square_brackets && (text.include? R_SB)
1512
1495
  end
1496
+ text
1497
+ end
1513
1498
 
1514
- @subs
1499
+ # Internal: Split text formatted as CSV with support
1500
+ # for double-quoted values (in which commas are ignored)
1501
+ def split_simple_csv str
1502
+ if str.empty?
1503
+ []
1504
+ elsif str.include? '"'
1505
+ values = []
1506
+ accum = ''
1507
+ quote_open = nil
1508
+ str.each_char do |c|
1509
+ case c
1510
+ when ','
1511
+ if quote_open
1512
+ accum = accum + c
1513
+ else
1514
+ values << accum.strip
1515
+ accum = ''
1516
+ end
1517
+ when '"'
1518
+ quote_open = !quote_open
1519
+ else
1520
+ accum = accum + c
1521
+ end
1522
+ end
1523
+ values << accum.strip
1524
+ else
1525
+ str.split(',').map {|it| it.strip }
1526
+ end
1515
1527
  end
1516
1528
  end
1517
1529
  end