kramdown 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of kramdown might be problematic. Click here for more details.

Files changed (66) hide show
  1. data/ChangeLog +346 -0
  2. data/Rakefile +36 -29
  3. data/VERSION +1 -1
  4. data/benchmark/testing.sh +1 -1
  5. data/bin/kramdown +0 -4
  6. data/doc/index.page +1 -1
  7. data/doc/links.markdown +4 -0
  8. data/doc/news.page +2 -1
  9. data/doc/quickref.page +134 -125
  10. data/doc/syntax.page +304 -302
  11. data/lib/kramdown/converter/base.rb +14 -0
  12. data/lib/kramdown/converter/html.rb +64 -2
  13. data/lib/kramdown/converter/latex.rb +14 -7
  14. data/lib/kramdown/document.rb +7 -3
  15. data/lib/kramdown/options.rb +13 -1
  16. data/lib/kramdown/parser/kramdown.rb +70 -17
  17. data/lib/kramdown/parser/kramdown/abbreviation.rb +65 -0
  18. data/lib/kramdown/parser/kramdown/attribute_list.rb +2 -1
  19. data/lib/kramdown/parser/kramdown/blank_line.rb +1 -1
  20. data/lib/kramdown/parser/kramdown/blockquote.rb +1 -1
  21. data/lib/kramdown/parser/kramdown/codeblock.rb +2 -2
  22. data/lib/kramdown/parser/kramdown/eob.rb +1 -1
  23. data/lib/kramdown/parser/kramdown/extension.rb +86 -6
  24. data/lib/kramdown/parser/kramdown/header.rb +2 -17
  25. data/lib/kramdown/parser/kramdown/horizontal_rule.rb +1 -1
  26. data/lib/kramdown/parser/kramdown/list.rb +8 -2
  27. data/lib/kramdown/parser/kramdown/math.rb +1 -1
  28. data/lib/kramdown/parser/kramdown/paragraph.rb +1 -1
  29. data/lib/kramdown/parser/kramdown/smart_quotes.rb +2 -2
  30. data/lib/kramdown/parser/kramdown/table.rb +1 -1
  31. data/lib/kramdown/version.rb +1 -1
  32. data/man/man1/kramdown.1 +77 -63
  33. data/test/testcases/block/04_header/with_auto_id_prefix.html +3 -0
  34. data/test/testcases/block/04_header/with_auto_id_prefix.options +2 -0
  35. data/test/testcases/block/04_header/with_auto_id_prefix.text +3 -0
  36. data/test/testcases/block/08_list/item_ial.html +9 -0
  37. data/test/testcases/block/08_list/item_ial.text +5 -0
  38. data/test/testcases/block/11_ial/auto_id_and_ial.html +1 -1
  39. data/test/testcases/block/11_ial/auto_id_and_ial.text +1 -1
  40. data/test/testcases/block/11_ial/simple.html +5 -0
  41. data/test/testcases/block/11_ial/simple.text +7 -0
  42. data/test/testcases/block/12_extension/comment.text +5 -5
  43. data/test/testcases/block/12_extension/ignored.html +0 -2
  44. data/test/testcases/block/12_extension/ignored.text +3 -6
  45. data/test/testcases/block/12_extension/nomarkdown.text +4 -4
  46. data/test/testcases/block/12_extension/options.html +1 -1
  47. data/test/testcases/block/12_extension/options.text +5 -6
  48. data/test/testcases/block/12_extension/options2.text +1 -1
  49. data/test/testcases/block/12_extension/options3.text +1 -1
  50. data/test/testcases/span/abbreviations/abbrev.html +8 -0
  51. data/test/testcases/span/abbreviations/abbrev.text +15 -0
  52. data/test/testcases/span/abbreviations/abbrev_defs.html +2 -0
  53. data/test/testcases/span/abbreviations/abbrev_defs.text +5 -0
  54. data/test/testcases/span/extension/comment.html +6 -0
  55. data/test/testcases/span/extension/comment.text +6 -0
  56. data/test/testcases/span/extension/ignored.html +1 -0
  57. data/test/testcases/span/extension/ignored.text +1 -0
  58. data/test/testcases/span/extension/nomarkdown.html +1 -0
  59. data/test/testcases/span/extension/nomarkdown.text +1 -0
  60. data/test/testcases/span/extension/options.html +1 -0
  61. data/test/testcases/span/extension/options.text +1 -0
  62. data/test/testcases/span/ial/simple.html +2 -1
  63. data/test/testcases/span/ial/simple.text +1 -0
  64. data/test/testcases/span/text_substitutions/typography.html +3 -0
  65. data/test/testcases/span/text_substitutions/typography.text +3 -0
  66. metadata +275 -263
