PageRenderTime 35ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/kramdown/converter/kramdown.rb

http://github.com/gettalong/kramdown
Ruby | 448 lines | 393 code | 44 blank | 11 comment | 106 complexity | 9ec30f83e00053cf8da9f23d7206c200 MD5 | raw file
Possible License(s): GPL-3.0
  1. # -*- coding: utf-8; frozen_string_literal: true -*-
  2. #
  3. #--
  4. # Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
  5. #
  6. # This file is part of kramdown which is licensed under the MIT.
  7. #++
  8. #
  9. require 'kramdown/converter'
  10. require 'kramdown/utils'
  11. module Kramdown
  12. module Converter
  13. # Converts an element tree to the kramdown format.
  14. class Kramdown < Base
  15. # :stopdoc:
  16. include ::Kramdown::Utils::Html
  17. def initialize(root, options)
  18. super
  19. @linkrefs = []
  20. @footnotes = []
  21. @abbrevs = []
  22. @stack = []
  23. end
  24. def convert(el, opts = {indent: 0})
  25. res = send("convert_#{el.type}", el, opts)
  26. res = res.dup if res.frozen?
  27. if ![:html_element, :li, :dt, :dd, :td].include?(el.type) && (ial = ial_for_element(el))
  28. res << ial
  29. res << "\n\n" if el.block?
  30. elsif [:ul, :dl, :ol, :codeblock].include?(el.type) && opts[:next] &&
  31. ([el.type, :codeblock].include?(opts[:next].type) ||
  32. (opts[:next].type == :blank && opts[:nnext] &&
  33. [el.type, :codeblock].include?(opts[:nnext].type)))
  34. res << "^\n\n"
  35. elsif el.block? &&
  36. ![:li, :dd, :dt, :td, :th, :tr, :thead, :tbody, :tfoot, :blank].include?(el.type) &&
  37. (el.type != :html_element || @stack.last.type != :html_element) &&
  38. (el.type != :p || !el.options[:transparent])
  39. res << "\n"
  40. end
  41. res
  42. end
  43. def inner(el, opts = {indent: 0})
  44. @stack.push(el)
  45. result = +''
  46. el.children.each_with_index do |inner_el, index|
  47. options = opts.dup
  48. options[:index] = index
  49. options[:prev] = (index == 0 ? nil : el.children[index - 1])
  50. options[:pprev] = (index <= 1 ? nil : el.children[index - 2])
  51. options[:next] = (index == el.children.length - 1 ? nil : el.children[index + 1])
  52. options[:nnext] = (index >= el.children.length - 2 ? nil : el.children[index + 2])
  53. result << convert(inner_el, options)
  54. end
  55. @stack.pop
  56. result
  57. end
  58. def convert_blank(_el, _opts)
  59. ""
  60. end
  61. ESCAPED_CHAR_RE = /(\$\$|[\\*_`\[\]\{"'|])|^[ ]{0,3}(:)/
  62. def convert_text(el, opts)
  63. if opts[:raw_text]
  64. el.value
  65. else
  66. el.value.gsub(/\A\n/) do
  67. opts[:prev] && opts[:prev].type == :br ? '' : "\n"
  68. end.gsub(/\s+/, ' ').gsub(ESCAPED_CHAR_RE) { "\\#{$1 || $2}" }
  69. end
  70. end
  71. def convert_p(el, opts)
  72. w = @options[:line_width] - opts[:indent].to_s.to_i
  73. first, second, *rest = inner(el, opts).strip.gsub(/(.{1,#{w}})( +|$\n?)/, "\\1\n").split(/\n/)
  74. first&.gsub!(/^(?:(#|>)|(\d+)\.|([+-]\s))/) { $1 || $3 ? "\\#{$1 || $3}" : "#{$2}\\." }
  75. second&.gsub!(/^([=-]+\s*?)$/, "\\\1")
  76. res = [first, second, *rest].compact.join("\n") + "\n"
  77. if el.children.length == 1 && el.children.first.type == :math
  78. res = "\\#{res}"
  79. elsif res.start_with?('\$$') && res.end_with?("\\$$\n")
  80. res.sub!(/^\\\$\$/, '\$\$')
  81. end
  82. res
  83. end
  84. def convert_codeblock(el, _opts)
  85. el.value.split(/\n/).map {|l| l.empty? ? " " : " #{l}" }.join("\n") + "\n"
  86. end
  87. def convert_blockquote(el, opts)
  88. opts[:indent] += 2
  89. inner(el, opts).chomp.split(/\n/).map {|l| "> #{l}" }.join("\n") << "\n"
  90. end
  91. def convert_header(el, opts)
  92. res = +''
  93. res << "#{'#' * output_header_level(el.options[:level])} #{inner(el, opts)}"
  94. res[-1, 1] = "\\#" if res[-1] == '#'
  95. res << " {##{el.attr['id']}}" if el.attr['id'] && !el.attr['id'].strip.empty?
  96. res << "\n"
  97. end
  98. def convert_hr(_el, _opts)
  99. "* * *\n"
  100. end
  101. def convert_ul(el, opts)
  102. inner(el, opts).sub(/\n+\Z/, "\n")
  103. end
  104. alias convert_ol convert_ul
  105. alias convert_dl convert_ul
  106. def convert_li(el, opts)
  107. sym, width = if @stack.last.type == :ul
  108. [+'* ', el.children.first && el.children.first.type == :codeblock ? 4 : 2]
  109. else
  110. ["#{opts[:index] + 1}.".ljust(4), 4]
  111. end
  112. if (ial = ial_for_element(el))
  113. sym << ial << " "
  114. end
  115. opts[:indent] += width
  116. text = inner(el, opts)
  117. newlines = text.scan(/\n*\Z/).first
  118. first, *last = text.split(/\n/)
  119. last = last.map {|l| " " * width + l }.join("\n")
  120. text = (first.nil? ? "\n" : first + (last.empty? ? "" : "\n") + last + newlines)
  121. if el.children.first && el.children.first.type == :p && !el.children.first.options[:transparent]
  122. res = +"#{sym}#{text}"
  123. res << "^\n" if el.children.size == 1 && @stack.last.children.last == el &&
  124. (@stack.last.children.any? {|c| c.children.first.type != :p } || @stack.last.children.size == 1)
  125. res
  126. elsif el.children.first && el.children.first.type == :codeblock
  127. "#{sym}\n #{text}"
  128. else
  129. "#{sym}#{text}"
  130. end
  131. end
  132. def convert_dd(el, opts)
  133. sym, width = +": ", (el.children.first && el.children.first.type == :codeblock ? 4 : 2)
  134. if (ial = ial_for_element(el))
  135. sym << ial << " "
  136. end
  137. opts[:indent] += width
  138. text = inner(el, opts)
  139. newlines = text.scan(/\n*\Z/).first
  140. first, *last = text.split(/\n/)
  141. last = last.map {|l| " " * width + l }.join("\n")
  142. text = first.to_s + (last.empty? ? "" : "\n") + last + newlines
  143. text.chomp! if text =~ /\n\n\Z/ && opts[:next] && opts[:next].type == :dd
  144. text << "\n" if text !~ /\n\n\Z/ && opts[:next] && opts[:next].type == :dt
  145. text << "\n" if el.children.empty?
  146. if el.children.first && el.children.first.type == :p && !el.children.first.options[:transparent]
  147. "\n#{sym}#{text}"
  148. elsif el.children.first && el.children.first.type == :codeblock
  149. "#{sym}\n #{text}"
  150. else
  151. "#{sym}#{text}"
  152. end
  153. end
  154. def convert_dt(el, opts)
  155. result = +''
  156. if (ial = ial_for_element(el))
  157. result << ial << " "
  158. end
  159. result << inner(el, opts) << "\n"
  160. end
  161. HTML_TAGS_WITH_BODY = ['div', 'script', 'iframe', 'textarea', 'th', 'td']
  162. HTML_ELEMENT_TYPES = [:entity, :text, :html_element].freeze
  163. private_constant :HTML_ELEMENT_TYPES
  164. def convert_html_element(el, opts)
  165. markdown_attr = el.options[:category] == :block && el.children.any? do |c|
  166. c.type != :html_element &&
  167. (c.type != :p || !c.options[:transparent] ||
  168. c.children.any? {|t| !HTML_ELEMENT_TYPES.member?(t.type) }) &&
  169. c.block?
  170. end
  171. opts[:force_raw_text] = true if %w[script pre code].include?(el.value)
  172. opts[:raw_text] = opts[:force_raw_text] || opts[:block_raw_text] || \
  173. (el.options[:category] != :span && !markdown_attr)
  174. opts[:block_raw_text] = true if el.options[:category] == :block && opts[:raw_text]
  175. res = inner(el, opts)
  176. if el.options[:category] == :span
  177. "<#{el.value}#{html_attributes(el.attr)}" + \
  178. (!res.empty? || HTML_TAGS_WITH_BODY.include?(el.value) ? ">#{res}</#{el.value}>" : " />")
  179. else
  180. output = +''
  181. attr = el.attr.dup
  182. attr['markdown'] = '1' if markdown_attr
  183. output << "<#{el.value}#{html_attributes(attr)}"
  184. if !res.empty? && el.options[:content_model] != :block
  185. output << ">#{res}</#{el.value}>"
  186. elsif !res.empty?
  187. output << ">\n#{res}" << "</#{el.value}>"
  188. elsif HTML_TAGS_WITH_BODY.include?(el.value)
  189. output << "></#{el.value}>"
  190. else
  191. output << " />"
  192. end
  193. output << "\n" if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw
  194. output
  195. end
  196. end
  197. def convert_xml_comment(el, _opts)
  198. if el.options[:category] == :block &&
  199. (@stack.last.type != :html_element || @stack.last.options[:content_model] != :raw)
  200. el.value + "\n"
  201. else
  202. el.value.dup
  203. end
  204. end
  205. alias convert_xml_pi convert_xml_comment
  206. def convert_table(el, opts)
  207. opts[:alignment] = el.options[:alignment]
  208. inner(el, opts)
  209. end
  210. def convert_thead(el, opts)
  211. rows = inner(el, opts)
  212. if opts[:alignment].all? {|a| a == :default }
  213. "#{rows}|#{'-' * 10}\n"
  214. else
  215. "#{rows}| " + opts[:alignment].map do |a|
  216. case a
  217. when :left then ":-"
  218. when :right then "-:"
  219. when :center then ":-:"
  220. when :default then "-"
  221. end
  222. end.join(' ') << "\n"
  223. end
  224. end
  225. def convert_tbody(el, opts)
  226. res = +''
  227. res << inner(el, opts)
  228. res << '|' << '-' * 10 << "\n" if opts[:next] && opts[:next].type == :tbody
  229. res
  230. end
  231. def convert_tfoot(el, opts)
  232. "|#{'=' * 10}\n#{inner(el, opts)}"
  233. end
  234. def convert_tr(el, opts)
  235. "| #{el.children.map {|c| convert(c, opts) }.join(' | ')} |\n"
  236. end
  237. def convert_td(el, opts)
  238. inner(el, opts)
  239. end
  240. def convert_comment(el, _opts)
  241. if el.options[:category] == :block
  242. "{::comment}\n#{el.value}\n{:/}\n"
  243. else
  244. "{::comment}#{el.value}{:/}"
  245. end
  246. end
  247. def convert_br(_el, _opts)
  248. " \n"
  249. end
  250. def convert_a(el, opts)
  251. if el.attr['href'].empty?
  252. "[#{inner(el, opts)}]()"
  253. elsif el.attr['href'] =~ /^(?:http|ftp)/ || el.attr['href'].count("()") > 0
  254. index = if (link_el = @linkrefs.find {|c| c.attr['href'] == el.attr['href'] })
  255. @linkrefs.index(link_el) + 1
  256. else
  257. @linkrefs << el
  258. @linkrefs.size
  259. end
  260. "[#{inner(el, opts)}][#{index}]"
  261. else
  262. title = parse_title(el.attr['title'])
  263. "[#{inner(el, opts)}](#{el.attr['href']}#{title})"
  264. end
  265. end
  266. def convert_img(el, _opts)
  267. alt_text = el.attr['alt'].to_s.gsub(ESCAPED_CHAR_RE) { $1 ? "\\#{$1}" : $2 }
  268. src = el.attr['src'].to_s
  269. if src.empty?
  270. "![#{alt_text}]()"
  271. else
  272. title = parse_title(el.attr['title'])
  273. link = if src.count("()") > 0
  274. "<#{src}>"
  275. else
  276. src
  277. end
  278. "![#{alt_text}](#{link}#{title})"
  279. end
  280. end
  281. def convert_codespan(el, _opts)
  282. delim = (el.value.scan(/`+/).max || '') + '`'
  283. "#{delim}#{' ' if delim.size > 1}#{el.value}#{' ' if delim.size > 1}#{delim}"
  284. end
  285. def convert_footnote(el, _opts)
  286. @footnotes << [el.options[:name], el.value]
  287. "[^#{el.options[:name]}]"
  288. end
  289. def convert_raw(el, _opts)
  290. attr = (el.options[:type] || []).join(' ')
  291. attr = " type=\"#{attr}\"" unless attr.empty?
  292. if @stack.last.type == :html_element
  293. el.value
  294. elsif el.options[:category] == :block
  295. "{::nomarkdown#{attr}}\n#{el.value}\n{:/}\n"
  296. else
  297. "{::nomarkdown#{attr}}#{el.value}{:/}"
  298. end
  299. end
  300. def convert_em(el, opts)
  301. "*#{inner(el, opts)}*" +
  302. (opts[:next] && [:em, :strong].include?(opts[:next].type) && !ial_for_element(el) ? '{::}' : '')
  303. end
  304. def convert_strong(el, opts)
  305. "**#{inner(el, opts)}**" +
  306. (opts[:next] && [:em, :strong].include?(opts[:next].type) && !ial_for_element(el) ? '{::}' : '')
  307. end
  308. def convert_entity(el, _opts)
  309. entity_to_str(el.value, el.options[:original])
  310. end
  311. TYPOGRAPHIC_SYMS = {
  312. mdash: '---', ndash: '--', hellip: '...',
  313. laquo_space: '<< ', raquo_space: ' >>',
  314. laquo: '<<', raquo: '>>'
  315. }
  316. def convert_typographic_sym(el, _opts)
  317. TYPOGRAPHIC_SYMS[el.value]
  318. end
  319. def convert_smart_quote(el, _opts)
  320. el.value.to_s =~ /[rl]dquo/ ? "\"" : "'"
  321. end
  322. def convert_math(el, _opts)
  323. "$$#{el.value}$$" + (el.options[:category] == :block ? "\n" : '')
  324. end
  325. def convert_abbreviation(el, _opts)
  326. el.value
  327. end
  328. def convert_root(el, opts)
  329. res = inner(el, opts)
  330. res << create_link_defs
  331. res << create_footnote_defs
  332. res << create_abbrev_defs
  333. res
  334. end
  335. def create_link_defs
  336. res = +''
  337. res << "\n\n" unless @linkrefs.empty?
  338. @linkrefs.each_with_index do |el, i|
  339. title = parse_title(el.attr['title'])
  340. res << "[#{i + 1}]: #{el.attr['href']}#{title}\n"
  341. end
  342. res
  343. end
  344. def create_footnote_defs
  345. res = +''
  346. @footnotes.each do |name, data|
  347. res << "[^#{name}]:\n"
  348. res << inner(data).chomp.split(/\n/).map {|l| " #{l}" }.join("\n") + "\n\n"
  349. end
  350. res
  351. end
  352. def create_abbrev_defs
  353. return '' unless @root.options[:abbrev_defs]
  354. res = +''
  355. @root.options[:abbrev_defs].each do |name, text|
  356. res << "*[#{name}]: #{text}\n"
  357. res << ial_for_element(Element.new(:unused, nil, @root.options[:abbrev_attr][name])).to_s << "\n\n"
  358. end
  359. res
  360. end
  361. # Return the IAL containing the attributes of the element +el+.
  362. def ial_for_element(el)
  363. res = el.attr.map do |k, v|
  364. next if [:img, :a].include?(el.type) && ['href', 'src', 'alt', 'title'].include?(k)
  365. next if el.type == :header && k == 'id' && !v.strip.empty?
  366. if v.nil?
  367. ''
  368. elsif k == 'class' && !v.empty? && !v.index(/[\.#]/)
  369. " " + v.split(/\s+/).map {|w| ".#{w}" }.join(" ")
  370. elsif k == 'id' && !v.strip.empty?
  371. " ##{v}"
  372. else
  373. " #{k}=\"#{v}\""
  374. end
  375. end.compact.join('')
  376. res = "toc" + (res.strip.empty? ? '' : " #{res}") if (el.type == :ul || el.type == :ol) &&
  377. el.options.dig(:ial, :refs)&.include?('toc')
  378. res = "footnotes" + (res.strip.empty? ? '' : " #{res}") if (el.type == :ul || el.type == :ol) &&
  379. el.options.dig(:ial, :refs)&.include?('footnotes')
  380. if el.type == :dl && el.options[:ial] && el.options[:ial][:refs]
  381. auto_ids = el.options[:ial][:refs].select {|ref| ref.start_with?('auto_ids') }.join(" ")
  382. res = auto_ids << (res.strip.empty? ? '' : " #{res}") unless auto_ids.empty?
  383. end
  384. res.strip.empty? ? nil : "{:#{res}}"
  385. end
  386. def parse_title(attr)
  387. attr.to_s.empty? ? '' : ' "' + attr.gsub(/"/, '&quot;') + '"'
  388. end
  389. # :startdoc:
  390. end
  391. end
  392. end