# class Kramdown::Parser::Kramdown

Used for parsing a document in kramdown format.

If you want to extend the functionality of the parser, you need to do the following:

- Create a new subclass

- add the needed parser methods

- modify the @block_parsers and @span_parsers variables and add the names of your parser methods

Here is a small example for an extended parser class that parses ERB style tags as raw text if they are used as span-level elements (an equivalent block-level parser should probably also be made to handle the block case):

    require 'kramdown/parser/kramdown'

    class Kramdown::Parser::ERBKramdown < Kramdown::Parser::Kramdown

       def initialize(source, options)
         super
         @span_parsers.unshift(:erb_tags)
       end

       ERB_TAGS_START = /<%.*?%>/

       def parse_erb_tags
         @src.pos += @src.matched_size
         @tree.children << Element.new(:raw, @src.matched)
       end
       define_parser(:erb_tags, ERB_TAGS_START, '<%')

    end

The new parser can be used like this:

    require 'kramdown/document'
    # require the file with the above parser class

    Kramdown::Document.new(input_text, :input => 'ERBKramdown').to_html

### Constants

ABBREV_DEFINITION_START
ACHARS
ALD_ANY_CHARS
ALD_CLASS_NAME
ALD_ID_CHARS
ALD_ID_NAME
ALD_START
ALD_TYPE_ANY
ALD_TYPE_CLASS_NAME
ALD_TYPE_ID_NAME
ALD_TYPE_ID_OR_CLASS
ALD_TYPE_ID_OR_CLASS_MULTI
ALD_TYPE_KEY_VALUE_PAIR
ALD_TYPE_REF
ATX_HEADER_START
AUTOLINK_START
AUTOLINK_START_STR
BLANK_LINE
BLOCKQUOTE_START
BLOCK_BOUNDARY
BLOCK_EXTENSIONS_START
BLOCK_MATH_START
CODEBLOCK_MATCH
CODEBLOCK_START
CODESPAN_DELIMITER
DEFINITION_LIST_START
Data

  Struct class holding all the needed data for one block/span-level parser method.

EMPHASIS_START
EOB_MARKER
ESCAPED_CHARS
EXT_BLOCK_START
EXT_BLOCK_STOP_STR
EXT_SPAN_START
EXT_START_STR
EXT_STOP_STR
FENCED_CODEBLOCK_MATCH
FENCED_CODEBLOCK_START
FOOTNOTE_DEFINITION_START
FOOTNOTE_MARKER_START
HEADER_ID
HR_START
HTML_BLOCK_START
HTML_MARKDOWN_ATTR_MAP

  Mapping of markdown attribute value to content model. I.e. :raw when “0”, :default when “1” (use default content model for the HTML element), :span when “span”, :block when block and for everything else `nil` is returned.

HTML_SPAN_START
IAL_BLOCK
IAL_BLOCK_START
IAL_CLASS_ATTR
IAL_SPAN_START
INDENT

  Regexp for matching indentation (one tab or four spaces)

INLINE_MATH_START
LAZY_END
LAZY_END_HTML_SPAN_ELEMENTS
LAZY_END_HTML_START
LAZY_END_HTML_STOP
LINE_BREAK
LINK_BRACKET_STOP_RE
LINK_DEFINITION_START
LINK_INLINE_ID_RE
LINK_INLINE_TITLE_RE
LINK_PAREN_STOP_RE
LINK_START
LIST_ITEM_IAL
LIST_ITEM_IAL_CHECK
LIST_START
LIST_START_OL
LIST_START_UL
OPT_SPACE

  Regexp for matching the optional space (zero or up to three spaces)

PARAGRAPH_END
PARAGRAPH_MATCH
PARAGRAPH_START
PARSE_FIRST_LIST_LINE_REGEXP_CACHE
PATTERN_TAIL
SETEXT_HEADER_START
SMART_QUOTES_RE
SPAN_EXTENSIONS_START
SQ_CLOSE
SQ_PUNCT
SQ_RULES
SQ_SUBSTS
TABLE_FSEP_LINE
TABLE_HSEP_ALIGN
TABLE_LINE
TABLE_PIPE_CHECK
TABLE_ROW_LINE
TABLE_SEP_LINE
TABLE_START
TRAILING_WHITESPACE
TYPOGRAPHIC_SYMS
TYPOGRAPHIC_SYMS_RE
TYPOGRAPHIC_SYMS_SUBST

### Protected Class Methods

