module Asciidoctor::Substitutors
def apply_header_subs(text)
text - String containing the text process
Public: Apply substitutions for header metadata and attribute assignments
def apply_header_subs(text) apply_subs text, SUBS[:header] end
def apply_normal_subs(lines)
lines - The lines of text to process. Can be a String or a String Array
Public: Apply normal substitutions.
def apply_normal_subs(lines) apply_subs lines.is_a?(::Array) ? (lines * EOL) : lines end
def apply_subs source, subs = :normal, expand = false
expand - A Boolean to control whether sub aliases are expanded (default: true)
subs - The substitutions to perform. Can be a Symbol or a Symbol Array (default: :normal)
source - The String or String Array of text to process
Public: Apply the specified substitutions to the lines of text
def apply_subs source, subs = :normal, expand = false if !subs return source elsif subs == :normal subs = SUBS[:normal] elsif expand if subs.is_a? ::Symbol subs = COMPOSITE_SUBS[subs] || [subs] else effective_subs = [] subs.each do |key| if COMPOSITE_SUBS.has_key? key effective_subs += COMPOSITE_SUBS[key] else effective_subs << key end end subs = effective_subs end end return source if subs.empty? text = (multiline = source.is_a? ::Array) ? (source * EOL) : source if (has_passthroughs = subs.include? :macros) text = extract_passthroughs text has_passthroughs = false if @passthroughs.empty? end subs.each do |type| case type when :specialcharacters text = sub_specialcharacters text when :quotes text = sub_quotes text when :attributes text = sub_attributes(text.split EOL) * EOL when :replacements text = sub_replacements text when :macros text = sub_macros text when :highlight text = highlight_source text, (subs.include? :callouts) when :callouts text = sub_callouts text unless subs.include? :highlight when :post_replacements text = sub_post_replacements text else warn %(asciidoctor: WARNING: unknown substitution type #{type}) end end text = restore_passthroughs text if has_passthroughs multiline ? (text.split EOL) : text end
def apply_title_subs(title)
title - The String title to process
Public: Apply substitutions for titles.
def apply_title_subs(title) apply_subs title, SUBS[:title] end
def convert_quoted_text(match, type, scope)
scope - The scope of the quoting (constrained or unconstrained)
type - The quoting type (single, double, strong, emphasis, monospaced, etc)
match - The MatchData for the quoted text region
Internal: Convert a quoted text region
def convert_quoted_text(match, type, scope) unescaped_attrs = nil if match[0].start_with? '\\' if scope == :constrained && !(attrs = match[2]).nil_or_empty? unescaped_attrs = %([#{attrs}]) else return match[0][1..-1] end end if scope == :constrained if unescaped_attrs %(#{unescaped_attrs}#{Inline.new(self, :quoted, match[3], :type => type).convert}) else if (attributes = parse_quoted_text_attributes(match[2])) id = attributes.delete 'id' type = :unquoted if type == :mark else id = nil end %(#{match[1]}#{Inline.new(self, :quoted, match[3], :type => type, :id => id, :attributes => attributes).convert}) end else if (attributes = parse_quoted_text_attributes(match[1])) id = attributes.delete 'id' type = :unquoted if type == :mark else id = nil end Inline.new(self, :quoted, match[2], :type => type, :id => id, :attributes => attributes).convert end end
def do_replacement m, replacement, restore
Internal: Substitute replacement text for matched location
def do_replacement m, replacement, restore if (matched = m[0]).include? '\\' matched.tr '\\', '' else case restore when :none replacement when :leading %(#{m[1]}#{replacement}) when :bounding %(#{m[1]}#{replacement}#{m[2]}) end end end
def extract_passthroughs(text)
text - The String from which to extract passthrough fragements
Internal: Extract the passthrough text from the document for reinsertion after processing.
def extract_passthroughs(text) compat_mode = @document.compat_mode text = text.gsub(PassInlineMacroRx) { # alias match for Ruby 1.8.7 compat m = $~ preceding = nil if (boundary = m[4]).nil_or_empty? # pass:[] if m[6] == '\\' # NOTE we don't look for nested pass:[] macros next m[0][1..-1] end @passthroughs[pass_key = @passthroughs.size] = {:text => (unescape_brackets m[8]), :subs => (m[7].nil_or_empty? ? [] : (resolve_pass_subs m[7]))} else # $$, ++ or +++ # skip ++ in compat mode, handled as normal quoted text if compat_mode && boundary == '++' next m[2].nil_or_empty? ? %(#{m[1]}#{m[3]}++#{extract_passthroughs m[5]}++) : %(#{m[1]}[#{m[2]}]#{m[3]}++#{extract_passthroughs m[5]}++) end attributes = m[2] # fix non-matching group results in Opal under Firefox if ::RUBY_ENGINE_OPAL attributes = nil if attributes == '' end escape_count = m[3].size content = m[5] old_behavior = false if attributes if escape_count > 0 # NOTE we don't look for nested unconstrained pass macros # must enclose string following next in " for Opal next "#{m[1]}[#{attributes}]#{'\\' * (escape_count - 1)}#{boundary}#{m[5]}#{boundary})" elsif m[1] == '\\' preceding = %([#{attributes}]) attributes = nil else if boundary == '++' && (attributes.end_with? 'x-') old_behavior = true attributes = attributes[0...-2] end attributes = parse_attributes attributes end elsif escape_count > 0 # NOTE we don't look for nested unconstrained pass macros # must enclose string following next in " for Opal next "#{m[1]}[#{attributes}]#{'\\' * (escape_count - 1)}#{boundary}#{m[5]}#{boundary}" end subs = (boundary == '+++' ? [] : [:specialcharacters]) pass_key = @passthroughs.size if attributes if old_behavior @passthroughs[pass_key] = {:text => content, :subs => SUBS[:normal], :type => :monospaced, :attributes => attributes} else @passthroughs[pass_key] = {:text => content, :subs => subs, :type => :unquoted, :attributes => attributes} end else @passthroughs[pass_key] = {:text => content, :subs => subs} end end %(#{preceding}#{PASS_START}#{pass_key}#{PASS_END}) } if (text.include? '++') || (text.include? '$$') || (text.include? 'ss:') pass_inline_char1, pass_inline_char2, pass_inline_rx = PassInlineRx[compat_mode] text = text.gsub(pass_inline_rx) { # alias match for Ruby 1.8.7 compat m = $~ preceding = m[1] attributes = m[2] escape_mark = (m[3].start_with? '\\') ? '\\' : nil format_mark = m[4] content = m[5] # fix non-matching group results in Opal under Firefox if ::RUBY_ENGINE_OPAL attributes = nil if attributes == '' end if compat_mode old_behavior = true else if (old_behavior = (attributes && (attributes.end_with? 'x-'))) attributes = attributes[0...-2] end end if attributes if format_mark == '`' && !old_behavior # must enclose string following next in " for Opal next "#{preceding}[#{attributes}]#{escape_mark}`#{extract_passthroughs content}`" end if escape_mark # honor the escape of the formatting mark (must enclose string following next in " for Opal) next "#{preceding}[#{attributes}]#{m[3][1..-1]}" elsif preceding == '\\' # honor the escape of the attributes preceding = %([#{attributes}]) attributes = nil else attributes = parse_attributes attributes end elsif format_mark == '`' && !old_behavior # must enclose string following next in " for Opal next "#{preceding}#{escape_mark}`#{extract_passthroughs content}`" elsif escape_mark # honor the escape of the formatting mark (must enclose string following next in " for Opal) next "#{preceding}#{m[3][1..-1]}" end pass_key = @passthroughs.size if compat_mode @passthroughs[pass_key] = {:text => content, :subs => [:specialcharacters], :attributes => attributes, :type => :monospaced} elsif attributes if old_behavior subs = (format_mark == '`' ? [:specialcharacters] : SUBS[:normal]) @passthroughs[pass_key] = {:text => content, :subs => subs, :attributes => attributes, :type => :monospaced} else @passthroughs[pass_key] = {:text => content, :subs => [:specialcharacters], :attributes => attributes, :type => :unquoted} end else @passthroughs[pass_key] = {:text => content, :subs => [:specialcharacters]} end %(#{preceding}#{PASS_START}#{pass_key}#{PASS_END}) } if (text.include? pass_inline_char1) || (pass_inline_char2 && (text.include? pass_inline_char2)) # NOTE we need to do the stem in a subsequent step to allow it to be escaped by the former text = text.gsub(StemInlineMacroRx) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end if (type = m[1].to_sym) == :stem type = ((default_stem_type = document.attributes['stem']).nil_or_empty? ? 'asciimath' : default_stem_type).to_sym end content = unescape_brackets m[3] if m[2].nil_or_empty? subs = (@document.basebackend? 'html') ? [:specialcharacters] : [] else subs = resolve_pass_subs m[2] end @passthroughs[pass_key = @passthroughs.size] = {:text => content, :subs => subs, :type => type} %(#{PASS_START}#{pass_key}#{PASS_END}) } if (text.include? ':') && ((text.include? 'stem:') || (text.include? 'math:')) text end
def highlight_source(source, process_callouts, highlighter = nil)
returns the highlighted source code, if a source highlighter is defined
process_callouts - a Boolean flag indicating whether callout marks should be substituted
source - the source code String to highlight
incorrectly processed by the source highlighter.
highlighter, then later restored in converted form, so they are not
Callout marks are stripped from the source prior to passing it to the
on the document, otherwise return the text unprocessed
Public: Highlight the source code if a source highlighter is defined
def highlight_source(source, process_callouts, highlighter = nil) highlighter ||= @document.attributes['source-highlighter'] Helpers.require_library highlighter, (highlighter == 'pygments' ? 'pygments.rb' : highlighter) lineno = 0 callout_on_last = false if process_callouts callout_marks = {} last = -1 # FIXME cache this dynamic regex callout_rx = (attr? 'line-comment') ? /(?:#{::Regexp.escape(attr 'line-comment')} )?#{CalloutExtractRxt}/ : CalloutExtractRx # extract callout marks, indexed by line number source = source.split(EOL).map {|line| lineno = lineno + 1 line.gsub(callout_rx) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[1] == '\\' m[0].sub('\\', '') else (callout_marks[lineno] ||= []) << m[3] last = lineno nil end } } * EOL callout_on_last = (last == lineno) callout_marks = nil if callout_marks.empty? else callout_marks = nil end linenums_mode = nil case highlighter when 'coderay' result = ::CodeRay::Duo[attr('language', :text, false).to_sym, :html, { :css => (@document.attributes['coderay-css'] || :class).to_sym, :line_numbers => (linenums_mode = ((attr? 'linenums') ? (@document.attributes['coderay-linenums-mode'] || :table).to_sym : nil)), :line_number_anchors => false}].highlight source when 'pygments' lexer = ::Pygments::Lexer[attr('language', nil, false)] || ::Pygments::Lexer['text'] opts = { :cssclass => 'pyhl', :classprefix => 'tok-', :nobackground => true } unless (@document.attributes['pygments-css'] || 'class') == 'class' opts[:noclasses] = true opts[:style] = (@document.attributes['pygments-style'] || Stylesheets::DEFAULT_PYGMENTS_STYLE) end if attr? 'linenums' # TODO we could add the line numbers in ourselves instead of having to strip out the junk # FIXME move these regular expressions into constants if (opts[:linenos] = @document.attributes['pygments-linenums-mode'] || 'table') == 'table' linenums_mode = :table # NOTE these subs clean out HTML that messes up our styles result = lexer.highlight(source, :options => opts). sub(/<div class="pyhl">(.*)<\/div>/m, '\1'). gsub(/<pre[^>]*>(.*?)<\/pre>\s*/m, '\1') else result = lexer.highlight(source, :options => opts). sub(/<div class="pyhl"><pre[^>]*>(.*?)<\/pre><\/div>/m, '\1') end else # nowrap gives us just the highlighted source; won't work when we need linenums though opts[:nowrap] = true result = lexer.highlight(source, :options => opts) end end # fix passthrough placeholders that got caught up in syntax highlighting unless @passthroughs.empty? result = result.gsub PASS_MATCH_HI, %(#{PASS_START}\\1#{PASS_END}) end if process_callouts && callout_marks lineno = 0 reached_code = linenums_mode != :table result.split(EOL).map {|line| unless reached_code unless line.include?('<td class="code">') next line end reached_code = true end lineno = lineno + 1 if (conums = callout_marks.delete(lineno)) tail = nil if callout_on_last && callout_marks.empty? # QUESTION when does this happen? if (pos = line.index '</pre>') tail = line[pos..-1] line = %(#{line[0...pos].chomp ' '} ) else # Give conum on final line breathing room if trailing space in source is dropped line = %(#{line.chomp ' '} ) end end if conums.size == 1 %(#{line}#{Inline.new(self, :callout, conums[0], :id => @document.callouts.read_next_id).convert }#{tail}) else conums_markup = conums.map {|conum| Inline.new(self, :callout, conum, :id => @document.callouts.read_next_id).convert } * ' ' %(#{line}#{conums_markup}#{tail}) end else line end } * EOL else result end end
def lock_in_subs
content model of the block.
Otherwise, assigns a set of default substitutions based on the
substitutions and assigns it to the subs property on this block.
Looks for an attribute named "subs". If present, resolves the
Internal: Lock-in the substitutions for this block
def lock_in_subs if @default_subs default_subs = @default_subs else case @content_model when :simple default_subs = SUBS[:normal] when :verbatim if @context == :listing || (@context == :literal && !(option? 'listparagraph')) default_subs = SUBS[:verbatim] elsif @context == :verse default_subs = SUBS[:normal] else default_subs = SUBS[:basic] end when :raw if @context == :stem default_subs = SUBS[:basic] else default_subs = SUBS[:pass] end else return end end if (custom_subs = @attributes['subs']) @subs = resolve_block_subs custom_subs, default_subs, @context else @subs = default_subs.dup end # QUESION delegate this logic to a method? if @context == :listing && @style == 'source' && @attributes['language'] && @document.basebackend?('html') && SUB_HIGHLIGHT.include?(@document.attributes['source-highlighter']) @subs = @subs.map {|sub| sub == :specialcharacters ? :highlight : sub } end end
def normalize_string str, unescape_brackets = false
def normalize_string str, unescape_brackets = false if str.empty? '' elsif unescape_brackets unescape_brackets str.strip.tr(EOL, ' ') else str.strip.tr(EOL, ' ') end end
def parse_attributes(attrline, posattrs = ['role'], opts = {})
posattrs - The keys for positional attributes
attrline - A String of unprocessed attributes (key/value pairs)
Internal: Parse the attributes in the attribute line
def parse_attributes(attrline, posattrs = ['role'], opts = {}) return unless attrline return {} if attrline.empty? attrline = @document.sub_attributes(attrline) if opts[:sub_input] attrline = unescape_bracketed_text(attrline) if opts[:unescape_input] block = nil if opts.fetch(:sub_result, true) # substitutions are only performed on attribute values if block is not nil block = self end if (into = opts[:into]) AttributeList.new(attrline, block).parse_into(into, posattrs) else AttributeList.new(attrline, block).parse(posattrs) end end
def parse_quoted_text_attributes(str)
str - A String of unprocessed attributes (space-separated roles or the id/role shorthand syntax)
Internal: Parse the attributes that are defined on quoted text
def parse_quoted_text_attributes(str) return unless str return {} if str.empty? str = sub_attributes(str) if str.include?('{') str = str.strip # for compliance, only consider first positional attribute str, _ = str.split(',', 2) if str.include?(',') if str.empty? {} elsif (str.start_with?('.') || str.start_with?('#')) && Compliance.shorthand_property_syntax segments = str.split('#', 2) if segments.length > 1 id, *more_roles = segments[1].split('.') else id = nil more_roles = [] end roles = segments[0].empty? ? [] : segments[0].split('.') if roles.length > 1 roles.shift end if more_roles.length > 0 roles.concat more_roles end attrs = {} attrs['id'] = id if id attrs['role'] = roles * ' ' unless roles.empty? attrs else {'role' => str} end end
def resolve_block_subs subs, defaults, subject
def resolve_block_subs subs, defaults, subject resolve_subs subs, :block, defaults, subject end
def resolve_pass_subs subs
def resolve_pass_subs subs resolve_subs subs, :inline, nil, 'passthrough macro' end
def resolve_subs subs, type = :block, defaults = nil, subject = nil
subs - A comma-delimited String of substitution aliases
Internal: Resolve the list of comma-delimited subs against the possible options.
def resolve_subs subs, type = :block, defaults = nil, subject = nil return [] if subs.nil_or_empty? candidates = nil modifiers_present = SubModifierSniffRx =~ subs subs.split(',').each do |val| key = val.strip modifier_operation = nil if modifiers_present if (first = key.chr) == '+' modifier_operation = :append key = key[1..-1] elsif first == '-' modifier_operation = :remove key = key[1..-1] elsif key.end_with? '+' modifier_operation = :prepend key = key.chop end end key = key.to_sym # special case to disable callouts for inline subs if type == :inline && (key == :verbatim || key == :v) resolved_keys = [:specialcharacters] elsif COMPOSITE_SUBS.key? key resolved_keys = COMPOSITE_SUBS[key] elsif type == :inline && key.length == 1 && (SUB_SYMBOLS.key? key) resolved_key = SUB_SYMBOLS[key] if (candidate = COMPOSITE_SUBS[resolved_key]) resolved_keys = candidate else resolved_keys = [resolved_key] end else resolved_keys = [key] end if modifier_operation candidates ||= (defaults ? defaults.dup : []) case modifier_operation when :append candidates += resolved_keys when :prepend candidates = resolved_keys + candidates when :remove candidates -= resolved_keys end else candidates ||= [] candidates += resolved_keys end end # weed out invalid options and remove duplicates (first wins) # TODO may be use a set instead? resolved = candidates & SUB_OPTIONS[type] unless (candidates - resolved).empty? invalid = candidates - resolved warn %(asciidoctor: WARNING: invalid substitution type#{invalid.size > 1 ? 's' : ''}#{subject ? ' for ' : nil}#{subject}: #{invalid * ', '}) end resolved end
def restore_passthroughs text, outer = true
outer - A Boolean indicating whether we are in the outer call (default: true)
text - The String text into which to restore the passthrough text
Internal: Restore the passthrough text by reinserting into the placeholder positions
def restore_passthroughs text, outer = true if outer && (@passthroughs.empty? || !text.include?(PASS_START)) return text end text.gsub(PASS_MATCH) { # NOTE we can't remove entry from map because placeholder may have been duplicated by other substitutions pass = @passthroughs[$~[1].to_i] subbed_text = (subs = pass[:subs]) ? apply_subs(pass[:text], subs) : pass[:text] if (type = pass[:type]) subbed_text = Inline.new(self, :quoted, subbed_text, :type => type, :attributes => pass[:attributes]).convert end subbed_text.include?(PASS_START) ? restore_passthroughs(subbed_text, false) : subbed_text } ensure # free memory if in outer call...we don't need these anymore @passthroughs.clear if outer end
def split_simple_csv str
Internal: Split text formatted as CSV with support
def split_simple_csv str if str.empty? values = [] elsif str.include? '"' values = [] current = [] quote_open = false str.each_char do |c| case c when ',' if quote_open current.push c else values << current.join.strip current = [] end when '"' quote_open = !quote_open else current.push c end end values << current.join.strip else values = str.split(',').map {|it| it.strip } end values end
def sub_attributes data, opts = {}
so that a missing key doesn't wipe out the whole block of data
NOTE it's necessary to perform this substitution line-by-line
--
returns The String text with the attribute references replaced with attribute values
text - The String text to process
If an attribute referenced in the line is missing, the line is dropped.
Attribute references are in the format +{name}+.
Public: Substitute attribute references
def sub_attributes data, opts = {} return data if data.nil_or_empty? # normalizes data type to an array (string becomes single-element array) if (string_data = String === data) data = [data] end doc_attrs = @document.attributes attribute_missing = nil result = [] data.each do |line| reject = false reject_if_empty = false line = line.gsub(AttributeReferenceRx) { # alias match for Ruby 1.8.7 compat m = $~ # escaped attribute, return unescaped if m[1] == '\\' || m[4] == '\\' %({#{m[2]}}) elsif !m[3].nil_or_empty? offset = (directive = m[3]).length + 1 expr = m[2][offset..-1] case directive when 'set' args = expr.split(':') _, value = Parser.store_attribute(args[0], args[1] || '', @document) unless value # since this is an assignment, only drop-line applies here (skip and drop imply the same result) if doc_attrs.fetch('attribute-undefined', Compliance.attribute_undefined) == 'drop-line' reject = true break '' end end reject_if_empty = true '' when 'counter', 'counter2' args = expr.split(':') val = @document.counter(args[0], args[1]) if directive == 'counter2' reject_if_empty = true '' else val end else # if we get here, our AttributeReference regex is too loose warn %(asciidoctor: WARNING: illegal attribute directive: #{m[3]}) m[0] end elsif doc_attrs.key?(key = m[2].downcase) doc_attrs[key] elsif INTRINSIC_ATTRIBUTES.key? key INTRINSIC_ATTRIBUTES[key] else case (attribute_missing ||= (opts[:attribute_missing] || doc_attrs.fetch('attribute-missing', Compliance.attribute_missing))) when 'skip' m[0] when 'drop-line' warn %(asciidoctor: WARNING: dropping line containing reference to missing attribute: #{key}) reject = true break '' when 'warn' warn %(asciidoctor: WARNING: skipping reference to missing attribute: #{key}) m[0] else # 'drop' # QUESTION should we warn in this case? reject_if_empty = true '' end end } if line.include? '{' result << line unless reject || (reject_if_empty && line.empty?) end string_data ? (result * EOL) : result end
def sub_callouts(text)
text - The String text to process
Public: Substitute callout source references
def sub_callouts(text) # FIXME cache this dynamic regex callout_rx = (attr? 'line-comment') ? /(?:#{::Regexp.escape(attr 'line-comment')} )?#{CalloutSourceRxt}/ : CalloutSourceRx text.gsub(callout_rx) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[1] == '\\' # we have to do a sub since we aren't sure it's the first char next m[0].sub('\\', '') end Inline.new(self, :callout, m[3], :id => @document.callouts.read_next_id).convert } end
def sub_inline_anchors(text, found = nil)
def sub_inline_anchors(text, found = nil) if (!found || found[:square_bracket]) && text.include?('[[[') text = text.gsub(InlineBiblioAnchorRx) { # 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).convert } end if ((!found || found[:square_bracket]) && text.include?('[[')) || ((!found || found[:macroish]) && text.include?('anchor:')) text = text.gsub(InlineAnchorRx) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end # fix non-matching group results in Opal under Firefox if ::RUBY_ENGINE_OPAL m[1] = nil if m[1] == '' m[2] = nil if m[2] == '' m[4] = nil if m[4] == '' end id = m[1] || m[3] reftext = m[2] || m[4] || %([#{id}]) # enable if we want to allow double quoted values #id = id.sub(DoubleQuotedRx, '\2') #if reftext # reftext = reftext.sub(DoubleQuotedMultiRx, '\2') #else # reftext = "[#{id}]" #end Inline.new(self, :anchor, reftext, :type => :ref, :target => id).convert } end text end
def sub_inline_xrefs(text, found = nil)
def sub_inline_xrefs(text, found = nil) if (!found || found[:macroish]) || text.include?('<<') text = text.gsub(XrefInlineMacroRx) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end # fix non-matching group results in Opal under Firefox if ::RUBY_ENGINE_OPAL m[1] = nil if m[1] == '' end if m[1] id, reftext = m[1].split(',', 2).map {|it| it.strip } id = id.sub(DoubleQuotedRx, '\2') # NOTE In Opal, reftext is set to empty string if comma is missing reftext = if reftext.nil_or_empty? nil else reftext.sub(DoubleQuotedMultiRx, '\2') end else id = m[2] reftext = m[3] unless m[3].nil_or_empty? end if id.include? '#' path, fragment = id.split('#') else path = nil fragment = id end # handles forms: doc#, doc.adoc#, doc#id and doc.adoc#id if path path = Helpers.rootname(path) # the referenced path is this document, or its contents has been included in this document if @document.attributes['docname'] == path || @document.references[:includes].include?(path) refid = fragment path = nil target = %(##{fragment}) else refid = fragment ? %(#{path}##{fragment}) : path path = "#{@document.attributes['relfileprefix']}#{path}#{@document.attributes.fetch 'outfilesuffix', '.html'}" target = fragment ? %(#{path}##{fragment}) : path end # handles form: id or Section Title else # resolve fragment as reftext if cannot be resolved as refid and looks like reftext if !(@document.references[:ids].has_key? fragment) && ((fragment.include? ' ') || fragment.downcase != fragment) && (resolved_id = RUBY_MIN_VERSION_1_9 ? (@document.references[:ids].key fragment) : (@document.references[:ids].index fragment)) fragment = resolved_id end refid = fragment target = %(##{fragment}) end Inline.new(self, :anchor, reftext, :type => :xref, :target => target, :attributes => {'path' => path, 'fragment' => fragment, 'refid' => refid}).convert } end text end
def sub_macros(source)
source - The String text to process
Replace inline macros, which may span multiple lines, in the provided text
Public: Substitute inline macros (e.g., links, images, etc)
def sub_macros(source) return source if source.nil_or_empty? # some look ahead assertions to cut unnecessary regex calls found = {} found[:square_bracket] = source.include?('[') found[:round_bracket] = source.include?('(') found[:colon] = found_colon = source.include?(':') found[:macroish] = (found[:square_bracket] && found_colon) found[:macroish_short_form] = (found[:square_bracket] && found_colon && source.include?(':[')) use_link_attrs = @document.attributes.has_key?('linkattrs') experimental = @document.attributes.has_key?('experimental') # NOTE interpolation is faster than String#dup result = %(#{source}) if experimental if found[:macroish_short_form] && (result.include?('kbd:') || result.include?('btn:')) result = result.gsub(KbdBtnInlineMacroRx) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if (captured = m[0]).start_with? '\\' next captured[1..-1] end if captured.start_with?('kbd') keys = unescape_bracketed_text m[1] if keys == '+' keys = ['+'] else # need to use closure to work around lack of negative lookbehind keys = keys.split(KbdDelimiterRx).inject([]) {|c, key| if key.end_with?('++') c << key[0..-3].strip c << '+' else c << key.strip end c } end Inline.new(self, :kbd, nil, :attributes => {'keys' => keys}).convert elsif captured.start_with?('btn') label = unescape_bracketed_text m[1] Inline.new(self, :button, label).convert end } end if found[:macroish] && result.include?('menu:') result = result.gsub(MenuInlineMacroRx) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if (captured = m[0]).start_with? '\\' next captured[1..-1] end menu = m[1] items = m[2] if !items submenus = [] menuitem = nil else if (delim = items.include?('>') ? '>' : (items.include?(',') ? ',' : nil)) submenus = items.split(delim).map {|it| it.strip } menuitem = submenus.pop else submenus = [] menuitem = items.rstrip end end Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).convert } end if result.include?('"') && result.include?('>') result = result.gsub(MenuInlineRx) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if (captured = m[0]).start_with? '\\' next captured[1..-1] end input = m[1] menu, *submenus = input.split('>').map {|it| it.strip } menuitem = submenus.pop Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).convert } end end # FIXME this location is somewhat arbitrary, probably need to be able to control ordering # TODO this handling needs some cleanup if (extensions = @document.extensions) && extensions.inline_macros? # && found[:macroish] extensions.inline_macros.each do |extension| result = result.gsub(extension.config[:regexp]) { # 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] attributes = if extension.config[:format] == :short {} else if extension.config[:content_model] == :attributes parse_attributes m[2], (extension.config[:pos_attrs] || []), :sub_input => true, :unescape_input => true else { 'text' => (unescape_bracketed_text m[2]) } end end extension.process_method[self, target, attributes] } end end if found[:macroish] && (result.include?('image:') || result.include?('icon:')) # image:filename.png[Alt Text] result = result.gsub(ImageInlineMacroRx) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end raw_attrs = unescape_bracketed_text m[2] if m[0].start_with? 'icon:' type = 'icon' posattrs = ['size'] else type = 'image' posattrs = ['alt', 'width', 'height'] end target = sub_attributes(m[1]) unless type == 'icon' @document.register(:images, target) end attrs = parse_attributes(raw_attrs, posattrs) attrs['alt'] ||= File.basename(target, File.extname(target)) Inline.new(self, :image, nil, :type => type, :target => target, :attributes => attrs).convert } end if found[:macroish_short_form] || found[:round_bracket] # indexterm:[Tigers,Big cats] # (((Tigers,Big cats))) # indexterm2:[Tigers] # ((Tigers)) result = result.gsub(IndextermInlineMacroRx) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end # fix non-matching group results in Opal under Firefox if ::RUBY_ENGINE_OPAL m[1] = nil if m[1] == '' end num_brackets = 0 text_in_brackets = nil unless (macro_name = m[1]) text_in_brackets = m[3] if (text_in_brackets.start_with? '(') && (text_in_brackets.end_with? ')') text_in_brackets = text_in_brackets[1...-1] num_brackets = 3 else num_brackets = 2 end end # non-visible if macro_name == 'indexterm' || num_brackets == 3 if !macro_name # (((Tigers,Big cats))) terms = split_simple_csv normalize_string(text_in_brackets) else # indexterm:[Tigers,Big cats] terms = split_simple_csv normalize_string(m[2], true) end @document.register(:indexterms, [*terms]) Inline.new(self, :indexterm, nil, :attributes => {'terms' => terms}).convert # visible else if !macro_name # ((Tigers)) text = normalize_string text_in_brackets else # indexterm2:[Tigers] text = normalize_string m[2], true end @document.register(:indexterms, [text]) Inline.new(self, :indexterm, text, :type => :visible).convert end } end if found_colon && (result.include? '://') # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>) result = result.gsub(LinkInlineRx) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[2].start_with? '\\' # must enclose string following next in " for Opal next "#{m[1]}#{m[2][1..-1]}#{m[3]}" end # fix non-matching group results in Opal under Firefox if ::RUBY_ENGINE_OPAL m[3] = nil if m[3] == '' end # not a valid macro syntax w/o trailing square brackets # we probably shouldn't even get here...our regex is doing too much if m[1] == 'link:' && !m[3] next m[0] end prefix = (m[1] != 'link:' ? m[1] : '') target = m[2] suffix = '' unless m[3] || target !~ UriTerminator case $~[0] when ')' # strip the trailing ) target = target[0..-2] suffix = ')' when ';' # strip the <> around the link if prefix.start_with?('<') && target.end_with?('>') prefix = prefix[4..-1] target = target[0..-5] # strip the ); from the end of the link elsif target.end_with?(');') target = target[0..-3] suffix = ');' else target = target[0..-2] suffix = ';' end when ':' # strip the ): from the end of the link if target.end_with?('):') target = target[0..-3] suffix = '):' else target = target[0..-2] suffix = ':' end end end @document.register(:links, target) link_opts = { :type => :link, :target => target } attrs = nil #text = m[3] ? sub_attributes(m[3].gsub('\]', ']')) : '' if m[3].nil_or_empty? text = '' else if use_link_attrs && (m[3].start_with?('"') || (m[3].include?(',') && m[3].include?('='))) attrs = parse_attributes(sub_attributes(m[3].gsub('\]', ']')), []) link_opts[:id] = (attrs.delete 'id') if attrs.has_key? 'id' text = attrs[1] || '' else text = sub_attributes(m[3].gsub('\]', ']')) end # TODO enable in Asciidoctor 1.5.1 # support pipe-separated text and title #unless attrs && (attrs.has_key? 'title') # if text.include? '|' # attrs ||= {} # text, attrs['title'] = text.split '|', 2 # end #end if text.end_with? '^' text = text.chop if attrs attrs['window'] ||= '_blank' else attrs = {'window' => '_blank'} end end end if text.empty? text = if @document.attr? 'hide-uri-scheme' target.sub UriSniffRx, '' else target end if attrs attrs['role'] = %(bare #{attrs['role']}).chomp ' ' else attrs = {'role' => 'bare'} end end link_opts[:attributes] = attrs if attrs %(#{prefix}#{Inline.new(self, :anchor, text, link_opts).convert}#{suffix}) } end if found[:macroish] && (result.include? 'link:') || (result.include? 'mailto:') # inline link macros, link:target[text] result = result.gsub(LinkInlineMacroRx) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end raw_target = m[1] mailto = m[0].start_with?('mailto:') target = mailto ? %(mailto:#{raw_target}) : raw_target link_opts = { :type => :link, :target => target } attrs = nil #text = sub_attributes(m[2].gsub('\]', ']')) text = if use_link_attrs && (m[2].start_with?('"') || m[2].include?(',')) attrs = parse_attributes(sub_attributes(m[2].gsub('\]', ']')), []) link_opts[:id] = (attrs.delete 'id') if attrs.key? 'id' if mailto if attrs.key? 2 target = link_opts[:target] = "#{target}?subject=#{Helpers.encode_uri(attrs[2])}" if attrs.key? 3 target = link_opts[:target] = "#{target}&body=#{Helpers.encode_uri(attrs[3])}" end end end attrs[1] else sub_attributes(m[2].gsub('\]', ']')) end # QUESTION should a mailto be registered as an e-mail address? @document.register(:links, target) # TODO enable in Asciidoctor 1.5.1 # support pipe-separated text and title #unless attrs && (attrs.key? 'title') # if text.include? '|' # attrs ||= {} # text, attrs['title'] = text.split '|', 2 # end #end if text.end_with? '^' text = text.chop if attrs attrs['window'] ||= '_blank' else attrs = {'window' => '_blank'} end end if text.empty? # mailto is a special case, already processed if mailto text = raw_target else if @document.attr? 'hide-uri-scheme' text = raw_target.sub UriSniffRx, '' else text = raw_target end if attrs attrs['role'] = %(bare #{attrs['role']}).chomp ' ' else attrs = {'role' => 'bare'} end end end link_opts[:attributes] = attrs if attrs Inline.new(self, :anchor, text, link_opts).convert } end if result.include? '@' result = result.gsub(EmailInlineMacroRx) { # alias match for Ruby 1.8.7 compat m = $~ address = m[0] if (lead = m[1]) case lead when '\\' next address[1..-1] else next address end end target = %(mailto:#{address}) # QUESTION should this be registered as an e-mail address? @document.register(:links, target) Inline.new(self, :anchor, address, :type => :link, :target => target).convert } end if found[:macroish_short_form] && result.include?('footnote') result = result.gsub(FootnoteInlineMacroRx) { # 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' id = nil # REVIEW it's a dirty job, but somebody's gotta do it text = restore_passthroughs(sub_inline_xrefs(sub_inline_anchors(normalize_string m[2], true)), false) index = @document.counter('footnote-number') @document.register(:footnotes, Document::Footnote.new(index, id, text)) type = nil target = nil else id, text = m[2].split(',', 2) id = id.strip # NOTE In Opal, text is set to empty string if comma is missing if text.nil_or_empty? if (footnote = @document.references[:footnotes].find {|fn| fn.id == id }) index = footnote.index text = footnote.text else index = nil text = id end target = id id = nil type = :xref else # REVIEW it's a dirty job, but somebody's gotta do it text = restore_passthroughs(sub_inline_xrefs(sub_inline_anchors(normalize_string text, true)), false) index = @document.counter('footnote-number') @document.register(:footnotes, Document::Footnote.new(index, id, text)) type = :ref target = nil end end Inline.new(self, :footnote, text, :attributes => {'index' => index}, :id => id, :target => target, :type => type).convert } end sub_inline_xrefs(sub_inline_anchors(result, found), found) end
def sub_post_replacements(text)
text - The String text to process
Public: Substitute post replacements
def sub_post_replacements(text) if (@document.attributes.has_key? 'hardbreaks') || (@attributes.has_key? 'hardbreaks-option') lines = (text.split EOL) return text if lines.size == 1 last = lines.pop lines.map {|line| Inline.new(self, :break, line.rstrip.chomp(LINE_BREAK), :type => :line).convert }.push(last) * EOL elsif text.include? '+' text.gsub(LineBreakRx) { Inline.new(self, :break, $~[1], :type => :line).convert } else text end end
def sub_quotes(text)
text - The String text to process
Public: Substitute quoted text (includes emphasis, strong, monospaced, etc)
def sub_quotes(text) if ::RUBY_ENGINE_OPAL result = text QUOTE_SUBS[@document.compat_mode].each {|type, scope, pattern| result = result.gsub(pattern) { convert_quoted_text $~, type, scope } } else # NOTE interpolation is faster than String#dup result = %(#{text}) # NOTE using gsub! here as an MRI Ruby optimization QUOTE_SUBS[@document.compat_mode].each {|type, scope, pattern| result.gsub!(pattern) { convert_quoted_text $~, type, scope } } end result end
def sub_replacements(text)
text - The String text to process
Public: Substitute replacement characters (e.g., copyright, trademark, etc)
def sub_replacements(text) if ::RUBY_ENGINE_OPAL result = text REPLACEMENTS.each {|pattern, replacement, restore| result = result.gsub(pattern) { do_replacement $~, replacement, restore } } else # NOTE interpolation is faster than String#dup result = %(#{text}) # NOTE Using gsub! as optimization REPLACEMENTS.each {|pattern, replacement, restore| result.gsub!(pattern) { do_replacement $~, replacement, restore } } end result end
def sub_specialcharacters(text)
text - The String text to process
Special characters are defined in the Asciidoctor::SPECIAL_CHARS Array constant
Public: Substitute special characters (i.e., encode XML)
def sub_specialcharacters(text) SUPPORTS_GSUB_RESULT_HASH ? text.gsub(SPECIAL_CHARS_PATTERN, SPECIAL_CHARS) : text.gsub(SPECIAL_CHARS_PATTERN) { SPECIAL_CHARS[$&] } end
def unescape_bracketed_text(text)
Internal: Strip bounding whitespace, fold endlines and unescaped closing
def unescape_bracketed_text(text) return '' if text.empty? # FIXME make \] a regex text.strip.tr(EOL, ' ').gsub('\]', ']') end
def unescape_brackets str
Internal: Unescape closing square brackets.
def unescape_brackets str # FIXME make \] a regex str.empty? ? '' : str.gsub('\]', ']') end