@@ -68,6 +68,20 @@ module Kramdown
68
68
  end
69
69
  end
70
70
 
71
+
72
+ # Generate an alpha-numeric ID from the the string +str+.
73
+ def generate_id(str)
74
+ gen_id = str.gsub(/[^a-zA-Z0-9 -]/, '').gsub(/^[^a-zA-Z]*/, '').gsub(' ', '-').downcase
75
+ gen_id = 'section' if gen_id.length == 0
76
+ @used_ids ||= {}
77
+ if @used_ids.has_key?(gen_id)
78
+ gen_id += '-' + (@used_ids[gen_id] += 1).to_s
79
+ else
80
+ @used_ids[gen_id] = 0
81
+ end
82
+ @doc.options[:auto_id_prefix] + gen_id
83
+ end
84
+
71
85
  end
72
86
 
73
87
  end
@@ -48,6 +48,8 @@ module Kramdown
48
48
  super
49
49
  @footnote_counter = @footnote_start = @doc.options[:footnote_nr]
50
50
  @footnotes = []
51
+ @toc = []
52
+ @toc_code = nil
51
53
  end
52
54
 
53
55
  def convert(el, indent = -INDENTATION, opts = {})
@@ -109,6 +111,11 @@ module Kramdown
109
111
  end
110
112
 
111
113
  def convert_header(el, indent, opts)
114
+ el = Marshal.load(Marshal.dump(el)) # so that the original is not changed
115
+ if @doc.options[:auto_ids] && !(el.options[:attr] && el.options[:attr]['id'])
116
+ (el.options[:attr] ||= {})['id'] = generate_id(el.options[:raw_text])
117
+ end
118
+ @toc << [el.options[:level], el.options[:attr]['id'], el.children] if el.options[:attr] && el.options[:attr]['id']
112
119
  "#{' '*indent}<h#{el.options[:level]}#{options_for_element(el)}>#{inner(el, indent, opts)}</h#{el.options[:level]}>\n"
113
120
  end
114
121
 
@@ -117,7 +124,12 @@ module Kramdown
117
124
  end
118
125
 
119
126
  def convert_ul(el, indent, opts)
120
- "#{' '*indent}<#{el.type}#{options_for_element(el)}>\n#{inner(el, indent, opts)}#{' '*indent}</#{el.type}>\n"
127
+ if !@toc_code && (el.options[:ial][:refs].include?('toc') rescue nil) && (el.type == :ul || el.type == :ol)
128
+ @toc_code = [el.type, (0..128).to_a.map{|a| rand(36).to_s(36)}.join]
129
+ @toc_code.last
130
+ else
131
+ "#{' '*indent}<#{el.type}#{options_for_element(el)}>\n#{inner(el, indent, opts)}#{' '*indent}</#{el.type}>\n"
132
+ end
121
133
  end
122
134
  alias :convert_ol :convert_ul
123
135
  alias :convert_dl :convert_ul
@@ -269,8 +281,58 @@ module Kramdown
269
281
  "<#{type}#{options_for_element(el)}>#{escape_html(el.value, :text)}</#{type}>#{type == 'div' ? "\n" : ''}"
270
282
  end
271
283
 
284
+ def convert_abbreviation(el, indent, opts)
285
+ title = @doc.parse_infos[:abbrev_defs][el.value]
286
+ title = nil if title.empty?
287
+ "<abbr#{title ? " title=\"#{title}\"" : ''}>#{el.value}</abbr>"
288
+ end
289
+
272
290
  def convert_root(el, indent, opts)
