summaryrefslogtreecommitdiff
path: root/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter')
-rw-r--r--vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/base.rb257
-rw-r--r--vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/hash_ast.rb38
-rw-r--r--vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/html.rb545
-rw-r--r--vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/kramdown.rb458
-rw-r--r--vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/latex.rb626
-rw-r--r--vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/man.rb300
-rw-r--r--vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/math_engine/mathjax.rb32
-rw-r--r--vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/remove_html_tags.rb58
-rw-r--r--vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/syntax_highlighter.rb56
-rw-r--r--vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/syntax_highlighter/minted.rb35
-rw-r--r--vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/syntax_highlighter/rouge.rb85
-rw-r--r--vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/toc.rb69
12 files changed, 2559 insertions, 0 deletions
diff --git a/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/base.rb b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/base.rb
new file mode 100644
index 0000000..8809782
--- /dev/null
+++ b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/base.rb
@@ -0,0 +1,257 @@
+# -*- coding: utf-8; frozen_string_literal: true -*-
+#
+#--
+# Copyright (C) 2009-2026 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
diff --git a/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/hash_ast.rb b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/hash_ast.rb
new file mode 100644
index 0000000..f69d388
--- /dev/null
+++ b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/hash_ast.rb
@@ -0,0 +1,38 @@
+# -*- coding: utf-8; frozen_string_literal: true -*-
+#
+#--
+# Copyright (C) 2009-2026 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
diff --git a/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/html.rb b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/html.rb
new file mode 100644
index 0000000..783c088
--- /dev/null
+++ b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/html.rb
@@ -0,0 +1,545 @@
+# -*- coding: utf-8; frozen_string_literal: true -*-
+#
+#--
+# Copyright (C) 2009-2026 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}\">&#8901;</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
diff --git a/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/kramdown.rb b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/kramdown.rb
new file mode 100644
index 0000000..07106de
--- /dev/null
+++ b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/kramdown.rb
@@ -0,0 +1,458 @@
+# -*- coding: utf-8; frozen_string_literal: true -*-
+#
+#--
+# Copyright (C) 2009-2026 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
+ "![#{alt_text}](#{link}#{title})"
+ 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(/"/, '&quot;') + '"'
+ end
+
+ # :startdoc:
+
+ end
+
+ end
+end
diff --git a/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/latex.rb b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/latex.rb
new file mode 100644
index 0000000..7f23338
--- /dev/null
+++ b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/latex.rb
@@ -0,0 +1,626 @@
+# -*- coding: utf-8; frozen_string_literal: true -*-
+#
+#--
+# Copyright (C) 2009-2026 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
diff --git a/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/man.rb b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/man.rb
new file mode 100644
index 0000000..dd84540
--- /dev/null
+++ b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/man.rb
@@ -0,0 +1,300 @@
+# -*- coding: utf-8; frozen_string_literal: true -*-
+#
+#--
+# Copyright (C) 2009-2026 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
diff --git a/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/math_engine/mathjax.rb b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/math_engine/mathjax.rb
new file mode 100644
index 0000000..3248e93
--- /dev/null
+++ b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/math_engine/mathjax.rb
@@ -0,0 +1,32 @@
+# -*- coding: utf-8; frozen_string_literal: true -*-
+#
+#--
+# Copyright (C) 2009-2026 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
diff --git a/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/remove_html_tags.rb b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/remove_html_tags.rb
new file mode 100644
index 0000000..a7db73e
--- /dev/null
+++ b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/remove_html_tags.rb
@@ -0,0 +1,58 @@
+# -*- coding: utf-8; frozen_string_literal: true -*-
+#
+#--
+# Copyright (C) 2009-2026 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
diff --git a/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/syntax_highlighter.rb b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/syntax_highlighter.rb
new file mode 100644
index 0000000..fd9316d
--- /dev/null
+++ b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/syntax_highlighter.rb
@@ -0,0 +1,56 @@
+# -*- coding: utf-8; frozen_string_literal: true -*-
+#
+#--
+# Copyright (C) 2009-2026 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
diff --git a/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/syntax_highlighter/minted.rb b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/syntax_highlighter/minted.rb
new file mode 100644
index 0000000..21c26ec
--- /dev/null
+++ b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/syntax_highlighter/minted.rb
@@ -0,0 +1,35 @@
+# -*- coding: utf-8; frozen_string_literal: true -*-
+#
+#--
+# Copyright (C) 2009-2026 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
diff --git a/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/syntax_highlighter/rouge.rb b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/syntax_highlighter/rouge.rb
new file mode 100644
index 0000000..a286c4d
--- /dev/null
+++ b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/syntax_highlighter/rouge.rb
@@ -0,0 +1,85 @@
+# -*- coding: utf-8; frozen_string_literal: true -*-
+#
+#--
+# Copyright (C) 2009-2026 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
diff --git a/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/toc.rb b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/toc.rb
new file mode 100644
index 0000000..725caa2
--- /dev/null
+++ b/vendor/bundle/ruby/3.4.0/gems/kramdown-2.5.2/lib/kramdown/converter/toc.rb
@@ -0,0 +1,69 @@
+# -*- coding: utf-8; frozen_string_literal: true -*-
+#
+#--
+# Copyright (C) 2009-2026 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