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.0.1'
NON_WORD_RE =
/[^\p{Word}\- \t]/
ATX_HEADER_START =
/^(?<level>\#{1,6})[\t ]+(?<contents>.*)\n/
FENCED_CODEBLOCK_START =
/^[ ]{0,3}[~`]{3,}/
FENCED_CODEBLOCK_MATCH =
/^[ ]{0,3}(([~`]){3,})\s*?((\S+?)(?:\?\S*)?)?\s*?\n(.*?)^[ ]{0,3}\1\2*\s*?\n/m
STRIKETHROUGH_DELIM =
/~~/
STRIKETHROUGH_MATCH =
/#{STRIKETHROUGH_DELIM}[^\s~](.*?)[^\s~]#{STRIKETHROUGH_DELIM}/m
ESCAPED_CHARS_GFM =
/\\([\\.*_+`<>()\[\]{}#!:\|"'\$=\-~])/
PARAGRAPH_END_GFM =
/#{LAZY_END}|#{LIST_START}|#{ATX_HEADER_START}|
#{DEFINITION_LIST_START}|#{BLOCKQUOTE_START}|#{FENCED_CODEBLOCK_START}/x

Instance Method Summary collapse

Constructor Details

#initialize(source, options) ⇒ GFM

Returns a new instance of GFM.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/kramdown/parser/gfm.rb', line 71

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
end

Instance Method Details

#generate_gfm_header_id(text) ⇒ Object



161
162
163
164
165
166
167
168
# File 'lib/kramdown/parser/gfm.rb', line 161

def generate_gfm_header_id(text)
  result = text.downcase
  result.gsub!(NON_WORD_RE, '')
  result.tr!(" \t", '-')
  @id_counter[result] += 1
  result << (@id_counter[result] > 0 ? "-#{@id_counter[result]}" : '')
  @options[:auto_id_prefix] + result
end

#paragraph_endObject



240
241
242
# File 'lib/kramdown/parser/gfm.rb', line 240

def paragraph_end
  @paragraph_end
end

#parseObject



98
99
100
101
# File 'lib/kramdown/parser/gfm.rb', line 98

def parse
  super
  update_elements(@root)
end

#parse_atx_header_gfm_quirkObject

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



175
176
177
178
179
180
181
# File 'lib/kramdown/parser/gfm.rb', line 175

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).



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/kramdown/parser/gfm.rb', line 210

def parse_list
  super
  current_list = @tree.children.select {|element| [:ul, :ol].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|
    next unless !li.children.empty? && li.children[0].type == :p
    # li -> p -> raw_text
    checked = li.children[0].children[0].value.gsub!(/\A\s*\[ \]\s+/, box_unchecked)
    unchecked = li.children[0].children[0].value.gsub!(/\A\s*\[x\]\s+/i, box_checked)
    is_tasklist ||= (!checked.nil? || !unchecked.nil?)

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

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

  true
end

#parse_strikethrough_gfmObject



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/kramdown/parser/gfm.rb', line 191

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



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/kramdown/parser/gfm.rb', line 103

def update_elements(element)
  element.children.map! do |child|
    if child.type == :text &&
        child.value.include?(hard_line_break = "#{@options[:hard_wrap] ? '' : '\\'}\n")
      children = []
      lines = child.value.split(hard_line_break, -1)
      omit_trailing_br = (Kramdown::Element.category(element) == :block &&
                          element.children[-1] == child && lines[-1].empty?)
      lines.each_with_index do |line, index|
        new_element_options = {location: child.options[:location] + index}

        children << Element.new(:text, (index > 0 ? "\n#{line}" : line), nil, new_element_options)
        children << Element.new(:br, nil, nil, new_element_options) if index < lines.size - 2 ||
          (index == lines.size - 2 && !omit_trailing_br)
      end
      children
    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.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/kramdown/parser/gfm.rb', line 132

def update_raw_text(item)
  raw_text = +''

  append_text = lambda do |child|
    if child.type == :text || child.type == :codespan || child.type == :math
      raw_text << child.value
    elsif child.type == :entity
      raw_text << child.value.char
    elsif child.type == :smart_quote
      raw_text << ::Kramdown::Utils::Entities.entity(child.value.to_s).char
    elsif child.type == :typographic_sym
      raw_text << if child.value == :laquo_space
                    "« "
                  elsif child.value == :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