273
- inner(el, indent, opts) << footnote_content
291
+ result = inner(el, indent, opts)
292
+ result << footnote_content
293
+ if @toc_code
294
+ toc_tree = generate_toc_tree(@toc, @toc_code.first)
295
+ text = if toc_tree.children.size > 0
296
+ convert(toc_tree, 0)
297
+ else
298
+ ''
299
+ end
300
+ result.sub!(/#{@toc_code.last}/, text)
301
+ end
302
+ result
303
+ end
304
+
305
+ def generate_toc_tree(toc, type)
306
+ sections = Element.new(type, nil, {:attr => {:id => 'markdown-toc'}})
307
+ stack = []
308
+ toc.each do |level, id, children|
309
+ li = Element.new(:li, nil, {:level => level})
310
+ a = Element.new(:a, nil, {:attr => {:href => "##{id}"}})
311
+ a.children += children
312
+ li.children << a
313
+ li.children << Element.new(type)
314
+
315
+ success = false
316
+ while !success
317
+ if stack.empty?
318
+ sections.children << li
319
+ stack << li
320
+ success = true
321
+ elsif stack.last.options[:level] < li.options[:level]
322
+ stack.last.children.last.children << li
323
+ stack << li
324
+ success = true
325
+ else
326
+ item = stack.pop
327
+ item.children.pop unless item.children.last.children.size > 0
328
+ end
329
+ end
330
+ end
331
+ while !stack.empty?
332
+ item = stack.pop
333
+ item.children.pop unless item.children.last.children.size > 0
334
+ end
335
+ sections
274
336
  end
275
337
 
276
338
  # Helper method for obfuscating the +text+ by using HTML entities.
@@ -103,8 +103,9 @@ module Kramdown
103
103
  }
104
104
  def convert_header(el, opts)
105
105
  type = HEADER_TYPES[el.options[:level]]
106
- if el.options[:attr] && (id = el.options[:attr]['id'])
107
- "\\hypertarget{#{id}}{}\\#{type}*{#{inner(el, opts)}}\\label{#{id}}\n\n"
106
+ if (el.options[:attr] && (id = el.options[:attr]['id'])) ||
107
+ (@doc.options[:auto_ids] && (id = generate_id(el.options[:raw_text])))
108
+ "\\hypertarget{#{id}}{}\\#{type}{#{inner(el, opts)}}\\label{#{id}}\n\n"
108
109
  else
109
110
  "\\#{type}*{#{inner(el, opts)}}\n\n"
110
111
  end
@@ -115,12 +116,14 @@ module Kramdown
115
116
  end
116
117
 
117
118
  def convert_ul(el, opts)
118
- latex_environment('itemize', inner(el, opts))
119
- end
120
-
121
- def convert_ol(el, opts)
122
- latex_environment('enumerate', inner(el, opts))
119
+ if !@doc.conversion_infos[:has_toc] && (el.options[:ial][:refs].include?('toc') rescue nil)
120
+ @doc.conversion_infos[:has_toc] = true
121
+ '\tableofcontents'
122
+ else
123
+ latex_environment(el.type == :ul ? 'itemize' : 'enumerate', inner(el, opts))
124
+ end
123
125
  end
126
+ alias :convert_ol :convert_ul
124
127
 
125
128
  def convert_dl(el, opts)
126
129
  latex_environment('description', inner(el, opts))
@@ -514,6 +517,10 @@ EOF
514
517
  end
515
518
  end
516
519
 
