Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,137 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/utils'
require 'kramdown/parser'
module Kramdown
module Parser
# == \Base class for parsers
#
# This class serves as base class for parsers. It provides common methods that can/should be
# used by all parsers, especially by those using StringScanner(Kramdown) for parsing.
#
# A parser object is used as a throw-away object, i.e. it is only used for storing the needed
# state information during parsing. Therefore one can't instantiate a parser object directly but
# only use the Base::parse method.
#
# == Implementing a parser
#
# Implementing a new parser is rather easy: just derive a new class from this class and put it
# in the Kramdown::Parser module -- the latter is needed so that the auto-detection of the new
# parser works correctly. Then you need to implement the +#parse+ method which has to contain
# the parsing code.
#
# Have a look at the Base::parse, Base::new and Base#parse methods for additional information!
class Base
# The hash with the parsing options.
attr_reader :options
# The array with the parser warnings.
attr_reader :warnings
# The original source string.
attr_reader :source
# The root element of element tree that is created from the source string.
attr_reader :root
# 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.
def initialize(source, options)
@source = source
@options = Kramdown::Options.merge(options)
@root = Element.new(:root, nil, nil, encoding: (source.encoding rescue nil), location: 1,
options: {}, abbrev_defs: {}, abbrev_attr: {})
@root.options[:abbrev_defs].default_proc = @root.options[:abbrev_attr].default_proc =
lambda do |h, k|
k_mod = k.gsub(/[\s\p{Z}]+/, " ")
k != k_mod ? h[k_mod] : nil
end
@warnings = []
@text_type = :text
end
private_class_method(:new, :allocate)
# Parse the +source+ string into an element tree, possibly using the parsing +options+, and
# return the root element of the element tree and an array with warning messages.
#
# Initializes a new instance of the calling class and then calls the +#parse+ method that must
# be implemented by each subclass.
def self.parse(source, options = {})
parser = new(source, options)
parser.parse
[parser.root, parser.warnings]
end
# Parse the source string into an element tree.
#
# The parsing code should parse the source provided in @source and build an element tree the
# root of which should be @root.
#
# This is the only method that has to be implemented by sub-classes!
def parse
raise NotImplementedError
end
# Add the given warning +text+ to the warning array.
def warning(text)
@warnings << text
# TODO: add position information
end
# Modify the string +source+ to be usable by the parser (unifies line ending characters to
# +\n+ and makes sure +source+ ends with a new line character).
def adapt_source(source)
unless source.valid_encoding?
raise "The source text contains invalid characters for the used encoding #{source.encoding}"
end
source = source.encode('UTF-8')
source.gsub!(/\r\n?/, "\n")
source.chomp!
source << "\n"
end
# This helper method adds the given +text+ either to the last element in the +tree+ if it is a
# +type+ element or creates a new text element with the given +type+.
def add_text(text, tree = @tree, type = @text_type)
last = tree.children.last
if last && last.type == type
last.value << text
elsif !text.empty?
location = (last && last.options[:location] || tree.options[:location])
tree.children << Element.new(type, text, nil, location: location)
end
end
# Extract the part of the StringScanner +strscan+ backed string specified by the +range+. This
# method works correctly under Ruby 1.8 and Ruby 1.9.
def extract_string(range, strscan)
result = nil
begin
enc = strscan.string.encoding
strscan.string.force_encoding('ASCII-8BIT')
result = strscan.string[range].force_encoding(enc)
ensure
strscan.string.force_encoding(enc)
end
result
end
end
end
end
@@ -0,0 +1,621 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'rexml/parsers/baseparser'
require 'strscan'
require 'kramdown/utils'
require 'kramdown/parser'
module Kramdown
module Parser
# Used for parsing an HTML document.
#
# The parsing code is in the Parser module that can also be used by other parsers.
class Html < Base
# Contains all constants that are used when parsing.
module Constants
# :stopdoc:
# The following regexps are based on the ones used by REXML, with some slight modifications.
HTML_DOCTYPE_RE = /<!DOCTYPE.*?>/im
HTML_COMMENT_RE = /<!--(.*?)-->/m
HTML_INSTRUCTION_RE = /<\?(.*?)\?>/m
HTML_CDATA_RE = /<!\[CDATA\[(.*?)\]\]>/m
HTML_ATTRIBUTE_RE = /\s*(#{REXML::Parsers::BaseParser::UNAME_STR})(?:\s*=\s*(?:(\p{Word}+)|("|')(.*?)\3))?/m
HTML_TAG_RE = /<((?>#{REXML::Parsers::BaseParser::UNAME_STR}))\s*((?>\s+#{REXML::Parsers::BaseParser::UNAME_STR}(?:\s*=\s*(?:\p{Word}+|("|').*?\3))?)*)\s*(\/)?>/m
HTML_TAG_CLOSE_RE = /<\/(#{REXML::Parsers::BaseParser::UNAME_STR})\s*>/m
HTML_ENTITY_RE = /&([\w:][\w.:-]*);|&#(\d+);|&\#x([0-9a-fA-F]+);/
HTML_CONTENT_MODEL_BLOCK = %w[address applet article aside blockquote body
dd details div dl fieldset figure figcaption
footer form header hgroup iframe li main
map menu nav noscript object section summary td]
HTML_CONTENT_MODEL_SPAN = %w[a abbr acronym b bdo big button cite caption del dfn dt em
h1 h2 h3 h4 h5 h6 i ins label legend optgroup p q rb rbc
rp rt rtc ruby select small span strong sub sup th tt]
HTML_CONTENT_MODEL_RAW = %w[script style math option textarea pre code kbd samp var]
# The following elements are also parsed as raw since they need child elements that cannot
# be expressed using kramdown syntax: colgroup table tbody thead tfoot tr ul ol
HTML_CONTENT_MODEL = Hash.new {|h, k| h[k] = :raw }
HTML_CONTENT_MODEL_BLOCK.each {|i| HTML_CONTENT_MODEL[i] = :block }
HTML_CONTENT_MODEL_SPAN.each {|i| HTML_CONTENT_MODEL[i] = :span }
HTML_CONTENT_MODEL_RAW.each {|i| HTML_CONTENT_MODEL[i] = :raw }
# Some HTML elements like script belong to both categories (i.e. are valid in block and
# span HTML) and don't appear therefore!
# script, textarea
HTML_SPAN_ELEMENTS = %w[a abbr acronym b big bdo br button cite code del dfn em i img input
ins kbd label mark option q rb rbc rp rt rtc ruby samp select small
span strong sub sup time tt u var]
HTML_BLOCK_ELEMENTS = %w[address article aside applet body blockquote caption col colgroup
dd div dl dt fieldset figcaption footer form h1 h2 h3 h4 h5 h6
header hgroup hr html head iframe legend menu li main map nav ol
optgroup p pre section summary table tbody td th thead tfoot tr ul]
HTML_ELEMENTS_WITHOUT_BODY = %w[area base br col command embed hr img input keygen link
meta param source track wbr]
HTML_ELEMENT = Hash.new(false)
(HTML_SPAN_ELEMENTS + HTML_BLOCK_ELEMENTS + HTML_ELEMENTS_WITHOUT_BODY +
HTML_CONTENT_MODEL.keys).each do |a|
HTML_ELEMENT[a] = true
end
end
# Contains the parsing methods. This module can be mixed into any parser to get HTML parsing
# functionality. The only thing that must be provided by the class are instance variable
# @stack for storing the needed state and @src (instance of StringScanner) for the actual
# parsing.
module Parser
include Constants
# Process the HTML start tag that has already be scanned/checked via @src.
#
# Does the common processing steps and then yields to the caller for further processing
# (first parameter is the created element; the second parameter is +true+ if the HTML
# element is already closed, ie. contains no body; the third parameter specifies whether the
# body - and the end tag - need to be handled in case closed=false).
def handle_html_start_tag(line = nil) # :yields: el, closed, handle_body
name = @src[1]
name.downcase! if HTML_ELEMENT[name.downcase]
closed = !@src[4].nil?
attrs = parse_html_attributes(@src[2], line, HTML_ELEMENT[name])
el = Element.new(:html_element, name, attrs, category: :block)
el.options[:location] = line if line
@tree.children << el
if !closed && HTML_ELEMENTS_WITHOUT_BODY.include?(el.value)
closed = true
end
if name == 'script' || name == 'style'
handle_raw_html_tag(name)
yield(el, false, false)
else
yield(el, closed, true)
end
end
# Parses the given string for HTML attributes and returns the resulting hash.
#
# If the optional +line+ parameter is supplied, it is used in warning messages.
#
# If the optional +in_html_tag+ parameter is set to +false+, attributes are not modified to
# contain only lowercase letters.
def parse_html_attributes(str, line = nil, in_html_tag = true)
attrs = {}
str.scan(HTML_ATTRIBUTE_RE).each do |attr, val, _sep, quoted_val|
attr.downcase! if in_html_tag
if attrs.key?(attr)
warning("Duplicate HTML attribute '#{attr}' on line #{line || '?'} - overwriting previous one")
end
attrs[attr] = val || quoted_val || ""
end
attrs
end
# Handle the raw HTML tag at the current position.
def handle_raw_html_tag(name)
curpos = @src.pos
if @src.scan_until(/(?=<\/#{name}\s*>)/mi)
add_text(extract_string(curpos...@src.pos, @src), @tree.children.last, :raw)
@src.scan(HTML_TAG_CLOSE_RE)
else
add_text(@src.rest, @tree.children.last, :raw)
@src.terminate
warning("Found no end tag for '#{name}' - auto-closing it")
end
end
HTML_RAW_START = /(?=<(#{REXML::Parsers::BaseParser::UNAME_STR}|\/|!--|\?|!\[CDATA\[))/ # :nodoc:
# Parse raw HTML from the current source position, storing the found elements in +el+.
# Parsing continues until one of the following criteria are fulfilled:
#
# - The end of the document is reached.
# - The matching end tag for the element +el+ is found (only used if +el+ is an HTML
# element).
#
# When an HTML start tag is found, processing is deferred to #handle_html_start_tag,
# providing the block given to this method.
def parse_raw_html(el, &block)
@stack.push(@tree)
@tree = el
done = false
while !@src.eos? && !done
if (result = @src.scan_until(HTML_RAW_START))
add_text(result, @tree, :text)
line = @src.current_line_number
if (result = @src.scan(HTML_COMMENT_RE))
@tree.children << Element.new(:xml_comment, result, nil, category: :block, location: line)
elsif (result = @src.scan(HTML_INSTRUCTION_RE))
@tree.children << Element.new(:xml_pi, result, nil, category: :block, location: line)
elsif @src.scan(HTML_CDATA_RE)
@tree.children << Element.new(:text, @src[1], nil, cdata: true, location: line)
elsif @src.scan(HTML_TAG_RE)
if method(:handle_html_start_tag).arity.abs >= 1
handle_html_start_tag(line, &block)
else
handle_html_start_tag(&block) # DEPRECATED: method needs to accept line number in 2.0
end
elsif @src.scan(HTML_TAG_CLOSE_RE)
if @tree.value == (HTML_ELEMENT[@tree.value] ? @src[1].downcase : @src[1])
done = true
else
add_text(@src.matched, @tree, :text)
warning("Found invalidly used HTML closing tag for '#{@src[1]}' on " \
"line #{line} - ignoring it")
end
else
add_text(@src.getch, @tree, :text)
end
else
add_text(@src.rest, @tree, :text)
@src.terminate
if @tree.type == :html_element
warning("Found no end tag for '#{@tree.value}' on line " \
"#{@tree.options[:location]} - auto-closing it")
end
done = true
end
end
@tree = @stack.pop
end
end
# Converts HTML elements to native elements if possible.
class ElementConverter
# :stopdoc:
include Constants
include ::Kramdown::Utils::Entities
REMOVE_TEXT_CHILDREN = %w[html head hgroup ol ul dl table colgroup tbody thead tfoot tr
select optgroup]
WRAP_TEXT_CHILDREN = %w[body section nav article aside header footer address div li dd
blockquote figure figcaption fieldset form]
REMOVE_WHITESPACE_CHILDREN = %w[body section nav article aside header footer address
div li dd blockquote figure figcaption td th fieldset form]
STRIP_WHITESPACE = %w[address article aside blockquote body caption dd div dl dt fieldset
figcaption form footer header h1 h2 h3 h4 h5 h6 legend li nav p
section td th]
SIMPLE_ELEMENTS = %w[em strong blockquote hr br img p thead tbody tfoot tr td th ul ol dl
li dl dt dd]
def initialize(root)
@root = root
end
def self.convert(root, el = root)
new(root).process(el)
end
# Convert the element +el+ and its children.
def process(el, do_conversion = true, preserve_text = false, parent = nil)
case el.type
when :xml_comment, :xml_pi
ptype = if parent.nil?
'div'
else
case parent.type
when :html_element then parent.value
when :code_span then 'code'
when :code_block then 'pre'
when :header then 'h1'
else parent.type.to_s
end
end
el.options.replace(category: (HTML_CONTENT_MODEL[ptype] == :span ? :span : :block))
return
when :html_element
# do nothing
when :root
el.children.map! do |c|
if c.type == :text
process_text(c.value, !do_conversion)
else
process(c)
c
end
end.flatten!
remove_whitespace_children(el)
return
else return
end
mname = "convert_#{el.value}"
if do_conversion && self.class.method_defined?(mname)
send(mname, el)
else
type = el.value
remove_text_children(el) if do_conversion && REMOVE_TEXT_CHILDREN.include?(type)
if do_conversion && SIMPLE_ELEMENTS.include?(type)
set_basics(el, type.intern)
process_children(el, do_conversion, preserve_text)
else
process_html_element(el, do_conversion, preserve_text)
end
if do_conversion
strip_whitespace(el) if STRIP_WHITESPACE.include?(type)
remove_whitespace_children(el) if REMOVE_WHITESPACE_CHILDREN.include?(type)
wrap_text_children(el) if WRAP_TEXT_CHILDREN.include?(type)
end
end
end
def process_children(el, do_conversion = true, preserve_text = false)
el.children.map! do |c|
if c.type == :text
process_text(c.value, preserve_text || !do_conversion)
else
process(c, do_conversion, preserve_text, el)
c
end
end.flatten!
end
# Process the HTML text +raw+: compress whitespace (if +preserve+ is +false+) and convert
# entities in entity elements.
def process_text(raw, preserve = false)
raw.gsub!(/\s+/, ' ') unless preserve
src = Kramdown::Utils::StringScanner.new(raw)
result = []
until src.eos?
if (tmp = src.scan_until(/(?=#{HTML_ENTITY_RE})/o))
result << Element.new(:text, tmp)
src.scan(HTML_ENTITY_RE)
val = src[1] || src[2]&.to_i || src[3].hex
result << if %w[lsquo rsquo ldquo rdquo].include?(val)
Element.new(:smart_quote, val.intern)
elsif %w[mdash ndash hellip laquo raquo].include?(val)
Element.new(:typographic_sym, val.intern)
else
begin
Element.new(:entity, entity(val), nil, original: src.matched)
rescue ::Kramdown::Error
src.pos -= src.matched_size - 1
Element.new(:entity, ::Kramdown::Utils::Entities.entity('amp'))
end
end
else
result << Element.new(:text, src.rest)
src.terminate
end
end
result
end
def process_html_element(el, do_conversion = true, preserve_text = false)
el.options.replace(category: HTML_SPAN_ELEMENTS.include?(el.value) ? :span : :block,
content_model: (do_conversion ? HTML_CONTENT_MODEL[el.value] : :raw))
process_children(el, do_conversion, preserve_text)
end
def remove_text_children(el)
el.children.delete_if {|c| c.type == :text }
end
def wrap_text_children(el)
tmp = []
last_is_p = false
el.children.each do |c|
if !c.block? || c.type == :text
unless last_is_p
tmp << Element.new(:p, nil, nil, transparent: true)
last_is_p = true
end
tmp.last.children << c
tmp
else
tmp << c
last_is_p = false
end
end
el.children = tmp
end
def strip_whitespace(el)
return if el.children.empty?
if el.children.first.type == :text
el.children.first.value.lstrip!
end
if el.children.last.type == :text
el.children.last.value.rstrip!
end
end
def remove_whitespace_children(el)
i = -1
el.children = el.children.reject do |c|
i += 1
c.type == :text && c.value.strip.empty? &&
(i == 0 || i == el.children.length - 1 || (el.children[i - 1].block? &&
el.children[i + 1].block?))
end
end
def set_basics(el, type, opts = {})
el.type = type
el.options.replace(opts)
el.value = nil
end
def extract_text(el, raw)
raw << el.value.to_s if el.type == :text
el.children.each {|c| extract_text(c, raw) }
end
def convert_textarea(el)
process_html_element(el, true, true)
end
def convert_a(el)
if el.attr['href']
set_basics(el, :a)
process_children(el)
else
process_html_element(el, false)
end
end
EMPHASIS_TYPE_MAP = {'em' => :em, 'i' => :em, 'strong' => :strong, 'b' => :strong}
def convert_em(el)
text = +''
extract_text(el, text)
if text =~ /\A\s/ || text =~ /\s\z/
process_html_element(el, false)
else
set_basics(el, EMPHASIS_TYPE_MAP[el.value])
process_children(el)
end
end
%w[b strong i].each do |i|
alias_method("convert_#{i}".to_sym, :convert_em)
end
def convert_h1(el)
set_basics(el, :header, level: el.value[1..1].to_i)
extract_text(el, el.options[:raw_text] = +'')
process_children(el)
end
%w[h2 h3 h4 h5 h6].each do |i|
alias_method("convert_#{i}".to_sym, :convert_h1)
end
def convert_code(el)
raw = +''
extract_text(el, raw)
result = process_text(raw, true)
begin
str = result.inject(+'') do |mem, c|
case c.type
when :text
mem << c.value
when :entity
mem << if [60, 62, 34, 38].include?(c.value.code_point)
c.value.code_point.chr
else
c.value.char
end
when :smart_quote, :typographic_sym
mem << entity(c.value.to_s).char
else
raise "Bug - please report"
end
end
result.clear
result << Element.new(:text, str)
rescue StandardError
end
if result.length > 1 || result.first.type != :text
process_html_element(el, false, true)
else
if el.value == 'code'
set_basics(el, :codespan)
el.attr['class']&.gsub!(/\s+\bhighlighter-\w+\b|\bhighlighter-\w+\b\s*/, '')
else
set_basics(el, :codeblock)
if el.children.size == 1 && el.children.first.value == 'code'
value = (el.children.first.attr['class'] || '').scan(/\blanguage-\S+/).first
el.attr['class'] = "#{value} #{el.attr['class']}".rstrip if value
end
end
el.value = result.first.value
el.children.clear
end
end
alias convert_pre convert_code
def convert_table(el)
unless is_simple_table?(el)
process_html_element(el, false)
return
end
remove_text_children(el)
process_children(el)
set_basics(el, :table)
calc_alignment = lambda do |c|
if c.type == :tr
el.options[:alignment] = c.children.map do |td|
if td.attr['style']
td.attr['style'].slice!(/(?:;\s*)?text-align:\s+(center|left|right)/)
td.attr.delete('style') if td.attr['style'].strip.empty?
$1 ? $1.to_sym : :default
else
:default
end
end
else
c.children.each {|cc| calc_alignment.call(cc) }
end
end
calc_alignment.call(el)
el.children.delete_if {|c| c.type == :html_element }
change_th_type = lambda do |c|
if c.type == :th
c.type = :td
else
c.children.each {|cc| change_th_type.call(cc) }
end
end
change_th_type.call(el)
if el.children.first.type == :tr
tbody = Element.new(:tbody)
tbody.children = el.children
el.children = [tbody]
end
end
def is_simple_table?(el)
only_phrasing_content = lambda do |c|
c.children.all? do |cc|
(cc.type == :text || !HTML_BLOCK_ELEMENTS.include?(cc.value)) && only_phrasing_content.call(cc)
end
end
check_cells = proc do |c|
if c.value == 'th' || c.value == 'td'
return false unless only_phrasing_content.call(c)
else
c.children.each {|cc| check_cells.call(cc) }
end
end
check_cells.call(el)
nr_cells = 0
check_nr_cells = lambda do |t|
if t.value == 'tr'
count = t.children.count {|cc| cc.value == 'th' || cc.value == 'td' }
if count != nr_cells
if nr_cells == 0
nr_cells = count
else
nr_cells = -1
break
end
end
else
t.children.each {|cc| check_nr_cells.call(cc) }
end
end
check_nr_cells.call(el)
return false if nr_cells == -1 || nr_cells == 0
alignment = nil
check_alignment = proc do |t|
if t.value == 'tr'
cur_alignment = t.children.select {|cc| cc.value == 'th' || cc.value == 'td' }.map do |cell|
md = /text-align:\s+(center|left|right|justify|inherit)/.match(cell.attr['style'].to_s)
return false if md && (md[1] == 'justify' || md[1] == 'inherit')
md.nil? ? :default : md[1]
end
alignment = cur_alignment if alignment.nil?
return false if alignment != cur_alignment
else
t.children.each {|cc| check_alignment.call(cc) }
end
end
check_alignment.call(el)
check_rows = lambda do |t, type|
t.children.all? do |r|
(r.value == 'tr' || r.type == :text) && r.children.all? {|c| c.value == type || c.type == :text }
end
end
check_rows.call(el, 'td') ||
(el.children.all? do |t|
t.type == :text || (t.value == 'thead' && check_rows.call(t, 'th')) ||
((t.value == 'tfoot' || t.value == 'tbody') && check_rows.call(t, 'td'))
end && el.children.any? {|t| t.value == 'tbody' })
end
def convert_script(el)
if is_math_tag?(el)
handle_math_tag(el)
else
process_html_element(el)
end
end
def is_math_tag?(el)
el.attr['type'].to_s =~ /\bmath\/tex\b/
end
def handle_math_tag(el)
set_basics(el, :math, category: (el.attr['type'].include?("mode=display") ? :block : :span))
el.value = el.children.shift.value.sub(/\A(?:%\s*)?<!\[CDATA\[\n?(.*?)(?:\s%)?\]\]>\z/m, '\1')
el.attr.delete('type')
end
end
include Parser
# Parse the source string provided on initialization as HTML document.
def parse
@stack, @tree = [], @root
@src = Kramdown::Utils::StringScanner.new(adapt_source(source))
while true
if (result = @src.scan(/\s*#{HTML_INSTRUCTION_RE}/o))
@tree.children << Element.new(:xml_pi, result.strip, nil, category: :block)
elsif (result = @src.scan(/\s*#{HTML_DOCTYPE_RE}/o))
# ignore the doctype
elsif (result = @src.scan(/\s*#{HTML_COMMENT_RE}/o))
@tree.children << Element.new(:xml_comment, result.strip, nil, category: :block)
else
break
end
end
tag_handler = lambda do |c, closed, handle_body|
parse_raw_html(c, &tag_handler) if !closed && handle_body
end
parse_raw_html(@tree, &tag_handler)
ElementConverter.convert(@tree)
end
end
end
end
@@ -0,0 +1,377 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'strscan'
require 'stringio'
require 'kramdown/parser'
# TODO: use [[:alpha:]] in all regexp to allow parsing of international values in 1.9.1
# NOTE: use @src.pre_match only before other check/match?/... operations, otherwise the content is changed
module Kramdown
module Parser
# 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
#
class Kramdown < Base
include ::Kramdown
# Create a new Kramdown parser object with the given +options+.
def initialize(source, options)
super
reset_env
@alds = {}
@footnotes = {}
@link_defs = {}
update_link_definitions(@options[:link_defs])
@block_parsers = [:blank_line, :codeblock, :codeblock_fenced, :blockquote, :atx_header,
:horizontal_rule, :list, :definition_list, :block_html, :setext_header,
:block_math, :table, :footnote_definition, :link_definition,
:abbrev_definition, :block_extensions, :eob_marker, :paragraph]
@span_parsers = [:emphasis, :codespan, :autolink, :span_html, :footnote_marker, :link,
:smart_quotes, :inline_math, :span_extensions, :html_entity,
:typographic_syms, :line_break, :escaped_chars]
@span_pattern_cache ||= Hash.new {|h, k| h[k] = {} }
end
private_class_method(:new, :allocate)
# The source string provided on initialization is parsed into the @root element.
def parse
configure_parser
parse_blocks(@root, adapt_source(source))
update_tree(@root)
correct_abbreviations_attributes
replace_abbreviations(@root)
@footnotes.each do |_name, data|
update_tree(data[:content])
replace_abbreviations(data[:content])
end
footnote_count = 0
@footnotes.each do |name, data|
(footnote_count += 1; next) if data.key?(:marker)
line = data[:content].options[:location]
warning("Footnote definition for '#{name}' on line #{line} is unreferenced - ignoring")
end
@root.options[:footnote_count] = footnote_count
end
protected
# :doc:
#
# 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+).
def update_link_definitions(link_defs)
link_defs.each {|k, v| @link_defs[normalize_link_id(k)] = v }
end
# Adapt the object to allow parsing like specified in the options.
def configure_parser
@parsers = {}
(@block_parsers + @span_parsers).each do |name|
if self.class.has_parser?(name)
@parsers[name] = self.class.parser(name)
else
raise Kramdown::Error, "Unknown parser: #{name}"
end
end
@span_start, @span_start_re = span_parser_regexps
end
# Create the needed span parser regexps.
def span_parser_regexps(parsers = @span_parsers)
span_start = /#{parsers.map {|name| @parsers[name].span_start }.join('|')}/
[span_start, /(?=#{span_start})/]
end
# Parse all block-level elements in +text+ into the element +el+.
def parse_blocks(el, text = nil)
@stack.push([@tree, @src, @block_ial])
@tree, @block_ial = el, nil
@src = (text.nil? ? @src : ::Kramdown::Utils::StringScanner.new(text, el.options[:location]))
status = catch(:stop_block_parsing) do
until @src.eos?
@block_parsers.any? do |name|
if @src.check(@parsers[name].start_re)
send(@parsers[name].method)
else
false
end
end || begin
warning('Warning: this should not occur - no block parser handled the line')
add_text(@src.scan(/.*\n/))
end
end
end
@tree, @src, @block_ial = *@stack.pop
status
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.
def update_tree(element)
last_blank = nil
element.children.map! do |child|
case child.type
when :raw_text
last_blank = nil
reset_env(src: ::Kramdown::Utils::StringScanner.new(child.value, element.options[:location]),
text_type: :text)
parse_spans(child)
child.children
when :eob
update_attr_with_ial(child.attr, child.options[:ial]) if child.options[:ial]
[]
when :blank
if last_blank
last_blank.value << child.value
[]
else
last_blank = child
child
end
else
last_blank = nil
update_tree(child)
update_attr_with_ial(child.attr, child.options[:ial]) if child.options[:ial]
# DEPRECATED: option auto_id_stripping will be removed in 2.0 because then this will be
# the default behaviour
if child.type == :dt || (child.type == :header && @options[:auto_id_stripping])
update_raw_text(child)
end
child
end
end.flatten!
end
def span_pattern_cache(stop_re, span_start)
@span_pattern_cache[stop_re][span_start] ||= /(?=#{Regexp.union(stop_re, span_start)})/
end
private :span_pattern_cache
# 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.
def parse_spans(el, stop_re = nil, parsers = nil, text_type = @text_type)
@stack.push([@tree, @text_type]) unless @tree.nil?
@tree, @text_type = el, text_type
span_start = @span_start
span_start_re = @span_start_re
span_start, span_start_re = span_parser_regexps(parsers) if parsers
parsers ||= @span_parsers
used_re = (stop_re.nil? ? span_start_re : span_pattern_cache(stop_re, span_start))
stop_re_found = false
while !@src.eos? && !stop_re_found
if (result = @src.scan_until(used_re))
add_text(result)
if stop_re && @src.check(stop_re)
stop_re_found = (block_given? ? yield : true)
end
processed = parsers.any? do |name|
if @src.check(@parsers[name].start_re)
send(@parsers[name].method)
true
else
false
end
end unless stop_re_found
add_text(@src.getch) if !processed && !stop_re_found
else
(add_text(@src.rest); @src.terminate) unless stop_re
break
end
end
@tree, @text_type = @stack.pop
stop_re_found
end
# Reset the current parsing environment. The parameter +env+ can be used to set initial
# values for one or more environment variables.
def reset_env(opts = {})
opts = {text_type: :raw_text, stack: []}.merge(opts)
@src = opts[:src]
@tree = opts[:tree]
@block_ial = opts[:block_ial]
@stack = opts[:stack]
@text_type = opts[:text_type]
end
# Return the current parsing environment.
def save_env
[@src, @tree, @block_ial, @stack, @text_type]
end
# Restore the current parsing environment.
def restore_env(env)
@src, @tree, @block_ial, @stack, @text_type = *env
end
# Update the given attributes hash +attr+ with the information from the inline attribute list
# +ial+ and all referenced ALDs.
def update_attr_with_ial(attr, ial)
ial[:refs]&.each do |ref|
update_attr_with_ial(attr, ref) if (ref = @alds[ref])
end
ial.each do |k, v|
if k == IAL_CLASS_ATTR
attr[k] = "#{attr[k]} #{v}".lstrip
elsif k.kind_of?(String)
attr[k] = v
end
end
end
# Update the raw text for automatic ID generation.
def update_raw_text(item)
raw_text = +''
append_text = lambda do |child|
if child.type == :text
raw_text << child.value
else
child.children.each {|c| append_text.call(c) }
end
end
append_text.call(item)
item.options[:raw_text] = raw_text
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!
def new_block_el(*args)
el = Element.new(*args)
if @block_ial
el.options[:ial] = @block_ial
@block_ial = nil
end
el
end
@@parsers = {}
# Struct class holding all the needed data for one block/span-level parser method.
Data = Struct.new(:name, :start_re, :span_start, :method)
# 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.
def self.define_parser(name, start_re, span_start = nil, meth_name = "parse_#{name}")
raise "A parser with the name #{name} already exists!" if @@parsers.key?(name)
@@parsers[name] = Data.new(name, start_re, span_start, meth_name)
end
# Return the Data structure for the parser +name+.
def self.parser(name = nil)
@@parsers[name]
end
# Return +true+ if there is a parser called +name+.
def self.has_parser?(name)
@@parsers.key?(name)
end
# Regexp for matching indentation (one tab or four spaces)
INDENT = /^(?:\t| {4})/
# Regexp for matching the optional space (zero or up to three spaces)
OPT_SPACE = / {0,3}/
require 'kramdown/parser/kramdown/blank_line'
require 'kramdown/parser/kramdown/eob'
require 'kramdown/parser/kramdown/paragraph'
require 'kramdown/parser/kramdown/header'
require 'kramdown/parser/kramdown/blockquote'
require 'kramdown/parser/kramdown/table'
require 'kramdown/parser/kramdown/codeblock'
require 'kramdown/parser/kramdown/horizontal_rule'
require 'kramdown/parser/kramdown/list'
require 'kramdown/parser/kramdown/link'
require 'kramdown/parser/kramdown/extensions'
require 'kramdown/parser/kramdown/footnote'
require 'kramdown/parser/kramdown/html'
require 'kramdown/parser/kramdown/escaped_chars'
require 'kramdown/parser/kramdown/html_entity'
require 'kramdown/parser/kramdown/line_break'
require 'kramdown/parser/kramdown/typographic_symbol'
require 'kramdown/parser/kramdown/autolink'
require 'kramdown/parser/kramdown/codespan'
require 'kramdown/parser/kramdown/emphasis'
require 'kramdown/parser/kramdown/smart_quotes'
require 'kramdown/parser/kramdown/math'
require 'kramdown/parser/kramdown/abbreviation'
end
end
end
@@ -0,0 +1,80 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown
module Parser
class Kramdown
ABBREV_DEFINITION_START = /^#{OPT_SPACE}\*\[(.+?)\]:(.*?)\n/
# Parse the link definition at the current location.
def parse_abbrev_definition
start_line_number = @src.current_line_number
@src.pos += @src.matched_size
abbrev_id, abbrev_text = @src[1], @src[2]
abbrev_text.strip!
if @root.options[:abbrev_defs][abbrev_id]
warning("Duplicate abbreviation ID '#{abbrev_id}' on line #{start_line_number} " \
"- overwriting")
end
@tree.children << new_block_el(:eob, :abbrev_def)
@root.options[:abbrev_defs][abbrev_id] = abbrev_text
@root.options[:abbrev_attr][abbrev_id] = @tree.children.last
true
end
define_parser(:abbrev_definition, ABBREV_DEFINITION_START)
# Correct abbreviation attributes.
def correct_abbreviations_attributes
@root.options[:abbrev_attr].keys.each do |k|
@root.options[:abbrev_attr][k] = @root.options[:abbrev_attr][k].attr
end
end
# Replace the abbreviation text with elements.
def replace_abbreviations(el, regexps = nil)
return if @root.options[:abbrev_defs].empty?
unless regexps
sorted_abbrevs = @root.options[:abbrev_defs].keys.sort {|a, b| b.length <=> a.length }
regexps = [Regexp.union(*sorted_abbrevs.map do |k|
/#{Regexp.escape(k).gsub(/\\\s/, "[\\s\\p{Z}]+").force_encoding(Encoding::UTF_8)}/
end)]
regexps << /(?=(?:\W|^)#{regexps.first}(?!\w))/ # regexp should only match on word boundaries
end
el.children.map! do |child|
if child.type == :text && el.options[:content_model] != :raw
if child.value.match?(regexps.first)
result = []
strscan = Kramdown::Utils::StringScanner.new(child.value, child.options[:location])
text_lineno = strscan.current_line_number
while (temp = strscan.scan_until(regexps.last))
abbr_lineno = strscan.current_line_number
abbr = strscan.scan(regexps.first) # begin of line case of abbr with \W char as first one
if abbr.nil?
temp << strscan.scan(/\W|^/)
abbr = strscan.scan(regexps.first)
end
result << Element.new(:text, temp, nil, location: text_lineno)
result << Element.new(:abbreviation, abbr, nil, location: abbr_lineno)
text_lineno = strscan.current_line_number
end
result << Element.new(:text, strscan.rest, nil, location: text_lineno)
else
child
end
else
replace_abbreviations(child, regexps)
child
end
end.flatten!
end
end
end
end
@@ -0,0 +1,31 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown
module Parser
class Kramdown
ACHARS = '[[:alnum:]]-_.'
AUTOLINK_START_STR = "<((mailto|https?|ftps?):.+?|[#{ACHARS}]+?@[#{ACHARS}]+?)>"
AUTOLINK_START = /#{AUTOLINK_START_STR}/u
# Parse the autolink at the current location.
def parse_autolink
start_line_number = @src.current_line_number
@src.pos += @src.matched_size
href = (@src[2].nil? ? "mailto:#{@src[1]}" : @src[1])
el = Element.new(:a, nil, {'href' => href}, location: start_line_number)
add_text(@src[1].sub(/^mailto:/, ''), el)
@tree.children << el
end
define_parser(:autolink, AUTOLINK_START, '<')
end
end
end
@@ -0,0 +1,30 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown
module Parser
class Kramdown
BLANK_LINE = /(?>^\s*\n)+/
# Parse the blank line at the current postition.
def parse_blank_line
@src.pos += @src.matched_size
if (last_child = @tree.children.last) && last_child.type == :blank
last_child.value << @src.matched
else
@tree.children << new_block_el(:blank, @src.matched)
end
true
end
define_parser(:blank_line, BLANK_LINE)
end
end
end
@@ -0,0 +1,34 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser/kramdown/extensions'
require 'kramdown/parser/kramdown/blank_line'
require 'kramdown/parser/kramdown/eob'
module Kramdown
module Parser
class Kramdown
BLOCK_BOUNDARY = /#{BLANK_LINE}|#{EOB_MARKER}|#{IAL_BLOCK_START}|\Z/
# Return +true+ if we are after a block boundary.
def after_block_boundary?
last_child = @tree.children.last
!last_child || last_child.type == :blank ||
(last_child.type == :eob && last_child.value.nil?) || @block_ial
end
# Return +true+ if we are before a block boundary.
def before_block_boundary?
@src.check(self.class::BLOCK_BOUNDARY)
end
end
end
end
@@ -0,0 +1,38 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser/kramdown/blank_line'
require 'kramdown/parser/kramdown/extensions'
require 'kramdown/parser/kramdown/eob'
module Kramdown
module Parser
class Kramdown
BLOCKQUOTE_START = /^#{OPT_SPACE}> ?/
# Parse the blockquote at the current location.
def parse_blockquote
start_line_number = @src.current_line_number
result = @src.scan(PARAGRAPH_MATCH)
until @src.match?(self.class::LAZY_END)
result << @src.scan(PARAGRAPH_MATCH)
end
result.gsub!(BLOCKQUOTE_START, '')
el = new_block_el(:blockquote, nil, nil, location: start_line_number)
@tree.children << el
parse_blocks(el, result)
true
end
define_parser(:blockquote, BLOCKQUOTE_START)
end
end
end
@@ -0,0 +1,57 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser/kramdown/blank_line'
require 'kramdown/parser/kramdown/extensions'
require 'kramdown/parser/kramdown/eob'
require 'kramdown/parser/kramdown/paragraph'
module Kramdown
module Parser
class Kramdown
CODEBLOCK_START = INDENT
CODEBLOCK_MATCH = /(?:#{BLANK_LINE}?(?:#{INDENT}[ \t]*\S.*\n)+(?:(?!#{IAL_BLOCK_START}|#{EOB_MARKER}|^#{OPT_SPACE}#{LAZY_END_HTML_STOP}|^#{OPT_SPACE}#{LAZY_END_HTML_START})^[ \t]*\S.*\n)*)*/
# Parse the indented codeblock at the current location.
def parse_codeblock
start_line_number = @src.current_line_number
data = @src.scan(self.class::CODEBLOCK_MATCH)
data.gsub!(/\n( {0,3}\S)/, ' \\1')
data.gsub!(INDENT, '')
@tree.children << new_block_el(:codeblock, data, nil, location: start_line_number)
true
end
define_parser(:codeblock, CODEBLOCK_START)
FENCED_CODEBLOCK_START = /^~{3,}/
FENCED_CODEBLOCK_MATCH = /^((~){3,})\s*?((\S+?)(?:\?\S*)?)?\s*?\n(.*?)^\1\2*\s*?\n/m
# Parse the fenced codeblock at the current location.
def parse_codeblock_fenced
if @src.check(self.class::FENCED_CODEBLOCK_MATCH)
start_line_number = @src.current_line_number
@src.pos += @src.matched_size
el = new_block_el(:codeblock, @src[5], nil, location: start_line_number, fenced: true)
lang = @src[3].to_s.strip
unless lang.empty?
el.options[:lang] = lang
el.attr['class'] = "language-#{@src[4]}"
end
@tree.children << el
true
else
false
end
end
define_parser(:codeblock_fenced, FENCED_CODEBLOCK_START)
end
end
end
@@ -0,0 +1,58 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown
module Parser
class Kramdown
CODESPAN_DELIMITER = /`+/
# Parse the codespan at the current scanner location.
def parse_codespan
start_line_number = @src.current_line_number
result = @src.scan(CODESPAN_DELIMITER)
simple = (result.length == 1)
saved_pos = @src.save_pos
if simple && @src.pre_match =~ /\s\Z|\A\Z/ && @src.match?(/\s/)
add_text(result)
return
end
# assign static regex to avoid allocating the same on every instance
# where +result+ equals a single-backtick. Interpolate otherwise.
if result == '`'
scan_pattern = /`/
str_sub_pattern = /`\Z/
else
scan_pattern = /#{result}/
str_sub_pattern = /#{result}\Z/
end
if (text = @src.scan_until(scan_pattern))
text.sub!(str_sub_pattern, '')
unless simple
text = text[1..-1] if text[0..0] == ' '
text = text[0..-2] if text[-1..-1] == ' '
end
@tree.children << Element.new(:codespan, text, nil, {
codespan_delimiter: result,
location: start_line_number,
})
else
@src.revert_pos(saved_pos)
add_text(result)
end
end
define_parser(:codespan, CODESPAN_DELIMITER, '`')
end
end
end
@@ -0,0 +1,66 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown
module Parser
class Kramdown
EMPHASIS_START = /(?:\*\*?|__?)/
# Parse the emphasis at the current location.
def parse_emphasis
start_line_number = @src.current_line_number
saved_pos = @src.save_pos
result = @src.scan(EMPHASIS_START)
element = (result.length == 2 ? :strong : :em)
type = result[0..0]
if (type == '_' && @src.pre_match =~ /[[:alpha:]]-?[[:alpha:]]*_*\z/) || @src.check(/\s/) ||
@tree.type == element || @stack.any? {|el, _| el.type == element }
add_text(result)
return
end
warnings_pos = @warnings.size
sub_parse = lambda do |delim, elem|
el = Element.new(elem, nil, nil, location: start_line_number)
stop_re = /#{Regexp.escape(delim)}/
found = parse_spans(el, stop_re) do
(@src.pre_match[-1, 1] !~ /\s/) &&
(elem != :em || !@src.match?(/#{Regexp.escape(delim * 2)}(?!#{Regexp.escape(delim)})/)) &&
(type != '_' || !@src.match?(/#{Regexp.escape(delim)}[[:alnum:]]/)) && !el.children.empty?
end
[found, el, stop_re]
end
found, el, stop_re = sub_parse.call(result, element)
if !found && element == :strong && @tree.type != :em
@src.revert_pos(saved_pos)
@src.pos += 1
found, el, stop_re = sub_parse.call(type, :em)
end
if found
# Useful for implementing underlines.
el.options[:char] = type
@src.scan(stop_re)
@tree.children << el
else
@warnings.slice!(0...warnings_pos)
@src.revert_pos(saved_pos)
@src.pos += result.length
add_text(result)
end
end
define_parser(:emphasis, EMPHASIS_START, '\*|_')
end
end
end
@@ -0,0 +1,26 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown
module Parser
class Kramdown
EOB_MARKER = /^\^\s*?\n/
# Parse the EOB marker at the current location.
def parse_eob_marker
@src.pos += @src.matched_size
@tree.children << new_block_el(:eob)
true
end
define_parser(:eob_marker, EOB_MARKER)
end
end
end
@@ -0,0 +1,25 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown
module Parser
class Kramdown
ESCAPED_CHARS = /\\([\\.*_+`<>()\[\]{}#!:|"'$=-])/
# Parse the backslash-escaped character at the current location.
def parse_escaped_chars
@src.pos += @src.matched_size
add_text(@src[1])
end
define_parser(:escaped_chars, ESCAPED_CHARS, '\\\\')
end
end
end
@@ -0,0 +1,214 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown
module Parser
class Kramdown
IAL_CLASS_ATTR = 'class'
# Parse the string +str+ and extract all attributes and add all found attributes to the hash
# +opts+.
def parse_attribute_list(str, opts)
return if str.strip.empty? || str.strip == ':'
attrs = str.scan(ALD_TYPE_ANY)
attrs.each do |key, sep, val, ref, id_and_or_class, _, _|
if ref
(opts[:refs] ||= []) << ref
elsif id_and_or_class
id_and_or_class.scan(ALD_TYPE_ID_OR_CLASS).each do |id_attr, class_attr|
if class_attr
opts[IAL_CLASS_ATTR] = "#{opts[IAL_CLASS_ATTR]} #{class_attr}".lstrip
else
opts['id'] = id_attr
end
end
else
val.gsub!(/\\(\}|#{sep})/, "\\1")
opts[key] = val
end
end
warning("No or invalid attributes found in IAL/ALD content: #{str}") if attrs.empty?
end
# Update the +ial+ with the information from the inline attribute list +opts+.
def update_ial_with_ial(ial, opts)
(ial[:refs] ||= []).concat(opts[:refs]) if opts.key?(:refs)
opts.each do |k, v|
if k == IAL_CLASS_ATTR
ial[k] = "#{ial[k]} #{v}".lstrip
elsif k.kind_of?(String)
ial[k] = v
end
end
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.
def parse_extension_start_tag(type)
saved_pos = @src.save_pos
start_line_number = @src.current_line_number
@src.pos += @src.matched_size
error_block = lambda do |msg|
warning(msg)
@src.revert_pos(saved_pos)
add_text(@src.getch) if type == :span
false
end
if @src[4] || @src.matched == '{:/}'
name = (@src[4] ? "for '#{@src[4]}' " : '')
return error_block.call("Invalid extension stop tag #{name} found on line " \
"#{start_line_number} - ignoring it")
end
ext = @src[1]
opts = {}
body = nil
parse_attribute_list(@src[2] || '', opts)
unless @src[3]
stop_re = (type == :block ? /#{EXT_BLOCK_STOP_STR % ext}/ : /#{EXT_STOP_STR % ext}/)
if (result = @src.scan_until(stop_re))
body = result.sub!(stop_re, '')
body.chomp! if type == :block
else
return error_block.call("No stop tag for extension '#{ext}' found on line " \
"#{start_line_number} - ignoring it")
end
end
if handle_extension(ext, opts, body, type, start_line_number)
true
else
error_block.call("Invalid extension with name '#{ext}' specified on line " \
"#{start_line_number} - ignoring it")
end
end
def handle_extension(name, opts, body, type, line_no = nil)
case name
when 'comment'
if body.kind_of?(String)
@tree.children << Element.new(:comment, body, nil, category: type, location: line_no)
end
true
when 'nomarkdown'
if body.kind_of?(String)
@tree.children << Element.new(:raw, body, nil, category: type,
location: line_no, type: opts['type'].to_s.split(/\s+/))
end
true
when 'options'
opts.select do |k, v|
k = k.to_sym
if Kramdown::Options.defined?(k)
if @options[:forbidden_inline_options].include?(k) ||
k == :forbidden_inline_options
warning("Option #{k} may not be set inline")
next false
end
begin
val = Kramdown::Options.parse(k, v)
@options[k] = val
(@root.options[:options] ||= {})[k] = val
rescue StandardError
end
false
else
true
end
end.each do |k, _v|
warning("Unknown kramdown option '#{k}'")
end
@tree.children << new_block_el(:eob, :extension) if type == :block
true
else
false
end
end
ALD_ID_CHARS = /[\w-]/
ALD_ANY_CHARS = /\\\}|[^}]/
ALD_ID_NAME = /\w#{ALD_ID_CHARS}*/
ALD_CLASS_NAME = /[^\s.#]+/
ALD_TYPE_KEY_VALUE_PAIR = /(#{ALD_ID_NAME})=("|')((?:\\\}|\\\2|[^}\2])*?)\2/
ALD_TYPE_CLASS_NAME = /\.(#{ALD_CLASS_NAME})/
ALD_TYPE_ID_NAME = /#([A-Za-z][\w:-]*)/
ALD_TYPE_ID_OR_CLASS = /#{ALD_TYPE_ID_NAME}|#{ALD_TYPE_CLASS_NAME}/
ALD_TYPE_ID_OR_CLASS_MULTI = /((?:#{ALD_TYPE_ID_NAME}|#{ALD_TYPE_CLASS_NAME})+)/
ALD_TYPE_REF = /(#{ALD_ID_NAME})/
ALD_TYPE_ANY = /(?:\A|\s)(?:#{ALD_TYPE_KEY_VALUE_PAIR}|#{ALD_TYPE_REF}|#{ALD_TYPE_ID_OR_CLASS_MULTI})(?=\s|\Z)/
ALD_START = /^#{OPT_SPACE}\{:(#{ALD_ID_NAME}):(#{ALD_ANY_CHARS}+)\}\s*?\n/
EXT_STOP_STR = "\\{:/(%s)?\\}"
EXT_START_STR = "\\{::(\\w+)(?:\\s(#{ALD_ANY_CHARS}*?)|)(\\/)?\\}"
EXT_BLOCK_START = /^#{OPT_SPACE}(?:#{EXT_START_STR}|#{EXT_STOP_STR % ALD_ID_NAME})\s*?\n/
EXT_BLOCK_STOP_STR = "^#{OPT_SPACE}#{EXT_STOP_STR}\s*?\n"
IAL_BLOCK = /\{:(?!:|\/)(#{ALD_ANY_CHARS}+)\}\s*?\n/
IAL_BLOCK_START = /^#{OPT_SPACE}#{IAL_BLOCK}/
BLOCK_EXTENSIONS_START = /^#{OPT_SPACE}\{:/
# Parse one of the block extensions (ALD, block IAL or generic extension) at the current
# location.
def parse_block_extensions
if @src.scan(ALD_START)
parse_attribute_list(@src[2], @alds[@src[1]] ||= {})
@tree.children << new_block_el(:eob, :ald)
true
elsif @src.check(EXT_BLOCK_START)
parse_extension_start_tag(:block)
elsif @src.scan(IAL_BLOCK_START)
if (last_child = @tree.children.last) && last_child.type != :blank &&
(last_child.type != :eob ||
[:link_def, :abbrev_def, :footnote_def].include?(last_child.value))
parse_attribute_list(@src[1], last_child.options[:ial] ||= {})
@tree.children << new_block_el(:eob, :ial) unless @src.check(IAL_BLOCK_START)
else
parse_attribute_list(@src[1], @block_ial ||= {})
end
true
else
false
end
end
define_parser(:block_extensions, BLOCK_EXTENSIONS_START)
EXT_SPAN_START = /#{EXT_START_STR}|#{EXT_STOP_STR % ALD_ID_NAME}/
IAL_SPAN_START = /\{:(#{ALD_ANY_CHARS}+)\}/
SPAN_EXTENSIONS_START = /\{:/
# Parse the extension span at the current location.
def parse_span_extensions
if @src.check(EXT_SPAN_START)
parse_extension_start_tag(:span)
elsif @src.check(IAL_SPAN_START)
if (last_child = @tree.children.last) && last_child.type != :text
@src.pos += @src.matched_size
attr = {}
parse_attribute_list(@src[1], attr)
update_ial_with_ial(last_child.options[:ial] ||= {}, attr)
update_attr_with_ial(last_child.attr, attr)
else
warning("Found span IAL after text - ignoring it")
add_text(@src.getch)
end
else
add_text(@src.getch)
end
end
define_parser(:span_extensions, SPAN_EXTENSIONS_START, '\{:')
end
end
end
@@ -0,0 +1,64 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser/kramdown/extensions'
require 'kramdown/parser/kramdown/blank_line'
require 'kramdown/parser/kramdown/codeblock'
module Kramdown
module Parser
class Kramdown
FOOTNOTE_DEFINITION_START = /^#{OPT_SPACE}\[\^(#{ALD_ID_NAME})\]:\s*?(.*?\n#{CODEBLOCK_MATCH})/
# Parse the foot note definition at the current location.
def parse_footnote_definition
start_line_number = @src.current_line_number
@src.pos += @src.matched_size
el = Element.new(:footnote_def, nil, nil, location: start_line_number)
parse_blocks(el, @src[2].gsub(INDENT, ''))
if @footnotes[@src[1]]
warning("Duplicate footnote name '#{@src[1]}' on line #{start_line_number} - overwriting")
end
@tree.children << new_block_el(:eob, :footnote_def)
(@footnotes[@src[1]] = {})[:content] = el
@footnotes[@src[1]][:eob] = @tree.children.last
true
end
define_parser(:footnote_definition, FOOTNOTE_DEFINITION_START)
FOOTNOTE_MARKER_START = /\[\^(#{ALD_ID_NAME})\]/
# Parse the footnote marker at the current location.
def parse_footnote_marker
start_line_number = @src.current_line_number
@src.pos += @src.matched_size
fn_def = @footnotes[@src[1]]
if fn_def
if fn_def[:eob]
update_attr_with_ial(fn_def[:eob].attr, fn_def[:eob].options[:ial] || {})
fn_def[:attr] = fn_def[:eob].attr
fn_def[:options] = fn_def[:eob].options
fn_def.delete(:eob)
end
fn_def[:marker] ||= []
fn_def[:marker].push(Element.new(:footnote, fn_def[:content], fn_def[:attr],
fn_def[:options].merge(name: @src[1], location: start_line_number)))
@tree.children << fn_def[:marker].last
else
warning("Footnote definition for '#{@src[1]}' not found on line #{start_line_number}")
add_text(@src.matched)
end
end
define_parser(:footnote_marker, FOOTNOTE_MARKER_START, '\[')
end
end
end
@@ -0,0 +1,70 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser/kramdown/block_boundary'
require 'rexml/xmltokens'
module Kramdown
module Parser
class Kramdown
SETEXT_HEADER_START = /^#{OPT_SPACE}(?<contents>[^ \t].*)\n(?<level>[-=])[-=]*[ \t\r\f\v]*\n/
# Parse the Setext header at the current location.
def parse_setext_header
return false unless after_block_boundary?
text, id = parse_header_contents
return false if text.empty?
add_header(@src["level"] == '-' ? 2 : 1, text, id)
true
end
define_parser(:setext_header, SETEXT_HEADER_START)
ATX_HEADER_START = /^(?<level>\#{1,6})[\t ]*(?<contents>[^ \t].*)\n/
# Parse the Atx header at the current location.
def parse_atx_header
return false unless after_block_boundary?
text, id = parse_header_contents
text.sub!(/(?<!\\)#+\z/, '') && text.rstrip!
return false if text.empty?
add_header(@src["level"].length, text, id)
true
end
define_parser(:atx_header, ATX_HEADER_START)
protected
HEADER_ID = /[\t ]{#(?<id>#{REXML::XMLTokens::NAME_START_CHAR}#{REXML::XMLTokens::NAME_CHAR}*)}\z/
# Returns header text and optional ID.
def parse_header_contents
text = @src["contents"]
text.rstrip!
id_match = HEADER_ID.match(text)
if id_match
id = id_match["id"]
text = text[0...-id_match[0].length]
text.rstrip!
end
[text, id]
end
def add_header(level, text, id)
start_line_number = @src.current_line_number
@src.pos += @src.matched_size
el = new_block_el(:header, nil, nil, level: level, raw_text: text, location: start_line_number)
add_text(text, el)
el.attr['id'] = id if id
@tree.children << el
end
end
end
end
@@ -0,0 +1,27 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown
module Parser
class Kramdown
HR_START = /^#{OPT_SPACE}(\*|-|_)[ \t]*\1[ \t]*\1(\1|[ \t])*\n/
# Parse the horizontal rule at the current location.
def parse_horizontal_rule
start_line_number = @src.current_line_number
@src.pos += @src.matched_size
@tree.children << new_block_el(:hr, nil, nil, location: start_line_number)
true
end
define_parser(:horizontal_rule, HR_START)
end
end
end
@@ -0,0 +1,165 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser/html'
module Kramdown
module Parser
class Kramdown
include Kramdown::Parser::Html::Parser
include Kramdown::Utils::Html
# 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_MARKDOWN_ATTR_MAP = {"0" => :raw, "1" => :default, "span" => :span, "block" => :block}
TRAILING_WHITESPACE = /[ \t]*\n/
def handle_kramdown_html_tag(el, closed, handle_body)
if @block_ial
el.options[:ial] = @block_ial
@block_ial = nil
end
content_model = if @tree.type != :html_element || @tree.options[:content_model] != :raw
(@options[:parse_block_html] ? HTML_CONTENT_MODEL[el.value] : :raw)
else
:raw
end
if (val = HTML_MARKDOWN_ATTR_MAP[el.attr.delete('markdown')])
content_model = (val == :default ? HTML_CONTENT_MODEL[el.value] : val)
end
@src.scan(TRAILING_WHITESPACE) if content_model == :block
el.options[:content_model] = content_model
el.options[:is_closed] = closed
if !closed && handle_body
case content_model
when :block
unless parse_blocks(el)
warning("Found no end tag for '#{el.value}' (line #{el.options[:location]}) - auto-closing it")
end
when :span
curpos = @src.pos
if @src.scan_until(/(?=<\/#{el.value}\s*>)/mi)
add_text(extract_string(curpos...@src.pos, @src), el)
@src.scan(HTML_TAG_CLOSE_RE)
else
add_text(@src.rest, el)
@src.terminate
warning("Found no end tag for '#{el.value}' (line #{el.options[:location]}) - auto-closing it")
end
else
parse_raw_html(el) {|iel, ic, ih| handle_kramdown_html_tag(iel, ic, ih) }
end
unless @tree.type == :html_element && @tree.options[:content_model] == :raw
@src.scan(TRAILING_WHITESPACE)
end
end
end
HTML_BLOCK_START = /^#{OPT_SPACE}<(#{REXML::Parsers::BaseParser::UNAME_STR}|!--|\/)/
# Parse the HTML at the current position as block-level HTML.
def parse_block_html
line = @src.current_line_number
if (result = @src.scan(HTML_COMMENT_RE))
@tree.children << Element.new(:xml_comment, result, nil, category: :block, location: line)
@src.scan(TRAILING_WHITESPACE)
true
elsif @src.check(/^#{OPT_SPACE}#{HTML_TAG_RE}/o) && !HTML_SPAN_ELEMENTS.include?(@src[1].downcase)
@src.pos += @src.matched_size
handle_html_start_tag(line) {|iel, ic, ih| handle_kramdown_html_tag(iel, ic, ih) }
Kramdown::Parser::Html::ElementConverter.convert(@root, @tree.children.last) if @options[:html_to_native]
true
elsif @src.check(/^#{OPT_SPACE}#{HTML_TAG_CLOSE_RE}/o) && !HTML_SPAN_ELEMENTS.include?(@src[1].downcase)
name = @src[1].downcase
if @tree.type == :html_element && @tree.value == name
@src.pos += @src.matched_size
throw :stop_block_parsing, :found
else
false
end
else
false
end
end
define_parser(:block_html, HTML_BLOCK_START)
HTML_SPAN_START = /<(#{REXML::Parsers::BaseParser::UNAME_STR}|!--|\/|!\[CDATA\[)/
# Parse the HTML at the current position as span-level HTML.
def parse_span_html
line = @src.current_line_number
if (result = @src.scan(HTML_COMMENT_RE))
@tree.children << Element.new(:xml_comment, result, nil, category: :span, location: line)
elsif @src.scan(HTML_CDATA_RE)
add_text(escape_html(@src[1]))
elsif (result = @src.scan(HTML_TAG_CLOSE_RE))
warning("Found invalidly used HTML closing tag for '#{@src[1]}' on line #{line}")
add_text(result)
elsif (result = @src.scan(HTML_TAG_RE))
tag_name = @src[1]
tag_name.downcase! if HTML_ELEMENT[tag_name.downcase]
if HTML_BLOCK_ELEMENTS.include?(tag_name)
warning("Found block HTML tag '#{tag_name}' in span-level text on line #{line}")
add_text(result)
return
end
attrs = parse_html_attributes(@src[2], line, HTML_ELEMENT[tag_name])
attrs.each_value {|value| value.gsub!(/\n+/, ' ') unless value.empty? }
do_parsing = if HTML_CONTENT_MODEL[tag_name] == :raw || @tree.options[:content_model] == :raw
false
else
@options[:parse_span_html]
end
if (val = HTML_MARKDOWN_ATTR_MAP[attrs.delete('markdown')])
case val
when :block
warning("Cannot use block-level parsing in span-level HTML tag (line #{line}) " \
"- using default mode")
when :span
do_parsing = true
when :default
do_parsing = HTML_CONTENT_MODEL[tag_name] != :raw
when :raw
do_parsing = false
end
end
el = Element.new(:html_element, tag_name, attrs, category: :span, location: line,
content_model: (do_parsing ? :span : :raw), is_closed: !@src[4].nil?)
@tree.children << el
stop_re = /<\/#{Regexp.escape(tag_name)}\s*>/
stop_re = Regexp.new(stop_re.source, Regexp::IGNORECASE) if HTML_ELEMENT[tag_name]
if !@src[4] && !HTML_ELEMENTS_WITHOUT_BODY.include?(el.value)
if parse_spans(el, stop_re, (do_parsing ? nil : [:span_html]))
@src.scan(stop_re)
else
warning("Found no end tag for '#{el.value}' (line #{line}) - auto-closing it")
add_text(@src.rest, el)
@src.terminate
end
end
Kramdown::Parser::Html::ElementConverter.convert(@root, el) if @options[:html_to_native]
else
add_text(@src.getch)
end
end
define_parser(:span_html, HTML_SPAN_START, '<')
end
end
end
@@ -0,0 +1,34 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser/html'
module Kramdown
module Parser
class Kramdown
# Parse the HTML entity at the current location.
def parse_html_entity
start_line_number = @src.current_line_number
@src.pos += @src.matched_size
begin
value = ::Kramdown::Utils::Entities.entity(@src[1] || @src[2]&.to_i || @src[3].hex)
@tree.children << Element.new(:entity, value,
nil, original: @src.matched, location: start_line_number)
rescue ::Kramdown::Error
@tree.children << Element.new(:entity, ::Kramdown::Utils::Entities.entity('amp'),
nil, location: start_line_number)
add_text(@src.matched[1..-1])
end
end
define_parser(:html_entity, Kramdown::Parser::Html::Constants::HTML_ENTITY_RE, '&')
end
end
end
@@ -0,0 +1,25 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown
module Parser
class Kramdown
LINE_BREAK = /( |\\\\)(?=\n)/
# Parse the line break at the current location.
def parse_line_break
@tree.children << Element.new(:br, nil, nil, location: @src.current_line_number)
@src.pos += @src.matched_size
end
define_parser(:line_break, LINE_BREAK, '( |\\\\)(?=\n)')
end
end
end
@@ -0,0 +1,149 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser/kramdown/escaped_chars'
module Kramdown
module Parser
class Kramdown
# Normalize the link identifier.
def normalize_link_id(id)
id.gsub(/\s+/, ' ').downcase
end
LINK_DEFINITION_START = /^#{OPT_SPACE}\[([^\n\]]+)\]:[ \t]*(?:<(.*?)>|([^\n]*?\S[^\n]*?))(?:(?:[ \t]*?\n|[ \t]+?)[ \t]*?(["'])(.+?)\4)?[ \t]*?\n/
# Parse the link definition at the current location.
def parse_link_definition
return false if @src[3].to_s.match?(/[ \t]+["']/)
@src.pos += @src.matched_size
link_id, link_url, link_title = normalize_link_id(@src[1]), @src[2] || @src[3], @src[5]
if @link_defs[link_id]
warning("Duplicate link ID '#{link_id}' on line #{@src.current_line_number} - overwriting")
end
@tree.children << new_block_el(:eob, :link_def)
@link_defs[link_id] = [link_url, link_title, @tree.children.last]
true
end
define_parser(:link_definition, LINK_DEFINITION_START)
# This helper methods adds the approriate attributes to the element +el+ of type +a+ or +img+
# and the element itself to the @tree.
def add_link(el, href, title, alt_text = nil, ial = nil)
el.options[:ial] = ial
update_attr_with_ial(el.attr, ial) if ial
if el.type == :a
el.attr['href'] = href
else
el.attr['src'] = href
el.attr['alt'] = alt_text
el.children.clear
end
el.attr['title'] = title if title
@tree.children << el
end
LINK_BRACKET_STOP_RE = /(\])|!?\[/
LINK_PAREN_STOP_RE = /(\()|(\))|\s(?=['"])/
LINK_INLINE_ID_RE = /\s*?\[([^\]]+)?\]/
LINK_INLINE_TITLE_RE = /\s*?(["'])(.+?)\1\s*?\)/m
LINK_START = /!?\[(?=[^^])/
# Parse the link at the current scanner position. This method is used to parse normal links as
# well as image links.
def parse_link
start_line_number = @src.current_line_number
result = @src.scan(LINK_START)
cur_pos = @src.pos
saved_pos = @src.save_pos
link_type = (result.match?(/^!/) ? :img : :a)
# no nested links allowed
if link_type == :a && (@tree.type == :img || @tree.type == :a ||
@stack.any? {|t, _| t && (t.type == :img || t.type == :a) })
add_text(result)
return
end
el = Element.new(link_type, nil, nil, location: start_line_number)
count = 1
found = parse_spans(el, LINK_BRACKET_STOP_RE) do
count += (@src[1] ? -1 : 1)
count - el.children.count {|c| c.type == :img } == 0
end
unless found
@src.revert_pos(saved_pos)
add_text(result)
return
end
alt_text = extract_string(cur_pos...@src.pos, @src).gsub(ESCAPED_CHARS, '\1')
@src.scan(LINK_BRACKET_STOP_RE)
# reference style link or no link url
if @src.scan(LINK_INLINE_ID_RE) || !@src.check(/\(/)
emit_warning = !@src[1]
link_id = normalize_link_id(@src[1] || alt_text)
if @link_defs.key?(link_id)
link_def = @link_defs[link_id]
add_link(el, link_def[0], link_def[1], alt_text,
link_def[2] && link_def[2].options[:ial])
else
if emit_warning
warning("No link definition for link ID '#{link_id}' found on line #{start_line_number}")
end
@src.revert_pos(saved_pos)
add_text(result)
end
return
end
# link url in parentheses
if @src.scan(/\(<(.*?)>/)
link_url = @src[1]
if @src.scan(/\)/)
add_link(el, link_url, nil, alt_text)
return
end
else
link_url = +''
nr_of_brackets = 0
while (temp = @src.scan_until(LINK_PAREN_STOP_RE))
link_url << temp
if @src[2]
nr_of_brackets -= 1
break if nr_of_brackets == 0
elsif @src[1]
nr_of_brackets += 1
else
break
end
end
link_url = link_url[1..-2]
link_url.strip!
if nr_of_brackets == 0
add_link(el, link_url, nil, alt_text)
return
end
end
if @src.scan(LINK_INLINE_TITLE_RE)
add_link(el, link_url, @src[2], alt_text)
else
@src.revert_pos(saved_pos)
add_text(result)
end
end
define_parser(:link, LINK_START, '!?\[')
end
end
end
@@ -0,0 +1,286 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser/kramdown/blank_line'
require 'kramdown/parser/kramdown/eob'
require 'kramdown/parser/kramdown/horizontal_rule'
require 'kramdown/parser/kramdown/extensions'
module Kramdown
module Parser
class Kramdown
LIST_ITEM_IAL = /^\s*(?:\{:(?!(?:#{ALD_ID_NAME})?:|\/)(#{ALD_ANY_CHARS}+)\})\s*/
LIST_ITEM_IAL_CHECK = /^#{LIST_ITEM_IAL}?\s*\n/
PARSE_FIRST_LIST_LINE_REGEXP_CACHE = Hash.new do |h, indentation|
indent_re = /^ {#{indentation}}/
content_re = /^(?:(?:\t| {4}){#{indentation / 4}} {#{indentation % 4}}|(?:\t| {4}){#{indentation / 4 + 1}}).*\S.*\n/
lazy_re = /(?!^ {0,#{[indentation, 3].min}}(?:#{IAL_BLOCK}|#{LAZY_END_HTML_STOP}|#{LAZY_END_HTML_START})).*\S.*\n/
h[indentation] = [content_re, lazy_re, indent_re]
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.
def parse_first_list_line(indentation, content)
if content.match?(self.class::LIST_ITEM_IAL_CHECK)
indentation = 4
else
while content.match?(/^ *\t/)
temp = content.scan(/^ */).first.length + indentation
content.sub!(/^( *)(\t+)/) { $1 << " " * (4 - (temp % 4) + ($2.length - 1) * 4) }
end
indentation += content[/^ */].length
end
content.sub!(/^\s*/, '')
[content, indentation, *PARSE_FIRST_LIST_LINE_REGEXP_CACHE[indentation]]
end
PATTERN_TAIL = /[\t| ].*?\n/
LIST_START_UL = /^(#{OPT_SPACE}[+*-])(#{PATTERN_TAIL})/
LIST_START_OL = /^(#{OPT_SPACE}\d+\.)(#{PATTERN_TAIL})/
LIST_START = /#{LIST_START_UL}|#{LIST_START_OL}/
# Parse the ordered or unordered list at the current location.
def parse_list
start_line_number = @src.current_line_number
type, list_start_re = (@src.check(LIST_START_UL) ? [:ul, LIST_START_UL] : [:ol, LIST_START_OL])
list = new_block_el(type, nil, nil, location: start_line_number)
item = nil
content_re, lazy_re, indent_re = nil
eob_found = false
nested_list_found = false
last_is_blank = false
until @src.eos?
start_line_number = @src.current_line_number
if last_is_blank && @src.check(HR_START)
break
elsif @src.scan(EOB_MARKER)
eob_found = true
break
elsif @src.scan(list_start_re)
list.options[:first_list_marker] ||= @src[1].strip
item = Element.new(:li, nil, nil, location: start_line_number)
item.value, indentation, content_re, lazy_re, indent_re =
parse_first_list_line(@src[1].length, @src[2])
list.children << item
item.value.sub!(self.class::LIST_ITEM_IAL) do
parse_attribute_list($1, item.options[:ial] ||= {})
''
end
list_start_re = fetch_pattern(type, indentation)
nested_list_found = (item.value =~ LIST_START)
last_is_blank = false
item.value = [item.value]
elsif (result = @src.scan(content_re)) || (!last_is_blank && (result = @src.scan(lazy_re)))
result.sub!(/^(\t+)/) { " " * 4 * $1.length }
indentation_found = result.sub!(indent_re, '')
if !nested_list_found && indentation_found && result =~ LIST_START
item.value << +''
nested_list_found = true
elsif nested_list_found && !indentation_found && result =~ LIST_START
result = " " * (indentation + 4) << result
end
item.value.last << result
last_is_blank = false
elsif (result = @src.scan(BLANK_LINE))
nested_list_found = true
last_is_blank = true
item.value.last << result
else
break
end
end
@tree.children << list
last = nil
list.children.each do |it|
temp = Element.new(:temp, nil, nil, location: it.options[:location])
env = save_env
location = it.options[:location]
it.value.each do |val|
@src = ::Kramdown::Utils::StringScanner.new(val, location)
parse_blocks(temp)
location = @src.current_line_number
end
restore_env(env)
it.children = temp.children
it.value = nil
it_children = it.children
next if it_children.empty?
# Handle the case where an EOB marker is inserted by a block IAL for the first paragraph
it_children.delete_at(1) if it_children.first.type == :p &&
it_children.length >= 2 && it_children[1].type == :eob && it_children.first.options[:ial]
if it_children.first.type == :p &&
(it_children.length < 2 || it_children[1].type != :blank ||
(it == list.children.last && it_children.length == 2 && !eob_found)) &&
(list.children.last != it || list.children.size == 1 ||
list.children[0..-2].any? {|cit| !cit.children.first || cit.children.first.type != :p || cit.children.first.options[:transparent] })
it_children.first.children.first.value << "\n" if it_children.size > 1 && it_children[1].type != :blank
it_children.first.options[:transparent] = true
end
last = (it_children.last.type == :blank ? it_children.pop : nil)
end
@tree.children << last if !last.nil? && !eob_found
true
end
define_parser(:list, LIST_START)
DEFINITION_LIST_START = /^(#{OPT_SPACE}:)(#{PATTERN_TAIL})/
# Parse the ordered or unordered list at the current location.
def parse_definition_list
children = @tree.children
if !children.last || (children.length == 1 && children.last.type != :p) ||
(children.length >= 2 && children[-1].type != :p &&
(children[-1].type != :blank || children[-1].value != "\n" || children[-2].type != :p))
return false
end
first_as_para = false
deflist = new_block_el(:dl)
para = @tree.children.pop
if para.type == :blank
para = @tree.children.pop
first_as_para = true
end
# take location from preceding para which is the first definition term
deflist.options[:location] = para.options[:location]
para.children.first.value.split("\n").each do |term|
el = Element.new(:dt, nil, nil, location: @src.current_line_number)
term.sub!(self.class::LIST_ITEM_IAL) do
parse_attribute_list($1, el.options[:ial] ||= {})
''
end
el.options[:raw_text] = term
el.children << Element.new(:raw_text, term)
deflist.children << el
end
deflist.options[:ial] = para.options[:ial]
item = nil
content_re, lazy_re, indent_re = nil
def_start_re = DEFINITION_LIST_START
last_is_blank = false
until @src.eos?
start_line_number = @src.current_line_number
if @src.scan(def_start_re)
item = Element.new(:dd, nil, nil, location: start_line_number)
item.options[:first_as_para] = first_as_para
item.value, indentation, content_re, lazy_re, indent_re =
parse_first_list_line(@src[1].length, @src[2])
deflist.children << item
item.value.sub!(self.class::LIST_ITEM_IAL) do |_match|
parse_attribute_list($1, item.options[:ial] ||= {})
''
end
def_start_re = fetch_pattern(:dl, indentation)
first_as_para = false
last_is_blank = false
elsif @src.check(EOB_MARKER)
break
elsif (result = @src.scan(content_re)) || (!last_is_blank && (result = @src.scan(lazy_re)))
result.sub!(/^(\t+)/) { " " * ($1 ? 4 * $1.length : 0) }
result.sub!(indent_re, '')
item.value << result
first_as_para = false
last_is_blank = false
elsif (result = @src.scan(BLANK_LINE))
first_as_para = true
item.value << result
last_is_blank = true
else
break
end
end
last = nil
deflist.children.each do |it|
next if it.type == :dt
parse_blocks(it, it.value)
it.value = nil
it_children = it.children
next if it_children.empty?
last = (it_children.last.type == :blank ? it_children.pop : nil)
if it_children.first && it_children.first.type == :p && !it.options.delete(:first_as_para)
it_children.first.children.first.value << "\n" if it_children.size > 1
it_children.first.options[:transparent] = true
end
end
children = @tree.children
if children.length >= 1 && children.last.type == :dl
children[-1].children.concat(deflist.children)
elsif children.length >= 2 && children[-1].type == :blank &&
children[-2].type == :dl
children.pop
children[-1].children.concat(deflist.children)
else
children << deflist
end
children << last if last
true
end
define_parser(:definition_list, DEFINITION_LIST_START)
private
# precomputed patterns for indentations 1..4 and fallback expression
# to compute pattern when indentation is outside the 1..4 range.
def fetch_pattern(type, indentation)
case type
when :ul
case indentation
when 1 then /^( {0}[+*-])(#{PATTERN_TAIL})/o
when 2 then /^( {0,1}[+*-])(#{PATTERN_TAIL})/o
when 3 then /^( {0,2}[+*-])(#{PATTERN_TAIL})/o
else /^( {0,3}[+*-])(#{PATTERN_TAIL})/o
end
when :ol
case indentation
when 1 then /^( {0}\d+\.)(#{PATTERN_TAIL})/o
when 2 then /^( {0,1}\d+\.)(#{PATTERN_TAIL})/o
when 3 then /^( {0,2}\d+\.)(#{PATTERN_TAIL})/o
else /^( {0,3}\d+\.)(#{PATTERN_TAIL})/o
end
when :dl
case indentation
when 1 then /^( {0}:)(#{PATTERN_TAIL})/o
when 2 then /^( {0,1}:)(#{PATTERN_TAIL})/o
when 3 then /^( {0,2}:)(#{PATTERN_TAIL})/o
else /^( {0,3}:)(#{PATTERN_TAIL})/o
end
end
end
end
end
end
@@ -0,0 +1,53 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser/kramdown/block_boundary'
module Kramdown
module Parser
class Kramdown
BLOCK_MATH_START = /^#{OPT_SPACE}(\\)?\$\$(.*?)\$\$(\s*?\n)?/m
# Parse the math block at the current location.
def parse_block_math
start_line_number = @src.current_line_number
if !after_block_boundary?
return false
elsif @src[1]
@src.scan(/^#{OPT_SPACE}\\/o) if @src[3]
return false
end
saved_pos = @src.save_pos
@src.pos += @src.matched_size
data = @src[2].strip
if before_block_boundary?
@tree.children << new_block_el(:math, data, nil, category: :block, location: start_line_number)
true
else
@src.revert_pos(saved_pos)
false
end
end
define_parser(:block_math, BLOCK_MATH_START)
INLINE_MATH_START = /\$\$(.*?)\$\$/m
# Parse the inline math at the current location.
def parse_inline_math
start_line_number = @src.current_line_number
@src.pos += @src.matched_size
@tree.children << Element.new(:math, @src[1].strip, nil, category: :span, location: start_line_number)
end
define_parser(:inline_math, INLINE_MATH_START, '\$')
end
end
end
@@ -0,0 +1,62 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser/kramdown/blank_line'
require 'kramdown/parser/kramdown/extensions'
require 'kramdown/parser/kramdown/eob'
require 'kramdown/parser/kramdown/list'
require 'kramdown/parser/kramdown/html'
module Kramdown
module Parser
class Kramdown
LAZY_END_HTML_SPAN_ELEMENTS = HTML_SPAN_ELEMENTS + %w[script]
LAZY_END_HTML_START = /<(?>(?!(?:#{LAZY_END_HTML_SPAN_ELEMENTS.join('|')})\b)#{REXML::Parsers::BaseParser::UNAME_STR})/
LAZY_END_HTML_STOP = /<\/(?!(?:#{LAZY_END_HTML_SPAN_ELEMENTS.join('|')})\b)#{REXML::Parsers::BaseParser::UNAME_STR}\s*>/m
LAZY_END = /#{BLANK_LINE}|#{IAL_BLOCK_START}|#{EOB_MARKER}|^#{OPT_SPACE}#{LAZY_END_HTML_STOP}|^#{OPT_SPACE}#{LAZY_END_HTML_START}|\Z/
PARAGRAPH_START = /^#{OPT_SPACE}[^ \t].*?\n/
PARAGRAPH_MATCH = /^.*?\n/
PARAGRAPH_END = /#{LAZY_END}|#{DEFINITION_LIST_START}/
# Parse the paragraph at the current location.
def parse_paragraph
pos = @src.pos
start_line_number = @src.current_line_number
result = @src.scan(PARAGRAPH_MATCH)
until @src.match?(paragraph_end)
result << @src.scan(PARAGRAPH_MATCH)
end
result.rstrip!
if (last_child = @tree.children.last) && last_child.type == :p
last_item_in_para = last_child.children.last
if last_item_in_para && last_item_in_para.type == @text_type
joiner = (extract_string((pos - 3)...pos, @src) == " \n" ? " \n" : "\n")
last_item_in_para.value << joiner << result
else
add_text(result, last_child)
end
else
@tree.children << new_block_el(:p, nil, nil, location: start_line_number)
result.lstrip!
add_text(result, @tree.children.last)
end
true
end
define_parser(:paragraph, PARAGRAPH_START)
def paragraph_end
self.class::PARAGRAPH_END
end
end
end
end
@@ -0,0 +1,174 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
#--
# Parts of this file are based on code from RubyPants:
#
# = RubyPants -- SmartyPants ported to Ruby
#
# Ported by Christian Neukirchen <mailto:chneukirchen@gmail.com>
# Copyright (C) 2004 Christian Neukirchen
#
# Incooporates ideas, comments and documentation by Chad Miller
# Copyright (C) 2004 Chad Miller
#
# Original SmartyPants by John Gruber
# Copyright (C) 2003 John Gruber
#
#
# = RubyPants -- SmartyPants ported to Ruby
#
#
# [snip]
#
# == Authors
#
# John Gruber did all of the hard work of writing this software in
# Perl for Movable Type and almost all of this useful documentation.
# Chad Miller ported it to Python to use with Pyblosxom.
#
# Christian Neukirchen provided the Ruby port, as a general-purpose
# library that follows the *Cloth API.
#
#
# == Copyright and License
#
# === SmartyPants license:
#
# Copyright (c) 2003 John Gruber
# (http://daringfireball.net)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# * Neither the name "SmartyPants" nor the names of its contributors
# may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# This software is provided by the copyright holders and contributors
# "as is" and any express or implied warranties, including, but not
# limited to, the implied warranties of merchantability and fitness
# for a particular purpose are disclaimed. In no event shall the
# copyright owner or contributors be liable for any direct, indirect,
# incidental, special, exemplary, or consequential damages (including,
# but not limited to, procurement of substitute goods or services;
# loss of use, data, or profits; or business interruption) however
# caused and on any theory of liability, whether in contract, strict
# liability, or tort (including negligence or otherwise) arising in
# any way out of the use of this software, even if advised of the
# possibility of such damage.
#
# === RubyPants license
#
# RubyPants is a derivative work of SmartyPants and smartypants.py.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# This software is provided by the copyright holders and contributors
# "as is" and any express or implied warranties, including, but not
# limited to, the implied warranties of merchantability and fitness
# for a particular purpose are disclaimed. In no event shall the
# copyright owner or contributors be liable for any direct, indirect,
# incidental, special, exemplary, or consequential damages (including,
# but not limited to, procurement of substitute goods or services;
# loss of use, data, or profits; or business interruption) however
# caused and on any theory of liability, whether in contract, strict
# liability, or tort (including negligence or otherwise) arising in
# any way out of the use of this software, even if advised of the
# possibility of such damage.
#
# == Links
#
# John Gruber:: http://daringfireball.net
# SmartyPants:: http://daringfireball.net/projects/smartypants
#
# Chad Miller:: http://web.chad.org
#
# Christian Neukirchen:: http://kronavita.de/chris
#
#++
#
module Kramdown
module Parser
class Kramdown
SQ_PUNCT = '[!"#\$\%\'()*+,\-.\/:;<=>?\@\[\\\\\]\^_`{|}~]'
SQ_CLOSE = %![^ \\\\\t\r\n\\[{(-]!
SQ_RULES = [
[/("|')(?=[_*]{1,2}\S)/, [:lquote1]],
[/("|')(?=#{SQ_PUNCT}(?!\.\.)\B)/, [:rquote1]],
# Special case for double sets of quotes, e.g.:
# <p>He said, "'Quoted' words in a larger quote."</p>
[/(\s?)"'(?=\w)/, [1, :ldquo, :lsquo]],
[/(\s?)'"(?=\w)/, [1, :lsquo, :ldquo]],
# Special case for decade abbreviations (the '80s):
[/(\s?)'(?=\d\ds)/, [1, :rsquo]],
# Get most opening single/double quotes:
[/(\s)('|")(?=\w)/, [1, :lquote2]],
# Single/double closing quotes:
[/(#{SQ_CLOSE})('|")/, [1, :rquote2]],
# Special case for e.g. "<i>Custer</i>'s Last Stand."
[/("|')(?=\s|s\b|$)/, [:rquote1]],
# Any remaining single quotes should be opening ones:
[/(.?)'/m, [1, :lsquo]],
[/(.?)"/m, [1, :ldquo]],
] # '"
SQ_SUBSTS = {
[:rquote1, '"'] => :rdquo,
[:rquote1, "'"] => :rsquo,
[:rquote2, '"'] => :rdquo,
[:rquote2, "'"] => :rsquo,
[:lquote1, '"'] => :ldquo,
[:lquote1, "'"] => :lsquo,
[:lquote2, '"'] => :ldquo,
[:lquote2, "'"] => :lsquo,
}
SMART_QUOTES_RE = /[^\\]?["']/
# Parse the smart quotes at current location.
def parse_smart_quotes
start_line_number = @src.current_line_number
substs = SQ_RULES.find {|reg, _subst| @src.scan(reg) }[1]
substs.each do |subst|
if subst.kind_of?(Integer)
add_text(@src[subst])
else
val = SQ_SUBSTS[[subst, @src[subst.to_s[-1, 1].to_i]]] || subst
@tree.children << Element.new(:smart_quote, val, nil, location: start_line_number)
end
end
end
define_parser(:smart_quotes, SMART_QUOTES_RE, '[^\\\\]?["\']')
end
end
end
@@ -0,0 +1,171 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser/kramdown/block_boundary'
module Kramdown
module Parser
class Kramdown
TABLE_SEP_LINE = /^([+|: \t-]*?-[+|: \t-]*?)[ \t]*\n/
TABLE_HSEP_ALIGN = /[ \t]?(:?)-+(:?)[ \t]?/
TABLE_FSEP_LINE = /^[+|: \t=]*?=[+|: \t=]*?[ \t]*\n/
TABLE_ROW_LINE = /^(.*?)[ \t]*\n/
TABLE_PIPE_CHECK = /(?:\||.*?[^\\\n]\|)/
TABLE_LINE = /#{TABLE_PIPE_CHECK}.*?\n/
TABLE_START = /^#{OPT_SPACE}(?=\S)#{TABLE_LINE}/
# Parse the table at the current location.
def parse_table
return false unless after_block_boundary?
saved_pos = @src.save_pos
orig_pos = @src.pos
table = new_block_el(:table, nil, nil, alignment: [], location: @src.current_line_number)
leading_pipe = (@src.check(TABLE_LINE) =~ /^\s*\|/)
@src.scan(TABLE_SEP_LINE)
rows = []
has_footer = false
columns = 0
add_container = lambda do |type, force|
if !has_footer || type != :tbody || force
cont = Element.new(type)
cont.children, rows = rows, []
table.children << cont
end
end
until @src.eos?
break unless @src.check(TABLE_LINE)
if @src.scan(TABLE_SEP_LINE)
if rows.empty?
# nothing to do, ignoring multiple consecutive separator lines
elsif table.options[:alignment].empty? && !has_footer
add_container.call(:thead, false)
table.options[:alignment] = @src[1].scan(TABLE_HSEP_ALIGN).map do |left, right|
(left.empty? && right.empty? && :default) || (right.empty? && :left) ||
(left.empty? && :right) || :center
end
else # treat as normal separator line
add_container.call(:tbody, false)
end
elsif @src.scan(TABLE_FSEP_LINE)
add_container.call(:tbody, true) unless rows.empty?
has_footer = true
elsif @src.scan(TABLE_ROW_LINE)
trow = Element.new(:tr)
# parse possible code spans on the line and correctly split the line into cells
env = save_env
cells = []
@src[1].split(/(<code.*?>.*?<\/code>)/).each_with_index do |str, i|
if i.odd?
(cells.empty? ? cells : cells.last) << str
else
reset_env(src: Kramdown::Utils::StringScanner.new(str, @src.current_line_number))
root = Element.new(:root)
parse_spans(root, nil, [:codespan])
root.children.each do |c|
if c.type == :raw_text
f, *l = c.value.split(/(?<!\\)\|/, -1).map {|t| t.gsub(/\\\|/, '|') }
(cells.empty? ? cells : cells.last) << f
cells.concat(l)
else
delim = (c.value.scan(/`+/).max || '') + '`'
tmp = +"#{delim}#{' ' if delim.size > 1}#{c.value}#{' ' if delim.size > 1}#{delim}"
(cells.empty? ? cells : cells.last) << tmp
end
end
end
end
restore_env(env)
cells.shift if leading_pipe && cells.first.strip.empty?
cells.pop if cells.last.strip.empty?
cells.each do |cell_text|
tcell = Element.new(:td)
tcell.children << Element.new(:raw_text, cell_text.strip)
trow.children << tcell
end
columns = [columns, cells.length].max
rows << trow
else
break
end
end
unless before_block_boundary?
@src.revert_pos(saved_pos)
return false
end
# Parse all lines of the table with the code span parser
env = save_env
l_src = ::Kramdown::Utils::StringScanner.new(extract_string(orig_pos...(@src.pos - 1), @src),
@src.current_line_number)
reset_env(src: l_src)
root = Element.new(:root)
parse_spans(root, nil, [:codespan, :span_html])
restore_env(env)
# Check if each line has at least one unescaped pipe that is not inside a code span/code
# HTML element
# Note: It doesn't matter that we parse *all* span HTML elements because the row splitting
# algorithm above only takes <code> elements into account!
pipe_on_line = false
while (c = root.children.shift)
next unless (lines = c.value)
lines = lines.split("\n")
if c.type == :codespan
if lines.size > 2 || (lines.size == 2 && !pipe_on_line)
break
elsif lines.size == 2 && pipe_on_line
pipe_on_line = false
end
else
break if lines.size > 1 && !pipe_on_line && lines.first !~ /^#{TABLE_PIPE_CHECK}/o
pipe_on_line = (lines.size > 1 ? false : pipe_on_line) || (lines.last =~ /^#{TABLE_PIPE_CHECK}/o)
end
end
@src.revert_pos(saved_pos) and return false unless pipe_on_line
add_container.call(has_footer ? :tfoot : :tbody, false) unless rows.empty?
if table.children.none? {|el| el.type == :tbody }
warning("Found table without body on line #{table.options[:location]} - ignoring it")
@src.revert_pos(saved_pos)
return false
end
# adjust all table rows to have equal number of columns, same for alignment defs
table.children.each do |kind|
kind.children.each do |row|
(columns - row.children.length).times do
row.children << Element.new(:td)
end
end
end
if table.options[:alignment].length > columns
table.options[:alignment] = table.options[:alignment][0...columns]
else
table.options[:alignment] += [:default] * (columns - table.options[:alignment].length)
end
@tree.children << table
true
end
define_parser(:table, TABLE_START)
end
end
end
@@ -0,0 +1,44 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown
module Parser
class Kramdown
TYPOGRAPHIC_SYMS = [['---', :mdash], ['--', :ndash], ['...', :hellip],
['\\<<', '&lt;&lt;'], ['\\>>', '&gt;&gt;'],
['<< ', :laquo_space], [' >>', :raquo_space],
['<<', :laquo], ['>>', :raquo]]
TYPOGRAPHIC_SYMS_SUBST = Hash[*TYPOGRAPHIC_SYMS.flatten]
TYPOGRAPHIC_SYMS_RE = /#{TYPOGRAPHIC_SYMS.map {|k, _v| Regexp.escape(k) }.join('|')}/
# Parse the typographic symbols at the current location.
def parse_typographic_syms
start_line_number = @src.current_line_number
@src.pos += @src.matched_size
val = TYPOGRAPHIC_SYMS_SUBST[@src.matched]
if val.kind_of?(Symbol)
@tree.children << Element.new(:typographic_sym, val, nil, location: start_line_number)
elsif @src.matched == '\\<<'
@tree.children << Element.new(:entity, ::Kramdown::Utils::Entities.entity('lt'),
nil, location: start_line_number)
@tree.children << Element.new(:entity, ::Kramdown::Utils::Entities.entity('lt'),
nil, location: start_line_number)
else
@tree.children << Element.new(:entity, ::Kramdown::Utils::Entities.entity('gt'),
nil, location: start_line_number)
@tree.children << Element.new(:entity, ::Kramdown::Utils::Entities.entity('gt'),
nil, location: start_line_number)
end
end
define_parser(:typographic_syms, TYPOGRAPHIC_SYMS_RE, '--|\\.\\.\\.|(?:\\\\| )?(?:<<|>>)')
end
end
end
@@ -0,0 +1,57 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser'
module Kramdown
module Parser
# Used for parsing a document in Markdown format.
#
# This parser is based on the kramdown parser and removes the parser methods for the additional
# non-Markdown features. However, since some things are handled differently by the kramdown
# parser methods (like deciding when a list item contains just text), this parser differs from
# real Markdown parsers in some respects.
#
# Note, though, that the parser basically fails just one of the Markdown test cases (some others
# also fail but those failures are negligible).
class Markdown < Kramdown
# Array with all the parsing methods that should be removed from the standard kramdown parser.
EXTENDED = [:codeblock_fenced, :table, :definition_list, :footnote_definition,
:abbrev_definition, :block_math, :block_extensions,
:footnote_marker, :smart_quotes, :inline_math, :span_extensions, :typographic_syms]
def initialize(source, options) # :nodoc:
super
@block_parsers.delete_if {|i| EXTENDED.include?(i) }
@span_parsers.delete_if {|i| EXTENDED.include?(i) }
end
# :stopdoc:
BLOCK_BOUNDARY = /#{BLANK_LINE}|#{EOB_MARKER}|\Z/
LAZY_END = /#{BLANK_LINE}|#{EOB_MARKER}|^#{OPT_SPACE}#{LAZY_END_HTML_STOP}|
^#{OPT_SPACE}#{LAZY_END_HTML_START}|\Z/x
CODEBLOCK_MATCH = /(?:#{BLANK_LINE}?(?:#{INDENT}[ \t]*\S.*\n)+)*/
PARAGRAPH_END = LAZY_END
IAL_RAND_CHARS = (('a'..'z').to_a + ('0'..'9').to_a)
IAL_RAND_STRING = (1..20).collect { IAL_RAND_CHARS[rand(IAL_RAND_CHARS.size)] }.join
LIST_ITEM_IAL = /^\s*(#{IAL_RAND_STRING})?\s*\n/
IAL_SPAN_START = LIST_ITEM_IAL
# :startdoc:
end
end
end