define_parser(name, start_re, span_start = nil, meth_name = "parse_ click to toggle source

Add a parser method

- with the given `name`,

- using `start_re` as start regexp

- and, for span parsers, `span_start` as a String that can be used in a regexp and which identifies the starting character(s)

to the registry. The method name is automatically derived from the `name` or can explicitly be set by using the `meth_name` parameter.

        # File lib/kramdown/parser/kramdown.rb
    327 def self.define_parser(name, start_re, span_start = nil, meth_name = "parse_#{name}")
    328   raise "A parser with the name #{name} already exists!" if @@parsers.key?(name)
    329   @@parsers[name] = Data.new(name, start_re, span_start, meth_name)
    330 end

has_parser?(name) click to toggle source

Return `true` if there is a parser called `name`.

        # File lib/kramdown/parser/kramdown.rb
    338 def self.has_parser?(name)
    339   @@parsers.key?(name)
    340 end

parser(name = nil) click to toggle source

Return the `Data` structure for the parser `name`.

        # File lib/kramdown/parser/kramdown.rb
    333 def self.parser(name = nil)
    334   @@parsers[name]
    335 end

### Private Class Methods

new(source, options) click to toggle source

Create a new `Kramdown` parser object with the given `options`.

Calls superclass method `Kramdown::Parser::Base::new`

       # File lib/kramdown/parser/kramdown.rb
    64 def initialize(source, options)
    65   super
    66
    67   reset_env
    68
    69   @alds = {}
    70   @footnotes = {}
    71   @link_defs = {}
    72   update_link_definitions(@options[:link_defs])
    73
    74   @block_parsers = [:blank_line, :codeblock, :codeblock_fenced, :blockquote, :atx_header,
    75                     :horizontal_rule, :list, :definition_list, :block_html, :setext_header,
    76                     :block_math, :table, :footnote_definition, :link_definition,
    77                     :abbrev_definition, :block_extensions, :eob_marker, :paragraph]
    78   @span_parsers =  [:emphasis, :codespan, :autolink, :span_html, :footnote_marker, :link,
    79                     :smart_quotes, :inline_math, :span_extensions, :html_entity,
    80                     :typographic_syms, :line_break, :escaped_chars]
    81
    82   @span_pattern_cache ||= Hash.new { |h, k| h[k] = {} }
    83 end

new(source, options) click to toggle source

Initialize the parser object with the `source` string and the parsing `options`.

The @root element, the @warnings array and @text_type (specifies the default type for newly created text nodes) are automatically initialized.

       # File lib/kramdown/parser/base.rb
    51 def initialize(source, options)
    52   @source = source
    53   @options = Kramdown::Options.merge(options)
    54   @root = Element.new(:root, nil, nil, encoding: (source.encoding rescue nil), location: 1,
    55                       options: {}, abbrev_defs: {}, abbrev_attr: {})
    56   @warnings = []
    57   @text_type = :text
    58 end

### Public Instance Methods

add_link(el, href, title, alt_text = nil, ial = nil) click to toggle source

This helper methods adds the approriate attributes to the element `el` of type `a` or `img` and the element itself to the @tree.

       # File lib/kramdown/parser/kramdown/link.rb
    38 def add_link(el, href, title, alt_text = nil, ial = nil)
    39   el.options[:ial] = ial
    40   update_attr_with_ial(el.attr, ial) if ial
    41   if el.type == :a
    42     el.attr['href'] = href
    43   else
    44     el.attr['src'] = href
    45     el.attr['alt'] = alt_text
    46     el.children.clear
    47   end
    48   el.attr['title'] = title if title
    49   @tree.children << el
    50 end

after_block_boundary?() click to toggle source

Return `true` if we are after a block boundary.

       # File lib/kramdown/parser/kramdown/block_boundary.rb
    20 def after_block_boundary?
    21   last_child = @tree.children.last
    22   !last_child || last_child.type == :blank ||
    23     (last_child.type == :eob && last_child.value.nil?) || @block_ial
    24 end

before_block_boundary?() click to toggle source

Return `true` if we are before a block boundary.

       # File lib/kramdown/parser/kramdown/block_boundary.rb
    27 def before_block_boundary?
    28   @src.check(self.class::BLOCK_BOUNDARY)
    29 end

correct_abbreviations_attributes() click to toggle source

Correct abbreviation attributes.

       # File lib/kramdown/parser/kramdown/abbreviation.rb
    33 def correct_abbreviations_attributes
    34   @root.options[:abbrev_attr].keys.each do |k|
    35     @root.options[:abbrev_attr][k] = @root.options[:abbrev_attr][k].attr
    36   end
    37 end

handle_extension(name, opts, body, type, line_no = nil) click to toggle source

        # File lib/kramdown/parser/kramdown/extensions.rb
     95 def handle_extension(name, opts, body, type, line_no = nil)
     96   case name
     97   when 'comment'
     98     if body.kind_of?(String)
     99       @tree.children << Element.new(:comment, body, nil, category: type, location: line_no)
    100     end
    101     true
    102   when 'nomarkdown'
    103     if body.kind_of?(String)
    104       @tree.children << Element.new(:raw, body, nil, category: type,
    105                                     location: line_no, type: opts['type'].to_s.split(/\s+/))
    106     end
    107     true
    108   when 'options'
    109     opts.select do |k, v|
    110       k = k.to_sym
    111       if Kramdown::Options.defined?(k)
    112         if @options[:forbidden_inline_options].include?(k) ||
    113             k == :forbidden_inline_options
    114           warning("Option #{k} may not be set inline")
    115           next false
    116         end
    117
    118         begin
    119           val = Kramdown::Options.parse(k, v)
    120           @options[k] = val
    121           (@root.options[:options] ||= {})[k] = val
    122         rescue StandardError
    123         end
    124         false
    125       else
    126         true
    127       end
    128     end.each do |k, _v|
    129       warning("Unknown kramdown option '#{k}'")
    130     end
    131     @tree.children << new_block_el(:eob, :extension) if type == :block
    132     true
    133   else
    134     false
    135   end
    136 end

handle_kramdown_html_tag(el, closed, handle_body) click to toggle source

       # File lib/kramdown/parser/kramdown/html.rb
    24 def handle_kramdown_html_tag(el, closed, handle_body)
    25   if @block_ial
    26     el.options[:ial] = @block_ial
    27     @block_ial = nil
    28   end
    29
    30   content_model = if @tree.type != :html_element || @tree.options[:content_model] != :raw
    31                     (@options[:parse_block_html] ? HTML_CONTENT_MODEL[el.value] : :raw)
    32                   else
    33                     :raw
    34                   end
    35   if (val = HTML_MARKDOWN_ATTR_MAP[el.attr.delete('markdown')])
    36     content_model = (val == :default ? HTML_CONTENT_MODEL[el.value] : val)
    37   end
    38
    39   @src.scan(TRAILING_WHITESPACE) if content_model == :block
    40   el.options[:content_model] = content_model
    41   el.options[:is_closed] = closed
    42
    43   if !closed && handle_body
    44     if content_model == :block
    45       unless parse_blocks(el)
    46         warning("Found no end tag for '#{el.value}' (line #{el.options[:location]}) - auto-closing it")
    47       end
    48     elsif content_model == :span
    49       curpos = @src.pos
    50       if @src.scan_until(/(?=<\/#{el.value}\s*>)/mi)
    51         add_text(extract_string(curpos...@src.pos, @src), el)
    52         @src.scan(HTML_TAG_CLOSE_RE)
    53       else
    54         add_text(@src.rest, el)
    55         @src.terminate
    56         warning("Found no end tag for '#{el.value}' (line #{el.options[:location]}) - auto-closing it")
    57       end
    58     else
    59       parse_raw_html(el, &method(:handle_kramdown_html_tag))
    60     end
    61     unless @tree.type == :html_element && @tree.options[:content_model] == :raw
    62       @src.scan(TRAILING_WHITESPACE)
    63     end
    64   end
    65 end

normalize_link_id(id) click to toggle source

Normalize the link identifier.

       # File lib/kramdown/parser/kramdown/link.rb
    16 def normalize_link_id(id)
    17   id.gsub(/[\s]+/, ' ').downcase
    18 end

paragraph_end() click to toggle source

       # File lib/kramdown/parser/kramdown/paragraph.rb
    55 def paragraph_end
    56   self.class::PARAGRAPH_END
    57 end

parse() click to toggle source

The source string provided on initialization is parsed into the @root element.

        # File lib/kramdown/parser/kramdown.rb
     87 def parse
     88   configure_parser
     89   parse_blocks(@root, adapt_source(source))
     90   update_tree(@root)
     91   correct_abbreviations_attributes
     92   replace_abbreviations(@root)
     93   @footnotes.each do |_name, data|
     94     update_tree(data[:content])
     95     replace_abbreviations(data[:content])
     96   end
     97   footnote_count = 0
     98   @footnotes.each do |name, data|
     99     (footnote_count += 1; next) if data.key?(:marker)
    100     line = data[:content].options[:location]
    101     warning("Footnote definition for '#{name}' on line #{line} is unreferenced - ignoring")
    102   end
    103   @root.options[:footnote_count] = footnote_count
    104 end

parse_abbrev_definition() click to toggle source

Parse the link definition at the current location.

       # File lib/kramdown/parser/kramdown/abbreviation.rb
    16 def parse_abbrev_definition
    17   start_line_number = @src.current_line_number
    18   @src.pos += @src.matched_size
    19   abbrev_id, abbrev_text = @src[1], @src[2]
    20   abbrev_text.strip!
    21   if @root.options[:abbrev_defs][abbrev_id]
    22     warning("Duplicate abbreviation ID '#{abbrev_id}' on line #{start_line_number} " \
    23             "- overwriting")
    24   end
    25   @tree.children << new_block_el(:eob, :abbrev_def)
    26   @root.options[:abbrev_defs][abbrev_id] = abbrev_text
    27   @root.options[:abbrev_attr][abbrev_id] = @tree.children.last
    28   true
    29 end

parse_attribute_list(str, opts) click to toggle source

Parse the string `str` and extract all attributes and add all found attributes to the hash `opts`.

       # File lib/kramdown/parser/kramdown/extensions.rb
    17 def parse_attribute_list(str, opts)
    18   return if str.strip.empty? || str.strip == ':'
    19   attrs = str.scan(ALD_TYPE_ANY)
    20   attrs.each do |key, sep, val, ref, id_and_or_class, _, _|
    21     if ref
    22       (opts[:refs] ||= []) << ref
    23     elsif id_and_or_class
    24       id_and_or_class.scan(ALD_TYPE_ID_OR_CLASS).each do |id_attr, class_attr|
    25         if class_attr
    26           opts[IAL_CLASS_ATTR] = "#{opts[IAL_CLASS_ATTR]} #{class_attr}".lstrip
    27         else
    28           opts['id'] = id_attr
    29         end
    30       end
    31     else
    32       val.gsub!(/\\(\}|#{sep})/, "\\1")
    33       opts[key] = val
    34     end
    35   end
    36   warning("No or invalid attributes found in IAL/ALD content: #{str}") if attrs.empty?
    37 end

parse_atx_header() click to toggle source

Parse the Atx header at the current location.

       # File lib/kramdown/parser/kramdown/header.rb
    31 def parse_atx_header
    32   return false unless after_block_boundary?
    33   text, id = parse_header_contents
    34   text.sub!(/(?<!\\)#+\z/, '') && text.rstrip!
    35   return false if text.empty?
    36   add_header(@src["level"].length, text, id)
    37   true
    38 end

parse_autolink() click to toggle source

Parse the autolink at the current location.

       # File lib/kramdown/parser/kramdown/autolink.rb
    18 def parse_autolink
    19   start_line_number = @src.current_line_number
    20   @src.pos += @src.matched_size
    21   href = (@src[2].nil? ? "mailto:#{@src[1]}" : @src[1])
    22   el = Element.new(:a, nil, {'href' => href}, location: start_line_number)
    23   add_text(@src[1].sub(/^mailto:/, ''), el)
    24   @tree.children << el
    25 end

parse_blank_line() click to toggle source

Parse the blank line at the current postition.

       # File lib/kramdown/parser/kramdown/blank_line.rb
    16 def parse_blank_line
    17   @src.pos += @src.matched_size
    18   if (last_child = @tree.children.last) && last_child.type == :blank
    19     last_child.value << @src.matched
    20   else
    21     @tree.children << new_block_el(:blank, @src.matched)
    22   end
    23   true
    24 end

parse_block_extensions() click to toggle source

Parse one of the block extensions (ALD, block IAL or generic extension) at the current location.

        # File lib/kramdown/parser/kramdown/extensions.rb
    163 def parse_block_extensions
    164   if @src.scan(ALD_START)
    165     parse_attribute_list(@src[2], @alds[@src[1]] ||= {})
    166     @tree.children << new_block_el(:eob, :ald)
    167     true
    168   elsif @src.check(EXT_BLOCK_START)
    169     parse_extension_start_tag(:block)
    170   elsif @src.scan(IAL_BLOCK_START)
    171     if (last_child = @tree.children.last) && last_child.type != :blank &&
    172         (last_child.type != :eob ||
    173          [:link_def, :abbrev_def, :footnote_def].include?(last_child.value))
    174       parse_attribute_list(@src[1], last_child.options[:ial] ||= {})
    175       @tree.children << new_block_el(:eob, :ial) unless @src.check(IAL_BLOCK_START)
    176     else
    177       parse_attribute_list(@src[1], @block_ial ||= {})
    178     end
    179     true
    180   else
    181     false
    182   end
    183 end

parse_block_html() click to toggle source

Parse the HTML at the current position as block-level HTML.

       # File lib/kramdown/parser/kramdown/html.rb
    70 def parse_block_html
    71   line = @src.current_line_number
    72   if (result = @src.scan(HTML_COMMENT_RE))
    73     @tree.children << Element.new(:xml_comment, result, nil, category: :block, location: line)
    74     @src.scan(TRAILING_WHITESPACE)
    75     true
    76   else
    77     if @src.check(/^#{OPT_SPACE}#{HTML_TAG_RE}/o) && !HTML_SPAN_ELEMENTS.include?(@src[1].downcase)
    78       @src.pos += @src.matched_size
    79       handle_html_start_tag(line, &method(:handle_kramdown_html_tag))
    80       Kramdown::Parser::Html::ElementConverter.convert(@root, @tree.children.last) if @options[:html_to_native]
    81       true
    82     elsif @src.check(/^#{OPT_SPACE}#{HTML_TAG_CLOSE_RE}/o) && !HTML_SPAN_ELEMENTS.include?(@src[1].downcase)
    83       name = @src[1].downcase
    84
    85       if @tree.type == :html_element && @tree.value == name
    86         @src.pos += @src.matched_size
    87         throw :stop_block_parsing, :found
    88       else
    89         false
    90       end
    91     else
    92       false
    93     end
    94   end
    95 end

parse_block_math() click to toggle source

Parse the math block at the current location.

       # File lib/kramdown/parser/kramdown/math.rb
    18 def parse_block_math
    19   start_line_number = @src.current_line_number
    20   if !after_block_boundary?
    21     return false
    22   elsif @src[1]
    23     @src.scan(/^#{OPT_SPACE}\\/o) if @src[3]
    24     return false
    25   end
    26
    27   saved_pos = @src.save_pos
    28   @src.pos += @src.matched_size
    29   data = @src[2].strip
    30   if before_block_boundary?
    31     @tree.children << new_block_el(:math, data, nil, category: :block, location: start_line_number)
    32     true
    33   else
    34     @src.revert_pos(saved_pos)
    35     false
    36   end
    37 end

parse_blockquote() click to toggle source

Parse the blockquote at the current location.

       # File lib/kramdown/parser/kramdown/blockquote.rb
    20 def parse_blockquote
    21   start_line_number = @src.current_line_number
    22   result = @src.scan(PARAGRAPH_MATCH)
    23   until @src.match?(self.class::LAZY_END)
    24     result << @src.scan(PARAGRAPH_MATCH)
    25   end
    26   result.gsub!(BLOCKQUOTE_START, '')
    27
    28   el = new_block_el(:blockquote, nil, nil, location: start_line_number)
    29   @tree.children << el
    30   parse_blocks(el, result)
    31   true
    32 end

parse_codeblock() click to toggle source

Parse the indented codeblock at the current location.

       # File lib/kramdown/parser/kramdown/codeblock.rb
    22 def parse_codeblock
    23   start_line_number = @src.current_line_number
    24   data = @src.scan(self.class::CODEBLOCK_MATCH)
    25   data.gsub!(/\n( {0,3}\S)/, ' \\1')
    26   data.gsub!(INDENT, '')
    27   @tree.children << new_block_el(:codeblock, data, nil, location: start_line_number)
    28   true
    29 end

parse_codeblock_fenced() click to toggle source

Parse the fenced codeblock at the current location.

       # File lib/kramdown/parser/kramdown/codeblock.rb
    36 def parse_codeblock_fenced
    37   if @src.check(self.class::FENCED_CODEBLOCK_MATCH)
    38     start_line_number = @src.current_line_number
    39     @src.pos += @src.matched_size
    40     el = new_block_el(:codeblock, @src[5], nil, location: start_line_number, fenced: true)
    41     lang = @src[3].to_s.strip
    42     unless lang.empty?
    43       el.options[:lang] = lang
    44       el.attr['class'] = "language-#{@src[4]}"
    45     end
    46     @tree.children << el
    47     true
    48   else
    49     false
    50   end
    51 end

parse_codespan() click to toggle source

Parse the codespan at the current scanner location.

       # File lib/kramdown/parser/kramdown/codespan.rb
    16 def parse_codespan
    17   start_line_number = @src.current_line_number
    18   result = @src.scan(CODESPAN_DELIMITER)
    19   simple = (result.length == 1)
    20   saved_pos = @src.save_pos
    21
    22   if simple && @src.pre_match =~ /\s\Z|\A\Z/ && @src.match?(/\s/)
    23     add_text(result)
    24     return
    25   end
    26
    27   # assign static regex to avoid allocating the same on every instance
    28   # where +result+ equals a single-backtick. Interpolate otherwise.
    29   if result == '`'
    30     scan_pattern = /`/
    31     str_sub_pattern = /`\Z/
    32   else
    33     scan_pattern = /#{result}/
    34     str_sub_pattern = /#{result}\Z/
    35   end
    36
    37   if (text = @src.scan_until(scan_pattern))
    38     text.sub!(str_sub_pattern, '')
    39     unless simple
    40       text = text[1..-1] if text[0..0] == ' '
    41       text = text[0..-2] if text[-1..-1] == ' '
    42     end
    43     @tree.children << Element.new(:codespan, text, nil, location: start_line_number)
    44   else
    45     @src.revert_pos(saved_pos)
    46     add_text(result)
    47   end
    48 end

parse_definition_list() click to toggle source

Parse the ordered or unordered list at the current location.

        # File lib/kramdown/parser/kramdown/list.rb
    151 def parse_definition_list
    152   children = @tree.children
    153   if !children.last || (children.length == 1 && children.last.type != :p) ||
    154       (children.length >= 2 && children[-1].type != :p &&
    155        (children[-1].type != :blank || children[-1].value != "\n" || children[-2].type != :p))
    156     return false
    157   end
    158
    159   first_as_para = false
    160   deflist = new_block_el(:dl)
    161   para = @tree.children.pop
    162   if para.type == :blank
    163     para = @tree.children.pop
    164     first_as_para = true
    165   end
    166   # take location from preceding para which is the first definition term
    167   deflist.options[:location] = para.options[:location]
    168   para.children.first.value.split(/\n/).each do |term|
    169     el = Element.new(:dt, nil, nil, location: @src.current_line_number)
    170     term.sub!(self.class::LIST_ITEM_IAL) do
    171       parse_attribute_list($1, el.options[:ial] ||= {})
    172       ''
    173     end
    174     el.options[:raw_text] = term
    175     el.children << Element.new(:raw_text, term)
    176     deflist.children << el
    177   end
    178   deflist.options[:ial] = para.options[:ial]
    179
    180   item = nil
    181   content_re, lazy_re, indent_re = nil
    182   def_start_re = DEFINITION_LIST_START
    183   last_is_blank = false
    184   until @src.eos?
    185     start_line_number = @src.current_line_number
    186     if @src.scan(def_start_re)
    187       item = Element.new(:dd, nil, nil, location: start_line_number)
    188       item.options[:first_as_para] = first_as_para
    189       item.value, indentation, content_re, lazy_re, indent_re =
    190         parse_first_list_line(@src[1].length, @src[2])
    191       deflist.children << item
    192
    193       item.value.sub!(self.class::LIST_ITEM_IAL) do |_match|
    194         parse_attribute_list($1, item.options[:ial] ||= {})
    195         ''
    196       end
    197
    198       def_start_re = fetch_pattern(:dl, indentation)
    199       first_as_para = false
    200       last_is_blank = false
    201     elsif @src.check(EOB_MARKER)
    202       break
    203     elsif (result = @src.scan(content_re)) || (!last_is_blank && (result = @src.scan(lazy_re)))
    204       result.sub!(/^(\t+)/) { " " * ($1 ? 4 * $1.length : 0) }
    205       result.sub!(indent_re, '')
    206       item.value << result
    207       first_as_para = false
    208       last_is_blank = false
    209     elsif (result = @src.scan(BLANK_LINE))
    210       first_as_para = true
    211       item.value << result
    212       last_is_blank = true
    213     else
    214       break
    215     end
    216   end
    217
    218   last = nil
    219   deflist.children.each do |it|
    220     next if it.type == :dt
    221
    222     parse_blocks(it, it.value)
    223     it.value = nil
    224     it_children = it.children
    225     next if it_children.empty?
    226
    227     last = (it_children.last.type == :blank ? it_children.pop : nil)
    228
    229     if it_children.first && it_children.first.type == :p && !it.options.delete(:first_as_para)
    230       it_children.first.children.first.value << "\n" if it_children.size > 1
    231       it_children.first.options[:transparent] = true
    232     end
    233   end
    234
    235   children = @tree.children
    236   if children.length >= 1 && children.last.type == :dl
    237     children[-1].children.concat(deflist.children)
    238   elsif children.length >= 2 && children[-1].type == :blank &&
    239       children[-2].type == :dl
    240     children.pop
    241     children[-1].children.concat(deflist.children)
    242   else
    243     children << deflist
    244   end
    245
    246   children << last if last
    247
    248   true
    249 end

parse_emphasis() click to toggle source

Parse the emphasis at the current location.

       # File lib/kramdown/parser/kramdown/emphasis.rb
    16 def parse_emphasis
    17   start_line_number = @src.current_line_number
    18   saved_pos = @src.save_pos
    19
    20   result = @src.scan(EMPHASIS_START)
    21   element = (result.length == 2 ? :strong : :em)
    22   type = result[0..0]
    23
    24   if (type == '_' && @src.pre_match =~ /[[:alpha:]]-?[[:alpha:]]*\z/) || @src.check(/\s/) ||
    25       @tree.type == element || @stack.any? {|el, _| el.type == element }
    26     add_text(result)
    27     return
    28   end
    29
    30   sub_parse = lambda do |delim, elem|
    31     el = Element.new(elem, nil, nil, location: start_line_number)
    32     stop_re = /#{Regexp.escape(delim)}/
    33     found = parse_spans(el, stop_re) do
    34       (@src.pre_match[-1, 1] !~ /\s/) &&
    35         (elem != :em || !@src.match?(/#{Regexp.escape(delim * 2)}(?!#{Regexp.escape(delim)})/)) &&
    36         (type != '_' || !@src.match?(/#{Regexp.escape(delim)}[[:alnum:]]/)) && !el.children.empty?
    37     end
    38     [found, el, stop_re]
    39   end
    40
    41   found, el, stop_re = sub_parse.call(result, element)
    42   if !found && element == :strong && @tree.type != :em
    43     @src.revert_pos(saved_pos)
    44     @src.pos += 1
    45     found, el, stop_re = sub_parse.call(type, :em)
    46   end
    47   if found
    48     @src.scan(stop_re)
    49     @tree.children << el
    50   else
    51     @src.revert_pos(saved_pos)
    52     @src.pos += result.length
    53     add_text(result)
    54   end
    55 end

parse_eob_marker() click to toggle source

Parse the EOB marker at the current location.

       # File lib/kramdown/parser/kramdown/eob.rb
    16 def parse_eob_marker
    17   @src.pos += @src.matched_size
    18   @tree.children << new_block_el(:eob)
    19   true
    20 end

parse_escaped_chars() click to toggle source

Parse the backslash-escaped character at the current location.

       # File lib/kramdown/parser/kramdown/escaped_chars.rb
    16 def parse_escaped_chars
    17   @src.pos += @src.matched_size
    18   add_text(@src[1])
    19 end

parse_extension_start_tag(type) click to toggle source

Parse the generic extension at the current point. The parameter `type` can either be :block or :span depending whether we parse a block or span extension tag.

       # File lib/kramdown/parser/kramdown/extensions.rb
    53 def parse_extension_start_tag(type)
    54   saved_pos = @src.save_pos
    55   start_line_number = @src.current_line_number
    56   @src.pos += @src.matched_size
    57
    58   error_block = lambda do |msg|
    59     warning(msg)
    60     @src.revert_pos(saved_pos)
    61     add_text(@src.getch) if type == :span
    62     false
    63   end
    64
    65   if @src[4] || @src.matched == '{:/}'
    66     name = (@src[4] ? "for '#{@src[4]}' " : '')
    67     return error_block.call("Invalid extension stop tag #{name} found on line " \
    68                             "#{start_line_number} - ignoring it")
    69   end
    70
    71   ext = @src[1]
    72   opts = {}
    73   body = nil
    74   parse_attribute_list(@src[2] || '', opts)
    75
    76   unless @src[3]
    77     stop_re = (type == :block ? /#{EXT_BLOCK_STOP_STR % ext}/ : /#{EXT_STOP_STR % ext}/)
    78     if (result = @src.scan_until(stop_re))
    79       body = result.sub!(stop_re, '')
    80       body.chomp! if type == :block
    81     else
    82       return error_block.call("No stop tag for extension '#{ext}' found on line " \
    83                               "#{start_line_number} - ignoring it")
    84     end
    85   end
    86
    87   if !handle_extension(ext, opts, body, type, start_line_number)
    88     error_block.call("Invalid extension with name '#{ext}' specified on line " \
    89                      "#{start_line_number} - ignoring it")
    90   else
    91     true
    92   end
    93 end

parse_first_list_line(indentation, content) click to toggle source

Used for parsing the first line of a list item or a definition, i.e. the line with list item marker or the definition marker.

       # File lib/kramdown/parser/kramdown/list.rb
    31 def parse_first_list_line(indentation, content)
    32   if content =~ self.class::LIST_ITEM_IAL_CHECK
    33     indentation = 4
    34   else
    35     while content =~ /^ *\t/
    36       temp = content.scan(/^ */).first.length + indentation
    37       content.sub!(/^( *)(\t+)/) { $1 << " " * (4 - (temp % 4) + ($2.length - 1) * 4) }
    38     end
    39     indentation += content[/^ */].length
    40   end
    41   content.sub!(/^\s*/, '')
    42
    43   [content, indentation, *PARSE_FIRST_LIST_LINE_REGEXP_CACHE[indentation]]
    44 end

parse_footnote_definition() click to toggle source

Parse the foot note definition at the current location.

       # File lib/kramdown/parser/kramdown/footnote.rb
    20 def parse_footnote_definition
    21   start_line_number = @src.current_line_number
    22   @src.pos += @src.matched_size
    23
    24   el = Element.new(:footnote_def, nil, nil, location: start_line_number)
    25   parse_blocks(el, @src[2].gsub(INDENT, ''))
    26   if @footnotes[@src[1]]
    27     warning("Duplicate footnote name '#{@src[1]}' on line #{start_line_number} - overwriting")
    28   end
    29   @tree.children << new_block_el(:eob, :footnote_def)
    30   (@footnotes[@src[1]] = {})[:content] = el
    31   @footnotes[@src[1]][:eob] = @tree.children.last
    32   true
    33 end

parse_footnote_marker() click to toggle source

Parse the footnote marker at the current location.

       # File lib/kramdown/parser/kramdown/footnote.rb
    39 def parse_footnote_marker
    40   start_line_number = @src.current_line_number
    41   @src.pos += @src.matched_size
    42   fn_def = @footnotes[@src[1]]
    43   if fn_def
    44     if fn_def[:eob]
    45       update_attr_with_ial(fn_def[:eob].attr, fn_def[:eob].options[:ial] || {})
    46       fn_def[:attr] = fn_def[:eob].attr
    47       fn_def[:options] = fn_def[:eob].options
    48       fn_def.delete(:eob)
    49     end
    50     fn_def[:marker] ||= []
    51     fn_def[:marker].push(Element.new(:footnote, fn_def[:content], fn_def[:attr],
    52                                      fn_def[:options].merge(name: @src[1], location: start_line_number)))
    53     @tree.children << fn_def[:marker].last
    54   else
    55     warning("Footnote definition for '#{@src[1]}' not found on line #{start_line_number}")
    56     add_text(@src.matched)
    57   end
    58 end

parse_horizontal_rule() click to toggle source

Parse the horizontal rule at the current location.

       # File lib/kramdown/parser/kramdown/horizontal_rule.rb
    16 def parse_horizontal_rule
    17   start_line_number = @src.current_line_number
    18   @src.pos += @src.matched_size
    19   @tree.children << new_block_el(:hr, nil, nil, location: start_line_number)
    20   true
    21 end

parse_html_entity() click to toggle source

Parse the HTML entity at the current location.

       # File lib/kramdown/parser/kramdown/html_entity.rb
    16 def parse_html_entity
    17   start_line_number = @src.current_line_number
    18   @src.pos += @src.matched_size
    19   begin
    20     value = ::Kramdown::Utils::Entities.entity(@src[1] || (@src[2]&.to_i) || @src[3].hex)
    21     @tree.children << Element.new(:entity, value,
    22                                   nil, original: @src.matched, location: start_line_number)
    23   rescue ::Kramdown::Error
    24     @tree.children << Element.new(:entity, ::Kramdown::Utils::Entities.entity('amp'),
    25                                   nil, location: start_line_number)
    26     add_text(@src.matched[1..-1])
    27   end
    28 end

parse_inline_math() click to toggle source

Parse the inline math at the current location.

       # File lib/kramdown/parser/kramdown/math.rb
    43 def parse_inline_math
    44   start_line_number = @src.current_line_number
    45   @src.pos += @src.matched_size
    46   @tree.children << Element.new(:math, @src[1].strip, nil, category: :span, location: start_line_number)
    47 end

parse_line_break() click to toggle source

Parse the line break at the current location.

       # File lib/kramdown/parser/kramdown/line_break.rb
    16 def parse_line_break
    17   @tree.children << Element.new(:br, nil, nil, location: @src.current_line_number)
    18   @src.pos += @src.matched_size
    19 end

parse_link() click to toggle source

Parse the link at the current scanner position. This method is used to parse normal links as well as image links.

        # File lib/kramdown/parser/kramdown/link.rb
     60 def parse_link
     61   start_line_number = @src.current_line_number
     62   result = @src.scan(LINK_START)
     63   cur_pos = @src.pos
     64   saved_pos = @src.save_pos
     65
     66   link_type = (result =~ /^!/ ? :img : :a)
     67
     68   # no nested links allowed
     69   if link_type == :a && (@tree.type == :img || @tree.type == :a ||
     70                          @stack.any? {|t, _| t && (t.type == :img || t.type == :a) })
     71     add_text(result)
     72     return
     73   end
     74   el = Element.new(link_type, nil, nil, location: start_line_number)
     75
     76   count = 1
     77   found = parse_spans(el, LINK_BRACKET_STOP_RE) do
     78     count += (@src[1] ? -1 : 1)
     79     count - el.children.select {|c| c.type == :img }.size == 0
     80   end
     81   unless found
     82     @src.revert_pos(saved_pos)
     83     add_text(result)
     84     return
     85   end
     86   alt_text = extract_string(cur_pos...@src.pos, @src).gsub(ESCAPED_CHARS, '\1')
     87   @src.scan(LINK_BRACKET_STOP_RE)
     88
     89   # reference style link or no link url
     90   if @src.scan(LINK_INLINE_ID_RE) || !@src.check(/\(/)
     91     emit_warning = !@src[1]
     92     link_id = normalize_link_id(@src[1] || alt_text)
     93     if @link_defs.key?(link_id)
     94       link_def = @link_defs[link_id]
     95       add_link(el, link_def[0], link_def[1], alt_text,
     96                link_def[2] && link_def[2].options[:ial])
     97     else
     98       if emit_warning
     99         warning("No link definition for link ID '#{link_id}' found on line #{start_line_number}")
    100       end
    101       @src.revert_pos(saved_pos)
    102       add_text(result)
    103     end
    104     return
    105   end
    106
    107   # link url in parentheses
    108   if @src.scan(/\(<(.*?)>/)
    109     link_url = @src[1]
    110     if @src.scan(/\)/)
    111       add_link(el, link_url, nil, alt_text)
    112       return
    113     end
    114   else
    115     link_url = +''
    116     nr_of_brackets = 0
    117     while (temp = @src.scan_until(LINK_PAREN_STOP_RE))
    118       link_url << temp
    119       if @src[2]
    120         nr_of_brackets -= 1
    121         break if nr_of_brackets == 0
    122       elsif @src[1]
    123         nr_of_brackets += 1
    124       else
    125         break
    126       end
    127     end
    128     link_url = link_url[1..-2]
    129     link_url.strip!
    130
    131     if nr_of_brackets == 0
    132       add_link(el, link_url, nil, alt_text)
    133       return
    134     end
    135   end
    136
    137   if @src.scan(LINK_INLINE_TITLE_RE)
    138     add_link(el, link_url, @src[2], alt_text)
    139   else
    140     @src.revert_pos(saved_pos)
    141     add_text(result)
    142   end
    143 end

parse_link_definition() click to toggle source

Parse the link definition at the current location.

       # File lib/kramdown/parser/kramdown/link.rb
    23 def parse_link_definition
    24   return false if @src[3].to_s =~ /[ \t]+["']/
    25   @src.pos += @src.matched_size
    26   link_id, link_url, link_title = normalize_link_id(@src[1]), @src[2] || @src[3], @src[5]
    27   if @link_defs[link_id]
    28     warning("Duplicate link ID '#{link_id}' on line #{@src.current_line_number} - overwriting")
    29   end
    30   @tree.children << new_block_el(:eob, :link_def)
    31   @link_defs[link_id] = [link_url, link_title, @tree.children.last]
    32   true
    33 end

parse_list() click to toggle source

Parse the ordered or unordered list at the current location.

        # File lib/kramdown/parser/kramdown/list.rb
     53 def parse_list
     54   start_line_number = @src.current_line_number
     55   type, list_start_re = (@src.check(LIST_START_UL) ? [:ul, LIST_START_UL] : [:ol, LIST_START_OL])
     56   list = new_block_el(type, nil, nil, location: start_line_number)
     57
     58   item = nil
     59   content_re, lazy_re, indent_re = nil
     60   eob_found = false
     61   nested_list_found = false
     62   last_is_blank = false
     63   until @src.eos?
     64     start_line_number = @src.current_line_number
     65     if last_is_blank && @src.check(HR_START)
     66       break
     67     elsif @src.scan(EOB_MARKER)
     68       eob_found = true
     69       break
     70     elsif @src.scan(list_start_re)
     71       item = Element.new(:li, nil, nil, location: start_line_number)
     72       item.value, indentation, content_re, lazy_re, indent_re =
     73         parse_first_list_line(@src[1].length, @src[2])
     74       list.children << item
     75
     76       item.value.sub!(self.class::LIST_ITEM_IAL) do
     77         parse_attribute_list($1, item.options[:ial] ||= {})
     78         ''
     79       end
     80
     81       list_start_re = fetch_pattern(type, indentation)
     82       nested_list_found = (item.value =~ LIST_START)
     83       last_is_blank = false
     84       item.value = [item.value]
     85     elsif (result = @src.scan(content_re)) || (!last_is_blank && (result = @src.scan(lazy_re)))
     86       result.sub!(/^(\t+)/) { " " * 4 * $1.length }
     87       indentation_found = result.sub!(indent_re, '')
     88       if !nested_list_found && indentation_found && result =~ LIST_START
     89         item.value << +''
     90         nested_list_found = true
     91       elsif nested_list_found && !indentation_found && result =~ LIST_START
     92         result = " " * (indentation + 4) << result
     93       end
     94       item.value.last << result
     95       last_is_blank = false
     96     elsif (result = @src.scan(BLANK_LINE))
     97       nested_list_found = true
     98       last_is_blank = true
     99       item.value.last << result
    100     else
    101       break
    102     end
    103   end
    104
    105   @tree.children << list
    106
    107   last = nil
    108   list.children.each do |it|
    109     temp = Element.new(:temp, nil, nil, location: it.options[:location])
    110
    111     env = save_env
    112     location = it.options[:location]
    113     it.value.each do |val|
    114       @src = ::Kramdown::Utils::StringScanner.new(val, location)
    115       parse_blocks(temp)
    116       location = @src.current_line_number
    117     end
    118     restore_env(env)
    119
    120     it.children = temp.children
    121     it.value = nil
    122
    123     it_children = it.children
    124     next if it_children.empty?
    125
    126     # Handle the case where an EOB marker is inserted by a block IAL for the first paragraph
    127     it_children.delete_at(1) if it_children.first.type == :p &&
    128       it_children.length >= 2 && it_children[1].type == :eob && it_children.first.options[:ial]
    129
    130     if it_children.first.type == :p &&
    131         (it_children.length < 2 || it_children[1].type != :blank ||
    132         (it == list.children.last && it_children.length == 2 && !eob_found)) &&
    133         (list.children.last != it || list.children.size == 1 ||
    134         list.children[0..-2].any? {|cit| !cit.children.first || cit.children.first.type != :p || cit.children.first.options[:transparent] })
    135       it_children.first.children.first.value << "\n" if it_children.size > 1 && it_children[1].type != :blank
    136       it_children.first.options[:transparent] = true
    137     end
    138
    139     last = (it_children.last.type == :blank ? it_children.pop : nil)
    140   end
    141
    142   @tree.children << last if !last.nil? && !eob_found
    143
    144   true
    145 end

parse_paragraph() click to toggle source

Parse the paragraph at the current location.

       # File lib/kramdown/parser/kramdown/paragraph.rb
    30 def parse_paragraph
    31   pos = @src.pos
    32   start_line_number = @src.current_line_number
    33   result = @src.scan(PARAGRAPH_MATCH)
    34   until @src.match?(paragraph_end)
    35     result << @src.scan(PARAGRAPH_MATCH)
    36   end
    37   result.rstrip!
    38   if (last_child = @tree.children.last) && last_child.type == :p
    39     last_item_in_para = last_child.children.last
    40     if last_item_in_para && last_item_in_para.type == @text_type
    41       joiner = (extract_string((pos - 3)...pos, @src) == "  \n" ? "  \n" : "\n")
    42       last_item_in_para.value << joiner << result
    43     else
    44       add_text(result, last_child)
    45     end
    46   else
    47     @tree.children << new_block_el(:p, nil, nil, location: start_line_number)
    48     result.lstrip!
    49     add_text(result, @tree.children.last)
    50   end
    51   true
    52 end

parse_setext_header() click to toggle source

Parse the Setext header at the current location.

       # File lib/kramdown/parser/kramdown/header.rb
    19 def parse_setext_header
    20   return false unless after_block_boundary?
    21   text, id = parse_header_contents
    22   return false if text.empty?
    23   add_header(@src["level"] == '-' ? 2 : 1, text, id)
    24   true
    25 end

parse_smart_quotes() click to toggle source

Parse the smart quotes at current location.

        # File lib/kramdown/parser/kramdown/smart_quotes.rb
    157 def parse_smart_quotes
    158   start_line_number = @src.current_line_number
    159   substs = SQ_RULES.find {|reg, _subst| @src.scan(reg) }[1]
    160   substs.each do |subst|
    161     if subst.kind_of?(Integer)
    162       add_text(@src[subst])
    163     else
    164       val = SQ_SUBSTS[[subst, @src[subst.to_s[-1, 1].to_i]]] || subst
    165       @tree.children << Element.new(:smart_quote, val, nil, location: start_line_number)
    166     end
    167   end
    168 end

parse_span_extensions() click to toggle source

Parse the extension span at the current location.

        # File lib/kramdown/parser/kramdown/extensions.rb
    191 def parse_span_extensions
    192   if @src.check(EXT_SPAN_START)
    193     parse_extension_start_tag(:span)
    194   elsif @src.check(IAL_SPAN_START)
    195     if (last_child = @tree.children.last) && last_child.type != :text
    196       @src.pos += @src.matched_size
    197       attr = {}
    198       parse_attribute_list(@src[1], attr)
    199       update_ial_with_ial(last_child.options[:ial] ||= {}, attr)
    200       update_attr_with_ial(last_child.attr, attr)
    201     else
    202       warning("Found span IAL after text - ignoring it")
    203       add_text(@src.getch)
    204     end
    205   else
    206     add_text(@src.getch)
    207   end
    208 end

parse_span_html() click to toggle source

Parse the HTML at the current position as span-level HTML.

        # File lib/kramdown/parser/kramdown/html.rb
    101 def parse_span_html
    102   line = @src.current_line_number
    103   if (result = @src.scan(HTML_COMMENT_RE))
    104     @tree.children << Element.new(:xml_comment, result, nil, category: :span, location: line)
    105   elsif (result = @src.scan(HTML_TAG_CLOSE_RE))
    106     warning("Found invalidly used HTML closing tag for '#{@src[1]}' on line #{line}")
    107     add_text(result)
    108   elsif (result = @src.scan(HTML_TAG_RE))
    109     tag_name = @src[1]
    110     tag_name.downcase! if HTML_ELEMENT[tag_name.downcase]
    111     if HTML_BLOCK_ELEMENTS.include?(tag_name)
    112       warning("Found block HTML tag '#{tag_name}' in span-level text on line #{line}")
    113       add_text(result)
    114       return
    115     end
    116
    117     attrs = parse_html_attributes(@src[2], line, HTML_ELEMENT[tag_name])
    118     attrs.each_value {|value| value.gsub!(/\n+/, ' ') unless value.empty? }
    119
    120     do_parsing = if HTML_CONTENT_MODEL[tag_name] == :raw || @tree.options[:content_model] == :raw
    121                    false
    122                  else
    123                    @options[:parse_span_html]
    124                  end
    125     if (val = HTML_MARKDOWN_ATTR_MAP[attrs.delete('markdown')])
    126       if val == :block
    127         warning("Cannot use block-level parsing in span-level HTML tag (line #{line}) " \
    128                 "- using default mode")
    129       elsif val == :span
    130         do_parsing = true
    131       elsif val == :default
    132         do_parsing = HTML_CONTENT_MODEL[tag_name] != :raw
    133       elsif val == :raw
    134         do_parsing = false
    135       end
    136     end
    137
    138     el = Element.new(:html_element, tag_name, attrs, category: :span, location: line,
    139                      content_model: (do_parsing ? :span : :raw), is_closed: !!@src[4])
    140     @tree.children << el
    141     stop_re = /<\/#{Regexp.escape(tag_name)}\s*>/
    142     stop_re = Regexp.new(stop_re.source, Regexp::IGNORECASE) if HTML_ELEMENT[tag_name]
    143     if !@src[4] && !HTML_ELEMENTS_WITHOUT_BODY.include?(el.value)
    144       if parse_spans(el, stop_re, (do_parsing ? nil : [:span_html]))
    145         @src.scan(stop_re)
    146       else
    147         warning("Found no end tag for '#{el.value}' (line #{line}) - auto-closing it")
    148         add_text(@src.rest, el)
    149         @src.terminate
    150       end
    151     end
    152     Kramdown::Parser::Html::ElementConverter.convert(@root, el) if @options[:html_to_native]
    153   else
    154     add_text(@src.getch)
    155   end
    156 end

parse_table() click to toggle source

Parse the table at the current location.

        # File lib/kramdown/parser/kramdown/table.rb
     24 def parse_table
     25   return false unless after_block_boundary?
     26
     27   saved_pos = @src.save_pos
     28   orig_pos = @src.pos
     29   table = new_block_el(:table, nil, nil, alignment: [], location: @src.current_line_number)
     30   leading_pipe = (@src.check(TABLE_LINE) =~ /^\s*\|/)
     31   @src.scan(TABLE_SEP_LINE)
     32
     33   rows = []
     34   has_footer = false
     35   columns = 0
     36
     37   add_container = lambda do |type, force|
     38     if !has_footer || type != :tbody || force
     39       cont = Element.new(type)
     40       cont.children, rows = rows, []
     41       table.children << cont
     42     end
     43   end
     44
     45   until @src.eos?
     46     break unless @src.check(TABLE_LINE)
     47     if @src.scan(TABLE_SEP_LINE)
     48       if rows.empty?
     49         # nothing to do, ignoring multiple consecutive separator lines
     50       elsif table.options[:alignment].empty? && !has_footer
     51         add_container.call(:thead, false)
     52         table.options[:alignment] = @src[1].scan(TABLE_HSEP_ALIGN).map do |left, right|
     53           (left.empty? && right.empty? && :default) || (right.empty? && :left) ||
     54             (left.empty? && :right) || :center
     55         end
     56       else # treat as normal separator line
     57         add_container.call(:tbody, false)
     58       end
     59     elsif @src.scan(TABLE_FSEP_LINE)
     60       add_container.call(:tbody, true) unless rows.empty?
     61       has_footer = true
     62     elsif @src.scan(TABLE_ROW_LINE)
     63       trow = Element.new(:tr)
     64
     65       # parse possible code spans on the line and correctly split the line into cells
     66       env = save_env
     67       cells = []
     68       @src[1].split(/(<code.*?>.*?<\/code>)/).each_with_index do |str, i|
     69         if i.odd?
     70           (cells.empty? ? cells : cells.last) << str
     71         else
     72           reset_env(src: Kramdown::Utils::StringScanner.new(str, @src.current_line_number))
     73           root = Element.new(:root)
     74           parse_spans(root, nil, [:codespan])
     75
     76           root.children.each do |c|
     77             if c.type == :raw_text
     78               f, *l = c.value.split(/(?<!\\)\|/, -1).map {|t| t.gsub(/\\\|/, '|') }
     79               (cells.empty? ? cells : cells.last) << f
     80               cells.concat(l)
     81             else
     82               delim = (c.value.scan(/`+/).max || '') + '`'
     83               tmp = +"#{delim}#{' ' if delim.size > 1}#{c.value}#{' ' if delim.size > 1}#{delim}"
     84               (cells.empty? ? cells : cells.last) << tmp
     85             end
     86           end
     87         end
     88       end
     89       restore_env(env)
     90
     91       cells.shift if leading_pipe && cells.first.strip.empty?
     92       cells.pop if cells.last.strip.empty?
     93       cells.each do |cell_text|
     94         tcell = Element.new(:td)
     95         tcell.children << Element.new(:raw_text, cell_text.strip)
     96         trow.children << tcell
     97       end
     98       columns = [columns, cells.length].max
     99       rows << trow
    100     else
    101       break
    102     end
    103   end
    104
    105   unless before_block_boundary?
    106     @src.revert_pos(saved_pos)
    107     return false
    108   end
    109
    110   # Parse all lines of the table with the code span parser
    111   env = save_env
    112   l_src = ::Kramdown::Utils::StringScanner.new(extract_string(orig_pos...(@src.pos - 1), @src),
    113                                                @src.current_line_number)
    114   reset_env(src: l_src)
    115   root = Element.new(:root)
    116   parse_spans(root, nil, [:codespan, :span_html])
    117   restore_env(env)
    118
    119   # Check if each line has at least one unescaped pipe that is not inside a code span/code
    120   # HTML element
    121   # Note: It doesn't matter that we parse *all* span HTML elements because the row splitting
    122   # algorithm above only takes <code> elements into account!
    123   pipe_on_line = false
    124   while (c = root.children.shift)
    125     next unless (lines = c.value)
    126     lines = lines.split("\n")
    127     if c.type == :codespan
    128       if lines.size > 2 || (lines.size == 2 && !pipe_on_line)
    129         break
    130       elsif lines.size == 2 && pipe_on_line
    131         pipe_on_line = false
    132       end
    133     else
    134       break if lines.size > 1 && !pipe_on_line && lines.first !~ /^#{TABLE_PIPE_CHECK}/o
    135       pipe_on_line = (lines.size > 1 ? false : pipe_on_line) || (lines.last =~ /^#{TABLE_PIPE_CHECK}/o)
    136     end
    137   end
    138   @src.revert_pos(saved_pos) and return false unless pipe_on_line
    139
    140   add_container.call(has_footer ? :tfoot : :tbody, false) unless rows.empty?
    141
    142   if table.children.none? {|el| el.type == :tbody }
    143     warning("Found table without body on line #{table.options[:location]} - ignoring it")
    144     @src.revert_pos(saved_pos)
    145     return false
    146   end
    147
    148   # adjust all table rows to have equal number of columns, same for alignment defs
    149   table.children.each do |kind|
    150     kind.children.each do |row|
    151       (columns - row.children.length).times do
    152         row.children << Element.new(:td)
    153       end
    154     end
    155   end
    156   if table.options[:alignment].length > columns
    157     table.options[:alignment] = table.options[:alignment][0...columns]
    158   else
    159     table.options[:alignment] += [:default] * (columns - table.options[:alignment].length)
    160   end
    161
    162   @tree.children << table
    163
    164   true
    165 end

parse_typographic_syms() click to toggle source

Parse the typographic symbols at the current location.

       # File lib/kramdown/parser/kramdown/typographic_symbol.rb
    21 def parse_typographic_syms
    22   start_line_number = @src.current_line_number
    23   @src.pos += @src.matched_size
    24   val = TYPOGRAPHIC_SYMS_SUBST[@src.matched]
    25   if val.kind_of?(Symbol)
    26     @tree.children << Element.new(:typographic_sym, val, nil, location: start_line_number)
    27   elsif @src.matched == '\\<<'
    28     @tree.children << Element.new(:entity, ::Kramdown::Utils::Entities.entity('lt'),
    29                                   nil, location: start_line_number)
    30     @tree.children << Element.new(:entity, ::Kramdown::Utils::Entities.entity('lt'),
    31                                   nil, location: start_line_number)
    32   else
    33     @tree.children << Element.new(:entity, ::Kramdown::Utils::Entities.entity('gt'),
    34                                   nil, location: start_line_number)
    35     @tree.children << Element.new(:entity, ::Kramdown::Utils::Entities.entity('gt'),
    36                                   nil, location: start_line_number)
    37   end
    38 end

replace_abbreviations(el, regexps = nil) click to toggle source

Replace the abbreviation text with elements.

       # File lib/kramdown/parser/kramdown/abbreviation.rb
    40 def replace_abbreviations(el, regexps = nil)
    41   return if @root.options[:abbrev_defs].empty?
    42   unless regexps
    43     sorted_abbrevs = @root.options[:abbrev_defs].keys.sort {|a, b| b.length <=> a.length }
    44     regexps = [Regexp.union(*sorted_abbrevs.map {|k| /#{Regexp.escape(k)}/ })]
    45     regexps << /(?=(?:\W|^)#{regexps.first}(?!\w))/ # regexp should only match on word boundaries
    46   end
    47   el.children.map! do |child|
    48     if child.type == :text && el.options[:content_model] != :raw
    49       if child.value =~ regexps.first
    50         result = []
    51         strscan = Kramdown::Utils::StringScanner.new(child.value, child.options[:location])
    52         text_lineno = strscan.current_line_number
    53         while (temp = strscan.scan_until(regexps.last))
    54           abbr_lineno = strscan.current_line_number
    55           abbr = strscan.scan(regexps.first) # begin of line case of abbr with \W char as first one
    56           if abbr.nil?
    57             temp << strscan.scan(/\W|^/)
    58             abbr = strscan.scan(regexps.first)
    59           end
    60           result << Element.new(:text, temp, nil, location: text_lineno)
    61           result << Element.new(:abbreviation, abbr, nil, location: abbr_lineno)
    62           text_lineno = strscan.current_line_number
    63         end
    64         result << Element.new(:text, strscan.rest, nil, location: text_lineno)
    65       else
    66         child
    67       end
    68     else
    69       replace_abbreviations(child, regexps)
    70       child
    71     end
    72   end.flatten!
    73 end

update_ial_with_ial(ial, opts) click to toggle source

Update the `ial` with the information from the inline attribute list `opts`.

       # File lib/kramdown/parser/kramdown/extensions.rb
    40 def update_ial_with_ial(ial, opts)
    41   (ial[:refs] ||= []).concat(opts[:refs]) if opts.key?(:refs)
    42   opts.each do |k, v|
    43     if k == IAL_CLASS_ATTR
    44       ial[k] = "#{ial[k]} #{v}".lstrip
    45     elsif k.kind_of?(String)
    46       ial[k] = v
    47     end
    48   end
    49 end

### Protected Instance Methods

add_header(level, text, id) click to toggle source

       # File lib/kramdown/parser/kramdown/header.rb
    58 def add_header(level, text, id)
    59   start_line_number = @src.current_line_number
    60   @src.pos += @src.matched_size
    61   el = new_block_el(:header, nil, nil, level: level, raw_text: text, location: start_line_number)
    62   add_text(text, el)
    63   el.attr['id'] = id if id
    64   @tree.children << el
    65 end

configure_parser() click to toggle source

Adapt the object to allow parsing like specified in the options.

        # File lib/kramdown/parser/kramdown.rb
    120 def configure_parser
    121   @parsers = {}
    122   (@block_parsers + @span_parsers).each do |name|
    123     if self.class.has_parser?(name)
    124       @parsers[name] = self.class.parser(name)
    125     else
    126       raise Kramdown::Error, "Unknown parser: #{name}"
    127     end
    128   end
    129   @span_start, @span_start_re = span_parser_regexps
    130 end

new_block_el(*args) click to toggle source

Create a new block-level element, taking care of applying a preceding block IAL if it exists. This method should always be used for creating a block-level element!

        # File lib/kramdown/parser/kramdown.rb
    304 def new_block_el(*args)
    305   el = Element.new(*args)
    306   if @block_ial
    307     el.options[:ial] = @block_ial
    308     @block_ial = nil
    309   end
    310   el
    311 end

parse_blocks(el, text = nil) click to toggle source

Parse all block-level elements in `text` into the element `el`.

        # File lib/kramdown/parser/kramdown.rb
    139 def parse_blocks(el, text = nil)
    140   @stack.push([@tree, @src, @block_ial])
    141   @tree, @block_ial = el, nil
    142   @src = (text.nil? ? @src : ::Kramdown::Utils::StringScanner.new(text, el.options[:location]))
    143
    144   status = catch(:stop_block_parsing) do
    145     until @src.eos?
    146       @block_parsers.any? do |name|
    147         if @src.check(@parsers[name].start_re)
    148           send(@parsers[name].method)
    149         else
    150           false
    151         end
    152       end || begin
    153         warning('Warning: this should not occur - no block parser handled the line')
    154         add_text(@src.scan(/.*\n/))
    155       end
    156     end
    157   end
    158
    159   @tree, @src, @block_ial = *@stack.pop
    160   status
    161 end

parse_header_contents() click to toggle source

Returns header text and optional ID.

       # File lib/kramdown/parser/kramdown/header.rb
    46 def parse_header_contents
    47   text = @src["contents"]
    48   text.rstrip!
    49   id_match = HEADER_ID.match(text)
    50   if id_match
    51     id = id_match["id"]
    52     text = text[0...-id_match[0].length]
    53     text.rstrip!
    54   end
    55   [text, id]
    56 end

parse_spans(el, stop_re = nil, parsers = nil, text_type = @text_type) { |: true)| ... } click to toggle source

Parse all span-level elements in the source string of @src into `el`.

If the parameter `stop_re` (a regexp) is used, parsing is immediately stopped if the regexp matches and if no block is given or if a block is given and it returns `true`.

The parameter `parsers` can be used to specify the (span-level) parsing methods that should be used for parsing.

The parameter `text_type` specifies the type which should be used for created text nodes.

        # File lib/kramdown/parser/kramdown.rb
    213 def parse_spans(el, stop_re = nil, parsers = nil, text_type = @text_type)
    214   @stack.push([@tree, @text_type]) unless @tree.nil?
    215   @tree, @text_type = el, text_type
    216
    217   span_start = @span_start
    218   span_start_re = @span_start_re
    219   span_start, span_start_re = span_parser_regexps(parsers) if parsers
    220   parsers ||= @span_parsers
    221
    222   used_re = (stop_re.nil? ? span_start_re : span_pattern_cache(stop_re, span_start))
    223   stop_re_found = false
    224   while !@src.eos? && !stop_re_found
    225     if (result = @src.scan_until(used_re))
    226       add_text(result)
    227       if stop_re && @src.check(stop_re)
    228         stop_re_found = (block_given? ? yield : true)
    229       end
    230       processed = parsers.any? do |name|
    231         if @src.check(@parsers[name].start_re)
    232           send(@parsers[name].method)
    233           true
    234         else
    235           false
    236         end
    237       end unless stop_re_found
    238       add_text(@src.getch) if !processed && !stop_re_found
    239     else
    240       (add_text(@src.rest); @src.terminate) unless stop_re
    241       break
    242     end
    243   end
    244
    245   @tree, @text_type = @stack.pop
    246
    247   stop_re_found
    248 end

reset_env(opts = {}) click to toggle source

Reset the current parsing environment. The parameter `env` can be used to set initial values for one or more environment variables.

        # File lib/kramdown/parser/kramdown.rb
    252 def reset_env(opts = {})
    253   opts = {text_type: :raw_text, stack: []}.merge(opts)
    254   @src = opts[:src]
    255   @tree = opts[:tree]
    256   @block_ial = opts[:block_ial]
    257   @stack = opts[:stack]
    258   @text_type = opts[:text_type]
    259 end

restore_env(env) click to toggle source

Restore the current parsing environment.

        # File lib/kramdown/parser/kramdown.rb
    267 def restore_env(env)
    268   @src, @tree, @block_ial, @stack, @text_type = *env
    269 end

save_env() click to toggle source

Return the current parsing environment.

        # File lib/kramdown/parser/kramdown.rb
    262 def save_env
    263   [@src, @tree, @block_ial, @stack, @text_type]
    264 end

span_parser_regexps(parsers = @span_parsers) click to toggle source

Create the needed span parser regexps.

        # File lib/kramdown/parser/kramdown.rb
    133 def span_parser_regexps(parsers = @span_parsers)
    134   span_start = /#{parsers.map {|name| @parsers[name].span_start }.join('|')}/
    135   [span_start, /(?=#{span_start})/]
    136 end

update_attr_with_ial(attr, ial) click to toggle source

Update the given attributes hash `attr` with the information from the inline attribute list `ial` and all referenced ALDs.

        # File lib/kramdown/parser/kramdown.rb
    273 def update_attr_with_ial(attr, ial)
    274   ial[:refs]&.each do |ref|
    275     update_attr_with_ial(attr, ref) if (ref = @alds[ref])
    276   end
    277   ial.each do |k, v|
    278     if k == IAL_CLASS_ATTR
    279       attr[k] = "#{attr[k]} #{v}".lstrip
    280     elsif k.kind_of?(String)
    281       attr[k] = v
    282     end
    283   end
    284 end

update_link_definitions(link_defs) click to toggle source

Update the parser specific link definitions with the data from `link_defs` (the value of the :link_defs option).

The parameter `link_defs` is a hash where the keys are possibly unnormalized link IDs and the values are two element arrays consisting of the link target and a title (can be `nil`).

        # File lib/kramdown/parser/kramdown.rb
    115 def update_link_definitions(link_defs)
    116   link_defs.each {|k, v| @link_defs[normalize_link_id(k)] = v }
    117 end

update_raw_text(item) click to toggle source

Update the raw text for automatic ID generation.

        # File lib/kramdown/parser/kramdown.rb
    287 def update_raw_text(item)
    288   raw_text = +''
    289
    290   append_text = lambda do |child|
    291     if child.type == :text
    292       raw_text << child.value
    293     else
    294       child.children.each {|c| append_text.call(c) }
    295     end
    296   end
    297
    298   append_text.call(item)
    299   item.options[:raw_text] = raw_text
    300 end

update_tree(element) click to toggle source

Update the tree by parsing all :`raw_text` elements with the span-level parser (resets the environment) and by updating the attributes from the IALs.

        # File lib/kramdown/parser/kramdown.rb
    165 def update_tree(element)
    166   last_blank = nil
    167   element.children.map! do |child|
    168     if child.type == :raw_text
    169       last_blank = nil
    170       reset_env(src: ::Kramdown::Utils::StringScanner.new(child.value, element.options[:location]),
    171                 text_type: :text)
    172       parse_spans(child)
    173       child.children
    174     elsif child.type == :eob
    175       update_attr_with_ial(child.attr, child.options[:ial]) if child.options[:ial]
    176       []
    177     elsif child.type == :blank
    178       if last_blank
    179         last_blank.value << child.value
    180         []
    181       else
    182         last_blank = child
    183         child
    184       end
    185     else
    186       last_blank = nil
    187       update_tree(child)
    188       update_attr_with_ial(child.attr, child.options[:ial]) if child.options[:ial]
    189       # DEPRECATED: option auto_id_stripping will be removed in 2.0 because then this will be
    190       # the default behaviour
    191       if child.type == :dt || (child.type == :header && @options[:auto_id_stripping])
    192         update_raw_text(child)
    193       end
    194       child
    195     end
    196   end.flatten!
    197 end

### Private Instance Methods

fetch_pattern(type, indentation) click to toggle source

precomputed patterns for indentations 1..4 and fallback expression to compute pattern when indentation is outside the 1..4 range.

        # File lib/kramdown/parser/kramdown/list.rb
    256 def fetch_pattern(type, indentation)
    257   if type == :ul
    258     case indentation
    259     when 1 then %r/^( {0}[+*-])(#{PATTERN_TAIL})/o
    260     when 2 then %r/^( {0,1}[+*-])(#{PATTERN_TAIL})/o
    261     when 3 then %r/^( {0,2}[+*-])(#{PATTERN_TAIL})/o
    262     else %r/^( {0,3}[+*-])(#{PATTERN_TAIL})/o
    263     end
    264   elsif type == :ol
    265     case indentation
    266     when 1 then %r/^( {0}\d+\.)(#{PATTERN_TAIL})/o
    267     when 2 then %r/^( {0,1}\d+\.)(#{PATTERN_TAIL})/o
    268     when 3 then %r/^( {0,2}\d+\.)(#{PATTERN_TAIL})/o
    269     else %r/^( {0,3}\d+\.)(#{PATTERN_TAIL})/o
    270     end
    271   elsif type == :dl
    272     case indentation
    273     when 1 then %r/^( {0}:)(#{PATTERN_TAIL})/o
    274     when 2 then %r/^( {0,1}:)(#{PATTERN_TAIL})/o
    275     when 3 then %r/^( {0,2}:)(#{PATTERN_TAIL})/o
    276     else %r/^( {0,3}:)(#{PATTERN_TAIL})/o
    277     end
    278   end
    279 end

span_pattern_cache(stop_re, span_start) click to toggle source

        # File lib/kramdown/parser/kramdown.rb
    199 def span_pattern_cache(stop_re, span_start)
    200   @span_pattern_cache[stop_re][span_start] ||= /(?=#{Regexp.union(stop_re, span_start)})/
    201 end