PageRenderTime 26ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/org-ruby/html_output_buffer.rb

https://github.com/wallyqs/org-ruby
Ruby | 430 lines | 341 code | 45 blank | 44 comment | 49 complexity | 9dc02280901cf6758e389e6e08dffd02 MD5 | raw file
  1. module Orgmode
  2. class HtmlOutputBuffer < OutputBuffer
  3. HtmlBlockTag = {
  4. :paragraph => "p",
  5. :ordered_list => "ol",
  6. :unordered_list => "ul",
  7. :list_item => "li",
  8. :definition_list => "dl",
  9. :definition_term => "dt",
  10. :definition_descr => "dd",
  11. :table => "table",
  12. :table_row => "tr",
  13. :quote => "blockquote",
  14. :example => "pre",
  15. :src => "pre",
  16. :inline_example => "pre",
  17. :center => "div",
  18. :heading1 => "h1",
  19. :heading2 => "h2",
  20. :heading3 => "h3",
  21. :heading4 => "h4",
  22. :heading5 => "h5",
  23. :heading6 => "h6",
  24. :title => "h1"
  25. }
  26. attr_reader :options
  27. def initialize(output, opts = {})
  28. super(output)
  29. @buffer_tag = "HTML"
  30. @options = opts
  31. @new_paragraph = :start
  32. @footnotes = {}
  33. @unclosed_tags = []
  34. @logger.debug "HTML export options: #{@options.inspect}"
  35. @custom_blocktags = {} if @options[:markup_file]
  36. unless @options[:skip_syntax_highlight]
  37. begin
  38. require 'pygments'
  39. rescue LoadError
  40. # Pygments is not supported so we try instead with CodeRay
  41. begin
  42. require 'coderay'
  43. rescue LoadError
  44. # No code syntax highlighting
  45. end
  46. end
  47. end
  48. if @options[:markup_file]
  49. do_custom_markup
  50. end
  51. end
  52. # Output buffer is entering a new mode. Use this opportunity to
  53. # write out one of the block tags in the HtmlBlockTag constant to
  54. # put this information in the HTML stream.
  55. def push_mode(mode, indent, properties={})
  56. @logger.debug "Properties: #{properties}"
  57. super(mode, indent, properties)
  58. if HtmlBlockTag[mode]
  59. unless ((mode_is_table?(mode) and skip_tables?) or
  60. (mode == :src and !@options[:skip_syntax_highlight] and defined? Pygments))
  61. css_class = case
  62. when (mode == :src and @block_lang.empty?)
  63. " class=\"src\""
  64. when (mode == :src and not @block_lang.empty?)
  65. " class=\"src\" lang=\"#{@block_lang}\""
  66. when (mode == :example || mode == :inline_example)
  67. " class=\"example\""
  68. when mode == :center
  69. " style=\"text-align: center\""
  70. when @options[:decorate_title]
  71. " class=\"title\""
  72. end
  73. add_paragraph unless @new_paragraph == :start
  74. @new_paragraph = true
  75. @logger.debug "#{mode}: <#{HtmlBlockTag[mode]}#{css_class}>"
  76. # Check to see if we need to restart numbering from a
  77. # previous interrupted li
  78. if mode_is_ol?(mode) && properties.key?(HtmlBlockTag[:list_item])
  79. @output << "<#{HtmlBlockTag[mode]} start=#{properties[HtmlBlockTag[:list_item]]}#{css_class}>"
  80. else
  81. @output << "<#{HtmlBlockTag[mode]}#{css_class}>"
  82. end
  83. # Entering a new mode obliterates the title decoration
  84. @options[:decorate_title] = nil
  85. end
  86. end
  87. end
  88. # We are leaving a mode. Close any tags that were opened when
  89. # entering this mode.
  90. def pop_mode(mode = nil)
  91. m = super(mode)
  92. if HtmlBlockTag[m]
  93. unless ((mode_is_table?(m) and skip_tables?) or
  94. (m == :src and !@options[:skip_syntax_highlight] and defined? Pygments))
  95. add_paragraph if @new_paragraph
  96. @new_paragraph = true
  97. @logger.debug "</#{HtmlBlockTag[m]}>"
  98. @output << "</#{HtmlBlockTag[m]}>"
  99. end
  100. end
  101. @list_indent_stack.pop
  102. end
  103. def flush!
  104. return false if @buffer.empty?
  105. case
  106. when preserve_whitespace?
  107. strip_code_block! if mode_is_code? current_mode
  108. # NOTE: CodeRay and Pygments already escape the html once, so
  109. # no need to escapeHTML
  110. case
  111. when (current_mode == :src and @options[:skip_syntax_highlight])
  112. @buffer = escapeHTML @buffer
  113. when (current_mode == :src and defined? Pygments)
  114. lang = normalize_lang @block_lang
  115. @output << "\n" unless @new_paragraph == :start
  116. @new_paragraph = true
  117. begin
  118. @buffer = Pygments.highlight(@buffer, :lexer => lang)
  119. rescue
  120. # Not supported lexer from Pygments, we fallback on using the text lexer
  121. @buffer = Pygments.highlight(@buffer, :lexer => 'text')
  122. end
  123. when (current_mode == :src and defined? CodeRay)
  124. lang = normalize_lang @block_lang
  125. # CodeRay might throw a warning when unsupported lang is set,
  126. # then fallback to using the text lexer
  127. silence_warnings do
  128. begin
  129. @buffer = CodeRay.scan(@buffer, lang).html(:wrap => nil, :css => :style)
  130. rescue ArgumentError
  131. @buffer = CodeRay.scan(@buffer, 'text').html(:wrap => nil, :css => :style)
  132. end
  133. end
  134. when (current_mode == :html or current_mode == :raw_text)
  135. @buffer.gsub!(/\A\n/, "") if @new_paragraph == :start
  136. @new_paragraph = true
  137. else
  138. # *NOTE* Don't use escape_string! through its sensitivity to @@html:<text>@@ forms
  139. @buffer = escapeHTML @buffer
  140. end
  141. # Whitespace is significant in :code mode. Always output the
  142. # buffer and do not do any additional translation.
  143. @logger.debug "FLUSH CODE ==========> #{@buffer.inspect}"
  144. @output << @buffer
  145. when (mode_is_table? current_mode and skip_tables?)
  146. @logger.debug "SKIP ==========> #{current_mode}"
  147. else
  148. @buffer.lstrip!
  149. @new_paragraph = nil
  150. @logger.debug "FLUSH ==========> #{current_mode}"
  151. case current_mode
  152. when :definition_term
  153. d = @buffer.split(/\A(.*[ \t]+|)::(|[ \t]+.*?)$/, 4)
  154. d[1] = d[1].strip
  155. unless d[1].empty?
  156. @output << inline_formatting(d[1])
  157. else
  158. @output << "???"
  159. end
  160. indent = @list_indent_stack.last
  161. pop_mode
  162. @new_paragraph = :start
  163. push_mode(:definition_descr, indent)
  164. @output << inline_formatting(d[2].strip + d[3])
  165. @new_paragraph = nil
  166. when :horizontal_rule
  167. add_paragraph unless @new_paragraph == :start
  168. @new_paragraph = true
  169. @output << "<hr />"
  170. else
  171. @output << inline_formatting(@buffer)
  172. end
  173. end
  174. @buffer = ""
  175. end
  176. def add_line_attributes headline
  177. if @options[:export_heading_number] then
  178. level = headline.level
  179. heading_number = get_next_headline_number(level)
  180. @output << "<span class=\"heading-number heading-number-#{level}\">#{heading_number}</span> "
  181. end
  182. if @options[:export_todo] and headline.keyword then
  183. keyword = headline.keyword
  184. @output << "<span class=\"todo-keyword #{keyword}\">#{keyword}</span> "
  185. end
  186. end
  187. def output_footnotes!
  188. # Only footnotes defined in the footnote (i.e., [fn:0:this is the footnote definition]) will be automatically
  189. # added to a separate Footnotes section at the end of the document. All footnotes that are defined separately
  190. # from their references will be rendered where they appear in the original Org document.
  191. return false unless @options[:export_footnotes] and not @footnotes.empty?
  192. @output << "\n<div id=\"footnotes\">\n<h2 class=\"footnotes\">Footnotes:</h2>\n<div id=\"text-footnotes\">\n"
  193. @footnotes.each do |name, (defi, content)|
  194. @buffer = defi
  195. @output << "<div class=\"footdef\"><sup><a id=\"fn.#{name}\" href=\"#fnr.#{name}\">#{name}</a></sup>" \
  196. << "<p class=\"footpara\">" \
  197. << inline_formatting(@buffer) \
  198. << "</p></div>\n"
  199. end
  200. @output << "</div>\n</div>"
  201. return true
  202. end
  203. # Test if we're in an output mode in which whitespace is significant.
  204. def preserve_whitespace?
  205. super or current_mode == :html
  206. end
  207. ######################################################################
  208. private
  209. def skip_tables?
  210. @options[:skip_tables]
  211. end
  212. def mode_is_table?(mode)
  213. (mode == :table or mode == :table_row or
  214. mode == :table_separator or mode == :table_header)
  215. end
  216. def mode_is_ol?(mode)
  217. mode == :ordered_list
  218. end
  219. # Escapes any HTML content in string
  220. def escape_string! str
  221. str.gsub!(/&/, "&amp;")
  222. # Escapes the left and right angular brackets but construction
  223. # @@html:<text>@@ which is formatted to <text>
  224. str.gsub! /<([^<>\n]*)/ do |match|
  225. ($`[-7..-1] == "@@html:" and $'[0..2] == ">@@") ? $& : "&lt;#{$1}"
  226. end
  227. str.gsub! /([^<>\n]*)>/ do |match|
  228. $`[-8..-1] == "@@html:<" ? $& : "#{$1}&gt;"
  229. end
  230. str.gsub! /@@html:(<[^<>\n]*>)@@/, "\\1"
  231. end
  232. def quote_tags str
  233. str.gsub /(<[^<>\n]*>)/, "@@html:\\1@@"
  234. end
  235. def buffer_indentation
  236. indent = " " * @list_indent_stack.length
  237. @buffer << indent
  238. end
  239. def add_paragraph
  240. indent = " " * (@list_indent_stack.length - 1)
  241. @output << "\n" << indent
  242. end
  243. Tags = {
  244. "*" => { :open => "b", :close => "b" },
  245. "/" => { :open => "i", :close => "i" },
  246. "_" => { :open => "span style=\"text-decoration:underline;\"",
  247. :close => "span" },
  248. "=" => { :open => "code", :close => "code" },
  249. "~" => { :open => "code", :close => "code" },
  250. "+" => { :open => "del", :close => "del" }
  251. }
  252. # Applies inline formatting rules to a string.
  253. def inline_formatting(str)
  254. @re_help.rewrite_emphasis str do |marker, s|
  255. if marker == "=" or marker == "~"
  256. s = escapeHTML s
  257. "<#{Tags[marker][:open]}>#{s}</#{Tags[marker][:close]}>"
  258. else
  259. quote_tags("<#{Tags[marker][:open]}>") + s +
  260. quote_tags("</#{Tags[marker][:close]}>")
  261. end
  262. end
  263. if @options[:use_sub_superscripts] then
  264. @re_help.rewrite_subp str do |type, text|
  265. if type == "_" then
  266. quote_tags("<sub>") + text + quote_tags("</sub>")
  267. elsif type == "^" then
  268. quote_tags("<sup>") + text + quote_tags("</sup>")
  269. end
  270. end
  271. end
  272. @re_help.rewrite_links str do |link, defi|
  273. [link, defi].compact.each do |text|
  274. # We don't support search links right now. Get rid of it.
  275. text.sub!(/\A(file:[^\s]+)::[^\s]*?\Z/, "\\1")
  276. text.sub!(/\Afile(|\+emacs|\+sys):(?=[^\s]+\Z)/, "")
  277. end
  278. # We don't add a description for images in links, because its
  279. # empty value forces the image to be inlined.
  280. defi ||= link unless link =~ @re_help.org_image_file_regexp
  281. if defi =~ @re_help.org_image_file_regexp
  282. defi = quote_tags "<img src=\"#{defi}\" alt=\"#{defi}\" />"
  283. end
  284. if defi
  285. link = @options[:link_abbrevs][link] if @options[:link_abbrevs].has_key? link
  286. quote_tags("<a href=\"#{link}\">") + defi + quote_tags("</a>")
  287. else
  288. quote_tags "<img src=\"#{link}\" alt=\"#{link}\" />"
  289. end
  290. end
  291. if @output_type == :table_row
  292. str.gsub! /^\|\s*/, quote_tags("<td>")
  293. str.gsub! /\s*\|$/, quote_tags("</td>")
  294. str.gsub! /\s*\|\s*/, quote_tags("</td><td>")
  295. end
  296. if @output_type == :table_header
  297. str.gsub! /^\|\s*/, quote_tags("<th>")
  298. str.gsub! /\s*\|$/, quote_tags("</th>")
  299. str.gsub! /\s*\|\s*/, quote_tags("</th><th>")
  300. end
  301. if @options[:export_footnotes] then
  302. @re_help.rewrite_footnote_definition str do |name, content|
  303. quote_tags("<sup><a id=\"fn.#{name}\" class=\"footnum\" href=\"#fnr.#{name}\">") +
  304. name + quote_tags("</a></sup> ") + content
  305. end
  306. @re_help.rewrite_footnote str do |name, defi|
  307. @footnotes[name] = defi if defi
  308. quote_tags("<sup><a id=\"fnr.#{name}\" class=\"footref\" href=\"#fn.#{name}\">") +
  309. name + quote_tags("</a></sup>")
  310. end
  311. end
  312. # Two backslashes \\ at the end of the line make a line break without breaking paragraph.
  313. if @output_type != :table_row and @output_type != :table_header then
  314. str.sub! /\\\\$/, quote_tags("<br />")
  315. end
  316. escape_string! str
  317. Orgmode.special_symbols_to_html str
  318. str = @re_help.restore_code_snippets str
  319. end
  320. def normalize_lang(lang)
  321. case lang
  322. when 'emacs-lisp', 'common-lisp', 'lisp'
  323. 'scheme'
  324. when 'ipython'
  325. 'python'
  326. when 'js2'
  327. 'javascript'
  328. when ''
  329. 'text'
  330. else
  331. lang
  332. end
  333. end
  334. # Helper method taken from Rails
  335. # https://github.com/rails/rails/blob/c2c8ef57d6f00d1c22743dc43746f95704d67a95/activesupport/lib/active_support/core_ext/kernel/reporting.rb#L10
  336. def silence_warnings
  337. warn_level = $VERBOSE
  338. $VERBOSE = nil
  339. yield
  340. ensure
  341. $VERBOSE = warn_level
  342. end
  343. def strip_code_block!
  344. if @code_block_indent and @code_block_indent > 0
  345. strip_regexp = Regexp.new("^" + " " * @code_block_indent)
  346. @buffer.gsub!(strip_regexp, "")
  347. end
  348. @code_block_indent = nil
  349. # Strip proctective commas generated by Org mode (C-c ')
  350. @buffer.gsub! /^(\s*)(,)(\s*)([*]|#\+)/ do |match|
  351. "#{$1}#{$3}#{$4}"
  352. end
  353. end
  354. # The CGI::escapeHTML function backported from the Ruby standard library
  355. # as of commit fd2fc885b43283aa3d76820b2dfa9de19a77012f
  356. #
  357. # Implementation of the cgi module can change among Ruby versions
  358. # so stabilizing on a single one here to avoid surprises.
  359. #
  360. # https://github.com/ruby/ruby/blob/trunk/lib/cgi/util.rb
  361. #
  362. # The set of special characters and their escaped values
  363. TABLE_FOR_ESCAPE_HTML__ = {
  364. "'" => '&#39;',
  365. '&' => '&amp;',
  366. '"' => '&quot;',
  367. '<' => '&lt;',
  368. '>' => '&gt;',
  369. }
  370. # Escape special characters in HTML, namely &\"<>
  371. # escapeHTML('Usage: foo "bar" <baz>')
  372. # # => "Usage: foo &quot;bar&quot; &lt;baz&gt;"
  373. private
  374. def escapeHTML(string)
  375. string.gsub(/['&\"<>]/, TABLE_FOR_ESCAPE_HTML__)
  376. end
  377. end # class HtmlOutputBuffer
  378. end # module Orgmode