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
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
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
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
Create a new Kramdown
parser object with the given options
.
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
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
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
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
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 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
# 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
# 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 the link identifier.
# File lib/kramdown/parser/kramdown/link.rb 16 def normalize_link_id(id) 17 id.gsub(/[\s]+/, ' ').downcase 18 end
# File lib/kramdown/parser/kramdown/paragraph.rb 55 def paragraph_end 56 self.class::PARAGRAPH_END 57 end
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
# 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
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
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 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
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 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 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 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
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
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 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 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 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 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
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
# 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