asciidoctor 2.0.0.rc.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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