# -*- coding: utf-8; frozen_string_literal: true -*- # #-- # Copyright (C) 2009-2019 Thomas Leitner # # 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