lib/asciidoctor/substituters.rb in asciidoctor-0.1.0 vs lib/asciidoctor/substituters.rb in asciidoctor-0.1.1

- old
+ new

@@ -1,587 +1,612 @@ +module Asciidoctor # Public: Methods to perform substitutions on lines of AsciiDoc text. This module # is intented to be mixed-in to Section and Block to provide operations for performing # the necessary substitutions. -module Asciidoctor - module Substituters +module Substituters - COMPOSITE_SUBS = { - :none => [], - :normal => [:specialcharacters, :quotes, :attributes, :replacements, :macros, :post_replacements], - :verbatim => [:specialcharacters, :callouts] - } + COMPOSITE_SUBS = { + :none => [], + :normal => [:specialcharacters, :quotes, :attributes, :replacements, :macros, :post_replacements], + :verbatim => [:specialcharacters, :callouts] + } - SUB_OPTIONS = COMPOSITE_SUBS.keys + COMPOSITE_SUBS[:normal] + SUB_OPTIONS = COMPOSITE_SUBS.keys + COMPOSITE_SUBS[:normal] - # Internal: A String Array of passthough (unprocessed) text captured from this block - attr_reader :passthroughs + # Internal: A String Array of passthough (unprocessed) text captured from this block + attr_reader :passthroughs - # Public: Apply the specified substitutions to the lines of text - # - # lines - The lines of text to process. Can be a String or a String Array - # subs - The substitutions to perform. Can be a Symbol or a Symbol Array (default: COMPOSITE_SUBS[:normal]) - # - # returns Either a String or String Array, whichever matches the type of the first argument - def apply_subs(lines, subs = COMPOSITE_SUBS[:normal]) - if subs.nil? - subs = [] - elsif subs.is_a? Symbol - subs = [subs] - end + # Public: Apply the specified substitutions to the lines of text + # + # lines - The lines of text to process. Can be a String or a String Array + # subs - The substitutions to perform. Can be a Symbol or a Symbol Array (default: COMPOSITE_SUBS[:normal]) + # + # returns Either a String or String Array, whichever matches the type of the first argument + def apply_subs(lines, subs = COMPOSITE_SUBS[:normal]) + if subs.nil? + subs = [] + elsif subs.is_a? Symbol + subs = [subs] + end - if !subs.empty? - # QUESTION is this most efficient operation? - subs = subs.map {|key| - COMPOSITE_SUBS.has_key?(key) ? COMPOSITE_SUBS[key] : key - }.flatten + if !subs.empty? + # QUESTION is this most efficient operation? + subs = subs.map {|key| + COMPOSITE_SUBS.has_key?(key) ? COMPOSITE_SUBS[key] : key + }.flatten + end + + return lines if subs.empty? + + multiline = lines.is_a?(Array) + text = multiline ? lines.join : lines + + passthroughs = subs.include?(:macros) + text = extract_passthroughs(text) if passthroughs + + subs.each {|type| + case type + when :specialcharacters + text = sub_specialcharacters(text) + when :quotes + text = sub_quotes(text) + when :attributes + text = sub_attributes(text.lines.entries).join + when :replacements + text = sub_replacements(text) + when :macros + text = sub_macros(text) + when :callouts + text = sub_callouts(text) + when :post_replacements + text = sub_post_replacements(text) + else + puts "asciidoctor: WARNING: unknown substitution type #{type}" end + } + text = restore_passthroughs(text) if passthroughs - return lines if subs.empty? + multiline ? text.lines.entries : text + end - multiline = lines.is_a?(Array) - text = multiline ? lines.join : lines + # Public: Apply normal substitutions. + # + # lines - The lines of text to process. Can be a String or a String Array + # + # returns - A String with normal substitutions performed + def apply_normal_subs(lines) + apply_subs(lines.is_a?(Array) ? lines.join : lines) + end - passthroughs = subs.include?(:macros) - text = extract_passthroughs(text) if passthroughs - - subs.each {|type| - case type - when :specialcharacters - text = sub_specialcharacters(text) - when :quotes - text = sub_quotes(text) - when :attributes - text = sub_attributes(text.lines.entries).join - when :replacements - text = sub_replacements(text) - when :macros - text = sub_macros(text) - when :callouts - text = sub_callouts(text) - when :post_replacements - text = sub_post_replacements(text) - else - puts "asciidoctor: WARNING: unknown substitution type #{type}" - end - } - text = restore_passthroughs(text) if passthroughs + # Public: Apply substitutions for titles. + # + # title - The String title to process + # + # returns - A String with title substitutions performed + def apply_title_subs(title) + apply_subs(title, [:specialcharacters, :quotes, :replacements, :macros, :attributes, :post_replacements]) + end - multiline ? text.lines.entries : text + # Public: Apply substitutions for titles + # + # lines - A String Array containing the lines of text process + # + # returns - A String with literal (verbatim) substitutions performed + def apply_literal_subs(lines) + if @document.attributes['basebackend'] == 'html' && attr('style') == 'source' && + @document.attributes['source-highlighter'] == 'coderay' && attr?('language') + sub_callouts(highlight_source(lines.join)) + else + apply_subs(lines.join, COMPOSITE_SUBS[:verbatim]) end + end - # Public: Apply normal substitutions. - # - # lines - The lines of text to process. Can be a String or a String Array - # - # returns - A String with normal substitutions performed - def apply_normal_subs(lines) - apply_subs(lines.is_a?(Array) ? lines.join : lines) - end + # Public: Apply substitutions for header metadata and attribute assignments + # + # text - String containing the text process + # + # returns - A String with header substitutions performed + def apply_header_subs(text) + apply_subs(text, [:specialcharacters, :attributes]) + end - # Public: Apply substitutions for titles. - # - # title - The String title to process - # - # returns - A String with title substitutions performed - def apply_title_subs(title) - apply_subs(title, [:specialcharacters, :quotes, :replacements, :macros, :attributes, :post_replacements]) + # Public: Apply substitutions for passthrough text + # + # lines - A String Array containing the lines of text process + # + # returns - A String Array with passthrough substitutions performed + def apply_passthrough_subs(lines) + if attr? 'subs' + subs = resolve_subs(attr('subs')) + else + subs = [:attributes, :macros] end + apply_subs(lines.join, subs) + end - # Public: Apply substitutions for titles - # - # lines - A String Array containing the lines of text process - # - # returns - A String with literal (verbatim) substitutions performed - def apply_literal_subs(lines) - if @document.attr('basebackend') == 'html' && attr('style') == 'source' && - @document.attr('source-highlighter') == 'coderay' && attr?('language') - sub_callouts(highlight_source(lines.join)) + # Internal: Extract the passthrough text from the document for reinsertion after processing. + # + # text - The String from which to extract passthrough fragements + # + # returns - The text with the passthrough region substituted with placeholders + def extract_passthroughs(text) + result = text.dup + + result.gsub!(REGEXP[:pass_macro]) { + # alias match for Ruby 1.8.7 compat + m = $~ + # honor the escape + if m[0].start_with? '\\' + next m[0][1..-1] + end + + if m[1] == '$$' + subs = [:specialcharacters] + elsif !m[3].nil? && !m[3].empty? + subs = resolve_subs(m[3]) else - apply_subs(lines.join, COMPOSITE_SUBS[:verbatim]) + subs = [] end - end - # Public: Apply substitutions for header metadata and attribute assignments - # - # text - String containing the text process - # - # returns - A String with header substitutions performed - def apply_header_subs(text) - apply_subs(text, [:specialcharacters, :attributes]) - end + # TODO move unescaping closing square bracket to an operation + @passthroughs << {:text => m[2] || m[4].gsub('\]', ']'), :subs => subs} + index = @passthroughs.size - 1 + "\x0#{index}\x0" + } unless !(result.include?('+++') || result.include?('$$') || result.include?('pass:')) - # Public: Apply substitutions for passthrough text - # - # lines - A String Array containing the lines of text process - # - # returns - A String Array with passthrough substitutions performed - def apply_passthrough_subs(lines) - if attr? 'subs' - subs = resolve_subs(attr('subs')) - else - subs = [:attributes, :macros] + result.gsub!(REGEXP[:pass_lit]) { + # alias match for Ruby 1.8.7 compat + m = $~ + + # honor the escape + if m[2].start_with? '\\' + next "#{m[1]}#{m[2][1..-1]}" end - apply_subs(lines.join, subs) - end + + @passthroughs << {:text => m[3], :subs => [:specialcharacters], :literal => true} + index = @passthroughs.size - 1 + "#{m[1]}\x0#{index}\x0" + } unless !result.include?('`') - # Internal: Extract the passthrough text from the document for reinsertion after processing. - # - # text - The String from which to extract passthrough fragements - # - # returns - The text with the passthrough region substituted with placeholders - def extract_passthroughs(text) - result = text.dup + result + end - result.gsub!(REGEXP[:pass_macro]) { - # alias match for Ruby 1.8.7 compat - m = $~ - # honor the escape - if m[0].start_with? '\\' - next m[0][1..-1] - end + # Internal: Restore the passthrough text by reinserting into the placeholder positions + # + # text - The String text into which to restore the passthrough text + # + # returns The String text with the passthrough text restored + def restore_passthroughs(text) + return text if @passthroughs.nil? || @passthroughs.empty? || !text.include?("\x0") + + text.gsub(REGEXP[:pass_placeholder]) { + pass = @passthroughs[$1.to_i]; + text = apply_subs(pass[:text], pass.fetch(:subs, [])) + pass[:literal] ? Inline.new(self, :quoted, text, :type => :monospaced).render : text + } + end - if m[1] == '$$' - subs = [:specialcharacters] - elsif !m[3].nil? && !m[3].empty? - subs = resolve_subs(m[3]) - else - subs = [] - end + # Public: Substitute special characters (i.e., encode XML) + # + # Special characters are defined in the Asciidoctor::SPECIAL_CHARS Array constant + # + # text - The String text to process + # + # returns The String text with special characters replaced + def sub_specialcharacters(text) + # this syntax only available in Ruby 1.9 + #text.gsub(SPECIAL_CHARS_PATTERN, SPECIAL_CHARS) - @passthroughs << {:text => m[2] || m[4].gsub('\]', ']'), :subs => subs} - index = @passthroughs.size - 1 - "\x0#{index}\x0" - } unless !(result.include?('+++') || result.include?('$$') || result.include?('pass:')) + text.gsub(SPECIAL_CHARS_PATTERN) { SPECIAL_CHARS[$&] } + end - result.gsub!(REGEXP[:pass_lit]) { + # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc) + # + # text - The String text to process + # + # returns The String text with quoted text rendered using the backend templates + def sub_quotes(text) + result = text.dup + + QUOTE_SUBS.each {|type, scope, pattern| + result.gsub!(pattern) { transform_quoted_text($~, type, scope) } + } + + result + end + + # Public: Substitute replacement characters (e.g., copyright, trademark, etc) + # + # text - The String text to process + # + # returns The String text with the replacement characters substituted + def sub_replacements(text) + result = text.dup + + REPLACEMENTS.each {|pattern, replacement| + result.gsub!(pattern, replacement) + } + + result + end + + # Public: Substitute attribute references + # + # Attribute references are in the format {name}. + # + # If an attribute referenced in the line is missing, the line is dropped. + # + # text - The String text to process + # + # returns The String text with the attribute references replaced with attribute values + #-- + # NOTE it's necessary to perform this substitution line-by-line + # so that a missing key doesn't wipe out the whole block of data + def sub_attributes(data) + return data if data.nil? || data.empty? + + # normalizes data type to an array (string becomes single-element array) + lines = Array(data) + + result = lines.map {|line| + reject = false + subject = line.dup + subject.gsub!(REGEXP[:attr_ref]) { # alias match for Ruby 1.8.7 compat m = $~ - - # honor the escape - if m[2].start_with? '\\' - next "#{m[1]}#{m[2][1..-1]}" + key = m[2].downcase + # escaped attribute + if !$1.empty? || !$3.empty? + "{#$2}" + elsif m[2].start_with?('counter:') + args = m[2].split(':') + @document.counter(args[1], args[2]) + elsif m[2].start_with?('counter2:') + args = m[2].split(':') + @document.counter(args[1], args[2]) + '' + elsif document.attributes.has_key? key + @document.attributes[key] + elsif INTRINSICS.has_key? key + INTRINSICS[key] + else + Debug.debug { "Missing attribute: #{m[2]}, line marked for removal" } + reject = true + break '{undefined}' end - - @passthroughs << {:text => m[3], :subs => [:specialcharacters], :literal => true} - index = @passthroughs.size - 1 - "#{m[1]}\x0#{index}\x0" - } unless !result.include?('`') + } if subject.include?('{') - result - end + !reject ? subject : nil + }.compact - # Internal: Restore the passthrough text by reinserting into the placeholder positions - # - # text - The String text into which to restore the passthrough text - # - # returns The String text with the passthrough text restored - def restore_passthroughs(text) - return text if @passthroughs.nil? || @passthroughs.empty? || !text.include?("\x0") - - text.gsub(REGEXP[:pass_placeholder]) { - pass = @passthroughs[$1.to_i]; - text = apply_subs(pass[:text], pass.fetch(:subs, [])) - pass[:literal] ? Inline.new(self, :quoted, text, :type => :monospaced).render : text - } - end + data.is_a?(String) ? result.join : result + end - # Public: Substitute special characters (i.e., encode XML) - # - # Special characters are defined in the Asciidoctor::SPECIAL_CHARS Array constant - # - # text - The String text to process - # - # returns The String text with special characters replaced - def sub_specialcharacters(text) - # this syntax only available in Ruby 1.9 - #text.gsub(SPECIAL_CHARS_PATTERN, SPECIAL_CHARS) + # Public: Substitute inline macros (e.g., links, images, etc) + # + # Replace inline macros, which may span multiple lines, in the provided text + # + # text - The String text to process + # + # returns The String with the inline macros rendered using the backend templates + def sub_macros(text) + return text if text.nil? || text.empty? - text.gsub(SPECIAL_CHARS_PATTERN) { SPECIAL_CHARS[$&] } - end + result = text.dup - # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc) - # - # text - The String text to process - # - # returns The String text with quoted text rendered using the backend templates - def sub_quotes(text) - result = text.dup + # some look ahead assertions to cut unnecessary regex calls + found = {} + found[:square_bracket] = result.include?('[') + found[:round_bracket] = result.include?('(') + found[:colon] = result.include?(':') + found[:macroish] = (found[:square_bracket] && found[:colon]) + found[:macroish_short_form] = (found[:square_bracket] && found[:colon] && result.include?(':[')) + found[:uri] = (found[:colon] && result.include?('://')) + link_attrs = @document.attributes.has_key?('linkattrs') - QUOTE_SUBS.each {|type, scope, pattern| - result.gsub!(pattern) { transform_quoted_text($~, type, scope) } + if found[:macroish] && result.include?('image:') + # image:filename.png[Alt Text] + result.gsub!(REGEXP[:image_macro]) { + # alias match for Ruby 1.8.7 compat + m = $~ + # honor the escape + if m[0].start_with? '\\' + next m[0][1..-1] + end + target = sub_attributes(m[1]) + @document.register(:images, target) + attrs = parse_attributes(m[2], ['alt', 'width', 'height']) + if !attrs.has_key?('alt') || attrs['alt'].empty? + attrs['alt'] = File.basename(target, File.extname(target)) + end + Inline.new(self, :image, nil, :target => target, :attributes => attrs).render } - - result end - # Public: Substitute replacement characters (e.g., copyright, trademark, etc) - # - # text - The String text to process - # - # returns The String text with the replacement characters substituted - def sub_replacements(text) - result = text.dup + if found[:macroish_short_form] || found[:round_bracket] + # indexterm:[Tigers,Big cats] + # (((Tigers,Big cats))) + result.gsub!(REGEXP[:indexterm_macro]) { + # alias match for Ruby 1.8.7 compat + m = $~ + # honor the escape + if m[0].start_with? '\\' + next m[0][1..-1] + end - REPLACEMENTS.each {|pattern, replacement| - result.gsub!(pattern, replacement) + terms = (m[1] || m[2]).strip.tr("\n", ' ').gsub('\]', ']').split(REGEXP[:csv_delimiter]) + document.register(:indexterms, [*terms]) + Inline.new(self, :indexterm, text, :attributes => {'terms' => terms}).render } - - result + + # indexterm2:[Tigers] + # ((Tigers)) + result.gsub!(REGEXP[:indexterm2_macro]) { + # alias match for Ruby 1.8.7 compat + m = $~ + # honor the escape + if m[0].start_with? '\\' + next m[0][1..-1] + end + + text = (m[1] || m[2]).strip.tr("\n", ' ').gsub('\]', ']') + document.register(:indexterms, [text]) + Inline.new(self, :indexterm, text, :type => :visible).render + } end - # Public: Substitute attribute references - # - # Attribute references are in the format {name}. - # - # If an attribute referenced in the line is missing, the line is dropped. - # - # text - The String text to process - # - # returns The String text with the attribute references replaced with attribute values - #-- - # NOTE it's necessary to perform this substitution line-by-line - # so that a missing key doesn't wipe out the whole block of data - def sub_attributes(data) - return data if data.nil? || data.empty? + if found[:uri] + # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>) + result.gsub!(REGEXP[:link_inline]) { + # alias match for Ruby 1.8.7 compat + m = $~ + # honor the escape + if m[2].start_with? '\\' + next "#{m[1]}#{m[2][1..-1]}#{m[3]}" + # not a valid macro syntax w/o trailing square brackets + # we probably shouldn't even get here...our regex is doing too much + elsif m[1] == 'link:' && m[3].nil? + next m[0] + end + prefix = (m[1] != 'link:' ? m[1] : '') + target = m[2] + # strip the <> around the link + if prefix.end_with? '&lt;' + prefix = prefix[0..-5] + end + if target.end_with? '&gt;' + target = target[0..-5] + end + @document.register(:links, target) - # normalizes data type to an array (string becomes single-element array) - lines = Array(data) - - result = lines.map {|line| - reject = false - subject = line.dup - subject.gsub!(REGEXP[:attr_ref]) { - # alias match for Ruby 1.8.7 compat - m = $~ - key = m[2].downcase - # escaped attribute - if !$1.empty? || !$3.empty? - "{#$2}" - elsif m[2].start_with?('counter:') - args = m[2].split(':') - @document.counter(args[1], args[2]) - elsif m[2].start_with?('counter2:') - args = m[2].split(':') - @document.counter(args[1], args[2]) - '' - elsif document.attributes.has_key? key - @document.attributes[key] - elsif INTRINSICS.has_key? key - INTRINSICS[key] + attrs = nil + #text = !m[3].nil? ? sub_attributes(m[3].gsub('\]', ']')) : '' + if !m[3].to_s.empty? + if link_attrs && (m[3].start_with?('"') || m[3].include?(',')) + attrs = parse_attributes(sub_attributes(m[3].gsub('\]', ']'))) + text = attrs[1] else - Asciidoctor.debug { "Missing attribute: #{m[2]}, line marked for removal" } - reject = true - break '{undefined}' + text = sub_attributes(m[3].gsub('\]', ']')) end - } if subject.include?('{') + else + text = '' + end - !reject ? subject : nil - }.compact - - data.is_a?(String) ? result.join : result + "#{prefix}#{Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target, :attributes => attrs).render}" + } end - # Public: Substitute inline macros (e.g., links, images, etc) - # - # Replace inline macros, which may span multiple lines, in the provided text - # - # text - The String text to process - # - # returns The String with the inline macros rendered using the backend templates - def sub_macros(text) - return text if text.nil? || text.empty? + if found[:macroish] && result.include?('link:') + # inline link macros, link:target[text] + result.gsub!(REGEXP[:link_macro]) { + # alias match for Ruby 1.8.7 compat + m = $~ + # honor the escape + if m[0].start_with? '\\' + next m[0][1..-1] + end + target = m[1] + @document.register(:links, target) - result = text.dup - - # some look ahead assertions to cut unnecessary regex calls - found = {} - found[:square_bracket] = result.include?('[') - found[:round_bracket] = result.include?('(') - found[:macroish] = (found[:square_bracket] && result.include?(':')) - found[:macroish_short_form] = (found[:square_bracket] && result.include?(':[')) - found[:uri] = result.include?('://') - - if found[:macroish] && result.include?('image:') - # image:filename.png[Alt Text] - result.gsub!(REGEXP[:image_macro]) { - # alias match for Ruby 1.8.7 compat - m = $~ - # honor the escape - if m[0].start_with? '\\' - next m[0][1..-1] - end - target = sub_attributes(m[1]) - @document.register(:images, target) - attrs = parse_attributes(m[2], ['alt', 'width', 'height']) - if !attrs.has_key?('alt') || attrs['alt'].empty? - attrs['alt'] = File.basename(target, File.extname(target)) - end - Inline.new(self, :image, nil, :target => target, :attributes => attrs).render - } - end - - if found[:macroish_short_form] || found[:round_bracket] - # indexterm:[Tigers,Big cats] - # (((Tigers,Big cats))) - result.gsub!(REGEXP[:indexterm_macro]) { - # alias match for Ruby 1.8.7 compat - m = $~ - # honor the escape - if m[0].start_with? '\\' - next m[0][1..-1] - end - - terms = (m[1] || m[2]).strip.tr("\n", ' ').gsub('\]', ']').split(REGEXP[:csv_delimiter]) - document.register(:indexterms, [*terms]) - Inline.new(self, :indexterm, text, :attributes => {'terms' => terms}).render - } - - # indexterm2:[Tigers] - # ((Tigers)) - result.gsub!(REGEXP[:indexterm2_macro]) { - # alias match for Ruby 1.8.7 compat - m = $~ - # honor the escape - if m[0].start_with? '\\' - next m[0][1..-1] - end - - text = (m[1] || m[2]).strip.tr("\n", ' ').gsub('\]', ']') - document.register(:indexterms, [text]) - Inline.new(self, :indexterm, text, :type => :visible).render - } - end - - if found[:uri] - # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>) - result.gsub!(REGEXP[:link_inline]) { - # alias match for Ruby 1.8.7 compat - m = $~ - # honor the escape - if m[2].start_with? '\\' - next "#{m[1]}#{m[2][1..-1]}#{m[3]}" - # not a valid macro syntax w/o trailing square brackets - # we probably shouldn't even get here...our regex is doing too much - elsif m[1] == 'link:' && m[3].nil? - next m[0] - end - prefix = (m[1] != 'link:' ? m[1] : '') - target = m[2] - # strip the <> around the link - if prefix.end_with? '&lt;' - prefix = prefix[0..-5] - end - if target.end_with? '&gt;' - target = target[0..-5] - end - @document.register(:links, target) - text = !m[3].nil? ? sub_attributes(m[3].gsub('\]', ']')) : '' - "#{prefix}#{Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target).render}" - } - end - - if found[:macroish] && result.include?('link:') - # inline link macros, link:target[text] - result.gsub!(REGEXP[:link_macro]) { - # alias match for Ruby 1.8.7 compat - m = $~ - # honor the escape - if m[0].start_with? '\\' - next m[0][1..-1] - end - target = m[1] - @document.register(:links, target) + attrs = nil + #text = sub_attributes(m[2].gsub('\]', ']')) + if link_attrs && (m[2].start_with?('"') || m[2].include?(',')) + attrs = parse_attributes(sub_attributes(m[2].gsub('\]', ']'))) + text = attrs[1] + else text = sub_attributes(m[2].gsub('\]', ']')) - Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target).render - } - end + end - if found[:macroish_short_form] && result.include?('footnote') - result.gsub!(REGEXP[:footnote_macro]) { - # alias match for Ruby 1.8.7 compat - m = $~ - # honor the escape - if m[0].start_with? '\\' - next m[0][1..-1] - end - if m[1] == 'footnote' + Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target, :attributes => attrs).render + } + end + + if found[:macroish_short_form] && result.include?('footnote') + result.gsub!(REGEXP[:footnote_macro]) { + # alias match for Ruby 1.8.7 compat + m = $~ + # honor the escape + if m[0].start_with? '\\' + next m[0][1..-1] + end + if m[1] == 'footnote' + # hmmmm + text = restore_passthroughs(m[2]) + id = nil + index = @document.counter('footnote-number') + @document.register(:footnotes, Document::Footnote.new(index, id, text)) + type = nil + target = nil + else + id, text = m[2].split(REGEXP[:csv_delimiter], 2) + if !text.nil? # hmmmm - text = restore_passthroughs(m[2]) - id = nil + text = restore_passthroughs(text) index = @document.counter('footnote-number') @document.register(:footnotes, Document::Footnote.new(index, id, text)) - type = nil + type = :ref target = nil else - id, text = m[2].split(/ *, */, 2) - if !text.nil? - # hmmmm - text = restore_passthroughs(text) - index = @document.counter('footnote-number') - @document.register(:footnotes, Document::Footnote.new(index, id, text)) - type = :ref - target = nil - else - fn = @document.references[:footnotes].find {|fn| fn.id == id } - target = id - id = nil - index = fn.index - text = fn.text - type = :xref - end + footnote = @document.references[:footnotes].find {|fn| fn.id == id } + target = id + id = nil + index = footnote.index + text = footnote.text + type = :xref end - Inline.new(self, :footnote, text, :attributes => {'index' => index}, :id => id, :target => target, :type => type).render - } - end + end + Inline.new(self, :footnote, text, :attributes => {'index' => index}, :id => id, :target => target, :type => type).render + } + end - if found[:macroish] || result.include?('&lt;&lt;') - result.gsub!(REGEXP[:xref_macro]) { - # alias match for Ruby 1.8.7 compat - m = $~ - # honor the escape - if m[0].start_with? '\\' - next m[0][1..-1] - end - if !m[1].nil? - id, reftext = m[1].split(REGEXP[:csv_delimiter], 2) - id.sub!(/^("|)(.*)\1$/, '\2') - reftext.sub!(/^("|)(.*)\1$/m, '\2') unless reftext.nil? - else - id = m[2] - reftext = !m[3].empty? ? m[3] : nil - end - Inline.new(self, :anchor, reftext, :type => :xref, :target => id).render - } - end - - if found[:square_bracket] && result.include?('[[[') - result.gsub!(REGEXP[:biblio_macro]) { - # alias match for Ruby 1.8.7 compat - m = $~ - # honor the escape - if m[0].start_with? '\\' - next m[0][1..-1] - end - id = reftext = m[1] - Inline.new(self, :anchor, reftext, :type => :bibref, :target => id).render - } - end - - if found[:square_bracket] && result.include?('[[') - result.gsub!(REGEXP[:anchor_macro]) { - # alias match for Ruby 1.8.7 compat - m = $~ - # honor the escape - if m[0].start_with? '\\' - next m[0][1..-1] - end - id, reftext = m[1].split(REGEXP[:csv_delimiter]) - id.sub!(/^("|)(.*)\1$/, '\2') - if reftext.nil? - reftext = "[#{id}]" - else - reftext.sub!(/^("|)(.*)\1$/m, '\2') - end - # NOTE the reftext should also match what's in our references dic - if !@document.references[:ids].has_key? id - Asciidoctor.debug { "Missing reference for anchor #{id}" } - end - Inline.new(self, :anchor, reftext, :type => :ref, :target => id).render - } - end - - result + if found[:macroish] || result.include?('&lt;&lt;') + result.gsub!(REGEXP[:xref_macro]) { + # alias match for Ruby 1.8.7 compat + m = $~ + # honor the escape + if m[0].start_with? '\\' + next m[0][1..-1] + end + if !m[1].nil? + id, reftext = m[1].split(REGEXP[:csv_delimiter], 2) + id.sub!(REGEXP[:dbl_quoted], '\2') + reftext.sub!(REGEXP[:m_dbl_quoted], '\2') unless reftext.nil? + else + id = m[2] + reftext = !m[3].empty? ? m[3] : nil + end + Inline.new(self, :anchor, reftext, :type => :xref, :target => id).render + } end - # Public: Substitute callout references - # - # text - The String text to process - # - # returns The String with the callout references rendered using the backend templates - def sub_callouts(text) - text.gsub(REGEXP[:callout_render]) { + if found[:square_bracket] && result.include?('[[[') + result.gsub!(REGEXP[:biblio_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' - next "&lt;#{m[1]}&gt;" + next m[0][1..-1] end - Inline.new(self, :callout, m[1], :id => document.callouts.read_next_id).render + id = reftext = m[1] + Inline.new(self, :anchor, reftext, :type => :bibref, :target => id).render } end - # Public: Substitute post replacements - # - # text - The String text to process - # - # returns The String with the post replacements rendered using the backend templates - def sub_post_replacements(text) - if @document.attr? 'hardbreaks' - lines = text.lines.entries - return text if lines.size == 1 - last = lines.pop - "#{lines.map {|line| Inline.new(self, :break, line.rstrip.chomp(' +'), :type => :line).render } * "\n"}\n#{last}" - else - text.gsub(REGEXP[:line_break]) { Inline.new(self, :break, $1, :type => :line).render } - end + if found[:square_bracket] && result.include?('[[') + result.gsub!(REGEXP[:anchor_macro]) { + # alias match for Ruby 1.8.7 compat + m = $~ + # honor the escape + if m[0].start_with? '\\' + next m[0][1..-1] + end + id, reftext = m[1].split(REGEXP[:csv_delimiter]) + id.sub!(REGEXP[:dbl_quoted], '\2') + if reftext.nil? + reftext = "[#{id}]" + else + reftext.sub!(REGEXP[:m_dbl_quoted], '\2') + end + # NOTE the reftext should also match what's in our references dic + if !@document.references[:ids].has_key? id + Debug.debug { "Missing reference for anchor #{id}" } + end + Inline.new(self, :anchor, reftext, :type => :ref, :target => id).render + } end - # Internal: Transform (render) a quoted text region - # - # match - The MatchData for the quoted text region - # type - The quoting type (single, double, strong, emphasis, monospaced, etc) - # scope - The scope of the quoting (constrained or unconstrained) - # - # returns The rendered text for the quoted text region - def transform_quoted_text(match, type, scope) - if match[0].start_with? '\\' - match[0][1..-1] - elsif scope == :constrained - "#{match[1]}#{Inline.new(self, :quoted, match[3], :type => type, :attributes => parse_attributes(match[2])).render}" - else - Inline.new(self, :quoted, match[2], :type => type, :attributes => parse_attributes(match[1])).render + result + end + + # Public: Substitute callout references + # + # text - The String text to process + # + # returns The String with the callout references rendered using the backend templates + def sub_callouts(text) + text.gsub(REGEXP[:callout_render]) { + # alias match for Ruby 1.8.7 compat + m = $~ + # honor the escape + if m[0].start_with? '\\' + next "&lt;#{m[1]}&gt;" end - end + Inline.new(self, :callout, m[1], :id => document.callouts.read_next_id).render + } + end - # Internal: Parse the attributes in the attribute line - # - # attrline - A String of unprocessed attributes (key/value pairs) - # posattrs - The keys for positional attributes - # - # returns nil if attrline is nil, an empty Hash if attrline is empty, otherwise a Hash of parsed attributes - def parse_attributes(attrline, posattrs = ['role']) - return nil if attrline.nil? - return {} if attrline.empty? - - AttributeList.new(attrline, self).parse(posattrs) + # Public: Substitute post replacements + # + # text - The String text to process + # + # returns The String with the post replacements rendered using the backend templates + def sub_post_replacements(text) + if @document.attr? 'hardbreaks' + lines = text.lines.entries + return text if lines.size == 1 + last = lines.pop + "#{lines.map {|line| Inline.new(self, :break, line.rstrip.chomp(LINE_BREAK), :type => :line).render } * "\n"}\n#{last}" + else + text.gsub(REGEXP[:line_break]) { Inline.new(self, :break, $1, :type => :line).render } end + end - # Internal: Resolve the list of comma-delimited subs against the possible options. - # - # subs - A comma-delimited String of substitution aliases - # - # returns An Array of Symbols representing the substitution operation - def resolve_subs(subs) - candidates = subs.split(',').map {|sub| sub.strip.to_sym} - resolved = candidates & SUB_OPTIONS - if (invalid = candidates - resolved).size > 0 - puts "asciidoctor: WARNING: invalid passthrough macro substitution operation#{invalid.size > 1 ? 's' : ''}: #{invalid * ', '}" - end - resolved + # Internal: Transform (render) a quoted text region + # + # match - The MatchData for the quoted text region + # type - The quoting type (single, double, strong, emphasis, monospaced, etc) + # scope - The scope of the quoting (constrained or unconstrained) + # + # returns The rendered text for the quoted text region + def transform_quoted_text(match, type, scope) + if match[0].start_with? '\\' + match[0][1..-1] + elsif scope == :constrained + "#{match[1]}#{Inline.new(self, :quoted, match[3], :type => type, :attributes => parse_attributes(match[2])).render}" + else + Inline.new(self, :quoted, match[2], :type => type, :attributes => parse_attributes(match[1])).render end + end - # Public: Highlight the source code if a source highlighter is defined - # on the document, otherwise return the text unprocessed - # - # source - the source code String to highlight - # - # returns the highlighted source code, if a source highlighter is defined - # on the document, otherwise the unprocessed text - def highlight_source(source) - Asciidoctor.require_library 'coderay' - ::CodeRay::Duo[attr('language', 'text').to_sym, :html, { - :css => @document.attr('coderay-css', 'class').to_sym, - :line_numbers => (attr?('linenums') ? @document.attr('coderay-linenums-mode', 'table').to_sym : nil), - :line_number_anchors => false}].highlight(source).chomp - end + # Internal: Parse the attributes in the attribute line + # + # attrline - A String of unprocessed attributes (key/value pairs) + # posattrs - The keys for positional attributes + # + # returns nil if attrline is nil, an empty Hash if attrline is empty, otherwise a Hash of parsed attributes + def parse_attributes(attrline, posattrs = ['role']) + return nil if attrline.nil? + return {} if attrline.empty? + + AttributeList.new(attrline, self).parse(posattrs) end + + # Internal: Resolve the list of comma-delimited subs against the possible options. + # + # subs - A comma-delimited String of substitution aliases + # + # returns An Array of Symbols representing the substitution operation + def resolve_subs(subs) + candidates = subs.split(',').map {|sub| sub.strip.to_sym} + resolved = candidates & SUB_OPTIONS + if (invalid = candidates - resolved).size > 0 + puts "asciidoctor: WARNING: invalid passthrough macro substitution operation#{invalid.size > 1 ? 's' : ''}: #{invalid * ', '}" + end + resolved + end + + # Public: Highlight the source code if a source highlighter is defined + # on the document, otherwise return the text unprocessed + # + # source - the source code String to highlight + # + # returns the highlighted source code, if a source highlighter is defined + # on the document, otherwise the unprocessed text + def highlight_source(source) + Helpers.require_library 'coderay' + ::CodeRay::Duo[attr('language', 'text').to_sym, :html, { + :css => @document.attributes.fetch('coderay-css', 'class').to_sym, + :line_numbers => (attr?('linenums') ? @document.attributes.fetch('coderay-linenums-mode', 'table').to_sym : nil), + :line_number_anchors => false}].highlight(source).chomp + end +end end