This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# -*- 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/document'
|
||||
@@ -0,0 +1,68 @@
|
||||
# -*- 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'
|
||||
|
||||
module Kramdown
|
||||
|
||||
# This module contains all available converters, i.e. classes that take a root Element and convert
|
||||
# it to a specific output format. The result is normally a string. For example, the
|
||||
# Converter::Html module converts an element tree into valid HTML.
|
||||
#
|
||||
# Converters use the Base class for common functionality (like applying a template to the output)
|
||||
# \- see its API documentation for how to create a custom converter class.
|
||||
module Converter
|
||||
|
||||
autoload :Base, 'kramdown/converter/base'
|
||||
autoload :Html, 'kramdown/converter/html'
|
||||
autoload :Latex, 'kramdown/converter/latex'
|
||||
autoload :Kramdown, 'kramdown/converter/kramdown'
|
||||
autoload :Toc, 'kramdown/converter/toc'
|
||||
autoload :RemoveHtmlTags, 'kramdown/converter/remove_html_tags'
|
||||
autoload :HashAST, 'kramdown/converter/hash_ast'
|
||||
autoload :HashAst, 'kramdown/converter/hash_ast'
|
||||
autoload :Man, 'kramdown/converter/man'
|
||||
|
||||
extend ::Kramdown::Utils::Configurable
|
||||
|
||||
configurable(:syntax_highlighter)
|
||||
|
||||
['Minted', "Rouge"].each do |klass_name|
|
||||
kn_down = klass_name.downcase.intern
|
||||
add_syntax_highlighter(kn_down) do |converter, text, lang, type, opts|
|
||||
require "kramdown/converter/syntax_highlighter/#{kn_down}"
|
||||
klass = ::Kramdown::Utils.deep_const_get("::Kramdown::Converter::SyntaxHighlighter::#{klass_name}")
|
||||
if !klass.const_defined?(:AVAILABLE) || klass::AVAILABLE
|
||||
add_syntax_highlighter(kn_down, klass)
|
||||
else
|
||||
add_syntax_highlighter(kn_down) { nil }
|
||||
end
|
||||
syntax_highlighter(kn_down).call(converter, text, lang, type, opts)
|
||||
end
|
||||
end
|
||||
|
||||
configurable(:math_engine)
|
||||
|
||||
["Mathjax"].each do |klass_name|
|
||||
kn_down = klass_name.downcase.intern
|
||||
add_math_engine(kn_down) do |converter, el, opts|
|
||||
require "kramdown/converter/math_engine/#{kn_down}"
|
||||
klass = ::Kramdown::Utils.deep_const_get("::Kramdown::Converter::MathEngine::#{klass_name}")
|
||||
if !klass.const_defined?(:AVAILABLE) || klass::AVAILABLE
|
||||
add_math_engine(kn_down, klass)
|
||||
else
|
||||
add_math_engine(kn_down) { nil }
|
||||
end
|
||||
math_engine(kn_down).call(converter, el, opts)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,257 @@
|
||||
# -*- 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 'erb'
|
||||
require 'kramdown/utils'
|
||||
require 'kramdown/document'
|
||||
|
||||
module Kramdown
|
||||
|
||||
module Converter
|
||||
|
||||
# == \Base class for converters
|
||||
#
|
||||
# This class serves as base class for all converters. It provides methods that can/should be
|
||||
# used by all converters (like #generate_id) as well as common functionality that is
|
||||
# automatically applied to the result (for example, embedding the output into a template).
|
||||
#
|
||||
# A converter object is used as a throw-away object, i.e. it is only used for storing the needed
|
||||
# state information during conversion. Therefore one can't instantiate a converter object
|
||||
# directly but only use the Base::convert method.
|
||||
#
|
||||
# == Implementing a converter
|
||||
#
|
||||
# Implementing a new converter is rather easy: just derive a new class from this class and put
|
||||
# it in the Kramdown::Converter module (the latter is only needed if auto-detection should work
|
||||
# properly). Then you need to implement the #convert method which has to contain the conversion
|
||||
# code for converting an element and has to return the conversion result.
|
||||
#
|
||||
# The actual transformation of the document tree can be done in any way. However, writing one
|
||||
# method per element type is a straight forward way to do it - this is how the Html and Latex
|
||||
# converters do the transformation.
|
||||
#
|
||||
# Have a look at the Base::convert method for additional information!
|
||||
class Base
|
||||
|
||||
# Can be used by a converter for storing arbitrary information during the conversion process.
|
||||
attr_reader :data
|
||||
|
||||
# The hash with the conversion options.
|
||||
attr_reader :options
|
||||
|
||||
# The root element that is converted.
|
||||
attr_reader :root
|
||||
|
||||
# The warnings array.
|
||||
attr_reader :warnings
|
||||
|
||||
# Initialize the converter with the given +root+ element and +options+ hash.
|
||||
def initialize(root, options)
|
||||
@options = options
|
||||
@root = root
|
||||
@data = {}
|
||||
@warnings = []
|
||||
end
|
||||
private_class_method(:new, :allocate)
|
||||
|
||||
# Returns whether the template should be applied before the conversion of the tree.
|
||||
#
|
||||
# Defaults to false.
|
||||
def apply_template_before?
|
||||
false
|
||||
end
|
||||
|
||||
# Returns whether the template should be applied after the conversion of the tree.
|
||||
#
|
||||
# Defaults to true.
|
||||
def apply_template_after?
|
||||
true
|
||||
end
|
||||
|
||||
# Convert the element tree +tree+ and return the resulting conversion object (normally a
|
||||
# string) and an array with warning messages. The parameter +options+ specifies the conversion
|
||||
# options that should be used.
|
||||
#
|
||||
# Initializes a new instance of the calling class and then calls the #convert method with
|
||||
# +tree+ as parameter.
|
||||
#
|
||||
# If the +template+ option is specified and non-empty, the template is evaluate with ERB
|
||||
# before and/or after the tree conversion depending on the result of #apply_template_before?
|
||||
# and #apply_template_after?. If the template is evaluated before, an empty string is used for
|
||||
# the body; if evaluated after, the result is used as body. See ::apply_template.
|
||||
#
|
||||
# The template resolution is done in the following way (for the converter ConverterName):
|
||||
#
|
||||
# 1. Look in the current working directory for the template.
|
||||
#
|
||||
# 2. Append +.converter_name+ (e.g. +.html+) to the template name and look for the resulting
|
||||
# file in the current working directory (the form +.convertername+ is deprecated).
|
||||
#
|
||||
# 3. Append +.converter_name+ to the template name and look for it in the kramdown data
|
||||
# directory (the form +.convertername+ is deprecated).
|
||||
#
|
||||
# 4. Check if the template name starts with 'string://' and if so, strip this prefix away and
|
||||
# use the rest as template.
|
||||
def self.convert(tree, options = {})
|
||||
converter = new(tree, ::Kramdown::Options.merge(options.merge(tree.options[:options] || {})))
|
||||
|
||||
if !converter.options[:template].empty? && converter.apply_template_before?
|
||||
apply_template(converter, '')
|
||||
end
|
||||
result = converter.convert(tree)
|
||||
if result.respond_to?(:encode!) && result.encoding != Encoding::BINARY
|
||||
result.encode!(tree.options[:encoding] ||
|
||||
(raise ::Kramdown::Error, "Missing encoding option on root element"))
|
||||
end
|
||||
if !converter.options[:template].empty? && converter.apply_template_after?
|
||||
result = apply_template(converter, result)
|
||||
end
|
||||
|
||||
[result, converter.warnings]
|
||||
end
|
||||
|
||||
# Convert the element +el+ and return the resulting object.
|
||||
#
|
||||
# This is the only method that has to be implemented by sub-classes!
|
||||
def convert(_el)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Apply the +template+ using +body+ as the body string.
|
||||
#
|
||||
# The template is evaluated using ERB and the body is available in the @body instance variable
|
||||
# and the converter object in the @converter instance variable.
|
||||
def self.apply_template(converter, body) # :nodoc:
|
||||
erb = ERB.new(get_template(converter.options[:template]))
|
||||
obj = Object.new
|
||||
obj.instance_variable_set(:@converter, converter)
|
||||
obj.instance_variable_set(:@body, body)
|
||||
erb.result(obj.instance_eval { binding })
|
||||
end
|
||||
|
||||
# Return the template specified by +template+.
|
||||
def self.get_template(template) # :nodoc:
|
||||
format_ext = '.' + ::Kramdown::Utils.snake_case(name.split("::").last)
|
||||
shipped = File.join(::Kramdown.data_dir, template + format_ext)
|
||||
if File.exist?(template)
|
||||
File.read(template)
|
||||
elsif File.exist?(template + format_ext)
|
||||
File.read(template + format_ext)
|
||||
elsif File.exist?(shipped)
|
||||
File.read(shipped)
|
||||
elsif template.start_with?('string://')
|
||||
template.delete_prefix("string://")
|
||||
else
|
||||
raise "The specified template file #{template} does not exist"
|
||||
end
|
||||
end
|
||||
|
||||
# Add the given warning +text+ to the warning array.
|
||||
def warning(text)
|
||||
@warnings << text
|
||||
end
|
||||
|
||||
# Return +true+ if the header element +el+ should be used for the table of contents (as
|
||||
# specified by the +toc_levels+ option).
|
||||
def in_toc?(el)
|
||||
@options[:toc_levels].include?(el.options[:level]) && (el.attr['class'] || '') !~ /\bno_toc\b/
|
||||
end
|
||||
|
||||
# Return the output header level given a level.
|
||||
#
|
||||
# Uses the +header_offset+ option for adjusting the header level.
|
||||
def output_header_level(level)
|
||||
[[level + @options[:header_offset], 6].min, 1].max
|
||||
end
|
||||
|
||||
# Extract the code block/span language from the attributes.
|
||||
def extract_code_language(attr)
|
||||
if attr['class'] && attr['class'] =~ /\blanguage-\S+/
|
||||
attr['class'].scan(/\blanguage-(\S+)/).first.first
|
||||
end
|
||||
end
|
||||
|
||||
# See #extract_code_language
|
||||
#
|
||||
# *Warning*: This version will modify the given attributes if a language is present.
|
||||
def extract_code_language!(attr)
|
||||
lang = extract_code_language(attr)
|
||||
attr['class'] = attr['class'].sub(/\blanguage-\S+/, '').strip if lang
|
||||
attr.delete('class') if lang && attr['class'].empty?
|
||||
lang
|
||||
end
|
||||
|
||||
# Highlight the given +text+ in the language +lang+ with the syntax highlighter configured
|
||||
# through the option 'syntax_highlighter'.
|
||||
def highlight_code(text, lang, type, opts = {})
|
||||
return nil unless @options[:syntax_highlighter]
|
||||
|
||||
highlighter = ::Kramdown::Converter.syntax_highlighter(@options[:syntax_highlighter])
|
||||
if highlighter
|
||||
highlighter.call(self, text, lang, type, opts)
|
||||
else
|
||||
warning("The configured syntax highlighter #{@options[:syntax_highlighter]} is not available.")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Format the given math element with the math engine configured through the option
|
||||
# 'math_engine'.
|
||||
def format_math(el, opts = {})
|
||||
return nil unless @options[:math_engine]
|
||||
|
||||
engine = ::Kramdown::Converter.math_engine(@options[:math_engine])
|
||||
if engine
|
||||
engine.call(self, el, opts)
|
||||
else
|
||||
warning("The configured math engine #{@options[:math_engine]} is not available.")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Generate an unique alpha-numeric ID from the the string +str+ for use as a header ID.
|
||||
#
|
||||
# Uses the option +auto_id_prefix+: the value of this option is prepended to every generated
|
||||
# ID.
|
||||
def generate_id(str)
|
||||
str = ::Kramdown::Utils::Unidecoder.decode(str) if @options[:transliterated_header_ids]
|
||||
gen_id = basic_generate_id(str)
|
||||
gen_id = 'section' if gen_id.empty?
|
||||
@used_ids ||= {}
|
||||
if @used_ids.key?(gen_id)
|
||||
gen_id += "-#{@used_ids[gen_id] += 1}"
|
||||
else
|
||||
@used_ids[gen_id] = 0
|
||||
end
|
||||
@options[:auto_id_prefix] + gen_id
|
||||
end
|
||||
|
||||
# The basic version of the ID generator, without any special provisions for empty or unique
|
||||
# IDs.
|
||||
def basic_generate_id(str)
|
||||
gen_id = str.gsub(/^[^a-zA-Z]+/, '')
|
||||
gen_id.tr!('^a-zA-Z0-9 -', '')
|
||||
gen_id.tr!(' ', '-')
|
||||
gen_id.downcase!
|
||||
gen_id
|
||||
end
|
||||
|
||||
SMART_QUOTE_INDICES = {lsquo: 0, rsquo: 1, ldquo: 2, rdquo: 3} # :nodoc:
|
||||
|
||||
# Return the entity that represents the given smart_quote element.
|
||||
def smart_quote_entity(el)
|
||||
res = @options[:smart_quotes][SMART_QUOTE_INDICES[el.value]]
|
||||
::Kramdown::Utils::Entities.entity(res)
|
||||
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'
|
||||
require 'kramdown/converter'
|
||||
require 'kramdown/utils'
|
||||
|
||||
module Kramdown
|
||||
|
||||
module Converter
|
||||
|
||||
# Converts a Kramdown::Document to a nested hash for further processing or debug output.
|
||||
class HashAST < Base
|
||||
|
||||
def convert(el)
|
||||
hash = {type: el.type}
|
||||
hash[:attr] = el.attr unless el.attr.empty?
|
||||
hash[:value] = el.value unless el.value.nil?
|
||||
hash[:options] = el.options unless el.options.empty?
|
||||
unless el.children.empty?
|
||||
hash[:children] = []
|
||||
el.children.each {|child| hash[:children] << convert(child) }
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
HashAst = HashAST
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,545 @@
|
||||
# -*- 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'
|
||||
require 'kramdown/converter'
|
||||
require 'kramdown/utils'
|
||||
|
||||
module Kramdown
|
||||
|
||||
module Converter
|
||||
|
||||
# Converts a Kramdown::Document to HTML.
|
||||
#
|
||||
# You can customize the HTML converter by sub-classing it and overriding the +convert_NAME+
|
||||
# methods. Each such method takes the following parameters:
|
||||
#
|
||||
# [+el+] The element of type +NAME+ to be converted.
|
||||
#
|
||||
# [+indent+] A number representing the current amount of spaces for indent (only used for
|
||||
# block-level elements).
|
||||
#
|
||||
# The return value of such a method has to be a string containing the element +el+ formatted as
|
||||
# HTML element.
|
||||
class Html < Base
|
||||
|
||||
include ::Kramdown::Utils::Html
|
||||
include ::Kramdown::Parser::Html::Constants
|
||||
|
||||
# The amount of indentation used when nesting HTML tags.
|
||||
attr_accessor :indent
|
||||
|
||||
# Initialize the HTML converter with the given Kramdown document +doc+.
|
||||
def initialize(root, options)
|
||||
super
|
||||
@footnote_counter = @footnote_start = @options[:footnote_nr]
|
||||
@footnotes = []
|
||||
@footnotes_by_name = {}
|
||||
@footnote_location = nil
|
||||
@toc = []
|
||||
@toc_code = nil
|
||||
@indent = 2
|
||||
@stack = []
|
||||
|
||||
# stash string representation of symbol to avoid allocations from multiple interpolations.
|
||||
@highlighter_class = " highlighter-#{options[:syntax_highlighter]}"
|
||||
@dispatcher = Hash.new {|h, k| h[k] = :"convert_#{k}" }
|
||||
end
|
||||
|
||||
# Dispatch the conversion of the element +el+ to a +convert_TYPE+ method using the +type+ of
|
||||
# the element.
|
||||
def convert(el, indent = -@indent)
|
||||
send(@dispatcher[el.type], el, indent)
|
||||
end
|
||||
|
||||
# Return the converted content of the children of +el+ as a string. The parameter +indent+ has
|
||||
# to be the amount of indentation used for the element +el+.
|
||||
#
|
||||
# Pushes +el+ onto the @stack before converting the child elements and pops it from the stack
|
||||
# afterwards.
|
||||
def inner(el, indent)
|
||||
result = +''
|
||||
indent += @indent
|
||||
@stack.push(el)
|
||||
el.children.each do |inner_el|
|
||||
result << send(@dispatcher[inner_el.type], inner_el, indent)
|
||||
end
|
||||
@stack.pop
|
||||
result
|
||||
end
|
||||
|
||||
def convert_blank(_el, _indent)
|
||||
"\n"
|
||||
end
|
||||
|
||||
def convert_text(el, _indent)
|
||||
escaped = escape_html(el.value, :text)
|
||||
@options[:remove_line_breaks_for_cjk] ? fix_cjk_line_break(escaped) : escaped
|
||||
end
|
||||
|
||||
def convert_p(el, indent)
|
||||
if el.options[:transparent]
|
||||
inner(el, indent)
|
||||
elsif el.children.size == 1 && el.children.first.type == :img &&
|
||||
el.children.first.options[:ial]&.[](:refs)&.include?('standalone')
|
||||
convert_standalone_image(el, indent)
|
||||
else
|
||||
format_as_block_html("p", el.attr, inner(el, indent), indent)
|
||||
end
|
||||
end
|
||||
|
||||
# Helper method used by +convert_p+ to convert a paragraph that only contains a single :img
|
||||
# element.
|
||||
def convert_standalone_image(el, indent)
|
||||
figure_attr = el.attr.dup
|
||||
image_attr = el.children.first.attr.dup
|
||||
|
||||
if image_attr.key?('class') && !figure_attr.key?('class')
|
||||
figure_attr['class'] = image_attr.delete('class')
|
||||
end
|
||||
if image_attr.key?('id') && !figure_attr.key?('id')
|
||||
figure_attr['id'] = image_attr.delete('id')
|
||||
end
|
||||
|
||||
body = "#{' ' * (indent + @indent)}<img#{html_attributes(image_attr)} />\n" \
|
||||
"#{' ' * (indent + @indent)}<figcaption>#{image_attr['alt']}</figcaption>\n"
|
||||
format_as_indented_block_html("figure", figure_attr, body, indent)
|
||||
end
|
||||
|
||||
def convert_codeblock(el, indent)
|
||||
attr = el.attr.dup
|
||||
lang = extract_code_language!(attr)
|
||||
hl_opts = {}
|
||||
highlighted_code = highlight_code(el.value, el.options[:lang] || lang, :block, hl_opts)
|
||||
|
||||
if highlighted_code
|
||||
add_syntax_highlighter_to_class_attr(attr, lang || hl_opts[:default_lang])
|
||||
"#{' ' * indent}<div#{html_attributes(attr)}>#{highlighted_code}#{' ' * indent}</div>\n"
|
||||
else
|
||||
result = escape_html(el.value)
|
||||
result.chomp!
|
||||
if el.attr['class'].to_s =~ /\bshow-whitespaces\b/
|
||||
result.gsub!(/(?:(^[ \t]+)|([ \t]+$)|([ \t]+))/) do |m|
|
||||
suffix = ($1 ? '-l' : ($2 ? '-r' : ''))
|
||||
m.scan(/./).map do |c|
|
||||
case c
|
||||
when "\t" then "<span class=\"ws-tab#{suffix}\">\t</span>"
|
||||
when " " then "<span class=\"ws-space#{suffix}\">⋅</span>"
|
||||
end
|
||||
end.join
|
||||
end
|
||||
end
|
||||
code_attr = {}
|
||||
code_attr['class'] = "language-#{lang}" if lang
|
||||
"#{' ' * indent}<pre#{html_attributes(attr)}>" \
|
||||
"<code#{html_attributes(code_attr)}>#{result}\n</code></pre>\n"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_blockquote(el, indent)
|
||||
format_as_indented_block_html("blockquote", el.attr, inner(el, indent), indent)
|
||||
end
|
||||
|
||||
def convert_header(el, indent)
|
||||
attr = el.attr.dup
|
||||
if @options[:auto_ids] && !attr['id']
|
||||
attr['id'] = generate_id(el.options[:raw_text])
|
||||
end
|
||||
|
||||
if @options[:header_links] && attr['id'].to_s.length > 0
|
||||
link = Element.new(:a, nil, nil)
|
||||
link.attr['href'] = "##{attr['id']}"
|
||||
el.children.unshift(link)
|
||||
end
|
||||
|
||||
@toc << [el.options[:level], attr['id'], el.children] if attr['id'] && in_toc?(el)
|
||||
level = output_header_level(el.options[:level])
|
||||
format_as_block_html("h#{level}", attr, inner(el, indent), indent)
|
||||
end
|
||||
|
||||
def convert_hr(el, indent)
|
||||
"#{' ' * indent}<hr#{html_attributes(el.attr)} />\n"
|
||||
end
|
||||
|
||||
ZERO_TO_ONETWENTYEIGHT = (0..128).to_a.freeze
|
||||
private_constant :ZERO_TO_ONETWENTYEIGHT
|
||||
|
||||
def convert_ul(el, indent)
|
||||
if !@toc_code && el.options.dig(:ial, :refs)&.include?('toc')
|
||||
@toc_code = [el.type, el.attr, ZERO_TO_ONETWENTYEIGHT.map { rand(36).to_s(36) }.join]
|
||||
@toc_code.last
|
||||
elsif !@footnote_location && el.options.dig(:ial, :refs)&.include?('footnotes')
|
||||
@footnote_location = ZERO_TO_ONETWENTYEIGHT.map { rand(36).to_s(36) }.join
|
||||
else
|
||||
format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent)
|
||||
end
|
||||
end
|
||||
alias convert_ol convert_ul
|
||||
|
||||
def convert_dl(el, indent)
|
||||
format_as_indented_block_html("dl", el.attr, inner(el, indent), indent)
|
||||
end
|
||||
|
||||
def convert_li(el, indent)
|
||||
output = ' ' * indent << "<#{el.type}" << html_attributes(el.attr) << ">"
|
||||
res = inner(el, indent)
|
||||
if el.children.empty? || (el.children.first.type == :p && el.children.first.options[:transparent])
|
||||
output << res << (res.match?(/\n\Z/) ? ' ' * indent : '')
|
||||
else
|
||||
output << "\n" << res << ' ' * indent
|
||||
end
|
||||
output << "</#{el.type}>\n"
|
||||
end
|
||||
alias convert_dd convert_li
|
||||
|
||||
def convert_dt(el, indent)
|
||||
attr = el.attr.dup
|
||||
@stack.last.options[:ial][:refs].each do |ref|
|
||||
if ref =~ /\Aauto_ids(?:-([\w-]+))?/
|
||||
attr['id'] = "#{$1}#{basic_generate_id(el.options[:raw_text])}".lstrip
|
||||
break
|
||||
end
|
||||
end if !attr['id'] && @stack.last.options[:ial] && @stack.last.options[:ial][:refs]
|
||||
format_as_block_html("dt", attr, inner(el, indent), indent)
|
||||
end
|
||||
|
||||
def convert_html_element(el, indent)
|
||||
res = inner(el, indent)
|
||||
if el.options[:category] == :span
|
||||
"<#{el.value}#{html_attributes(el.attr)}" + \
|
||||
(res.empty? && HTML_ELEMENTS_WITHOUT_BODY.include?(el.value) ? " />" : ">#{res}</#{el.value}>")
|
||||
else
|
||||
output = +''
|
||||
if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw
|
||||
output << ' ' * indent
|
||||
end
|
||||
output << "<#{el.value}#{html_attributes(el.attr)}"
|
||||
if el.options[:is_closed] && el.options[:content_model] == :raw
|
||||
output << " />"
|
||||
elsif !res.empty? && el.options[:content_model] != :block
|
||||
output << ">#{res}</#{el.value}>"
|
||||
elsif !res.empty?
|
||||
output << ">\n#{res.chomp}\n" << ' ' * indent << "</#{el.value}>"
|
||||
elsif HTML_ELEMENTS_WITHOUT_BODY.include?(el.value)
|
||||
output << " />"
|
||||
else
|
||||
output << "></#{el.value}>"
|
||||
end
|
||||
output << "\n" if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
def convert_xml_comment(el, indent)
|
||||
if el.options[:category] == :block &&
|
||||
(@stack.last.type != :html_element || @stack.last.options[:content_model] != :raw)
|
||||
' ' * indent << el.value << "\n"
|
||||
else
|
||||
el.value
|
||||
end
|
||||
end
|
||||
alias convert_xml_pi convert_xml_comment
|
||||
|
||||
def convert_table(el, indent)
|
||||
format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent)
|
||||
end
|
||||
alias convert_thead convert_table
|
||||
alias convert_tbody convert_table
|
||||
alias convert_tfoot convert_table
|
||||
alias convert_tr convert_table
|
||||
|
||||
ENTITY_NBSP = ::Kramdown::Utils::Entities.entity('nbsp') # :nodoc:
|
||||
|
||||
def convert_td(el, indent)
|
||||
res = inner(el, indent)
|
||||
type = (@stack[-2].type == :thead ? :th : :td)
|
||||
attr = el.attr
|
||||
alignment = @stack[-3].options[:alignment][@stack.last.children.index(el)]
|
||||
if alignment != :default
|
||||
attr = el.attr.dup
|
||||
attr['style'] = (attr.key?('style') ? "#{attr['style']}; " : '') + "text-align: #{alignment}"
|
||||
end
|
||||
format_as_block_html(type, attr, res.empty? ? entity_to_str(ENTITY_NBSP) : res, indent)
|
||||
end
|
||||
|
||||
def convert_comment(el, indent)
|
||||
if el.options[:category] == :block
|
||||
"#{' ' * indent}<!-- #{el.value} -->\n"
|
||||
else
|
||||
"<!-- #{el.value} -->"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_br(_el, _indent)
|
||||
"<br />"
|
||||
end
|
||||
|
||||
def convert_a(el, indent)
|
||||
format_as_span_html("a", el.attr, inner(el, indent))
|
||||
end
|
||||
|
||||
def convert_img(el, _indent)
|
||||
"<img#{html_attributes(el.attr)} />"
|
||||
end
|
||||
|
||||
def convert_codespan(el, _indent)
|
||||
attr = el.attr.dup
|
||||
lang = extract_code_language(attr)
|
||||
hl_opts = {}
|
||||
result = highlight_code(el.value, lang, :span, hl_opts)
|
||||
if result
|
||||
add_syntax_highlighter_to_class_attr(attr, lang || hl_opts[:default_lang])
|
||||
else
|
||||
result = escape_html(el.value)
|
||||
end
|
||||
|
||||
format_as_span_html('code', attr, result)
|
||||
end
|
||||
|
||||
def convert_footnote(el, _indent)
|
||||
repeat = ''
|
||||
name = @options[:footnote_prefix] + el.options[:name]
|
||||
if (footnote = @footnotes_by_name[name])
|
||||
number = footnote[2]
|
||||
repeat = ":#{footnote[3] += 1}"
|
||||
else
|
||||
number = @footnote_counter
|
||||
@footnote_counter += 1
|
||||
@footnotes << [name, el.value, number, 0]
|
||||
@footnotes_by_name[name] = @footnotes.last
|
||||
end
|
||||
formatted_link_text = sprintf(@options[:footnote_link_text], number)
|
||||
"<sup id=\"fnref:#{name}#{repeat}\">" \
|
||||
"<a href=\"#fn:#{name}\" class=\"footnote\" rel=\"footnote\" role=\"doc-noteref\">" \
|
||||
"#{formatted_link_text}</a></sup>"
|
||||
end
|
||||
|
||||
def convert_raw(el, _indent)
|
||||
if !el.options[:type] || el.options[:type].empty? || el.options[:type].include?('html')
|
||||
el.value + (el.options[:category] == :block ? "\n" : '')
|
||||
else
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
def convert_em(el, indent)
|
||||
format_as_span_html(el.type, el.attr, inner(el, indent))
|
||||
end
|
||||
alias convert_strong convert_em
|
||||
|
||||
def convert_entity(el, _indent)
|
||||
entity_to_str(el.value, el.options[:original])
|
||||
end
|
||||
|
||||
TYPOGRAPHIC_SYMS = {
|
||||
mdash: [::Kramdown::Utils::Entities.entity('mdash')],
|
||||
ndash: [::Kramdown::Utils::Entities.entity('ndash')],
|
||||
hellip: [::Kramdown::Utils::Entities.entity('hellip')],
|
||||
laquo_space: [::Kramdown::Utils::Entities.entity('laquo'),
|
||||
::Kramdown::Utils::Entities.entity('nbsp')],
|
||||
raquo_space: [::Kramdown::Utils::Entities.entity('nbsp'),
|
||||
::Kramdown::Utils::Entities.entity('raquo')],
|
||||
laquo: [::Kramdown::Utils::Entities.entity('laquo')],
|
||||
raquo: [::Kramdown::Utils::Entities.entity('raquo')],
|
||||
} # :nodoc:
|
||||
def convert_typographic_sym(el, _indent)
|
||||
if (result = @options[:typographic_symbols][el.value])
|
||||
escape_html(result, :text)
|
||||
else
|
||||
TYPOGRAPHIC_SYMS[el.value].map {|e| entity_to_str(e) }.join
|
||||
end
|
||||
end
|
||||
|
||||
def convert_smart_quote(el, _indent)
|
||||
entity_to_str(smart_quote_entity(el))
|
||||
end
|
||||
|
||||
def convert_math(el, indent)
|
||||
if (result = format_math(el, indent: indent))
|
||||
result
|
||||
else
|
||||
attr = el.attr.dup
|
||||
attr['class'] = "#{attr['class']} kdmath".lstrip
|
||||
if el.options[:category] == :block
|
||||
format_as_block_html('div', attr, "$$\n#{el.value}\n$$", indent)
|
||||
else
|
||||
format_as_span_html('span', attr, "$#{el.value}$")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def convert_abbreviation(el, _indent)
|
||||
title = @root.options[:abbrev_defs][el.value]
|
||||
attr = @root.options[:abbrev_attr][el.value].dup
|
||||
attr['title'] = title unless title.empty?
|
||||
format_as_span_html("abbr", attr, el.value)
|
||||
end
|
||||
|
||||
def convert_root(el, indent)
|
||||
result = inner(el, indent)
|
||||
if @footnote_location
|
||||
result.sub!(/#{@footnote_location}/, footnote_content.gsub(/\\/, "\\\\\\\\"))
|
||||
else
|
||||
result << footnote_content
|
||||
end
|
||||
if @toc_code
|
||||
toc_tree = generate_toc_tree(@toc, @toc_code[0], @toc_code[1] || {})
|
||||
text = toc_tree.children.empty? ? '' : convert(toc_tree, 0)
|
||||
result.sub!(/#{@toc_code.last}/, text.gsub(/\\/, "\\\\\\\\"))
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
# Format the given element as span HTML.
|
||||
def format_as_span_html(name, attr, body)
|
||||
"<#{name}#{html_attributes(attr)}>#{body}</#{name}>"
|
||||
end
|
||||
|
||||
# Format the given element as block HTML.
|
||||
def format_as_block_html(name, attr, body, indent)
|
||||
"#{' ' * indent}<#{name}#{html_attributes(attr)}>#{body}</#{name}>\n"
|
||||
end
|
||||
|
||||
# Format the given element as block HTML with a newline after the start tag and indentation
|
||||
# before the end tag.
|
||||
def format_as_indented_block_html(name, attr, body, indent)
|
||||
"#{' ' * indent}<#{name}#{html_attributes(attr)}>\n#{body}#{' ' * indent}</#{name}>\n"
|
||||
end
|
||||
|
||||
# Add the syntax highlighter name to the 'class' attribute of the given attribute hash. And
|
||||
# overwrites or add a "language-LANG" part using the +lang+ parameter if +lang+ is not nil.
|
||||
def add_syntax_highlighter_to_class_attr(attr, lang = nil)
|
||||
(attr['class'] = (attr['class'] || '') + @highlighter_class).lstrip!
|
||||
attr['class'].sub!(/\blanguage-\S+|(^)/) { "language-#{lang}#{$1 ? ' ' : ''}" } if lang
|
||||
end
|
||||
|
||||
# Generate and return an element tree for the table of contents.
|
||||
def generate_toc_tree(toc, type, attr)
|
||||
sections = Element.new(type, nil, attr.dup)
|
||||
sections.attr['id'] ||= 'markdown-toc'
|
||||
stack = []
|
||||
toc.each do |level, id, children|
|
||||
li = Element.new(:li, nil, nil, level: level)
|
||||
li.children << Element.new(:p, nil, nil, transparent: true)
|
||||
a = Element.new(:a, nil)
|
||||
a.attr['href'] = "##{id}"
|
||||
a.attr['id'] = "#{sections.attr['id']}-#{id}"
|
||||
a.children.concat(fix_for_toc_entry(Marshal.load(Marshal.dump(children))))
|
||||
li.children.last.children << a
|
||||
li.children << Element.new(type)
|
||||
|
||||
success = false
|
||||
until success
|
||||
if stack.empty?
|
||||
sections.children << li
|
||||
stack << li
|
||||
success = true
|
||||
elsif stack.last.options[:level] < li.options[:level]
|
||||
stack.last.children.last.children << li
|
||||
stack << li
|
||||
success = true
|
||||
else
|
||||
item = stack.pop
|
||||
item.children.pop if item.children.last.children.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
until stack.empty?
|
||||
item = stack.pop
|
||||
item.children.pop if item.children.last.children.empty?
|
||||
end
|
||||
sections
|
||||
end
|
||||
|
||||
# Fixes the elements for use in a TOC entry.
|
||||
def fix_for_toc_entry(elements)
|
||||
remove_footnotes(elements)
|
||||
unwrap_links(elements)
|
||||
elements
|
||||
end
|
||||
|
||||
# Remove all link elements by unwrapping them.
|
||||
def unwrap_links(elements)
|
||||
elements.map! do |c|
|
||||
unwrap_links(c.children)
|
||||
c.type == :a ? c.children : c
|
||||
end.flatten!
|
||||
end
|
||||
|
||||
# Remove all footnotes from the given elements.
|
||||
def remove_footnotes(elements)
|
||||
elements.delete_if do |c|
|
||||
remove_footnotes(c.children)
|
||||
c.type == :footnote
|
||||
end
|
||||
end
|
||||
|
||||
# Obfuscate the +text+ by using HTML entities.
|
||||
def obfuscate(text)
|
||||
result = +''
|
||||
text.each_byte do |b|
|
||||
result << (b > 128 ? b.chr : sprintf("&#%03d;", b))
|
||||
end
|
||||
result.force_encoding(text.encoding)
|
||||
result
|
||||
end
|
||||
|
||||
FOOTNOTE_BACKLINK_FMT = "%s<a href=\"#fnref:%s\" class=\"reversefootnote\" role=\"doc-backlink\">%s</a>"
|
||||
|
||||
# Return an HTML ordered list with the footnote content for the used footnotes.
|
||||
def footnote_content
|
||||
ol = Element.new(:ol)
|
||||
ol.attr['start'] = @footnote_start if @footnote_start != 1
|
||||
i = 0
|
||||
backlink_text = escape_html(@options[:footnote_backlink], :text)
|
||||
while i < @footnotes.length
|
||||
name, data, _, repeat = *@footnotes[i]
|
||||
li = Element.new(:li, nil, 'id' => "fn:#{name}")
|
||||
li.children = Marshal.load(Marshal.dump(data.children))
|
||||
|
||||
para = nil
|
||||
if li.children.last.type == :p || @options[:footnote_backlink_inline]
|
||||
parent = li
|
||||
while !parent.children.empty? && ![:p, :header].include?(parent.children.last.type)
|
||||
parent = parent.children.last
|
||||
end
|
||||
para = parent.children.last
|
||||
insert_space = true
|
||||
end
|
||||
|
||||
unless para
|
||||
li.children << (para = Element.new(:p))
|
||||
insert_space = false
|
||||
end
|
||||
|
||||
unless @options[:footnote_backlink].empty?
|
||||
nbsp = entity_to_str(ENTITY_NBSP)
|
||||
value = sprintf(FOOTNOTE_BACKLINK_FMT, (insert_space ? nbsp : ''), name, backlink_text)
|
||||
para.children << Element.new(:raw, value)
|
||||
(1..repeat).each do |index|
|
||||
value = sprintf(FOOTNOTE_BACKLINK_FMT, nbsp, "#{name}:#{index}",
|
||||
"#{backlink_text}<sup>#{index + 1}</sup>")
|
||||
para.children << Element.new(:raw, value)
|
||||
end
|
||||
end
|
||||
|
||||
ol.children << Element.new(:raw, convert(li, 4))
|
||||
i += 1
|
||||
end
|
||||
if ol.children.empty?
|
||||
''
|
||||
else
|
||||
format_as_indented_block_html('div', {class: "footnotes", role: "doc-endnotes"}, convert(ol, 2), 0)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,458 @@
|
||||
# -*- 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/converter'
|
||||
require 'kramdown/utils'
|
||||
|
||||
module Kramdown
|
||||
|
||||
module Converter
|
||||
|
||||
# Converts an element tree to the kramdown format.
|
||||
class Kramdown < Base
|
||||
|
||||
# :stopdoc:
|
||||
|
||||
include ::Kramdown::Utils::Html
|
||||
|
||||
def initialize(root, options)
|
||||
super
|
||||
@linkrefs = []
|
||||
@footnotes = []
|
||||
@abbrevs = []
|
||||
@stack = []
|
||||
@list_indent = @options[:list_indent]
|
||||
@list_spacing = ' ' * (@list_indent - 2)
|
||||
end
|
||||
|
||||
def convert(el, opts = {indent: 0})
|
||||
res = send("convert_#{el.type}", el, opts)
|
||||
res = res.dup if res.frozen?
|
||||
if ![:html_element, :li, :dt, :dd, :td].include?(el.type) && (ial = ial_for_element(el))
|
||||
res << ial
|
||||
res << "\n\n" if el.block?
|
||||
elsif [:ul, :dl, :ol, :codeblock].include?(el.type) && opts[:next] &&
|
||||
([el.type, :codeblock].include?(opts[:next].type) ||
|
||||
(opts[:next].type == :blank && opts[:nnext] &&
|
||||
[el.type, :codeblock].include?(opts[:nnext].type)))
|
||||
res << "^\n\n"
|
||||
elsif el.block? &&
|
||||
![:li, :dd, :dt, :td, :th, :tr, :thead, :tbody, :tfoot, :blank].include?(el.type) &&
|
||||
(el.type != :html_element || @stack.last.type != :html_element) &&
|
||||
(el.type != :p || !el.options[:transparent]) &&
|
||||
!([:ul, :dl, :ol].include?(el.type) && @stack.last.type == :li)
|
||||
res << "\n"
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
def inner(el, opts = {indent: 0})
|
||||
@stack.push(el)
|
||||
result = +''
|
||||
el.children.each_with_index do |inner_el, index|
|
||||
options = opts.dup
|
||||
options[:index] = index
|
||||
options[:prev] = (index == 0 ? nil : el.children[index - 1])
|
||||
options[:pprev] = (index <= 1 ? nil : el.children[index - 2])
|
||||
options[:next] = (index == el.children.length - 1 ? nil : el.children[index + 1])
|
||||
options[:nnext] = (index >= el.children.length - 2 ? nil : el.children[index + 2])
|
||||
result << convert(inner_el, options)
|
||||
end
|
||||
@stack.pop
|
||||
result
|
||||
end
|
||||
|
||||
def convert_blank(_el, _opts)
|
||||
""
|
||||
end
|
||||
|
||||
ESCAPED_CHAR_RE = /(\$\$|[\\*_`\[\]{"'|])|^ {0,3}(:)/
|
||||
|
||||
def convert_text(el, opts)
|
||||
if opts[:raw_text] || (@stack.last.type == :html_element && @stack.last.options[:content_model] == :raw)
|
||||
el.value
|
||||
else
|
||||
result = el.value.gsub(/\A\n/) do
|
||||
opts[:prev] && opts[:prev].type == :br ? '' : "\n"
|
||||
end
|
||||
result.gsub!(/\s+/, ' ') unless el.options[:cdata]
|
||||
result.gsub!(ESCAPED_CHAR_RE) do
|
||||
$1 || !opts[:prev] || opts[:prev].type == :br ? "\\#{$1 || $2}" : $&
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
def convert_p(el, opts)
|
||||
w = @options[:line_width] - opts[:indent].to_s.to_i
|
||||
first, second, *rest = inner(el, opts).strip.gsub(/(.{1,#{w}})( +|$\n?)/, "\\1\n").split(/\n/)
|
||||
first&.gsub!(/^(?:(#|>)|(\d+)\.(\s)|([+-]\s))/) { $1 || $4 ? "\\#{$1 || $4}" : "#{$2}\\.#{$3}" }
|
||||
second&.gsub!(/^([=-]+\s*?)$/, "\\\1")
|
||||
res = [first, second, *rest].compact.join("\n") + "\n"
|
||||
res.gsub!(/^ {0,3}:/, "\\:")
|
||||
if el.children.length == 1 && el.children.first.type == :math
|
||||
res = "\\#{res}"
|
||||
elsif res.start_with?('\$$') && res.end_with?("\\$$\n")
|
||||
res.sub!(/^\\\$\$/, '\$\$')
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
def convert_codeblock(el, _opts)
|
||||
el.value.split("\n").map {|l| l.empty? ? " " : " #{l}" }.join("\n") + "\n"
|
||||
end
|
||||
|
||||
def convert_blockquote(el, opts)
|
||||
opts[:indent] += 2
|
||||
inner(el, opts).chomp.split("\n").map {|l| "> #{l}" }.join("\n") << "\n"
|
||||
end
|
||||
|
||||
def convert_header(el, opts)
|
||||
opts[:in_header] = true
|
||||
res = +''
|
||||
res << "#{'#' * output_header_level(el.options[:level])} #{inner(el, opts)}"
|
||||
res[-1, 1] = "\\#" if res[-1] == '#'
|
||||
res << " {##{el.attr['id']}}" if el.attr['id'] && !el.attr['id'].strip.empty?
|
||||
res << "\n"
|
||||
end
|
||||
|
||||
def convert_hr(_el, _opts)
|
||||
"* * *\n"
|
||||
end
|
||||
|
||||
def convert_ul(el, opts)
|
||||
inner(el, opts).sub(/\n+\Z/, "\n")
|
||||
end
|
||||
alias convert_ol convert_ul
|
||||
alias convert_dl convert_ul
|
||||
|
||||
def convert_li(el, opts)
|
||||
sym, width = if @stack.last.type == :ul
|
||||
['* ' + @list_spacing, el.children.first && el.children.first.type == :codeblock ? 4 : @list_indent]
|
||||
else
|
||||
["#{opts[:index] + 1}.".ljust(4), 4]
|
||||
end
|
||||
if (ial = ial_for_element(el))
|
||||
sym << ial << " "
|
||||
end
|
||||
|
||||
opts[:indent] += width
|
||||
text = inner(el, opts)
|
||||
newlines = text.scan(/\n*\Z/).first
|
||||
first, *last = text.split("\n")
|
||||
last = last.map {|l| " " * width + l }.join("\n")
|
||||
text = (first.nil? ? "\n" : first + (last.empty? ? "" : "\n") + last + newlines)
|
||||
if el.children.first && el.children.first.type == :p && !el.children.first.options[:transparent]
|
||||
res = +"#{sym}#{text}"
|
||||
res << "^\n" if el.children.size == 1 && @stack.last.children.last == el &&
|
||||
(@stack.last.children.any? {|c| !c.children.first || c.children.first.type != :p } || @stack.last.children.size == 1)
|
||||
res
|
||||
elsif el.children.first && el.children.first.type == :codeblock
|
||||
"#{sym}\n #{text}"
|
||||
else
|
||||
"#{sym}#{text}"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_dd(el, opts)
|
||||
sym, width = ": " + @list_spacing, (el.children.first && el.children.first.type == :codeblock ? 4 : @list_indent)
|
||||
if (ial = ial_for_element(el))
|
||||
sym << ial << " "
|
||||
end
|
||||
|
||||
opts[:indent] += width
|
||||
text = inner(el, opts)
|
||||
newlines = text.scan(/\n*\Z/).first
|
||||
first, *last = text.split("\n")
|
||||
last = last.map {|l| " " * width + l }.join("\n")
|
||||
text = first.to_s + (last.empty? ? "" : "\n") + last + newlines
|
||||
text.chomp! if text =~ /\n\n\Z/ && opts[:next] && opts[:next].type == :dd
|
||||
text << "\n" if text !~ /\n\n\Z/ && opts[:next] && opts[:next].type == :dt
|
||||
text << "\n" if el.children.empty?
|
||||
if el.children.first && el.children.first.type == :p && !el.children.first.options[:transparent]
|
||||
"\n#{sym}#{text}"
|
||||
elsif el.children.first && el.children.first.type == :codeblock
|
||||
"#{sym}\n #{text}"
|
||||
else
|
||||
"#{sym}#{text}"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_dt(el, opts)
|
||||
result = +''
|
||||
if (ial = ial_for_element(el))
|
||||
result << ial << " "
|
||||
end
|
||||
result << inner(el, opts) << "\n"
|
||||
end
|
||||
|
||||
HTML_TAGS_WITH_BODY = %w[div script iframe textarea th td]
|
||||
|
||||
HTML_ELEMENT_TYPES = [:entity, :text, :html_element].freeze
|
||||
private_constant :HTML_ELEMENT_TYPES
|
||||
|
||||
def convert_html_element(el, opts)
|
||||
markdown_attr = el.options[:category] == :block && el.children.any? do |c|
|
||||
c.type != :html_element &&
|
||||
(c.type != :p || !c.options[:transparent] ||
|
||||
c.children.any? {|t| !HTML_ELEMENT_TYPES.member?(t.type) }) &&
|
||||
c.block?
|
||||
end
|
||||
opts[:force_raw_text] = true if %w[script pre code].include?(el.value)
|
||||
opts[:raw_text] = opts[:force_raw_text] || opts[:block_raw_text] || \
|
||||
(el.options[:category] != :span && !markdown_attr)
|
||||
opts[:block_raw_text] = true if el.options[:category] == :block && opts[:raw_text]
|
||||
res = inner(el, opts)
|
||||
if el.options[:category] == :span
|
||||
"<#{el.value}#{html_attributes(el.attr)}" + \
|
||||
(!res.empty? || HTML_TAGS_WITH_BODY.include?(el.value) ? ">#{res}</#{el.value}>" : " />")
|
||||
else
|
||||
output = +''
|
||||
attr = el.attr.dup
|
||||
attr['markdown'] = '1' if markdown_attr
|
||||
output << "<#{el.value}#{html_attributes(attr)}"
|
||||
if !res.empty? && el.options[:content_model] != :block
|
||||
output << ">#{res}</#{el.value}>"
|
||||
elsif !res.empty?
|
||||
output << ">\n#{res}" << "</#{el.value}>"
|
||||
elsif HTML_TAGS_WITH_BODY.include?(el.value)
|
||||
output << "></#{el.value}>"
|
||||
else
|
||||
output << " />"
|
||||
end
|
||||
output << "\n" if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
def convert_xml_comment(el, _opts)
|
||||
if el.options[:category] == :block &&
|
||||
(@stack.last.type != :html_element || @stack.last.options[:content_model] != :raw)
|
||||
el.value + "\n"
|
||||
else
|
||||
el.value.dup
|
||||
end
|
||||
end
|
||||
alias convert_xml_pi convert_xml_comment
|
||||
|
||||
def convert_table(el, opts)
|
||||
opts[:alignment] = el.options[:alignment]
|
||||
inner(el, opts)
|
||||
end
|
||||
|
||||
def convert_thead(el, opts)
|
||||
rows = inner(el, opts)
|
||||
if opts[:alignment].all?(:default)
|
||||
"#{rows}|#{'-' * 10}\n"
|
||||
else
|
||||
"#{rows}| " + opts[:alignment].map do |a|
|
||||
case a
|
||||
when :left then ":-"
|
||||
when :right then "-:"
|
||||
when :center then ":-:"
|
||||
when :default then "-"
|
||||
end
|
||||
end.join(' ') << "\n"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_tbody(el, opts)
|
||||
res = +''
|
||||
res << inner(el, opts)
|
||||
res << '|' << '-' * 10 << "\n" if opts[:next] && opts[:next].type == :tbody
|
||||
res
|
||||
end
|
||||
|
||||
def convert_tfoot(el, opts)
|
||||
"|#{'=' * 10}\n#{inner(el, opts)}"
|
||||
end
|
||||
|
||||
def convert_tr(el, opts)
|
||||
"| #{el.children.map {|c| convert(c, opts) }.join(' | ')} |\n"
|
||||
end
|
||||
|
||||
def convert_td(el, opts)
|
||||
inner(el, opts)
|
||||
end
|
||||
|
||||
def convert_comment(el, _opts)
|
||||
if el.options[:category] == :block
|
||||
"{::comment}\n#{el.value}\n{:/}\n"
|
||||
else
|
||||
"{::comment}#{el.value}{:/}"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_br(_el, opts)
|
||||
opts[:in_header] ? "<br />" : " \n"
|
||||
end
|
||||
|
||||
def convert_a(el, opts)
|
||||
if el.attr['href'].empty?
|
||||
"[#{inner(el, opts)}]()"
|
||||
elsif el.attr['href'] =~ /^(?:http|ftp)/ || el.attr['href'].count("()") > 0
|
||||
index = if (link_el = @linkrefs.find {|c| c.attr['href'] == el.attr['href'] })
|
||||
@linkrefs.index(link_el) + 1
|
||||
else
|
||||
@linkrefs << el
|
||||
@linkrefs.size
|
||||
end
|
||||
"[#{inner(el, opts)}][#{index}]"
|
||||
else
|
||||
title = parse_title(el.attr['title'])
|
||||
"[#{inner(el, opts)}](#{el.attr['href']}#{title})"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_img(el, _opts)
|
||||
alt_text = el.attr['alt'].to_s.gsub(ESCAPED_CHAR_RE) { $1 ? "\\#{$1}" : $2 }
|
||||
src = el.attr['src'].to_s
|
||||
if src.empty?
|
||||
"![#{alt_text}]()"
|
||||
else
|
||||
title = parse_title(el.attr['title'])
|
||||
link = if src.count("()") > 0
|
||||
"<#{src}>"
|
||||
else
|
||||
src
|
||||
end
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
def convert_codespan(el, _opts)
|
||||
delim = (el.value.scan(/`+/).max || '') + '`'
|
||||
"#{delim}#{' ' if delim.size > 1}#{el.value}#{' ' if delim.size > 1}#{delim}"
|
||||
end
|
||||
|
||||
def convert_footnote(el, _opts)
|
||||
@footnotes << [el.options[:name], el.value]
|
||||
"[^#{el.options[:name]}]"
|
||||
end
|
||||
|
||||
def convert_raw(el, _opts)
|
||||
attr = (el.options[:type] || []).join(' ')
|
||||
attr = " type=\"#{attr}\"" unless attr.empty?
|
||||
if @stack.last.type == :html_element
|
||||
el.value
|
||||
elsif el.options[:category] == :block
|
||||
"{::nomarkdown#{attr}}\n#{el.value}\n{:/}\n"
|
||||
else
|
||||
"{::nomarkdown#{attr}}#{el.value}{:/}"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_em(el, opts)
|
||||
"*#{inner(el, opts)}*" +
|
||||
(opts[:next] && [:em, :strong].include?(opts[:next].type) && !ial_for_element(el) ? '{::}' : '')
|
||||
end
|
||||
|
||||
def convert_strong(el, opts)
|
||||
"**#{inner(el, opts)}**" +
|
||||
(opts[:next] && [:em, :strong].include?(opts[:next].type) && !ial_for_element(el) ? '{::}' : '')
|
||||
end
|
||||
|
||||
def convert_entity(el, _opts)
|
||||
entity_to_str(el.value, el.options[:original])
|
||||
end
|
||||
|
||||
TYPOGRAPHIC_SYMS = {
|
||||
mdash: '---', ndash: '--', hellip: '...',
|
||||
laquo_space: '<< ', raquo_space: ' >>',
|
||||
laquo: '<<', raquo: '>>'
|
||||
}
|
||||
def convert_typographic_sym(el, _opts)
|
||||
TYPOGRAPHIC_SYMS[el.value]
|
||||
end
|
||||
|
||||
def convert_smart_quote(el, _opts)
|
||||
el.value.to_s.match?(/[rl]dquo/) ? "\"" : "'"
|
||||
end
|
||||
|
||||
def convert_math(el, _opts)
|
||||
"$$#{el.value}$$" + (el.options[:category] == :block ? "\n" : '')
|
||||
end
|
||||
|
||||
def convert_abbreviation(el, _opts)
|
||||
el.value
|
||||
end
|
||||
|
||||
def convert_root(el, opts)
|
||||
res = inner(el, opts)
|
||||
res << create_link_defs
|
||||
res << create_footnote_defs
|
||||
res << create_abbrev_defs
|
||||
res
|
||||
end
|
||||
|
||||
def create_link_defs
|
||||
res = +''
|
||||
res << "\n\n" unless @linkrefs.empty?
|
||||
@linkrefs.each_with_index do |el, i|
|
||||
title = parse_title(el.attr['title'])
|
||||
res << "[#{i + 1}]: #{el.attr['href']}#{title}\n"
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
def create_footnote_defs
|
||||
res = +''
|
||||
@footnotes.each do |name, data|
|
||||
res << "[^#{name}]:\n"
|
||||
res << inner(data).chomp.split("\n").map {|l| " #{l}" }.join("\n") + "\n\n"
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
def create_abbrev_defs
|
||||
return '' unless @root.options[:abbrev_defs]
|
||||
res = +''
|
||||
@root.options[:abbrev_defs].each do |name, text|
|
||||
res << "*[#{name}]: #{text}\n"
|
||||
res << ial_for_element(Element.new(:unused, nil, @root.options[:abbrev_attr][name])).to_s << "\n\n"
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
# Return the IAL containing the attributes of the element +el+.
|
||||
def ial_for_element(el)
|
||||
res = el.attr.map do |k, v|
|
||||
next if [:img, :a].include?(el.type) && ['href', 'src', 'alt', 'title'].include?(k)
|
||||
next if el.type == :header && k == 'id' && !v.strip.empty?
|
||||
if v.nil?
|
||||
''
|
||||
elsif k == 'class' && !v.empty? && !v.index(/[.#]/)
|
||||
" " + v.split(/\s+/).map {|w| ".#{w}" }.join(" ")
|
||||
elsif k == 'id' && !v.strip.empty?
|
||||
" ##{v}"
|
||||
else
|
||||
" #{k}=\"#{v}\""
|
||||
end
|
||||
end.compact.join
|
||||
res = "toc" + (res.strip.empty? ? '' : " #{res}") if (el.type == :ul || el.type == :ol) &&
|
||||
el.options.dig(:ial, :refs)&.include?('toc')
|
||||
res = "footnotes" + (res.strip.empty? ? '' : " #{res}") if (el.type == :ul || el.type == :ol) &&
|
||||
el.options.dig(:ial, :refs)&.include?('footnotes')
|
||||
if el.type == :dl && el.options[:ial] && el.options[:ial][:refs]
|
||||
auto_ids = el.options[:ial][:refs].select {|ref| ref.start_with?('auto_ids') }.join(" ")
|
||||
res = auto_ids << (res.strip.empty? ? '' : " #{res}") unless auto_ids.empty?
|
||||
end
|
||||
res.strip.empty? ? nil : "{:#{res}}"
|
||||
end
|
||||
|
||||
def parse_title(attr)
|
||||
attr.to_s.empty? ? '' : ' "' + attr.gsub(/"/, '"') + '"'
|
||||
end
|
||||
|
||||
# :startdoc:
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,626 @@
|
||||
# -*- 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 'set'
|
||||
require 'kramdown/converter'
|
||||
|
||||
module Kramdown
|
||||
|
||||
module Converter
|
||||
|
||||
# Converts an element tree to LaTeX.
|
||||
#
|
||||
# This converter uses ideas from other Markdown-to-LaTeX converters like Pandoc and Maruku.
|
||||
#
|
||||
# You can customize this converter by sub-classing it and overriding the +convert_NAME+ methods.
|
||||
# Each such method takes the following parameters:
|
||||
#
|
||||
# [+el+] The element of type +NAME+ to be converted.
|
||||
#
|
||||
# [+opts+] A hash containing processing options that are passed down from parent elements. The
|
||||
# key :parent is always set and contains the parent element as value.
|
||||
#
|
||||
# The return value of such a method has to be a string containing the element +el+ formatted
|
||||
# correctly as LaTeX markup.
|
||||
class Latex < Base
|
||||
|
||||
# Initialize the LaTeX converter with the +root+ element and the conversion +options+.
|
||||
def initialize(root, options)
|
||||
super
|
||||
@data[:packages] = Set.new
|
||||
end
|
||||
|
||||
# Dispatch the conversion of the element +el+ to a +convert_TYPE+ method using the +type+ of
|
||||
# the element.
|
||||
def convert(el, opts = {})
|
||||
send("convert_#{el.type}", el, opts)
|
||||
end
|
||||
|
||||
# Return the converted content of the children of +el+ as a string.
|
||||
def inner(el, opts)
|
||||
result = +''
|
||||
options = opts.dup.merge(parent: el)
|
||||
el.children.each_with_index do |inner_el, index|
|
||||
options[:index] = index
|
||||
options[:result] = result
|
||||
result << send("convert_#{inner_el.type}", inner_el, options)
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def convert_root(el, opts)
|
||||
inner(el, opts)
|
||||
end
|
||||
|
||||
def convert_blank(_el, opts)
|
||||
opts[:result].match?(/\n\n\Z|\A\Z/) ? "" : "\n"
|
||||
end
|
||||
|
||||
def convert_text(el, _opts)
|
||||
escape(el.value)
|
||||
end
|
||||
|
||||
def convert_p(el, opts)
|
||||
if el.children.size == 1 && el.children.first.type == :img &&
|
||||
!(img = convert_img(el.children.first, opts)).empty?
|
||||
convert_standalone_image(el, opts, img)
|
||||
else
|
||||
"#{latex_link_target(el)}#{inner(el, opts)}\n\n"
|
||||
end
|
||||
end
|
||||
|
||||
# Helper method used by +convert_p+ to convert a paragraph that only contains a single :img
|
||||
# element.
|
||||
def convert_standalone_image(el, _opts, img)
|
||||
attrs = attribute_list(el)
|
||||
"\\begin{figure}#{attrs}\n\\begin{center}\n#{img}\n\\end{center}\n" \
|
||||
"\\caption{#{escape(el.children.first.attr['alt'])}}\n" \
|
||||
"#{latex_link_target(el, true)}\n\\end{figure}#{attrs}\n"
|
||||
end
|
||||
|
||||
def convert_codeblock(el, _opts)
|
||||
show_whitespace = el.attr['class'].to_s =~ /\bshow-whitespaces\b/
|
||||
lang = extract_code_language(el.attr)
|
||||
|
||||
if @options[:syntax_highlighter] == :minted &&
|
||||
(highlighted_code = highlight_code(el.value, lang, :block))
|
||||
@data[:packages] << 'minted'
|
||||
"#{latex_link_target(el)}#{highlighted_code}\n"
|
||||
elsif show_whitespace || lang
|
||||
options = []
|
||||
options << (show_whitespace ? "showspaces=true,showtabs=true" : "showspaces=false,showtabs=false")
|
||||
options << "language=#{lang}" if lang
|
||||
options << "basicstyle=\\ttfamily\\footnotesize,columns=fixed,frame=tlbr"
|
||||
id = el.attr['id']
|
||||
options << "label=#{id}" if id
|
||||
attrs = attribute_list(el)
|
||||
"#{latex_link_target(el)}\\begin{lstlisting}[#{options.join(',')}]\n" \
|
||||
"#{el.value}\n\\end{lstlisting}#{attrs}\n"
|
||||
else
|
||||
"#{latex_link_target(el)}\\begin{verbatim}#{el.value}\\end{verbatim}\n"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_blockquote(el, opts)
|
||||
latex_environment(el.children.size > 1 ? 'quotation' : 'quote', el, inner(el, opts))
|
||||
end
|
||||
|
||||
def convert_header(el, opts)
|
||||
type = @options[:latex_headers][output_header_level(el.options[:level]) - 1]
|
||||
if ((id = el.attr['id']) ||
|
||||
(@options[:auto_ids] && (id = generate_id(el.options[:raw_text])))) && in_toc?(el)
|
||||
"\\#{type}{#{inner(el, opts)}}\\hypertarget{#{id}}{}\\label{#{id}}\n\n"
|
||||
else
|
||||
"\\#{type}*{#{inner(el, opts)}}\n\n"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_hr(el, _opts)
|
||||
attrs = attribute_list(el)
|
||||
"#{latex_link_target(el)}\\begin{center}#{attrs}\n\\rule{3in}{0.4pt}\n\\end{center}#{attrs}\n"
|
||||
end
|
||||
|
||||
def convert_ul(el, opts)
|
||||
if !@data[:has_toc] && el.options.dig(:ial, :refs)&.include?('toc')
|
||||
@data[:has_toc] = true
|
||||
'\tableofcontents'
|
||||
else
|
||||
latex_environment(el.type == :ul ? 'itemize' : 'enumerate', el, inner(el, opts))
|
||||
end
|
||||
end
|
||||
alias convert_ol convert_ul
|
||||
|
||||
def convert_dl(el, opts)
|
||||
latex_environment('description', el, inner(el, opts))
|
||||
end
|
||||
|
||||
def convert_li(el, opts)
|
||||
"\\item{} #{latex_link_target(el, true)}#{inner(el, opts).sub(/\n+\Z/, '')}\n"
|
||||
end
|
||||
|
||||
def convert_dt(el, opts)
|
||||
"\\item[#{inner(el, opts)}] "
|
||||
end
|
||||
|
||||
def convert_dd(el, opts)
|
||||
"#{latex_link_target(el)}#{inner(el, opts)}\n\n"
|
||||
end
|
||||
|
||||
def convert_html_element(el, opts)
|
||||
case el.value
|
||||
when 'i', 'em'
|
||||
"\\emph{#{inner(el, opts)}}"
|
||||
when 'b', 'strong'
|
||||
"\\textbf{#{inner(el, opts)}}"
|
||||
else
|
||||
warning("Can't convert HTML element")
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
def convert_xml_comment(el, _opts)
|
||||
el.value.split("\n").map {|l| "% #{l}" }.join("\n") + "\n"
|
||||
end
|
||||
|
||||
def convert_xml_pi(_el, _opts)
|
||||
warning("Can't convert XML PI")
|
||||
''
|
||||
end
|
||||
|
||||
TABLE_ALIGNMENT_CHAR = {default: 'l', left: 'l', center: 'c', right: 'r'} # :nodoc:
|
||||
|
||||
def convert_table(el, opts)
|
||||
@data[:packages] << 'longtable'
|
||||
align = el.options[:alignment].map {|a| TABLE_ALIGNMENT_CHAR[a] }.join('|')
|
||||
attrs = attribute_list(el)
|
||||
"#{latex_link_target(el)}\\begin{longtable}{|#{align}|}#{attrs}\n" \
|
||||
"\\hline\n#{inner(el, opts)}\\hline\n\\end{longtable}#{attrs}\n\n"
|
||||
end
|
||||
|
||||
def convert_thead(el, opts)
|
||||
"#{inner(el, opts)}\\hline\n"
|
||||
end
|
||||
|
||||
def convert_tbody(el, opts)
|
||||
inner(el, opts)
|
||||
end
|
||||
|
||||
def convert_tfoot(el, opts)
|
||||
"\\hline \\hline \n#{inner(el, opts)}"
|
||||
end
|
||||
|
||||
def convert_tr(el, opts)
|
||||
el.children.map {|c| send("convert_#{c.type}", c, opts) }.join(' & ') << "\\\\\n"
|
||||
end
|
||||
|
||||
def convert_td(el, opts)
|
||||
inner(el, opts)
|
||||
end
|
||||
|
||||
def convert_comment(el, _opts)
|
||||
el.value.split("\n").map {|l| "% #{l}" }.join("\n") << "\n"
|
||||
end
|
||||
|
||||
def convert_br(_el, opts)
|
||||
res = +"\\newline"
|
||||
res << "\n" if (c = opts[:parent].children[opts[:index] + 1]) &&
|
||||
(c.type != :text || c.value !~ /^\s*\n/)
|
||||
res
|
||||
end
|
||||
|
||||
def convert_a(el, opts)
|
||||
url = el.attr['href']
|
||||
if url.start_with?('#')
|
||||
"\\hyperlink{#{url[1..-1].gsub('%', '\\%')}}{#{inner(el, opts)}}"
|
||||
else
|
||||
"\\href{#{url.gsub('%', '\\%')}}{#{inner(el, opts)}}"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_img(el, _opts)
|
||||
line = el.options[:location]
|
||||
if el.attr['src'].match?(/^(https?|ftps?):\/\//)
|
||||
warning("Cannot include non-local image#{line ? " (line #{line})" : ''}")
|
||||
''
|
||||
elsif !el.attr['src'].empty?
|
||||
@data[:packages] << 'graphicx'
|
||||
"#{latex_link_target(el)}\\includegraphics{#{el.attr['src']}}"
|
||||
else
|
||||
warning("Cannot include image with empty path#{line ? " (line #{line})" : ''}")
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
def convert_codespan(el, _opts)
|
||||
lang = extract_code_language(el.attr)
|
||||
if @options[:syntax_highlighter] == :minted &&
|
||||
(highlighted_code = highlight_code(el.value, lang, :span))
|
||||
@data[:packages] << 'minted'
|
||||
"#{latex_link_target(el)}#{highlighted_code}"
|
||||
else
|
||||
"\\texttt{#{latex_link_target(el)}#{escape(el.value)}}"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_footnote(el, opts)
|
||||
@data[:packages] << 'fancyvrb'
|
||||
"\\footnote{#{inner(el.value, opts).rstrip}}"
|
||||
end
|
||||
|
||||
def convert_raw(el, _opts)
|
||||
if !el.options[:type] || el.options[:type].empty? || el.options[:type].include?('latex')
|
||||
el.value + (el.options[:category] == :block ? "\n" : '')
|
||||
else
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
def convert_em(el, opts)
|
||||
"\\emph{#{latex_link_target(el)}#{inner(el, opts)}}"
|
||||
end
|
||||
|
||||
def convert_strong(el, opts)
|
||||
"\\textbf{#{latex_link_target(el)}#{inner(el, opts)}}"
|
||||
end
|
||||
|
||||
# Inspired by Maruku: entity conversion table based on the one from htmltolatex
|
||||
# (http://sourceforge.net/projects/htmltolatex/), with some small adjustments/additions
|
||||
ENTITY_CONV_TABLE = {
|
||||
913 => ['$A$'],
|
||||
914 => ['$B$'],
|
||||
915 => ['$\Gamma$'],
|
||||
916 => ['$\Delta$'],
|
||||
917 => ['$E$'],
|
||||
918 => ['$Z$'],
|
||||
919 => ['$H$'],
|
||||
920 => ['$\Theta$'],
|
||||
921 => ['$I$'],
|
||||
922 => ['$K$'],
|
||||
923 => ['$\Lambda$'],
|
||||
924 => ['$M$'],
|
||||
925 => ['$N$'],
|
||||
926 => ['$\Xi$'],
|
||||
927 => ['$O$'],
|
||||
928 => ['$\Pi$'],
|
||||
929 => ['$P$'],
|
||||
931 => ['$\Sigma$'],
|
||||
932 => ['$T$'],
|
||||
933 => ['$Y$'],
|
||||
934 => ['$\Phi$'],
|
||||
935 => ['$X$'],
|
||||
936 => ['$\Psi$'],
|
||||
937 => ['$\Omega$'],
|
||||
945 => ['$\alpha$'],
|
||||
946 => ['$\beta$'],
|
||||
947 => ['$\gamma$'],
|
||||
948 => ['$\delta$'],
|
||||
949 => ['$\epsilon$'],
|
||||
950 => ['$\zeta$'],
|
||||
951 => ['$\eta$'],
|
||||
952 => ['$\theta$'],
|
||||
953 => ['$\iota$'],
|
||||
954 => ['$\kappa$'],
|
||||
955 => ['$\lambda$'],
|
||||
956 => ['$\mu$'],
|
||||
957 => ['$\nu$'],
|
||||
958 => ['$\xi$'],
|
||||
959 => ['$o$'],
|
||||
960 => ['$\pi$'],
|
||||
961 => ['$\rho$'],
|
||||
963 => ['$\sigma$'],
|
||||
964 => ['$\tau$'],
|
||||
965 => ['$\upsilon$'],
|
||||
966 => ['$\phi$'],
|
||||
967 => ['$\chi$'],
|
||||
968 => ['$\psi$'],
|
||||
969 => ['$\omega$'],
|
||||
962 => ['$\varsigma$'],
|
||||
977 => ['$\vartheta$'],
|
||||
982 => ['$\varpi$'],
|
||||
8230 => ['\ldots'],
|
||||
8242 => ['$\prime$'],
|
||||
8254 => ['-'],
|
||||
8260 => ['/'],
|
||||
8472 => ['$\wp$'],
|
||||
8465 => ['$\Im$'],
|
||||
8476 => ['$\Re$'],
|
||||
8501 => ['$\aleph$'],
|
||||
8226 => ['$\bullet$'],
|
||||
8482 => ['$^{\rm TM}$'],
|
||||
8592 => ['$\leftarrow$'],
|
||||
8594 => ['$\rightarrow$'],
|
||||
8593 => ['$\uparrow$'],
|
||||
8595 => ['$\downarrow$'],
|
||||
8596 => ['$\leftrightarrow$'],
|
||||
8629 => ['$\hookleftarrow$'],
|
||||
8657 => ['$\Uparrow$'],
|
||||
8659 => ['$\Downarrow$'],
|
||||
8656 => ['$\Leftarrow$'],
|
||||
8658 => ['$\Rightarrow$'],
|
||||
8660 => ['$\Leftrightarrow$'],
|
||||
8704 => ['$\forall$'],
|
||||
8706 => ['$\partial$'],
|
||||
8707 => ['$\exists$'],
|
||||
8709 => ['$\emptyset$'],
|
||||
8711 => ['$\nabla$'],
|
||||
8712 => ['$\in$'],
|
||||
8715 => ['$\ni$'],
|
||||
8713 => ['$\notin$'],
|
||||
8721 => ['$\sum$'],
|
||||
8719 => ['$\prod$'],
|
||||
8722 => ['$-$'],
|
||||
8727 => ['$\ast$'],
|
||||
8730 => ['$\surd$'],
|
||||
8733 => ['$\propto$'],
|
||||
8734 => ['$\infty$'],
|
||||
8736 => ['$\angle$'],
|
||||
8743 => ['$\wedge$'],
|
||||
8744 => ['$\vee$'],
|
||||
8745 => ['$\cap$'],
|
||||
8746 => ['$\cup$'],
|
||||
8747 => ['$\int$'],
|
||||
8756 => ['$\therefore$', 'amssymb'],
|
||||
8764 => ['$\sim$'],
|
||||
8776 => ['$\approx$'],
|
||||
8773 => ['$\cong$'],
|
||||
8800 => ['$\neq$'],
|
||||
8801 => ['$\equiv$'],
|
||||
8804 => ['$\leq$'],
|
||||
8805 => ['$\geq$'],
|
||||
8834 => ['$\subset$'],
|
||||
8835 => ['$\supset$'],
|
||||
8838 => ['$\subseteq$'],
|
||||
8839 => ['$\supseteq$'],
|
||||
8836 => ['$\nsubset$', 'amssymb'],
|
||||
8853 => ['$\oplus$'],
|
||||
8855 => ['$\otimes$'],
|
||||
8869 => ['$\perp$'],
|
||||
8901 => ['$\cdot$'],
|
||||
8968 => ['$\rceil$'],
|
||||
8969 => ['$\lceil$'],
|
||||
8970 => ['$\lfloor$'],
|
||||
8971 => ['$\rfloor$'],
|
||||
9001 => ['$\rangle$'],
|
||||
9002 => ['$\langle$'],
|
||||
9674 => ['$\lozenge$', 'amssymb'],
|
||||
9824 => ['$\spadesuit$'],
|
||||
9827 => ['$\clubsuit$'],
|
||||
9829 => ['$\heartsuit$'],
|
||||
9830 => ['$\diamondsuit$'],
|
||||
38 => ['\&'],
|
||||
34 => ['"'],
|
||||
39 => ['\''],
|
||||
169 => ['\copyright'],
|
||||
60 => ['\textless'],
|
||||
62 => ['\textgreater'],
|
||||
338 => ['\OE'],
|
||||
339 => ['\oe'],
|
||||
352 => ['\v{S}'],
|
||||
353 => ['\v{s}'],
|
||||
376 => ['\"Y'],
|
||||
710 => ['\textasciicircum'],
|
||||
732 => ['\textasciitilde'],
|
||||
8211 => ['--'],
|
||||
8212 => ['---'],
|
||||
8216 => ['`'],
|
||||
8217 => ['\''],
|
||||
8220 => ['``'],
|
||||
8221 => ['\'\''],
|
||||
8224 => ['\dag'],
|
||||
8225 => ['\ddag'],
|
||||
8240 => ['\permil', 'wasysym'],
|
||||
8364 => ['\euro', 'eurosym'],
|
||||
8249 => ['\guilsinglleft'],
|
||||
8250 => ['\guilsinglright'],
|
||||
8218 => ['\quotesinglbase', 'mathcomp'],
|
||||
8222 => ['\quotedblbase', 'mathcomp'],
|
||||
402 => ['\textflorin', 'mathcomp'],
|
||||
381 => ['\v{Z}'],
|
||||
382 => ['\v{z}'],
|
||||
160 => ['~'],
|
||||
161 => ['\textexclamdown'],
|
||||
163 => ['\pounds'],
|
||||
164 => ['\currency', 'wasysym'],
|
||||
165 => ['\textyen', 'textcomp'],
|
||||
166 => ['\brokenvert', 'wasysym'],
|
||||
167 => ['\S'],
|
||||
171 => ['\guillemotleft'],
|
||||
187 => ['\guillemotright'],
|
||||
174 => ['\textregistered'],
|
||||
170 => ['\textordfeminine'],
|
||||
172 => ['$\neg$'],
|
||||
173 => ['\-'],
|
||||
176 => ['$\degree$', 'mathabx'],
|
||||
177 => ['$\pm$'],
|
||||
180 => ['\''],
|
||||
181 => ['$\mu$'],
|
||||
182 => ['\P'],
|
||||
183 => ['$\cdot$'],
|
||||
186 => ['\textordmasculine'],
|
||||
162 => ['\cent', 'wasysym'],
|
||||
185 => ['$^1$'],
|
||||
178 => ['$^2$'],
|
||||
179 => ['$^3$'],
|
||||
189 => ['$\frac{1}{2}$'],
|
||||
188 => ['$\frac{1}{4}$'],
|
||||
190 => ['$\frac{3}{4}'],
|
||||
192 => ['\`A'],
|
||||
193 => ['\\\'A'],
|
||||
194 => ['\^A'],
|
||||
195 => ['\~A'],
|
||||
196 => ['\"A'],
|
||||
197 => ['\AA'],
|
||||
198 => ['\AE'],
|
||||
199 => ['\cC'],
|
||||
200 => ['\`E'],
|
||||
201 => ['\\\'E'],
|
||||
202 => ['\^E'],
|
||||
203 => ['\"E'],
|
||||
204 => ['\`I'],
|
||||
205 => ['\\\'I'],
|
||||
206 => ['\^I'],
|
||||
207 => ['\"I'],
|
||||
208 => ['$\eth$', 'amssymb'],
|
||||
209 => ['\~N'],
|
||||
210 => ['\`O'],
|
||||
211 => ['\\\'O'],
|
||||
212 => ['\^O'],
|
||||
213 => ['\~O'],
|
||||
214 => ['\"O'],
|
||||
215 => ['$\times$'],
|
||||
216 => ['\O'],
|
||||
217 => ['\`U'],
|
||||
218 => ['\\\'U'],
|
||||
219 => ['\^U'],
|
||||
220 => ['\"U'],
|
||||
221 => ['\\\'Y'],
|
||||
222 => ['\Thorn', 'wasysym'],
|
||||
223 => ['\ss'],
|
||||
224 => ['\`a'],
|
||||
225 => ['\\\'a'],
|
||||
226 => ['\^a'],
|
||||
227 => ['\~a'],
|
||||
228 => ['\"a'],
|
||||
229 => ['\aa'],
|
||||
230 => ['\ae'],
|
||||
231 => ['\cc'],
|
||||
232 => ['\`e'],
|
||||
233 => ['\\\'e'],
|
||||
234 => ['\^e'],
|
||||
235 => ['\"e'],
|
||||
236 => ['\`i'],
|
||||
237 => ['\\\'i'],
|
||||
238 => ['\^i'],
|
||||
239 => ['\"i'],
|
||||
240 => ['$\eth$'],
|
||||
241 => ['\~n'],
|
||||
242 => ['\`o'],
|
||||
243 => ['\\\'o'],
|
||||
244 => ['\^o'],
|
||||
245 => ['\~o'],
|
||||
246 => ['\"o'],
|
||||
247 => ['$\divide$'],
|
||||
248 => ['\o'],
|
||||
249 => ['\`u'],
|
||||
250 => ['\\\'u'],
|
||||
251 => ['\^u'],
|
||||
252 => ['\"u'],
|
||||
253 => ['\\\'y'],
|
||||
254 => ['\thorn', 'wasysym'],
|
||||
255 => ['\"y'],
|
||||
8201 => ['\thinspace'],
|
||||
8194 => ['\hskip .5em\relax'],
|
||||
8195 => ['\quad'],
|
||||
} # :nodoc:
|
||||
ENTITY_CONV_TABLE.each_value {|v| v[0] = "#{v[0]}{}" }
|
||||
|
||||
def entity_to_latex(entity)
|
||||
text, package = ENTITY_CONV_TABLE[entity.code_point]
|
||||
if text
|
||||
@data[:packages] << package if package
|
||||
text
|
||||
else
|
||||
warning("Couldn't find entity with code #{entity.code_point} in substitution table!")
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
def convert_entity(el, _opts)
|
||||
entity_to_latex(el.value)
|
||||
end
|
||||
|
||||
TYPOGRAPHIC_SYMS = {
|
||||
mdash: '---', ndash: '--', hellip: '\ldots{}',
|
||||
laquo_space: '\guillemotleft{}~', raquo_space: '~\guillemotright{}',
|
||||
laquo: '\guillemotleft{}', raquo: '\guillemotright{}'
|
||||
} # :nodoc:
|
||||
def convert_typographic_sym(el, _opts)
|
||||
if (result = @options[:typographic_symbols][el.value])
|
||||
escape(result)
|
||||
else
|
||||
TYPOGRAPHIC_SYMS[el.value]
|
||||
end
|
||||
end
|
||||
|
||||
def convert_smart_quote(el, opts)
|
||||
res = entity_to_latex(smart_quote_entity(el)).chomp('{}')
|
||||
res << "{}" if ((nel = opts[:parent].children[opts[:index] + 1]) && nel.type == :smart_quote) || res =~ /\w$/
|
||||
res
|
||||
end
|
||||
|
||||
def convert_math(el, _opts)
|
||||
@data[:packages] += %w[amssymb amsmath amsthm amsfonts]
|
||||
if el.options[:category] == :block
|
||||
if el.value.match?(/\A\s*\\begin\{/)
|
||||
el.value
|
||||
else
|
||||
latex_environment('displaymath', el, el.value)
|
||||
end
|
||||
else
|
||||
"$#{el.value}$"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_abbreviation(el, _opts)
|
||||
@data[:packages] += %w[acronym]
|
||||
"\\ac{#{normalize_abbreviation_key(el.value)}}"
|
||||
end
|
||||
|
||||
# Normalize the abbreviation key so that it only contains allowed ASCII character
|
||||
def normalize_abbreviation_key(key)
|
||||
key.gsub(/\W/) {|m| m.unpack1('H*') }
|
||||
end
|
||||
|
||||
# Wrap the +text+ inside a LaTeX environment of type +type+. The element +el+ is passed on to
|
||||
# the method #attribute_list -- the resulting string is appended to both the \\begin and the
|
||||
# \\end lines of the LaTeX environment for easier post-processing of LaTeX environments.
|
||||
def latex_environment(type, el, text)
|
||||
attrs = attribute_list(el)
|
||||
"\\begin{#{type}}#{latex_link_target(el)}#{attrs}\n#{text.rstrip}\n\\end{#{type}}#{attrs}\n"
|
||||
end
|
||||
|
||||
# Return a string containing a valid \hypertarget command if the element has an ID defined, or
|
||||
# +nil+ otherwise. If the parameter +add_label+ is +true+, a \label command will also be used
|
||||
# additionally to the \hypertarget command.
|
||||
def latex_link_target(el, add_label = false)
|
||||
if (id = el.attr['id'])
|
||||
"\\hypertarget{#{id}}{}#{add_label ? "\\label{#{id}}" : ''}"
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Return a LaTeX comment containing all attributes as 'key="value"' pairs.
|
||||
def attribute_list(el)
|
||||
attrs = el.attr.map {|k, v| v.nil? ? '' : " #{k}=\"#{v}\"" }.compact.sort.join
|
||||
attrs = " % #{attrs}" unless attrs.empty?
|
||||
attrs
|
||||
end
|
||||
|
||||
ESCAPE_MAP = {
|
||||
"^" => "\\^{}",
|
||||
"\\" => "\\textbackslash{}",
|
||||
"~" => "\\ensuremath{\\sim}",
|
||||
"|" => "\\textbar{}",
|
||||
"<" => "\\textless{}",
|
||||
">" => "\\textgreater{}",
|
||||
"[" => "{[}",
|
||||
"]" => "{]}",
|
||||
}.merge(Hash[*"{}$%&_#".each_char.map {|c| [c, "\\#{c}"] }.flatten]) # :nodoc:
|
||||
ESCAPE_RE = Regexp.union(*ESCAPE_MAP.collect {|k, _v| k }) # :nodoc:
|
||||
|
||||
# Escape the special LaTeX characters in the string +str+.
|
||||
def escape(str)
|
||||
str.gsub(ESCAPE_RE) {|m| ESCAPE_MAP[m] }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,300 @@
|
||||
# -*- 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/converter'
|
||||
|
||||
module Kramdown
|
||||
|
||||
module Converter
|
||||
|
||||
# Converts a Kramdown::Document to a manpage in groff format. See man(7), groff_man(7) and
|
||||
# man-pages(7) for information regarding the output.
|
||||
class Man < Base
|
||||
|
||||
def convert(el, opts = {indent: 0, result: +''}) # :nodoc:
|
||||
send("convert_#{el.type}", el, opts)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def inner(el, opts, use = :all)
|
||||
arr = el.children.reject {|e| e.type == :blank }
|
||||
arr.each_with_index do |inner_el, index|
|
||||
next if use == :rest && index == 0
|
||||
break if use == :first && index > 0
|
||||
options = opts.dup
|
||||
options[:parent] = el
|
||||
options[:index] = index
|
||||
options[:prev] = (index == 0 ? nil : arr[index - 1])
|
||||
options[:next] = (index == arr.length - 1 ? nil : arr[index + 1])
|
||||
convert(inner_el, options)
|
||||
end
|
||||
end
|
||||
|
||||
def convert_root(el, opts)
|
||||
@title_done = false
|
||||
opts[:result] = +".\\\" generated by kramdown\n"
|
||||
inner(el, opts)
|
||||
opts[:result]
|
||||
end
|
||||
|
||||
def convert_blank(*)
|
||||
end
|
||||
alias convert_hr convert_blank
|
||||
alias convert_xml_pi convert_blank
|
||||
|
||||
def convert_p(el, opts)
|
||||
if (opts[:index] != 0 && opts[:prev].type != :header) ||
|
||||
(opts[:parent].type == :blockquote && opts[:index] == 0)
|
||||
opts[:result] << macro("P")
|
||||
end
|
||||
inner(el, opts)
|
||||
newline(opts[:result])
|
||||
end
|
||||
|
||||
def convert_header(el, opts)
|
||||
return unless opts[:parent].type == :root
|
||||
case el.options[:level]
|
||||
when 1
|
||||
unless @title_done
|
||||
@title_done = true
|
||||
data = el.options[:raw_text].scan(/([^(]+)\s*\((\d\w*)\)(?:\s*-+\s*(.*))?/).first ||
|
||||
el.options[:raw_text].scan(/([^\s]+)\s*(?:-*\s+)?()(.*)/).first
|
||||
return unless data && data[0]
|
||||
name = data[0]
|
||||
section = (data[1].to_s.empty? ? el.attr['data-section'] || '7' : data[1])
|
||||
description = (data[2].to_s.empty? ? nil : " - #{data[2]}")
|
||||
date = el.attr['data-date'] ? quote(el.attr['data-date']) : nil
|
||||
extra = (el.attr['data-extra'] ? quote(escape(el.attr['data-extra'].to_s)) : nil)
|
||||
opts[:result] << macro("TH", quote(escape(name.upcase)), quote(section), date, extra)
|
||||
if description
|
||||
opts[:result] << macro("SH", "NAME") << escape("#{name}#{description}") << "\n"
|
||||
end
|
||||
end
|
||||
when 2
|
||||
opts[:result] << macro("SH", quote(escape(el.options[:raw_text])))
|
||||
when 3
|
||||
opts[:result] << macro("SS", quote(escape(el.options[:raw_text])))
|
||||
else
|
||||
warning("Header levels greater than three are not supported")
|
||||
end
|
||||
end
|
||||
|
||||
def convert_codeblock(el, opts)
|
||||
opts[:result] << macro("sp") << macro("RS", 4) << macro("EX")
|
||||
opts[:result] << newline(escape(el.value, true))
|
||||
opts[:result] << macro("EE") << macro("RE")
|
||||
end
|
||||
|
||||
def convert_blockquote(el, opts)
|
||||
opts[:result] << macro("RS")
|
||||
inner(el, opts)
|
||||
opts[:result] << macro("RE")
|
||||
end
|
||||
|
||||
def convert_ul(el, opts)
|
||||
compact = (el.attr['class'] =~ /\bcompact\b/)
|
||||
opts[:result] << macro("sp") << macro("PD", 0) if compact
|
||||
inner(el, opts)
|
||||
opts[:result] << macro("PD") if compact
|
||||
end
|
||||
alias convert_dl convert_ul
|
||||
alias convert_ol convert_ul
|
||||
|
||||
def convert_li(el, opts)
|
||||
sym = (opts[:parent].type == :ul ? '\(bu' : "#{opts[:index] + 1}.")
|
||||
opts[:result] << macro("IP", sym, 4)
|
||||
inner(el, opts, :first)
|
||||
if el.children.size > 1
|
||||
opts[:result] << macro("RS")
|
||||
inner(el, opts, :rest)
|
||||
opts[:result] << macro("RE")
|
||||
end
|
||||
end
|
||||
|
||||
def convert_dt(el, opts)
|
||||
opts[:result] << macro(opts[:prev] && opts[:prev].type == :dt ? "TQ" : "TP")
|
||||
inner(el, opts)
|
||||
opts[:result] << "\n"
|
||||
end
|
||||
|
||||
def convert_dd(el, opts)
|
||||
inner(el, opts, :first)
|
||||
if el.children.size > 1
|
||||
opts[:result] << macro("RS")
|
||||
inner(el, opts, :rest)
|
||||
opts[:result] << macro("RE")
|
||||
end
|
||||
opts[:result] << macro("sp") if opts[:next] && opts[:next].type == :dd
|
||||
end
|
||||
|
||||
TABLE_CELL_ALIGNMENT = {left: 'l', center: 'c', right: 'r', default: 'l'}
|
||||
|
||||
def convert_table(el, opts)
|
||||
opts[:alignment] = el.options[:alignment].map {|a| TABLE_CELL_ALIGNMENT[a] }
|
||||
table_options = ["box"]
|
||||
table_options << "center" if el.attr['class']&.match?(/\bcenter\b/)
|
||||
opts[:result] << macro("TS") << "#{table_options.join(' ')} ;\n"
|
||||
inner(el, opts)
|
||||
opts[:result] << macro("TE") << macro("sp")
|
||||
end
|
||||
|
||||
def convert_thead(el, opts)
|
||||
opts[:result] << opts[:alignment].map {|a| "#{a}b" }.join(' ') << " .\n"
|
||||
inner(el, opts)
|
||||
opts[:result] << "=\n"
|
||||
end
|
||||
|
||||
def convert_tbody(el, opts)
|
||||
opts[:result] << ".T&\n" if opts[:index] != 0
|
||||
opts[:result] << opts[:alignment].join(' ') << " .\n"
|
||||
inner(el, opts)
|
||||
opts[:result] << (opts[:next].type == :tfoot ? "=\n" : "_\n") if opts[:next]
|
||||
end
|
||||
|
||||
def convert_tfoot(el, opts)
|
||||
inner(el, opts)
|
||||
end
|
||||
|
||||
def convert_tr(el, opts)
|
||||
inner(el, opts)
|
||||
opts[:result] << "\n"
|
||||
end
|
||||
|
||||
def convert_td(el, opts)
|
||||
result = opts[:result]
|
||||
opts[:result] = +''
|
||||
inner(el, opts)
|
||||
if opts[:result].include?("\n")
|
||||
warning("Table cells using links are not supported")
|
||||
result << "\t"
|
||||
else
|
||||
result << opts[:result] << "\t"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_html_element(*)
|
||||
warning("HTML elements are not supported")
|
||||
end
|
||||
|
||||
def convert_xml_comment(el, opts)
|
||||
newline(opts[:result]) << ".\"#{escape(el.value, true).rstrip.gsub(/\n/, "\n.\"")}\n"
|
||||
end
|
||||
alias convert_comment convert_xml_comment
|
||||
|
||||
def convert_a(el, opts)
|
||||
if el.children.size == 1 && el.children[0].type == :text &&
|
||||
el.attr['href'] == el.children[0].value
|
||||
newline(opts[:result]) << macro("UR", escape(el.attr['href'])) << macro("UE")
|
||||
elsif el.attr['href'].start_with?('mailto:')
|
||||
newline(opts[:result]) << macro("MT", escape(el.attr['href'].sub(/^mailto:/, ''))) <<
|
||||
macro("UE")
|
||||
else
|
||||
newline(opts[:result]) << macro("UR", escape(el.attr['href']))
|
||||
inner(el, opts)
|
||||
newline(opts[:result]) << macro("UE")
|
||||
end
|
||||
end
|
||||
|
||||
def convert_img(_el, _opts)
|
||||
warning("Images are not supported")
|
||||
end
|
||||
|
||||
def convert_em(el, opts)
|
||||
opts[:result] << '\fI'
|
||||
inner(el, opts)
|
||||
opts[:result] << '\fP'
|
||||
end
|
||||
|
||||
def convert_strong(el, opts)
|
||||
opts[:result] << '\fB'
|
||||
inner(el, opts)
|
||||
opts[:result] << '\fP'
|
||||
end
|
||||
|
||||
def convert_codespan(el, opts)
|
||||
opts[:result] << "\\fB#{escape(el.value)}\\fP"
|
||||
end
|
||||
|
||||
def convert_br(_el, opts)
|
||||
newline(opts[:result]) << macro("br")
|
||||
end
|
||||
|
||||
def convert_abbreviation(el, opts)
|
||||
opts[:result] << escape(el.value)
|
||||
end
|
||||
|
||||
def convert_math(el, opts)
|
||||
if el.options[:category] == :block
|
||||
convert_codeblock(el, opts)
|
||||
else
|
||||
convert_codespan(el, opts)
|
||||
end
|
||||
end
|
||||
|
||||
def convert_footnote(*)
|
||||
warning("Footnotes are not supported")
|
||||
end
|
||||
|
||||
def convert_raw(*)
|
||||
warning("Raw content is not supported")
|
||||
end
|
||||
|
||||
def convert_text(el, opts)
|
||||
text = escape(el.value)
|
||||
text.lstrip! if opts[:result][-1] == "\n"
|
||||
opts[:result] << text
|
||||
end
|
||||
|
||||
def convert_entity(el, opts)
|
||||
opts[:result] << unicode_char(el.value.code_point)
|
||||
end
|
||||
|
||||
def convert_smart_quote(el, opts)
|
||||
opts[:result] << unicode_char(::Kramdown::Utils::Entities.entity(el.value.to_s).code_point)
|
||||
end
|
||||
|
||||
TYPOGRAPHIC_SYMS_MAP = {
|
||||
mdash: '\(em', ndash: '\(em', hellip: '\.\.\.',
|
||||
laquo_space: '\[Fo]', raquo_space: '\[Fc]', laquo: '\[Fo]', raquo: '\[Fc]'
|
||||
}
|
||||
|
||||
def convert_typographic_sym(el, opts)
|
||||
opts[:result] << TYPOGRAPHIC_SYMS_MAP[el.value]
|
||||
end
|
||||
|
||||
def macro(name, *args)
|
||||
".#{[name, *args].compact.join(' ')}\n"
|
||||
end
|
||||
|
||||
def newline(text)
|
||||
text << "\n" unless text[-1] == "\n"
|
||||
text
|
||||
end
|
||||
|
||||
def quote(text)
|
||||
"\"#{text.gsub(/"/, '\\"')}\""
|
||||
end
|
||||
|
||||
def escape(text, preserve_whitespace = false)
|
||||
text = (preserve_whitespace ? text.dup : text.gsub(/\s+/, ' '))
|
||||
text.gsub!('\\', "\\e")
|
||||
text.gsub!(/^\./, '\\\\&.')
|
||||
text.gsub!(/[.'-]/) {|m| "\\#{m}" }
|
||||
text
|
||||
end
|
||||
|
||||
def unicode_char(codepoint)
|
||||
"\\[u#{codepoint.to_s(16).rjust(4, '0')}]"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- 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::Converter::MathEngine
|
||||
|
||||
# Uses the MathJax javascript library for displaying math.
|
||||
#
|
||||
# Note that the javascript library itself is not include or linked, this has to be done
|
||||
# separately. Only the math content is marked up correctly.
|
||||
module Mathjax
|
||||
|
||||
def self.call(converter, el, opts)
|
||||
value = converter.escape_html(el.value)
|
||||
result = el.options[:category] == :block ? "\\[#{value}\\]\n" : "\\(#{value}\\)"
|
||||
if el.attr.empty?
|
||||
result
|
||||
elsif el.options[:category] == :block
|
||||
converter.format_as_block_html('div', el.attr, result, opts[:indent])
|
||||
else
|
||||
converter.format_as_span_html('span', el.attr, "$#{el.value}$")
|
||||
end
|
||||
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.
|
||||
#++
|
||||
#
|
||||
|
||||
require 'kramdown/converter'
|
||||
|
||||
module Kramdown
|
||||
|
||||
module Converter
|
||||
|
||||
# Removes all block (and optionally span) level HTML tags from the element tree.
|
||||
#
|
||||
# This converter can be used on parsed HTML documents to get an element tree that will only
|
||||
# contain native kramdown elements.
|
||||
#
|
||||
# *Note* that the returned element tree may not be fully conformant (i.e. the content models of
|
||||
# *some elements may be violated)!
|
||||
#
|
||||
# This converter modifies the given tree in-place and returns it.
|
||||
class RemoveHtmlTags < Base
|
||||
|
||||
def initialize(root, options)
|
||||
super
|
||||
@options[:template] = ''
|
||||
end
|
||||
|
||||
def convert(el)
|
||||
real_el, el = el, el.value if el.type == :footnote
|
||||
|
||||
children = el.children.dup
|
||||
index = 0
|
||||
while index < children.length
|
||||
if children[index].type == :xml_pi ||
|
||||
(children[index].type == :html_element && (children[index].value == 'style' ||
|
||||
children[index].value == 'script'))
|
||||
children[index..index] = []
|
||||
elsif children[index].type == :html_element &&
|
||||
((@options[:remove_block_html_tags] && children[index].options[:category] == :block) ||
|
||||
(@options[:remove_span_html_tags] && children[index].options[:category] == :span))
|
||||
children[index..index] = children[index].children
|
||||
else
|
||||
convert(children[index])
|
||||
index += 1
|
||||
end
|
||||
end
|
||||
el.children = children
|
||||
real_el || el
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- 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 Converter
|
||||
|
||||
# == Container for Syntax Highlighters
|
||||
#
|
||||
# This module serves as container for the syntax highlighters that can be used together with
|
||||
# kramdown.
|
||||
#
|
||||
# A syntax highlighter should not store any data itself but should use the provided converter
|
||||
# object to do so (See Kramdown::Converter::Base#data).
|
||||
#
|
||||
# == Implementing a Syntax Highlighter
|
||||
#
|
||||
# Implementing a new syntax highlighter is easy because it is just an object that needs to
|
||||
# respond to #call.
|
||||
#
|
||||
# The method #call needs to take the following arguments:
|
||||
#
|
||||
# converter:: This argument contains the converter object that calls the syntax highlighter. It
|
||||
# can be used, for example, to store data in Kramdown::Converter::Base#data for one
|
||||
# conversion run.
|
||||
#
|
||||
# text:: The raw text that should be highlighted.
|
||||
#
|
||||
# lang:: The language that the text should be highlighted for (e.g. ruby, python, ...).
|
||||
#
|
||||
# type:: The type of text, either :span for span-level code or :block for a codeblock.
|
||||
#
|
||||
# opts:: A Hash with options that may be passed from the converter.
|
||||
#
|
||||
# The return value of the method should be the highlighted text, suitable for the given
|
||||
# converter (e.g. HTML for the HTML converter).
|
||||
#
|
||||
# == Special Implementation Details
|
||||
#
|
||||
# HTML converter:: If the syntax highlighter is used with an HTML converter, it should return
|
||||
# :block type text correctly wrapped (i.e. normally inside a pre-tag, but may
|
||||
# also be a table-tag or just a div-tag) but :span type text *without* a
|
||||
# code-tag!
|
||||
#
|
||||
# Also, a syntax highlighter should store the default highlighting language for
|
||||
# the invocation in the +opts+ hash under the key :default_lang.
|
||||
module SyntaxHighlighter
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- 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::Converter::SyntaxHighlighter
|
||||
|
||||
# Uses Minted to highlight code blocks and code spans.
|
||||
module Minted
|
||||
|
||||
def self.call(converter, text, lang, type, _opts)
|
||||
opts = converter.options[:syntax_highlighter_opts]
|
||||
|
||||
# Fallback to default language
|
||||
lang ||= opts[:default_lang]
|
||||
|
||||
options = []
|
||||
options << "breaklines" if opts[:wrap]
|
||||
options << "linenos" if opts[:line_numbers]
|
||||
options << "frame=#{opts[:frame]}" if opts[:frame]
|
||||
|
||||
if lang && type == :block
|
||||
"\\begin{minted}[#{options.join(',')}]{#{lang}}\n#{text}\n\\end{minted}"
|
||||
elsif lang && type == :span
|
||||
"\\mintinline{#{lang}}{#{text}}"
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,85 @@
|
||||
# -*- 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::Converter::SyntaxHighlighter
|
||||
|
||||
# Uses Rouge which is CSS-compatible to Pygments to highlight code blocks and code spans.
|
||||
module Rouge
|
||||
|
||||
begin
|
||||
require 'rouge'
|
||||
|
||||
# Highlighting via Rouge is available if this constant is +true+.
|
||||
AVAILABLE = true
|
||||
rescue LoadError, SyntaxError
|
||||
AVAILABLE = false # :nodoc:
|
||||
end
|
||||
|
||||
def self.call(converter, text, lang, type, call_opts)
|
||||
opts = options(converter, type)
|
||||
call_opts[:default_lang] = opts[:default_lang]
|
||||
return nil unless lang || opts[:default_lang] || opts[:guess_lang]
|
||||
|
||||
lexer = ::Rouge::Lexer.find_fancy(lang || opts[:default_lang], text)
|
||||
return nil if opts[:disable] || !lexer || (lexer.tag == "plaintext" && !opts[:guess_lang])
|
||||
|
||||
opts[:css_class] ||= 'highlight' # For backward compatibility when using Rouge 2.0
|
||||
formatter = formatter_class(opts).new(opts)
|
||||
formatter.format(lexer.lex(text))
|
||||
end
|
||||
|
||||
def self.options(converter, type)
|
||||
prepare_options(converter)
|
||||
converter.data[:syntax_highlighter_rouge][type]
|
||||
end
|
||||
|
||||
def self.prepare_options(converter)
|
||||
return if converter.data.key?(:syntax_highlighter_rouge)
|
||||
|
||||
cache = converter.data[:syntax_highlighter_rouge] = {}
|
||||
|
||||
opts = converter.options[:syntax_highlighter_opts].dup
|
||||
|
||||
span_opts = opts.delete(:span)&.dup || {}
|
||||
block_opts = opts.delete(:block)&.dup || {}
|
||||
normalize_keys(span_opts)
|
||||
normalize_keys(block_opts)
|
||||
|
||||
cache[:span] = opts.merge(span_opts)
|
||||
cache[:span][:wrap] = false
|
||||
|
||||
cache[:block] = opts.merge(block_opts)
|
||||
end
|
||||
|
||||
def self.normalize_keys(hash)
|
||||
return if hash.empty?
|
||||
|
||||
hash.keys.each do |k|
|
||||
hash[k.kind_of?(String) ? Kramdown::Options.str_to_sym(k) : k] = hash.delete(k)
|
||||
end
|
||||
end
|
||||
|
||||
def self.formatter_class(opts = {})
|
||||
case formatter = opts[:formatter]
|
||||
when Class
|
||||
formatter
|
||||
when /\A[[:upper:]][[:alnum:]_]*\z/
|
||||
::Rouge::Formatters.const_get(formatter, false)
|
||||
else
|
||||
# Available in Rouge 2.0 or later
|
||||
::Rouge::Formatters::HTMLLegacy
|
||||
end
|
||||
rescue NameError
|
||||
# Fallback to Rouge 1.x
|
||||
::Rouge::Formatters::HTML
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- 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/converter'
|
||||
|
||||
module Kramdown
|
||||
|
||||
module Converter
|
||||
|
||||
# Converts a Kramdown::Document to an element tree that represents the table of contents.
|
||||
#
|
||||
# The returned tree consists of Element objects of type :toc where the root element is just used
|
||||
# as container object. Each :toc element contains as value the wrapped :header element and under
|
||||
# the attribute key :id the header ID that should be used (note that this ID may not exist in
|
||||
# the wrapped element).
|
||||
#
|
||||
# Since the TOC tree consists of special :toc elements, one cannot directly feed this tree to
|
||||
# other converters!
|
||||
class Toc < Base
|
||||
|
||||
def initialize(root, options)
|
||||
super
|
||||
@toc = Element.new(:toc)
|
||||
@stack = []
|
||||
@options[:template] = ''
|
||||
end
|
||||
|
||||
def convert(el)
|
||||
if el.type == :header && in_toc?(el)
|
||||
attr = el.attr.dup
|
||||
attr['id'] = generate_id(el.options[:raw_text]) if @options[:auto_ids] && !attr['id']
|
||||
add_to_toc(el, attr['id']) if attr['id']
|
||||
else
|
||||
el.children.each {|child| convert(child) }
|
||||
end
|
||||
@toc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_to_toc(el, id)
|
||||
toc_element = Element.new(:toc, el, id: id)
|
||||
|
||||
success = false
|
||||
until success
|
||||
if @stack.empty?
|
||||
@toc.children << toc_element
|
||||
@stack << toc_element
|
||||
success = true
|
||||
elsif @stack.last.value.options[:level] < el.options[:level]
|
||||
@stack.last.children << toc_element
|
||||
@stack << toc_element
|
||||
success = true
|
||||
else
|
||||
@stack.pop
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,139 @@
|
||||
# -*- 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.
|
||||
#++
|
||||
#
|
||||
# = kramdown
|
||||
#
|
||||
# kramdown is fast, pure Ruby Markdown superset converter, using a strict syntax definition and
|
||||
# supporting several common extensions.
|
||||
#
|
||||
# The kramdown library is mainly written to support the kramdown-to-HTML conversion chain. However,
|
||||
# due to its flexibility it supports other input and output formats as well. Here is a list of the
|
||||
# supported formats:
|
||||
#
|
||||
# * input formats: kramdown (a Markdown superset), Markdown, GFM, HTML
|
||||
# * output formats: HTML, kramdown, LaTeX (and therefore PDF), PDF via Prawn
|
||||
#
|
||||
# All the documentation on the available input and output formats is available at
|
||||
# http://kramdown.gettalong.org.
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# kramdown has a simple API, so using kramdown is as easy as
|
||||
#
|
||||
# require 'kramdown'
|
||||
#
|
||||
# Kramdown::Document.new(text).to_html
|
||||
#
|
||||
# For detailed information have a look at the *\Kramdown::Document* class.
|
||||
#
|
||||
# == License
|
||||
#
|
||||
# MIT - see the COPYING file.
|
||||
|
||||
require 'kramdown/version'
|
||||
require 'kramdown/element'
|
||||
require 'kramdown/error'
|
||||
require 'kramdown/parser'
|
||||
require 'kramdown/converter'
|
||||
require 'kramdown/options'
|
||||
require 'kramdown/utils'
|
||||
|
||||
module Kramdown
|
||||
|
||||
# Return the data directory for kramdown.
|
||||
def self.data_dir
|
||||
unless defined?(@data_dir)
|
||||
require 'rbconfig'
|
||||
@data_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'data', 'kramdown'))
|
||||
@data_dir = File.expand_path(File.join(RbConfig::CONFIG["datadir"], "kramdown")) unless File.exist?(@data_dir)
|
||||
raise "kramdown data directory not found! This is a bug, please report it!" unless File.directory?(@data_dir)
|
||||
end
|
||||
@data_dir
|
||||
end
|
||||
|
||||
# The main interface to kramdown.
|
||||
#
|
||||
# This class provides a one-stop-shop for using kramdown to convert text into various output
|
||||
# formats. Use it like this:
|
||||
#
|
||||
# require 'kramdown'
|
||||
# doc = Kramdown::Document.new('This *is* some kramdown text')
|
||||
# puts doc.to_html
|
||||
#
|
||||
# The #to_html method is a shortcut for using the Converter::Html class. See #method_missing for
|
||||
# more information.
|
||||
#
|
||||
# The second argument to the ::new method is an options hash for customizing the behaviour of the
|
||||
# used parser and the converter. See ::new for more information!
|
||||
class Document
|
||||
|
||||
# The root Element of the element tree. It is immediately available after the ::new method has
|
||||
# been called.
|
||||
attr_accessor :root
|
||||
|
||||
# The options hash which holds the options for parsing/converting the Kramdown document.
|
||||
attr_reader :options
|
||||
|
||||
# An array of warning messages. It is filled with warnings during the parsing phase (i.e. in
|
||||
# ::new) and the conversion phase.
|
||||
attr_reader :warnings
|
||||
|
||||
# Create a new Kramdown document from the string +source+ and use the provided +options+. The
|
||||
# options that can be used are defined in the Options module.
|
||||
#
|
||||
# The special options key :input can be used to select the parser that should parse the
|
||||
# +source+. It has to be the name of a class in the Kramdown::Parser module. For example, to
|
||||
# select the kramdown parser, one would set the :input key to +Kramdown+. If this key is not
|
||||
# set, it defaults to +Kramdown+.
|
||||
#
|
||||
# The +source+ is immediately parsed by the selected parser so that the root element is
|
||||
# immediately available and the output can be generated.
|
||||
def initialize(source, options = {})
|
||||
@options = Options.merge(options).freeze
|
||||
parser = (@options[:input] || 'kramdown').to_s
|
||||
parser = parser[0..0].upcase + parser[1..-1]
|
||||
try_require('parser', parser)
|
||||
if Parser.const_defined?(parser)
|
||||
@root, @warnings = Parser.const_get(parser).parse(source, @options)
|
||||
else
|
||||
raise Kramdown::Error, "kramdown has no parser to handle the specified " \
|
||||
"input format: #{@options[:input]}"
|
||||
end
|
||||
end
|
||||
|
||||
# Check if a method is invoked that begins with +to_+ and if so, try to instantiate a converter
|
||||
# class (i.e. a class in the Kramdown::Converter module) and use it for converting the document.
|
||||
#
|
||||
# For example, +to_html+ would instantiate the Kramdown::Converter::Html class.
|
||||
def method_missing(id, *attr, &block)
|
||||
if id.to_s =~ /^to_(\w+)$/ && (name = Utils.camelize($1)) &&
|
||||
try_require('converter', name) && Converter.const_defined?(name)
|
||||
output, warnings = Converter.const_get(name).convert(@root, @options)
|
||||
@warnings.concat(warnings)
|
||||
output
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def inspect # :nodoc:
|
||||
"<KD:Document: options=#{@options.inspect} root=#{@root.inspect} warnings=#{@warnings.inspect}>"
|
||||
end
|
||||
|
||||
# Try requiring a parser or converter class and don't raise an error if the file is not found.
|
||||
def try_require(type, name)
|
||||
require("kramdown/#{type}/#{Utils.snake_case(name)}")
|
||||
true
|
||||
rescue LoadError
|
||||
true
|
||||
end
|
||||
protected :try_require
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,551 @@
|
||||
# -*- 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
|
||||
|
||||
# Represents all elements in the element tree.
|
||||
#
|
||||
# kramdown only uses this one class for representing all available elements in an element tree
|
||||
# (paragraphs, headers, emphasis, ...). The type of element can be set via the #type accessor.
|
||||
#
|
||||
# The root of a kramdown element tree has to be an element of type :root. It needs to have certain
|
||||
# option keys set so that conversions work correctly. If only a part of a tree should be
|
||||
# converted, duplicate the root node and assign the #children appropriately, e.g:
|
||||
#
|
||||
# root = doc.root
|
||||
# new_root = root.dup
|
||||
# new_root.children = [root.children[0]] # assign new array with elements to convert
|
||||
#
|
||||
# Following is a description of all supported element types.
|
||||
#
|
||||
# Note that the option :location may contain the start line number of an element in the source
|
||||
# document.
|
||||
#
|
||||
# == Structural Elements
|
||||
#
|
||||
# === :root
|
||||
#
|
||||
# [Category] None
|
||||
# [Usage context] As the root element of a document
|
||||
# [Content model] Block-level elements
|
||||
#
|
||||
# Represents the root of a kramdown document.
|
||||
#
|
||||
# The root element contains the following option keys:
|
||||
#
|
||||
# :encoding:: When running on Ruby 1.9 this key has to be set to the encoding used for the text
|
||||
# parts of the kramdown document.
|
||||
#
|
||||
# :abbrev_defs:: This key may be used to store the mapping of abbreviation to abbreviation
|
||||
# definition.
|
||||
#
|
||||
# :abbrev_attr:: This key may be used to store the mapping of abbreviation to abbreviation
|
||||
# attributes.
|
||||
#
|
||||
# :options:: This key may be used to store options that were set during parsing of the document.
|
||||
#
|
||||
# :footnote_count:: This key stores the number of actually referenced footnotes of the document.
|
||||
#
|
||||
# === :blank
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Where block-level elements are expected
|
||||
# [Content model] Empty
|
||||
#
|
||||
# Represents one or more blank lines. It is not allowed to have two or more consecutive blank
|
||||
# elements.
|
||||
#
|
||||
# The +value+ field may contain the original content of the blank lines.
|
||||
#
|
||||
#
|
||||
# === :p
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Where block-level elements are expected
|
||||
# [Content model] Span-level elements
|
||||
#
|
||||
# Represents a paragraph.
|
||||
#
|
||||
# If the option :transparent is +true+, this element just represents a block of text. I.e. this
|
||||
# element just functions as a container for span-level elements.
|
||||
#
|
||||
#
|
||||
# === :header
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Where block-level elements are expected
|
||||
# [Content model] Span-level elements
|
||||
#
|
||||
# Represents a header.
|
||||
#
|
||||
# The option :level specifies the header level and has to contain a number between 1 and \6. The
|
||||
# option :raw_text has to contain the raw header text.
|
||||
#
|
||||
#
|
||||
# === :blockquote
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Where block-level elements are expected
|
||||
# [Content model] Block-level elements
|
||||
#
|
||||
# Represents a blockquote.
|
||||
#
|
||||
#
|
||||
# === :codeblock
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Where block-level elements are expected
|
||||
# [Content model] Empty
|
||||
#
|
||||
# Represents a code block, i.e. a block of text that should be used as-is.
|
||||
#
|
||||
# The +value+ field has to contain the content of the code block.
|
||||
#
|
||||
# The option :lang specifies a highlighting language with possible HTML style options (e.g.
|
||||
# php?start_inline=1) and should be used instead of a possibly also available language embedded in
|
||||
# a class name of the form 'language-LANG'.
|
||||
#
|
||||
#
|
||||
# === :ul
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Where block-level elements are expected
|
||||
# [Content model] One or more :li elements
|
||||
#
|
||||
# Represents an unordered list.
|
||||
#
|
||||
#
|
||||
# === :ol
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Where block-level elements are expected
|
||||
# [Content model] One or more :li elements
|
||||
#
|
||||
# Represents an ordered list.
|
||||
#
|
||||
#
|
||||
# === :li
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Inside :ol and :ul elements
|
||||
# [Content model] Block-level elements
|
||||
#
|
||||
# Represents a list item of an ordered or unordered list.
|
||||
#
|
||||
# Note that the first child of a list item must not be a :blank element!
|
||||
#
|
||||
#
|
||||
# === :dl
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Where block-level elements are expected
|
||||
# [Content model] One or more groups each consisting of one or more :dt elements followed by one
|
||||
# or more :dd elements.
|
||||
#
|
||||
# Represents a definition list which contains groups consisting of terms and definitions for them.
|
||||
#
|
||||
#
|
||||
# === :dt
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Before :dt or :dd elements inside a :dl elment
|
||||
# [Content model] Span-level elements
|
||||
#
|
||||
# Represents the term part of a term-definition group in a definition list.
|
||||
#
|
||||
#
|
||||
# === :dd
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] After :dt or :dd elements inside a :dl elment
|
||||
# [Content model] Block-level elements
|
||||
#
|
||||
# Represents the definition part of a term-definition group in a definition list.
|
||||
#
|
||||
#
|
||||
# === :hr
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Where block-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents a horizontal line.
|
||||
#
|
||||
#
|
||||
# === :table
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Where block-level elements are expected
|
||||
# [Content model] Zero or one :thead elements, one or more :tbody elements, zero or one :tfoot
|
||||
# elements
|
||||
#
|
||||
# Represents a table. Each table row (i.e. :tr element) of the table has to contain the same
|
||||
# number of :td elements.
|
||||
#
|
||||
# The option :alignment has to be an array containing the alignment values, exactly one for each
|
||||
# column of the table. The possible alignment values are :left, :center, :right and :default.
|
||||
#
|
||||
#
|
||||
# === :thead
|
||||
#
|
||||
# [Category] None
|
||||
# [Usage context] As first element inside a :table element
|
||||
# [Content model] One or more :tr elements
|
||||
#
|
||||
# Represents the table header.
|
||||
#
|
||||
#
|
||||
# === :tbody
|
||||
#
|
||||
# [Category] None
|
||||
# [Usage context] After a :thead element but before a :tfoot element inside a :table element
|
||||
# [Content model] One or more :tr elements
|
||||
#
|
||||
# Represents a table body.
|
||||
#
|
||||
#
|
||||
# === :tfoot
|
||||
#
|
||||
# [Category] None
|
||||
# [Usage context] As last element inside a :table element
|
||||
# [Content model] One or more :tr elements
|
||||
#
|
||||
# Represents the table footer.
|
||||
#
|
||||
#
|
||||
# === :tr
|
||||
#
|
||||
# [Category] None
|
||||
# [Usage context] Inside :thead, :tbody and :tfoot elements
|
||||
# [Content model] One or more :td elements
|
||||
#
|
||||
# Represents a table row.
|
||||
#
|
||||
#
|
||||
# === :td
|
||||
#
|
||||
# [Category] Block-level element
|
||||
# [Usage context] Inside :tr elements
|
||||
# [Content model] As child of :thead/:tr span-level elements, as child of :tbody/:tr and
|
||||
# :tfoot/:tr block-level elements
|
||||
#
|
||||
# Represents a table cell.
|
||||
#
|
||||
#
|
||||
# === :math
|
||||
#
|
||||
# [Category] Block/span-level element
|
||||
# [Usage context] Where block/span-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents mathematical text that is written in LaTeX.
|
||||
#
|
||||
# The +value+ field has to contain the actual mathematical text.
|
||||
#
|
||||
# The option :category has to be set to either :span or :block depending on the context where the
|
||||
# element is used.
|
||||
#
|
||||
#
|
||||
# == Text Markup Elements
|
||||
#
|
||||
# === :text
|
||||
#
|
||||
# [Category] Span-level element
|
||||
# [Usage context] Where span-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents text.
|
||||
#
|
||||
# The +value+ field has to contain the text itself.
|
||||
#
|
||||
#
|
||||
# === :br
|
||||
#
|
||||
# [Category] Span-level element
|
||||
# [Usage context] Where span-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents a hard line break.
|
||||
#
|
||||
#
|
||||
# === :a
|
||||
#
|
||||
# [Category] Span-level element
|
||||
# [Usage context] Where span-level elements are expected
|
||||
# [Content model] Span-level elements
|
||||
#
|
||||
# Represents a link to an URL.
|
||||
#
|
||||
# The attribute +href+ has to be set to the URL to which the link points. The attribute +title+
|
||||
# optionally contains the title of the link.
|
||||
#
|
||||
#
|
||||
# === :img
|
||||
#
|
||||
# [Category] Span-level element
|
||||
# [Usage context] Where span-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents an image.
|
||||
#
|
||||
# The attribute +src+ has to be set to the URL of the image. The attribute +alt+ has to contain a
|
||||
# text description of the image. The attribute +title+ optionally contains the title of the image.
|
||||
#
|
||||
#
|
||||
# === :codespan
|
||||
#
|
||||
# [Category] Span-level element
|
||||
# [Usage context] Where span-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents verbatim text.
|
||||
#
|
||||
# The +value+ field has to contain the content of the code span.
|
||||
#
|
||||
#
|
||||
# === :footnote
|
||||
#
|
||||
# [Category] Span-level element
|
||||
# [Usage context] Where span-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents a footnote marker.
|
||||
#
|
||||
# The +value+ field has to contain an element whose children are the content of the footnote. The
|
||||
# option :name has to contain a valid and unique footnote name. A valid footnote name consists of
|
||||
# a word character or a digit and then optionally followed by other word characters, digits or
|
||||
# dashes.
|
||||
#
|
||||
#
|
||||
# === :em
|
||||
#
|
||||
# [Category] Span-level element
|
||||
# [Usage context] Where span-level elements are expected
|
||||
# [Content model] Span-level elements
|
||||
#
|
||||
# Represents emphasis of its contents.
|
||||
#
|
||||
#
|
||||
# === :strong
|
||||
#
|
||||
# [Category] Span-level element
|
||||
# [Usage context] Where span-level elements are expected
|
||||
# [Content model] Span-level elements
|
||||
#
|
||||
# Represents strong importance for its contents.
|
||||
#
|
||||
#
|
||||
# === :entity
|
||||
#
|
||||
# [Category] Span-level element
|
||||
# [Usage context] Where span-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents an HTML entity.
|
||||
#
|
||||
# The +value+ field has to contain an instance of Kramdown::Utils::Entities::Entity. The option
|
||||
# :original can be used to store the original representation of the entity.
|
||||
#
|
||||
#
|
||||
# === :typographic_sym
|
||||
#
|
||||
# [Category] Span-level element
|
||||
# [Usage context] Where span-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents a typographic symbol.
|
||||
#
|
||||
# The +value+ field needs to contain a Symbol representing the specific typographic symbol from
|
||||
# the following list:
|
||||
#
|
||||
# :mdash:: An mdash character (---)
|
||||
# :ndash:: An ndash character (--)
|
||||
# :hellip:: An ellipsis (...)
|
||||
# :laquo:: A left guillemet (<<)
|
||||
# :raquo:: A right guillemet (>>)
|
||||
# :laquo_space:: A left guillemet with a space (<< )
|
||||
# :raquo_space:: A right guillemet with a space ( >>)
|
||||
#
|
||||
#
|
||||
# === :smart_quote
|
||||
#
|
||||
# [Category] Span-level element
|
||||
# [Usage context] Where span-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents a quotation character.
|
||||
#
|
||||
# The +value+ field needs to contain a Symbol representing the specific quotation character:
|
||||
#
|
||||
# :lsquo:: Left single quote
|
||||
# :rsquo:: Right single quote
|
||||
# :ldquo:: Left double quote
|
||||
# :rdquo:: Right double quote
|
||||
#
|
||||
#
|
||||
# === :abbreviation
|
||||
#
|
||||
# [Category] Span-level element
|
||||
# [Usage context] Where span-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents a text part that is an abbreviation.
|
||||
#
|
||||
# The +value+ field has to contain the text part that is the abbreviation. The definition of the
|
||||
# abbreviation is stored in the :root element of the document.
|
||||
#
|
||||
#
|
||||
# == Other Elements
|
||||
#
|
||||
# === :html_element
|
||||
#
|
||||
# [Category] Block/span-level element
|
||||
# [Usage context] Where block/span-level elements or raw HTML elements are expected
|
||||
# [Content model] Depends on the element
|
||||
#
|
||||
# Represents an HTML element.
|
||||
#
|
||||
# The +value+ field has to contain the name of the HTML element the element is representing.
|
||||
#
|
||||
# The option :category has to be set to either :span or :block depending on the whether the
|
||||
# element is a block-level or a span-level element. The option :content_model has to be set to the
|
||||
# content model for the element (either :block if it contains block-level elements, :span if it
|
||||
# contains span-level elements or :raw if it contains raw content).
|
||||
#
|
||||
#
|
||||
# === :xml_comment
|
||||
#
|
||||
# [Category] Block/span-level element
|
||||
# [Usage context] Where block/span-level elements are expected or in raw HTML elements
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents an XML/HTML comment.
|
||||
#
|
||||
# The +value+ field has to contain the whole XML/HTML comment including the delimiters.
|
||||
#
|
||||
# The option :category has to be set to either :span or :block depending on the context where the
|
||||
# element is used.
|
||||
#
|
||||
#
|
||||
# === :xml_pi
|
||||
#
|
||||
# [Category] Block/span-level element
|
||||
# [Usage context] Where block/span-level elements are expected or in raw HTML elements
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents an XML/HTML processing instruction.
|
||||
#
|
||||
# The +value+ field has to contain the whole XML/HTML processing instruction including the
|
||||
# delimiters.
|
||||
#
|
||||
# The option :category has to be set to either :span or :block depending on the context where the
|
||||
# element is used.
|
||||
#
|
||||
#
|
||||
# === :comment
|
||||
#
|
||||
# [Category] Block/span-level element
|
||||
# [Usage context] Where block/span-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents a comment.
|
||||
#
|
||||
# The +value+ field has to contain the comment.
|
||||
#
|
||||
# The option :category has to be set to either :span or :block depending on the context where the
|
||||
# element is used. If it is set to :span, then no blank lines are allowed in the comment.
|
||||
#
|
||||
#
|
||||
# === :raw
|
||||
#
|
||||
# [Category] Block/span-level element
|
||||
# [Usage context] Where block/span-level elements are expected
|
||||
# [Content model] None
|
||||
#
|
||||
# Represents a raw string that should not be modified. For example, the element could contain some
|
||||
# HTML code that should be output as-is without modification and escaping.
|
||||
#
|
||||
# The +value+ field has to contain the actual raw text.
|
||||
#
|
||||
# The option :category has to be set to either :span or :block depending on the context where the
|
||||
# element is used. If it is set to :span, then no blank lines are allowed in the raw text.
|
||||
#
|
||||
# The option :type can be set to an array of strings to define for which converters the raw string
|
||||
# is valid.
|
||||
#
|
||||
class Element
|
||||
|
||||
# A symbol representing the element type. For example, :p or :blockquote.
|
||||
attr_accessor :type
|
||||
|
||||
# The value of the element. The interpretation of this field depends on the type of the element.
|
||||
# Many elements don't use this field.
|
||||
attr_accessor :value
|
||||
|
||||
# The child elements of this element.
|
||||
attr_accessor :children
|
||||
|
||||
# Create a new Element object of type +type+. The optional parameters +value+, +attr+ and
|
||||
# +options+ can also be set in this constructor for convenience.
|
||||
def initialize(type, value = nil, attr = nil, options = nil)
|
||||
@type, @value, @attr, @options = type, value, attr, options
|
||||
@children = []
|
||||
end
|
||||
|
||||
# The attributes of the element.
|
||||
def attr
|
||||
@attr ||= {}
|
||||
end
|
||||
|
||||
# The options hash for the element. It is used for storing arbitray options.
|
||||
def options
|
||||
@options ||= {}
|
||||
end
|
||||
|
||||
def inspect # :nodoc:
|
||||
"<kd:#{@type}" \
|
||||
"#{value.nil? ? '' : ' value=' + value.inspect}" \
|
||||
"#{attr.empty? ? '' : ' attr=' + attr.inspect}" \
|
||||
"#{options.empty? ? '' : ' options=' + options.inspect}" \
|
||||
"#{children.empty? ? '' : ' children=' + children.inspect}>"
|
||||
end
|
||||
|
||||
CATEGORY = {} # :nodoc:
|
||||
[:blank, :p, :header, :blockquote, :codeblock, :ul, :ol, :li, :dl, :dt, :dd,
|
||||
:table, :td, :hr].each {|b| CATEGORY[b] = :block }
|
||||
[:text, :a, :br, :img, :codespan, :footnote, :em, :strong, :entity, :typographic_sym,
|
||||
:smart_quote, :abbreviation].each {|b| CATEGORY[b] = :span }
|
||||
|
||||
# Return the category of +el+ which can be :block, :span or +nil+.
|
||||
#
|
||||
# Most elements have a fixed category, however, some elements can either appear in a block-level
|
||||
# or a span-level context. These elements need to have the option :category correctly set.
|
||||
def self.category(el)
|
||||
CATEGORY[el.type] || el.options[:category]
|
||||
end
|
||||
|
||||
# syntactic sugar to simplify calls such as +Kramdown::Element.category(el) == :block+ with
|
||||
# +el.block?+.
|
||||
#
|
||||
# Returns boolean true or false.
|
||||
def block?
|
||||
(CATEGORY[type] || options[:category]) == :block
|
||||
end
|
||||
|
||||
# syntactic sugar to simplify calls such as +Kramdown::Element.category(el) == :span+ with
|
||||
# +el.span?+.
|
||||
#
|
||||
# Returns boolean true or false.
|
||||
def span?
|
||||
(CATEGORY[type] || options[:category]) == :span
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- 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
|
||||
|
||||
# This error is raised when an error condition is encountered.
|
||||
#
|
||||
# *Note* that this error is only raised by the support framework for the parsers and converters.
|
||||
class Error < RuntimeError; end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,645 @@
|
||||
# -*- 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 'yaml'
|
||||
|
||||
module Kramdown
|
||||
|
||||
# This module defines all options that are used by parsers and/or converters as well as providing
|
||||
# methods to deal with the options.
|
||||
module Options
|
||||
|
||||
# Helper class introducing a boolean type for specifying boolean values (+true+ and +false+) as
|
||||
# option types.
|
||||
class Boolean
|
||||
|
||||
# Return +true+ if +other+ is either +true+ or +false+
|
||||
def self.===(other)
|
||||
FalseClass === other || TrueClass === other
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# ----------------------------
|
||||
# :section: Option definitions
|
||||
#
|
||||
# This sections describes the methods that can be used on the Options module.
|
||||
# ----------------------------
|
||||
|
||||
# Struct class for storing the definition of an option.
|
||||
Definition = Struct.new(:name, :type, :default, :desc, :validator)
|
||||
|
||||
# Allowed option types.
|
||||
ALLOWED_TYPES = [String, Integer, Float, Symbol, Boolean, Object]
|
||||
|
||||
@options = {}
|
||||
@defaults = nil
|
||||
|
||||
# Define a new option called +name+ (a Symbol) with the given +type+ (String, Integer, Float,
|
||||
# Symbol, Boolean, Object), default value +default+ and the description +desc+. If a block is
|
||||
# specified, it should validate the value and either raise an error or return a valid value.
|
||||
#
|
||||
# The type 'Object' should only be used for complex types for which none of the other types
|
||||
# suffices. A block needs to be specified when using type 'Object' and it has to cope with
|
||||
# a value given as string and as the opaque type.
|
||||
def self.define(name, type, default, desc, &block)
|
||||
name = name.to_sym
|
||||
raise ArgumentError, "Option name #{name} is already used" if @options.key?(name)
|
||||
raise ArgumentError, "Invalid option type #{type} specified" unless ALLOWED_TYPES.include?(type)
|
||||
raise ArgumentError, "Invalid type for default value" if !(type === default) && !default.nil?
|
||||
raise ArgumentError, "Missing validator block" if type == Object && block.nil?
|
||||
@options[name] = Definition.new(name, type, default, desc, block)
|
||||
@defaults = nil
|
||||
end
|
||||
|
||||
# Return all option definitions.
|
||||
def self.definitions
|
||||
@options
|
||||
end
|
||||
|
||||
# Return +true+ if an option called +name+ is defined.
|
||||
def self.defined?(name)
|
||||
@options.key?(name.to_sym)
|
||||
end
|
||||
|
||||
# Return a Hash with the default values for all options.
|
||||
def self.defaults
|
||||
@defaults ||=
|
||||
begin
|
||||
temp = {}
|
||||
@options.each {|_n, o| temp[o.name] = o.default }
|
||||
temp.freeze
|
||||
end
|
||||
end
|
||||
|
||||
# Merge the #defaults Hash with the *parsed* options from the given Hash, i.e. only valid option
|
||||
# names are considered and their value is run through the #parse method.
|
||||
def self.merge(hash)
|
||||
temp = defaults.dup
|
||||
hash.each do |k, v|
|
||||
k = k.to_sym
|
||||
temp[k] = @options.key?(k) ? parse(k, v) : v
|
||||
end
|
||||
temp
|
||||
end
|
||||
|
||||
# Parse the given value +data+ as if it was a value for the option +name+ and return the parsed
|
||||
# value with the correct type.
|
||||
#
|
||||
# If +data+ already has the correct type, it is just returned. Otherwise it is converted to a
|
||||
# String and then to the correct type.
|
||||
def self.parse(name, data)
|
||||
name = name.to_sym
|
||||
raise ArgumentError, "No option named #{name} defined" unless @options.key?(name)
|
||||
unless @options[name].type === data
|
||||
data = data.to_s
|
||||
data = if @options[name].type == String
|
||||
data
|
||||
elsif @options[name].type == Integer
|
||||
Integer(data) rescue raise Kramdown::Error, "Invalid integer value for option '#{name}': '#{data}'"
|
||||
elsif @options[name].type == Float
|
||||
Float(data) rescue raise Kramdown::Error, "Invalid float value for option '#{name}': '#{data}'"
|
||||
elsif @options[name].type == Symbol
|
||||
str_to_sym(data)
|
||||
elsif @options[name].type == Boolean
|
||||
data.downcase.strip != 'false' && !data.empty?
|
||||
end
|
||||
end
|
||||
data = @options[name].validator[data] if @options[name].validator
|
||||
data
|
||||
end
|
||||
|
||||
# Converts the given String +data+ into a Symbol or +nil+ with the
|
||||
# following provisions:
|
||||
#
|
||||
# - A leading colon is stripped from the string.
|
||||
# - An empty value or a value equal to "nil" results in +nil+.
|
||||
def self.str_to_sym(data)
|
||||
data = data.strip
|
||||
data = data[1..-1] if data[0] == ':'
|
||||
(data.empty? || data == 'nil' ? nil : data.to_sym)
|
||||
end
|
||||
|
||||
# ----------------------------
|
||||
# :section: Option Validators
|
||||
#
|
||||
# This sections contains all pre-defined option validators.
|
||||
# ----------------------------
|
||||
|
||||
# Ensures that the option value +val+ for the option called +name+ is a valid array. The
|
||||
# parameter +val+ can be
|
||||
#
|
||||
# - a comma separated string which is split into an array of values
|
||||
# - or an array.
|
||||
#
|
||||
# Optionally, the array is checked for the correct size.
|
||||
def self.simple_array_validator(val, name, size = nil)
|
||||
if String === val
|
||||
val = val.split(",")
|
||||
elsif !(Array === val)
|
||||
raise Kramdown::Error, "Invalid type #{val.class} for option #{name}"
|
||||
end
|
||||
if size && val.size != size
|
||||
raise Kramdown::Error, "Option #{name} needs exactly #{size} values"
|
||||
end
|
||||
val
|
||||
end
|
||||
|
||||
# Ensures that the option value +val+ for the option called +name+ is a valid hash. The
|
||||
# parameter +val+ can be
|
||||
#
|
||||
# - a hash in YAML format
|
||||
# - or a Ruby Hash object.
|
||||
def self.simple_hash_validator(val, name)
|
||||
if String === val
|
||||
begin
|
||||
val = YAML.safe_load(val)
|
||||
rescue RuntimeError, ArgumentError, SyntaxError
|
||||
raise Kramdown::Error, "Invalid YAML value for option #{name}"
|
||||
end
|
||||
end
|
||||
raise Kramdown::Error, "Invalid type #{val.class} for option #{name}" unless Hash === val
|
||||
val
|
||||
end
|
||||
|
||||
# ----------------------------
|
||||
# :section: Option Definitions
|
||||
#
|
||||
# This sections contains all option definitions that are used by the included
|
||||
# parsers/converters.
|
||||
# ----------------------------
|
||||
|
||||
define(:template, String, '', <<~EOF)
|
||||
The name of an ERB template file that should be used to wrap the output
|
||||
or the ERB template itself.
|
||||
|
||||
This is used to wrap the output in an environment so that the output can
|
||||
be used as a stand-alone document. For example, an HTML template would
|
||||
provide the needed header and body tags so that the whole output is a
|
||||
valid HTML file. If no template is specified, the output will be just
|
||||
the converted text.
|
||||
|
||||
When resolving the template file, the given template name is used first.
|
||||
If such a file is not found, the converter extension (the same as the
|
||||
converter name) is appended. If the file still cannot be found, the
|
||||
templates name is interpreted as a template name that is provided by
|
||||
kramdown (without the converter extension). If the file is still not
|
||||
found, the template name is checked if it starts with 'string://' and if
|
||||
it does, this prefix is removed and the rest is used as template
|
||||
content.
|
||||
|
||||
kramdown provides a default template named 'document' for each converter.
|
||||
|
||||
Default: ''
|
||||
Used by: all converters
|
||||
EOF
|
||||
|
||||
define(:auto_ids, Boolean, true, <<~EOF)
|
||||
Use automatic header ID generation
|
||||
|
||||
If this option is `true`, ID values for all headers are automatically
|
||||
generated if no ID is explicitly specified.
|
||||
|
||||
Default: true
|
||||
Used by: HTML/Latex converter
|
||||
EOF
|
||||
|
||||
define(:auto_id_stripping, Boolean, false, <<~EOF)
|
||||
Strip all formatting from header text for automatic ID generation
|
||||
|
||||
If this option is `true`, only the text elements of a header are used
|
||||
for generating the ID later (in contrast to just using the raw header
|
||||
text line).
|
||||
|
||||
This option will be removed in version 2.0 because this will be the
|
||||
default then.
|
||||
|
||||
Default: false
|
||||
Used by: kramdown parser
|
||||
EOF
|
||||
|
||||
define(:auto_id_prefix, String, '', <<~EOF)
|
||||
Prefix used for automatically generated header IDs
|
||||
|
||||
This option can be used to set a prefix for the automatically generated
|
||||
header IDs so that there is no conflict when rendering multiple kramdown
|
||||
documents into one output file separately. The prefix should only
|
||||
contain characters that are valid in an ID!
|
||||
|
||||
Default: ''
|
||||
Used by: HTML/Latex converter
|
||||
EOF
|
||||
|
||||
define(:header_links, Boolean, false, <<~EOF)
|
||||
Adds anchor tags within headers that can be used to generate permalinks
|
||||
when not using a table of contents.
|
||||
|
||||
The anchor tags are empty, but can be styled to your liking.
|
||||
|
||||
Default: false
|
||||
EOF
|
||||
|
||||
define(:transliterated_header_ids, Boolean, false, <<~EOF)
|
||||
Transliterate the header text before generating the ID
|
||||
|
||||
Only ASCII characters are used in headers IDs. This is not good for
|
||||
languages with many non-ASCII characters. By enabling this option
|
||||
the header text is transliterated to ASCII as good as possible so that
|
||||
the resulting header ID is more useful.
|
||||
|
||||
The stringex library needs to be installed for this feature to work!
|
||||
|
||||
Default: false
|
||||
Used by: HTML/Latex converter
|
||||
EOF
|
||||
|
||||
define(:parse_block_html, Boolean, false, <<~EOF)
|
||||
Process kramdown syntax in block HTML tags
|
||||
|
||||
If this option is `true`, the kramdown parser processes the content of
|
||||
block HTML tags as text containing block-level elements. Since this is
|
||||
not wanted normally, the default is `false`. It is normally better to
|
||||
selectively enable kramdown processing via the markdown attribute.
|
||||
|
||||
Default: false
|
||||
Used by: kramdown parser
|
||||
EOF
|
||||
|
||||
define(:parse_span_html, Boolean, true, <<~EOF)
|
||||
Process kramdown syntax in span HTML tags
|
||||
|
||||
If this option is `true`, the kramdown parser processes the content of
|
||||
span HTML tags as text containing span-level elements.
|
||||
|
||||
Default: true
|
||||
Used by: kramdown parser
|
||||
EOF
|
||||
|
||||
define(:html_to_native, Boolean, false, <<~EOF)
|
||||
Convert HTML elements to native elements
|
||||
|
||||
If this option is `true`, the parser converts HTML elements to native
|
||||
elements. For example, when parsing `<em>hallo</em>` the emphasis tag
|
||||
would normally be converted to an `:html` element with tag type `:em`.
|
||||
If `html_to_native` is `true`, then the emphasis would be converted to a
|
||||
native `:em` element.
|
||||
|
||||
This is useful for converters that cannot deal with HTML elements.
|
||||
|
||||
Default: false
|
||||
Used by: kramdown parser
|
||||
EOF
|
||||
|
||||
define(:link_defs, Object, {}, <<~EOF) do |val|
|
||||
Pre-defines link definitions
|
||||
|
||||
This option can be used to pre-define link definitions. The value needs
|
||||
to be a Hash where the keys are the link identifiers and the values are
|
||||
two element Arrays with the link URL and the link title.
|
||||
|
||||
If the value is a String, it has to contain a valid YAML hash and the
|
||||
hash has to follow the above guidelines.
|
||||
|
||||
Default: {}
|
||||
Used by: kramdown parser
|
||||
EOF
|
||||
val = simple_hash_validator(val, :link_defs)
|
||||
val.each do |_k, v|
|
||||
if !(Array === v) || v.size > 2 || v.empty?
|
||||
raise Kramdown::Error, "Invalid structure for hash value of option #{name}"
|
||||
end
|
||||
v << nil if v.size == 1
|
||||
end
|
||||
val
|
||||
end
|
||||
|
||||
define(:footnote_nr, Integer, 1, <<~EOF)
|
||||
The number of the first footnote
|
||||
|
||||
This option can be used to specify the number that is used for the first
|
||||
footnote.
|
||||
|
||||
Default: 1
|
||||
Used by: HTML converter
|
||||
EOF
|
||||
|
||||
define(:entity_output, Symbol, :as_char, <<~EOF)
|
||||
Defines how entities are output
|
||||
|
||||
The possible values are :as_input (entities are output in the same
|
||||
form as found in the input), :numeric (entities are output in numeric
|
||||
form), :symbolic (entities are output in symbolic form if possible) or
|
||||
:as_char (entities are output as characters if possible, only available
|
||||
on Ruby 1.9).
|
||||
|
||||
Default: :as_char
|
||||
Used by: HTML converter, kramdown converter
|
||||
EOF
|
||||
|
||||
TOC_LEVELS_RANGE = (1..6).freeze
|
||||
TOC_LEVELS_ARRAY = TOC_LEVELS_RANGE.to_a.freeze
|
||||
private_constant :TOC_LEVELS_RANGE, :TOC_LEVELS_ARRAY
|
||||
|
||||
define(:toc_levels, Object, TOC_LEVELS_ARRAY, <<~EOF) do |val|
|
||||
Defines the levels that are used for the table of contents
|
||||
|
||||
The individual levels can be specified by separating them with commas
|
||||
(e.g. 1,2,3) or by using the range syntax (e.g. 1..3). Only the
|
||||
specified levels are used for the table of contents.
|
||||
|
||||
Default: 1..6
|
||||
Used by: HTML/Latex converter
|
||||
EOF
|
||||
case val
|
||||
when String
|
||||
if val =~ /^(\d)\.\.(\d)$/
|
||||
val = Range.new($1.to_i, $2.to_i).to_a
|
||||
elsif val.match?(/^\d(?:,\d)*$/)
|
||||
val = val.split(",").map(&:to_i).uniq
|
||||
else
|
||||
raise Kramdown::Error, "Invalid syntax for option toc_levels"
|
||||
end
|
||||
when Array
|
||||
unless val.eql?(TOC_LEVELS_ARRAY)
|
||||
val = val.map(&:to_i).uniq
|
||||
end
|
||||
when Range
|
||||
val = if val.eql?(TOC_LEVELS_RANGE)
|
||||
TOC_LEVELS_ARRAY
|
||||
else
|
||||
val.map(&:to_i).uniq
|
||||
end
|
||||
else
|
||||
raise Kramdown::Error, "Invalid type #{val.class} for option toc_levels"
|
||||
end
|
||||
if val.any? {|i| !TOC_LEVELS_RANGE.cover?(i) }
|
||||
raise Kramdown::Error, "Level numbers for option toc_levels have to be integers from 1 to 6"
|
||||
end
|
||||
val
|
||||
end
|
||||
|
||||
define(:line_width, Integer, 72, <<~EOF)
|
||||
Defines the line width to be used when outputting a document
|
||||
|
||||
Default: 72
|
||||
Used by: kramdown converter
|
||||
EOF
|
||||
|
||||
define(:latex_headers, Object, %w[section subsection subsubsection paragraph subparagraph subparagraph], <<~EOF) do |val|
|
||||
Defines the LaTeX commands for different header levels
|
||||
|
||||
The commands for the header levels one to six can be specified by
|
||||
separating them with commas.
|
||||
|
||||
Default: section,subsection,subsubsection,paragraph,subparagraph,subparagraph
|
||||
Used by: Latex converter
|
||||
EOF
|
||||
simple_array_validator(val, :latex_headers, 6)
|
||||
end
|
||||
|
||||
SMART_QUOTES_ENTITIES = %w[lsquo rsquo ldquo rdquo].freeze
|
||||
SMART_QUOTES_STR = SMART_QUOTES_ENTITIES.join(',').freeze
|
||||
private_constant :SMART_QUOTES_ENTITIES, :SMART_QUOTES_STR
|
||||
|
||||
define(:smart_quotes, Object, SMART_QUOTES_ENTITIES, <<~EOF) do |val|
|
||||
Defines the HTML entity names or code points for smart quote output
|
||||
|
||||
The entities identified by entity name or code point that should be
|
||||
used for, in order, a left single quote, a right single quote, a left
|
||||
double and a right double quote are specified by separating them with
|
||||
commas.
|
||||
|
||||
Default: lsquo,rsquo,ldquo,rdquo
|
||||
Used by: HTML/Latex converter
|
||||
EOF
|
||||
if val == SMART_QUOTES_STR || val == SMART_QUOTES_ENTITIES
|
||||
SMART_QUOTES_ENTITIES
|
||||
else
|
||||
val = simple_array_validator(val, :smart_quotes, 4)
|
||||
val.map! {|v| Integer(v) rescue v }
|
||||
val
|
||||
end
|
||||
end
|
||||
|
||||
define(:typographic_symbols, Object, {}, <<~EOF) do |val|
|
||||
Defines a mapping from typographical symbol to output characters
|
||||
|
||||
Typographical symbols are normally output using their equivalent Unicode
|
||||
codepoint. However, sometimes one wants to change the output, mostly to
|
||||
fallback to a sequence of ASCII characters.
|
||||
|
||||
This option allows this by specifying a mapping from typographical
|
||||
symbol to its output string. For example, the mapping {hellip: ...} would
|
||||
output the standard ASCII representation of an ellipsis.
|
||||
|
||||
The available typographical symbol names are:
|
||||
|
||||
* hellip: ellipsis
|
||||
* mdash: em-dash
|
||||
* ndash: en-dash
|
||||
* laquo: left guillemet
|
||||
* raquo: right guillemet
|
||||
* laquo_space: left guillemet followed by a space
|
||||
* raquo_space: right guillemet preceeded by a space
|
||||
|
||||
Default: {}
|
||||
Used by: HTML/Latex converter
|
||||
EOF
|
||||
val = simple_hash_validator(val, :typographic_symbols)
|
||||
val.keys.each do |k|
|
||||
val[k.kind_of?(String) ? str_to_sym(k) : k] = val.delete(k).to_s
|
||||
end
|
||||
val
|
||||
end
|
||||
|
||||
define(:remove_block_html_tags, Boolean, true, <<~EOF)
|
||||
Remove block HTML tags
|
||||
|
||||
If this option is `true`, the RemoveHtmlTags converter removes
|
||||
block HTML tags.
|
||||
|
||||
Default: true
|
||||
Used by: RemoveHtmlTags converter
|
||||
EOF
|
||||
|
||||
define(:remove_span_html_tags, Boolean, false, <<~EOF)
|
||||
Remove span HTML tags
|
||||
|
||||
If this option is `true`, the RemoveHtmlTags converter removes
|
||||
span HTML tags.
|
||||
|
||||
Default: false
|
||||
Used by: RemoveHtmlTags converter
|
||||
EOF
|
||||
|
||||
define(:header_offset, Integer, 0, <<~EOF)
|
||||
Sets the output offset for headers
|
||||
|
||||
If this option is c (may also be negative) then a header with level n
|
||||
will be output as a header with level c+n. If c+n is lower than 1,
|
||||
level 1 will be used. If c+n is greater than 6, level 6 will be used.
|
||||
|
||||
Default: 0
|
||||
Used by: HTML converter, Kramdown converter, Latex converter
|
||||
EOF
|
||||
|
||||
define(:syntax_highlighter, Symbol, :rouge, <<~EOF)
|
||||
Set the syntax highlighter
|
||||
|
||||
Specifies the syntax highlighter that should be used for highlighting
|
||||
code blocks and spans. If this option is set to +nil+, no syntax
|
||||
highlighting is done.
|
||||
|
||||
Options for the syntax highlighter can be set with the
|
||||
syntax_highlighter_opts configuration option.
|
||||
|
||||
Default: rouge
|
||||
Used by: HTML/Latex converter
|
||||
EOF
|
||||
|
||||
define(:syntax_highlighter_opts, Object, {}, <<~EOF) do |val|
|
||||
Set the syntax highlighter options
|
||||
|
||||
Specifies options for the syntax highlighter set via the
|
||||
syntax_highlighter configuration option.
|
||||
|
||||
The value needs to be a hash with key-value pairs that are understood by
|
||||
the used syntax highlighter.
|
||||
|
||||
Default: {}
|
||||
Used by: HTML/Latex converter
|
||||
EOF
|
||||
val = simple_hash_validator(val, :syntax_highlighter_opts)
|
||||
val.keys.each do |k|
|
||||
val[k.kind_of?(String) ? str_to_sym(k) : k] = val.delete(k)
|
||||
end
|
||||
val
|
||||
end
|
||||
|
||||
define(:math_engine, Symbol, :mathjax, <<~EOF)
|
||||
Set the math engine
|
||||
|
||||
Specifies the math engine that should be used for converting math
|
||||
blocks/spans. If this option is set to +nil+, no math engine is used and
|
||||
the math blocks/spans are output as is.
|
||||
|
||||
Options for the selected math engine can be set with the
|
||||
math_engine_opts configuration option.
|
||||
|
||||
Default: mathjax
|
||||
Used by: HTML converter
|
||||
EOF
|
||||
|
||||
define(:math_engine_opts, Object, {}, <<~EOF) do |val|
|
||||
Set the math engine options
|
||||
|
||||
Specifies options for the math engine set via the math_engine
|
||||
configuration option.
|
||||
|
||||
The value needs to be a hash with key-value pairs that are understood by
|
||||
the used math engine.
|
||||
|
||||
Default: {}
|
||||
Used by: HTML converter
|
||||
EOF
|
||||
val = simple_hash_validator(val, :math_engine_opts)
|
||||
val.keys.each do |k|
|
||||
val[k.kind_of?(String) ? str_to_sym(k) : k] = val.delete(k)
|
||||
end
|
||||
val
|
||||
end
|
||||
|
||||
define(:footnote_backlink, String, '↩', <<~EOF)
|
||||
Defines the text that should be used for the footnote backlinks
|
||||
|
||||
The footnote backlink is just text, so any special HTML characters will
|
||||
be escaped.
|
||||
|
||||
If the footnote backlint text is an empty string, no footnote backlinks
|
||||
will be generated.
|
||||
|
||||
Default: '&8617;'
|
||||
Used by: HTML converter
|
||||
EOF
|
||||
|
||||
define(:footnote_backlink_inline, Boolean, false, <<~EOF)
|
||||
Specifies whether the footnote backlink should always be inline
|
||||
|
||||
With the default of false the footnote backlink is placed at the end of
|
||||
the last paragraph if there is one, or an extra paragraph with only the
|
||||
footnote backlink is created.
|
||||
|
||||
Setting this option to true tries to place the footnote backlink in the
|
||||
last, possibly nested paragraph or header. If this fails (e.g. in the
|
||||
case of a table), an extra paragraph with only the footnote backlink is
|
||||
created.
|
||||
|
||||
Default: false
|
||||
Used by: HTML converter
|
||||
EOF
|
||||
|
||||
define(:footnote_prefix, String, '', <<~EOF)
|
||||
Prefix used for footnote IDs
|
||||
|
||||
This option can be used to set a prefix for footnote IDs. This is useful
|
||||
when rendering multiple documents into the same output file to avoid
|
||||
duplicate IDs. The prefix should only contain characters that are valid
|
||||
in an ID!
|
||||
|
||||
Default: ''
|
||||
Used by: HTML
|
||||
EOF
|
||||
|
||||
define(:footnote_link_text, String, '%s', <<~EOF) do |val|
|
||||
The text used for the footnote number in a footnote link
|
||||
|
||||
This option can be used to add additional text to the footnote
|
||||
link. It should be a format string, and is passed the footnote
|
||||
number as the only argument to the format string.
|
||||
e.g. "[footnote %s]" would display as "[footnote 1]".
|
||||
|
||||
Default: '%s'
|
||||
Used by: HTML
|
||||
EOF
|
||||
if !val.include?('%s')
|
||||
raise Kramdown::Error, "option footnote_link_text needs to contain a '%s'"
|
||||
end
|
||||
val
|
||||
end
|
||||
|
||||
|
||||
define(:remove_line_breaks_for_cjk, Boolean, false, <<~EOF)
|
||||
Specifies whether line breaks should be removed between CJK characters
|
||||
|
||||
Default: false
|
||||
Used by: HTML converter
|
||||
EOF
|
||||
|
||||
define(:forbidden_inline_options, Object, %w[template], <<~EOF) do |val|
|
||||
Defines the options that may not be set using the {::options} extension
|
||||
|
||||
The value needs to be an array of option names.
|
||||
|
||||
Default: [template]
|
||||
Used by: HTML converter
|
||||
EOF
|
||||
val.map! {|item| item.kind_of?(String) ? str_to_sym(item) : item }
|
||||
simple_array_validator(val, :forbidden_inline_options)
|
||||
end
|
||||
|
||||
define(:list_indent, Integer, 2, <<~EOF)
|
||||
Sets the number of spaces to use for list indentation
|
||||
|
||||
Default: 2
|
||||
Used by: Kramdown converter
|
||||
EOF
|
||||
|
||||
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
|
||||
|
||||
# This module contains all available parsers. A parser takes an input string and converts the
|
||||
# string to an element tree.
|
||||
#
|
||||
# New parsers should be derived from the Base class which provides common functionality - see its
|
||||
# API documentation for how to create a custom converter class.
|
||||
module Parser
|
||||
|
||||
autoload :Base, 'kramdown/parser/base'
|
||||
autoload :Kramdown, 'kramdown/parser/kramdown'
|
||||
autoload :Html, 'kramdown/parser/html'
|
||||
autoload :Markdown, 'kramdown/parser/markdown'
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -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
|
||||
@@ -0,0 +1,45 @@
|
||||
# -*- 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
|
||||
|
||||
# == \Utils Module
|
||||
#
|
||||
# This module contains utility class/modules/methods that can be used by both parsers and
|
||||
# converters.
|
||||
module Utils
|
||||
|
||||
autoload :Entities, 'kramdown/utils/entities'
|
||||
autoload :Html, 'kramdown/utils/html'
|
||||
autoload :Unidecoder, 'kramdown/utils/unidecoder'
|
||||
autoload :StringScanner, 'kramdown/utils/string_scanner'
|
||||
autoload :Configurable, 'kramdown/utils/configurable'
|
||||
autoload :LRUCache, 'kramdown/utils/lru_cache'
|
||||
|
||||
# Treat +name+ as if it were snake cased (e.g. snake_case) and camelize it (e.g. SnakeCase).
|
||||
def self.camelize(name)
|
||||
name.split('_').inject(+'') {|s, x| s << x[0..0].upcase << x[1..-1] }
|
||||
end
|
||||
|
||||
# Treat +name+ as if it were camelized (e.g. CamelizedName) and snake-case it (e.g. camelized_name).
|
||||
def self.snake_case(name)
|
||||
name = name.dup
|
||||
name.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
||||
name.gsub!(/([a-z])([A-Z])/, '\1_\2')
|
||||
name.downcase!
|
||||
name
|
||||
end
|
||||
|
||||
def self.deep_const_get(str)
|
||||
::Object.const_get(str)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
# -*- 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 Utils
|
||||
|
||||
# Methods for registering configurable extensions.
|
||||
module Configurable
|
||||
|
||||
# Create a new configurable extension called +name+.
|
||||
#
|
||||
# Three methods will be defined on the calling object which allow to use this configurable
|
||||
# extension:
|
||||
#
|
||||
# configurables:: Returns a hash of hashes that is used to store all configurables of the
|
||||
# object.
|
||||
#
|
||||
# <name>(ext_name):: Return the configured extension +ext_name+.
|
||||
#
|
||||
# add_<name>(ext_name, data=nil, &block):: Define an extension +ext_name+ by specifying either
|
||||
# the data as argument or by using a block.
|
||||
def configurable(name)
|
||||
unless respond_to?(:configurables)
|
||||
singleton_class.send(:define_method, :configurables) do
|
||||
@_configurables ||= Hash.new {|h, k| h[k] = {} }
|
||||
end
|
||||
end
|
||||
singleton_class.send(:define_method, name) do |data|
|
||||
configurables[name][data]
|
||||
end
|
||||
singleton_class.send(:define_method, "add_#{name}".intern) do |data, *args, &block|
|
||||
configurables[name][data] = args.first || block
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
# -*- 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'
|
||||
|
||||
module Kramdown
|
||||
|
||||
module Utils
|
||||
|
||||
# Provides convenience methods for HTML related tasks.
|
||||
#
|
||||
# *Note* that this module has to be mixed into a class that has a @root (containing an element
|
||||
# of type :root) and an @options (containing an options hash) instance variable so that some of
|
||||
# the methods can work correctly.
|
||||
module Html
|
||||
|
||||
# Convert the entity +e+ to a string. The optional parameter +original+ may contain the
|
||||
# original representation of the entity.
|
||||
#
|
||||
# This method uses the option +entity_output+ to determine the output form for the entity.
|
||||
def entity_to_str(e, original = nil)
|
||||
entity_output = @options[:entity_output]
|
||||
|
||||
if entity_output == :as_char &&
|
||||
(c = e.char.encode(@root.options[:encoding]) rescue nil) &&
|
||||
((c = e.char) == '"' || !ESCAPE_MAP.key?(c))
|
||||
c
|
||||
elsif (entity_output == :as_input || entity_output == :as_char) && original
|
||||
original
|
||||
elsif (entity_output == :symbolic || ESCAPE_MAP.key?(e.char)) && !e.name.nil?
|
||||
"&#{e.name};"
|
||||
else # default to :numeric
|
||||
"&##{e.code_point};"
|
||||
end
|
||||
end
|
||||
|
||||
# Return the HTML representation of the attributes +attr+.
|
||||
def html_attributes(attr)
|
||||
return '' if attr.empty?
|
||||
|
||||
attr.map do |k, v|
|
||||
v.nil? || (k == 'id' && v.strip.empty?) ? '' : " #{k}=\"#{escape_html(v.to_s, :attribute)}\""
|
||||
end.join
|
||||
end
|
||||
|
||||
# :stopdoc:
|
||||
ESCAPE_MAP = {
|
||||
'<' => '<',
|
||||
'>' => '>',
|
||||
'&' => '&',
|
||||
'"' => '"',
|
||||
}
|
||||
ESCAPE_ALL_RE = /<|>|&/
|
||||
ESCAPE_TEXT_RE = Regexp.union(REXML::Parsers::BaseParser::REFERENCE_RE, /<|>|&/)
|
||||
ESCAPE_ATTRIBUTE_RE = Regexp.union(REXML::Parsers::BaseParser::REFERENCE_RE, /<|>|&|"/)
|
||||
ESCAPE_RE_FROM_TYPE = {all: ESCAPE_ALL_RE, text: ESCAPE_TEXT_RE, attribute: ESCAPE_ATTRIBUTE_RE}
|
||||
# :startdoc:
|
||||
|
||||
# Escape the special HTML characters in the string +str+. The parameter +type+ specifies what
|
||||
# is escaped: :all - all special HTML characters except the quotation mark as well as
|
||||
# entities, :text - all special HTML characters except the quotation mark but no entities and
|
||||
# :attribute - all special HTML characters including the quotation mark but no entities.
|
||||
def escape_html(str, type = :all)
|
||||
str.gsub(ESCAPE_RE_FROM_TYPE[type]) {|m| ESCAPE_MAP[m] || m }
|
||||
end
|
||||
|
||||
REDUNDANT_LINE_BREAK_REGEX = /([\p{Han}\p{Hiragana}\p{Katakana}]+)\n([\p{Han}\p{Hiragana}\p{Katakana}]+)/u
|
||||
def fix_cjk_line_break(str)
|
||||
while str.gsub!(REDUNDANT_LINE_BREAK_REGEX, '\1\2')
|
||||
end
|
||||
str
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
# -*- 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 Utils
|
||||
|
||||
# A simple least recently used (LRU) cache.
|
||||
#
|
||||
# The cache relies on the fact that Ruby's Hash class maintains insertion order. So deleting
|
||||
# and re-inserting a key-value pair on access moves the key to the last position. When an
|
||||
# entry is added and the cache is full, the first entry is removed.
|
||||
class LRUCache
|
||||
|
||||
# Creates a new LRUCache that can hold +size+ entries.
|
||||
def initialize(size)
|
||||
@size = size
|
||||
@cache = {}
|
||||
end
|
||||
|
||||
# Returns the stored value for +key+ or +nil+ if no value was stored under the key.
|
||||
def [](key)
|
||||
(val = @cache.delete(key)).nil? ? nil : @cache[key] = val
|
||||
end
|
||||
|
||||
# Stores the +value+ under the +key+.
|
||||
def []=(key, value)
|
||||
@cache.delete(key)
|
||||
@cache[key] = value
|
||||
@cache.shift if @cache.length > @size
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- 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'
|
||||
|
||||
module Kramdown
|
||||
module Utils
|
||||
|
||||
# This patched StringScanner adds line number information for current scan position and a
|
||||
# start_line_number override for nested StringScanners.
|
||||
class StringScanner < ::StringScanner
|
||||
|
||||
# The start line number. Used for nested StringScanners that scan a sub-string of the source
|
||||
# document. The kramdown parser uses this, e.g., for span level parsers.
|
||||
attr_reader :start_line_number
|
||||
|
||||
# Takes the start line number as optional second argument.
|
||||
#
|
||||
# Note: The original second argument is no longer used so this should be safe.
|
||||
def initialize(string, start_line_number = 1)
|
||||
super(string)
|
||||
@start_line_number = start_line_number || 1
|
||||
@previous_pos = 0
|
||||
@previous_line_number = @start_line_number
|
||||
end
|
||||
|
||||
# Sets the byte position of the scan pointer.
|
||||
#
|
||||
# Note: This also resets some internal variables, so always use pos= when setting the position
|
||||
# and don't use any other method for that!
|
||||
def pos=(pos)
|
||||
if self.pos > pos
|
||||
@previous_line_number = @start_line_number
|
||||
@previous_pos = 0
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
# Return information needed to revert the byte position of the string scanner in a performant
|
||||
# way.
|
||||
#
|
||||
# The returned data can be fed to #revert_pos to revert the position to the saved one.
|
||||
#
|
||||
# Note: Just saving #pos won't be enough.
|
||||
def save_pos
|
||||
[pos, @previous_pos, @previous_line_number]
|
||||
end
|
||||
|
||||
# Revert the position to one saved by #save_pos.
|
||||
def revert_pos(data)
|
||||
self.pos = data[0]
|
||||
@previous_pos, @previous_line_number = data[1], data[2]
|
||||
end
|
||||
|
||||
# Returns the line number for current charpos.
|
||||
#
|
||||
# NOTE: Requires that all line endings are normalized to '\n'
|
||||
#
|
||||
# NOTE: Normally we'd have to add one to the count of newlines to get the correct line number.
|
||||
# However we add the one indirectly by using a one-based start_line_number.
|
||||
def current_line_number
|
||||
# Not using string[@previous_pos..best_pos].count('\n') because it is slower
|
||||
strscan = ::StringScanner.new(string)
|
||||
strscan.pos = @previous_pos
|
||||
old_pos = pos + 1
|
||||
@previous_line_number += 1 while strscan.skip_until(/\n/) && strscan.pos <= old_pos
|
||||
|
||||
@previous_pos = (eos? ? pos : pos + 1)
|
||||
@previous_line_number
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# -*- 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.
|
||||
#++
|
||||
#
|
||||
# This file is based on code originally from the Stringex library and needs the data files from
|
||||
# Stringex to work correctly.
|
||||
|
||||
module Kramdown
|
||||
module Utils
|
||||
|
||||
# Provides the ability to tranliterate Unicode strings into plain ASCII ones.
|
||||
module Unidecoder
|
||||
|
||||
gem 'stringex'
|
||||
path = $:.find do |dir|
|
||||
File.directory?(File.join(File.expand_path(dir), "stringex", "unidecoder_data"))
|
||||
end
|
||||
|
||||
if path
|
||||
CODEPOINTS = Hash.new do |h, k|
|
||||
h[k] = YAML.load_file(File.join(path, "stringex", "unidecoder_data", "#{k}.yml"))
|
||||
end
|
||||
|
||||
# Transliterate string from Unicode into ASCII.
|
||||
def self.decode(string)
|
||||
string.gsub(/[^\x00-\x7f]/u) do |codepoint|
|
||||
unpacked = codepoint.unpack1("U")
|
||||
CODEPOINTS[sprintf("x%02x", unpacked >> 8)][unpacked & 255]
|
||||
rescue StandardError
|
||||
"?"
|
||||
end
|
||||
end
|
||||
else
|
||||
def self.decode(string)
|
||||
string
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- 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
|
||||
|
||||
# The kramdown version.
|
||||
VERSION = '2.5.1'
|
||||
|
||||
end
|
||||
Reference in New Issue
Block a user