Class: Kramdown::Parser::GFM

Inherits:
Kramdown
  • Object
show all
Defined in:
lib/kramdown/parser/gfm.rb

Overview

This class provides a parser implementation for the GFM dialect of Markdown.

Constant Summary collapse

VERSION =
'1.1.0'
NON_WORD_RE =
/[^\p{Word}\- \t]/.freeze
ATX_HEADER_START =
/^(?<level>\#{1,6})[\t ]+(?<contents>.*)\n/.freeze
FENCED_CODEBLOCK_START =
/^[ ]{0,3}[~`]{3,}/.freeze
FENCED_CODEBLOCK_MATCH =
/^[ ]{0,3}(([~`]){3,})\s*?((\S+?)(?:\?\S*)?)?\s*?\n(.*?)^[ ]{0,3}\1\2*\s*?\n/m.freeze
STRIKETHROUGH_DELIM =
/~~/.freeze
STRIKETHROUGH_MATCH =
/#{STRIKETHROUGH_DELIM}(?!\s|~).*?[^\s~]#{STRIKETHROUGH_DELIM}/m.freeze
LIST_TYPES =
[:ul, :ol].freeze
ESCAPED_CHARS_GFM =
/\\([\\.*_+`<>()\[\]{}#!:\|"'\$=\-~])/.freeze
PARAGRAPH_END_GFM =
Regexp.union(
  LAZY_END, LIST_START, ATX_HEADER_START, DEFINITION_LIST_START,
  BLOCKQUOTE_START, FENCED_CODEBLOCK_START
)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, options) ⇒ GFM

Returns a new instance of GFM.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/kramdown/parser/gfm.rb', line 25

def initialize(source, options)
  super
  @options[:auto_id_stripping] = true
  @id_counter = Hash.new(-1)

  @span_parsers.delete(:line_break)       if @options[:hard_wrap]
  @span_parsers.delete(:typographic_syms) if @options[:gfm_quirks].include?(:no_auto_typographic)

  if @options[:gfm_quirks].include?(:paragraph_end)
    atx_header_parser = :atx_header_gfm_quirk
    @paragraph_end    = self.class::PARAGRAPH_END_GFM
  else
    atx_header_parser = :atx_header_gfm
    @paragraph_end    = self.class::PARAGRAPH_END
  end

  {codeblock_fenced: :codeblock_fenced_gfm,
   atx_header: atx_header_parser}.each do |current, replacement|
    i = @block_parsers.index(current)
    @block_parsers.delete(current)
    @block_parsers.insert(i, replacement)
  end

  i = @span_parsers.index(:escaped_chars)
  @span_parsers[i] = :escaped_chars_gfm if i
  @span_parsers << :strikethrough_gfm

  @hard_line_break = "#{@options[:hard_wrap] ? '' : '\\'}\n"
end

Instance Attribute Details

#paragraph_endObject (readonly)

Returns the value of attribute paragraph_end.



23
24
25
# File 'lib/kramdown/parser/gfm.rb', line 23

def paragraph_end
  @paragraph_end
end

Instance Method Details

#generate_gfm_header_id(text) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
# File 'lib/kramdown/parser/gfm.rb', line 108

def generate_gfm_header_id(text)
  result = text.downcase
  result.gsub!(NON_WORD_RE, '')
  result.tr!(" \t", '-')

  @id_counter[result] += 1
  counter_result = @id_counter[result]
  result << "-#{counter_result}" if counter_result > 0

  @options[:auto_id_prefix] + result
end

#parseObject



55
56
57
58
# File 'lib/kramdown/parser/gfm.rb', line 55

def parse
  super
  update_elements(@root)
end

#parse_atx_header_gfm_quirkObject

Copied from kramdown/parser/kramdown/header.rb, removed the first line



125
126
127
128
129
130
131
132
# File 'lib/kramdown/parser/gfm.rb', line 125

def parse_atx_header_gfm_quirk
  text, id = parse_header_contents
  text.sub!(/[\t ]#+\z/, '') && text.rstrip!
  return false if text.empty?

  add_header(@src["level"].length, text, id)
  true
end

#parse_listObject

To handle task-lists we override the parse method for lists, converting matching text into checkbox input elements where necessary (as well as applying classes to the ul/ol and li elements).



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/kramdown/parser/gfm.rb', line 163

def parse_list
  super
  current_list = @tree.children.select { |element| LIST_TYPES.include?(element.type) }.last

  is_tasklist   = false
  box_unchecked = '<input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />'
  box_checked   = '<input type="checkbox" class="task-list-item-checkbox" ' \
    'disabled="disabled" checked="checked" />'

  current_list.children.each do |li|
    list_items = li.children
    next unless !list_items.empty? && list_items[0].type == :p

    # li -> p -> raw_text
    descendant = list_items[0].children[0].value
    checked    = descendant.gsub!(/\A\s*\[ \]\s+/,  box_unchecked)
    unchecked  = descendant.gsub!(/\A\s*\[x\]\s+/i, box_checked)
    is_tasklist ||= checked || unchecked

    li.attr['class'] = 'task-list-item' if is_tasklist
  end

  current_list.attr['class'] = 'task-list' if is_tasklist

  true
end

#parse_strikethrough_gfmObject



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/kramdown/parser/gfm.rb', line 142

def parse_strikethrough_gfm
  line_number = @src.current_line_number

  @src.pos += @src.matched_size
  el = Element.new(:html_element, 'del', {}, category: :span, line: line_number)
  @tree.children << el

  env = save_env
  reset_env(src: Kramdown::Utils::StringScanner.new(@src.matched[2..-3], line_number),
            text_type: :text)
  parse_spans(el)
  restore_env(env)

  el
end

#update_elements(element) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/kramdown/parser/gfm.rb', line 60

def update_elements(element)
  element.children.map! do |child|
    if child.type == :text && child.value.include?(@hard_line_break)
      update_text_type(element, child)
    elsif child.type == :html_element
      child
    elsif child.type == :header && @options[:auto_ids] && !child.attr.key?('id')
      child.attr['id'] = generate_gfm_header_id(child.options[:raw_text])
      child
    else
      update_elements(child)
      child
    end
  end.flatten!
end

#update_raw_text(item) ⇒ Object

Update the raw text for automatic ID generation.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/kramdown/parser/gfm.rb', line 77

def update_raw_text(item)
  raw_text = +''

  append_text = lambda do |child|
    case child.type
    when :text, :codespan, :math
      raw_text << child.value
    when :entity
      raw_text << child.value.char
    when :smart_quote
      raw_text << ::Kramdown::Utils::Entities.entity(child.value.to_s).char
    when :typographic_sym
      raw_text << case child.value
                  when :laquo_space
                    "« "
                  when :raquo_space
                    " »"
                  else
                    ::Kramdown::Utils::Entities.entity(child.value.to_s).char
                  end
    else
      child.children.each { |c| append_text.call(c) }
    end
  end

  append_text.call(item)
  item.options[:raw_text] = raw_text
end