520
+ def convert_abbreviation(el, indent, opts)
521
+ el.value
522
+ end
523
+
517
524
  ESCAPE_MAP = {
518
525
  "^" => "\\^{}",
519
526
  "\\" => "\\textbackslash{}",
@@ -91,9 +91,13 @@ module Kramdown
91
91
  @warnings = []
92
92
  @parse_infos = {}
93
93
  @conversion_infos = {}
94
- @tree = Parser.const_get((options[:input] || 'kramdown').to_s.capitalize).parse(source, self)
95
- rescue NameError
96
- raise Kramdown::Error.new("Invalid input format selected: #{options[:input]}")
94
+ parser = (options[:input] || 'kramdown').to_s
95
+ parser = parser[0..0].upcase + parser[1..-1]
96
+ if Parser.const_defined?(parser)
97
+ @tree = Parser.const_get(parser).parse(source, self)
98
+ else
99
+ raise Kramdown::Error.new("kramdown has no parser to handle the specified input format: #{options[:input]}")
100
+ end
97
101
  end
98
102
 
99
103
  # Check if a method is invoked that begins with +to_+ and if so, try to instantiate a converter
@@ -148,7 +148,19 @@ If this option is `true`, ID values for all headers are automatically
148
148
  generated if no ID is explicitly specified.
149
149
 
150
150
  Default: true
151
- Used by: kramdown parser
151
+ Used by: HTML/Latex converter
152
+ EOF
153
+
154
+ define(:auto_id_prefix, String, '', <<EOF)
155
+ Prefix used for automatically generated heaer IDs
156
+
157
+ This option can be used to set a prefix for the automatically generated
158
+ header IDs so that there is no conflict when rendering multiple kramdown
159
+ documents into one output file separately. The prefix should only
160
+ contain characters that are valid in an ID!
161
+
162
+ Default: ''
163
+ Used by: HTML/Latex converter
152
164
  EOF
153
165
 
154
166
  define(:parse_block_html, Boolean, false, <<EOF)
@@ -31,6 +31,44 @@ module Kramdown
31
31
  module Parser
32
32
 
33
33
  # Used for parsing a document in kramdown format.
34
+ #
35
+ # If you want to extend the functionality of the parser, you need to the following:
36
+ #
37
+ # * Create a new subclass
38
+ # * add the needed parser methods
39
+ # * modify the @block_parsers and @span_parsers variables and add the names of your parser
40
+ # methods
41
+ #
42
+ # Here is a small example for an extended parser class that parses ERB style tags as raw text if
43
+ # they are used as span level elements (an equivalent block level parser should probably also be
44
+ # made to handle the block case):
45
+ #
46
+ # require 'kramdown/parser/kramdown'
47
+ #
48
+ # class Kramdown::Parser::ERBKramdown < Kramdown::Parser::Kramdown
49
+ #
50
+ # def initialize(doc)
51
+ # super(doc)
52
+ # @span_parsers.unshift(:erb_tags)
53
+ # end
54
+ #
55
+ # ERB_TAGS_START = /<%.*?%>/
56
+ #
57
+ # def parse_erb_tags
58
+ # @src.pos += @src.matched_size
59
+ # @tree.children << Element.new(:raw, @src.matched)
60
+ # end
61
+ # define_parser(:erb_tags, ERB_TAGS_START, '<%')
62
+ #
63
+ # end
64
+ #
65
+ # The new parser can be used like this:
66
+ #
67
+ # require 'kramdown/document'
68
+ # # require the file with the above parser class
69
+ #
70
+ # Kramdown::Document.new(input_text, :input => 'ERBKramdown').to_html
71
+ #
34
72
  class Kramdown
35
73
 
36
74
  include ::Kramdown
@@ -48,10 +86,20 @@ module Kramdown
48
86
  @tree = nil
49
87
  @stack = []
50
88
  @text_type = :text
89
+ @block_ial = nil
51
90
 
52
91
  @doc.parse_infos[:ald] = {}
53
92
  @doc.parse_infos[:link_defs] = {}
93
+ @doc.parse_infos[:abbrev_defs] = {}
54
94
  @doc.parse_infos[:footnotes] = {}
95
+
96
+ @block_parsers = [:blank_line, :codeblock, :codeblock_fenced, :blockquote, :table, :atx_header,
97
+ :setext_header, :horizontal_rule, :list, :definition_list, :link_definition, :block_html,
98
+ :footnote_definition, :abbrev_definition, :ald, :block_math, :extension_block_depr,
99
+ :block_extension, :block_ial, :eob_marker, :paragraph]
100
+ @span_parsers = [:emphasis, :codespan, :autolink, :span_html, :footnote_marker, :link, :smart_quotes, :inline_math,
101
+ :span_extension, :span_ial, :html_entity, :typographic_syms, :line_break, :escaped_chars]
102
+
55
103
  end
56
104
  private_class_method(:new, :allocate)
57
105
 
@@ -67,6 +115,7 @@ module Kramdown
67
115
  tree = Element.new(:root)
68
116
  parse_blocks(tree, adapt_source(source))
69
117
  update_tree(tree)
118
+ replace_abbreviations(tree)
70
119
  @doc.parse_infos[:footnotes].each do |name, data|
71
120
  update_tree(data[:content])
72
121
  end
@@ -80,19 +129,13 @@ module Kramdown
80
129
  end
81
130
 
82
131
  #######
83
- private
132
+ protected
84
133
  #######
85
134
 
86
- BLOCK_PARSERS = [:blank_line, :codeblock, :codeblock_fenced, :blockquote, :table, :atx_header,
87
- :setext_header, :horizontal_rule, :list, :definition_list, :link_definition, :block_html,
88
- :footnote_definition, :ald, :block_ial, :block_math, :extension_block, :eob_marker, :paragraph]
89
- SPAN_PARSERS = [:emphasis, :codespan, :autolink, :span_html, :footnote_marker, :link, :smart_quotes, :inline_math,
90
- :span_ial, :html_entity, :typographic_syms, :line_break, :escaped_chars]
91
-
92
135
  # Adapt the object to allow parsing like specified in the options.
93
136
  def configure_parser
94
137
  @parsers = {}
95
- (BLOCK_PARSERS + SPAN_PARSERS).each do |name|
138
+ (@block_parsers + @span_parsers).each do |name|
96
139
  if self.class.has_parser?(name)
97
140
  @parsers[name] = self.class.parser(name)
98
141
  else
@@ -103,7 +146,7 @@ module Kramdown
103
146
  end
104
147
 
105
148
  # Create the needed span parser regexps.
106
- def span_parser_regexps(parsers = SPAN_PARSERS)
149
+ def span_parser_regexps(parsers = @span_parsers)
107
150
  span_start = /#{parsers.map {|name| @parsers[name].span_start}.join('|')}/
108
151
  [span_start, /(?=#{span_start})/]
109
152
  end
@@ -115,7 +158,8 @@ module Kramdown
115
158
 
116
159
  status = catch(:stop_block_parsing) do
117
160
  while !@src.eos?
118
- BLOCK_PARSERS.any? do |name|
161
+ block_ial_set = @block_ial
162
+ @block_parsers.any? do |name|
119
163
  if @src.check(@parsers[name].start_re)
120
164
  send(@parsers[name].method)
121
165
  else
@@ -125,6 +169,7 @@ module Kramdown
125
169
  warning('Warning: this should not occur - no block parser handled the line')
126
170
  add_text(@src.scan(/.*\n/))
127
171
  end
172
+ @block_ial = nil if block_ial_set
128
173
  end
129
174
  end
130
175
 
@@ -157,7 +202,7 @@ module Kramdown
157
202
  span_start = @span_start
158
203
  span_start_re = @span_start_re
159
204
  span_start, span_start_re = span_parser_regexps(parsers) if parsers
160
- parsers = parsers || SPAN_PARSERS
205
+ parsers = parsers || @span_parsers
161
206
 
162
207
  used_re = (stop_re.nil? ? span_start_re : /(?=#{Regexp.union(stop_re, span_start)})/)
163
208
  stop_re_found = false
@@ -211,20 +256,27 @@ module Kramdown
211
256
  ial.each {|k,v| attr[k] = v if k.kind_of?(String) && k != 'class' }
212
257
  end
213
258
 
259
+ # Create a new block level element, taking care of applying a preceding block IAL if it exists.
260
+ def new_block_el(*args)
261
+ el = Element.new(*args)
262
+ el.options[:ial] = @block_ial if @block_ial && el.type != :blank && el.type != :eob
263
+ el
264
+ end
265
+
214
266
  # Extract the part of the StringScanner backed string specified by the +range+. This method
215
267
  # also works correctly under Ruby 1.9.
216
- def extract_string(range)
268
+ def extract_string(range, strscan = @src)
217
269
  result = nil
218
270
  if RUBY_VERSION >= '1.9'
219
271
  begin
220
- enc = @src.string.encoding
221
- @src.string.force_encoding('ASCII-8BIT')
222
- result = @src.string[range].force_encoding(enc)
272
+ enc = strscan.string.encoding
273
+ strscan.string.force_encoding('ASCII-8BIT')
274
+ result = strscan.string[range].force_encoding(enc)
223
275
  ensure
224
- @src.string.force_encoding(enc)
276
+ strscan.string.force_encoding(enc)
225
277
  end
226
278
  else
227
- result = @src.string[range]
279
+ result = strscan.string[range]
228
280
  end
229
281
  result
230
282
  end
@@ -285,6 +337,7 @@ module Kramdown
285
337
  require 'kramdown/parser/kramdown/emphasis'
286
338
  require 'kramdown/parser/kramdown/smart_quotes'
287
339
  require 'kramdown/parser/kramdown/math'
340
+ require 'kramdown/parser/kramdown/abbreviation'
288
341
 
289
342
  end
290
343
 
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ #--
4
+ # Copyright (C) 2009-2010 Thomas Leitner <t_leitner@gmx.at>
5
+ #
6
+ # This file is part of kramdown.
7
+ #
8
+ # kramdown is free software: you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
+ #++
21
+ #
22
+
23
+ module Kramdown
24
+ module Parser
25
+ class Kramdown
26
+
27
+ ABBREV_DEFINITION_START = /^#{OPT_SPACE}\*\[(.+?)\]:(.*?)\n/
28
+
29
+ # Parse the link definition at the current location.
30
+ def parse_abbrev_definition
31
+ @src.pos += @src.matched_size
32
+ abbrev_id, abbrev_text = @src[1], @src[2].strip
33
+ warning("Duplicate abbreviation ID '#{abbrev_id}' - overwriting") if @doc.parse_infos[:abbrev_defs][abbrev_id]
34
+ @doc.parse_infos[:abbrev_defs][abbrev_id] = abbrev_text
35
+ true
36
+ end
37
+ define_parser(:abbrev_definition, ABBREV_DEFINITION_START)
38
+
39
+ # Replace the abbreviation text with elements.
40
+ def replace_abbreviations(el, regexps = nil)
41
+ return if @doc.parse_infos[:abbrev_defs].empty?
42
+ if !regexps
43
+ regexps = [Regexp.union(*@doc.parse_infos[:abbrev_defs].keys.map {|k| /#{Regexp.escape(k)}/})]
44
+ regexps << /(?=(?:\W|^)#{regexps.first}(?!\w))/ # regexp should only match on word boundaries
45
+ end
46
+ el.children.map! do |child|
47
+ if child.type == :text
48
+ result = []
49
+ strscan = StringScanner.new(child.value)
50
+ while temp = strscan.scan_until(regexps.last)
51
+ temp += strscan.scan(/\W|^/)
52
+ abbr = strscan.scan(regexps.first)
53
+ result += [Element.new(:text, temp), Element.new(:abbreviation, abbr)]
54
+ end
55
+ result + [Element.new(:text, extract_string(strscan.pos..-1, strscan))]
56
+ else
57
+ replace_abbreviations(child, regexps)
58
+ child
59
+ end
60
+ end.flatten!
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -74,6 +74,8 @@ module Kramdown
74
74
  @src.pos += @src.matched_size
75
75
  if @tree.children.last && @tree.children.last.type != :blank && @tree.children.last.type != :eob
76
76
  parse_attribute_list(@src[1], @tree.children.last.options[:ial] ||= {})
77
+ else
78
+ parse_attribute_list(@src[1], @block_ial = {})
77
79
  end
78
80
  true
79
81
  end
@@ -92,7 +94,6 @@ module Kramdown
92
94
  update_attr_with_ial(@tree.children.last.options[:attr] ||= {}, attr)
93
95
  else
94
96
  warning("Ignoring span IAL because preceding element is just text")
95
- add_text(@src.matched)
96
97
  end
97
98
  end
98
99
  define_parser(:span_ial, IAL_SPAN_START, '\{:')