This commit is contained in:
@@ -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],
|
||||
['\\<<', '<<'], ['\\>>', '>>'],
|
||||
['<< ', :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
|
||||
Reference in New Issue
Block a user