aboutsummaryrefslogtreecommitdiff
path: root/commonmark.rb
blob: 8767c04a336e36cead435b8da39acf8477ea1cd0 (plain)
  1. #!/usr/bin/env ruby
  2. require 'ffi'
  3. require 'stringio'
  4. require 'cgi'
  5. require 'set'
  6. require 'uri'
  7. module CMark
  8. extend FFI::Library
  9. ffi_lib ['libcmark', 'cmark']
  10. typedef :pointer, :node
  11. enum :node_type, [:document, :blockquote, :list, :list_item,
  12. :fenced_code, :indented_code, :html, :paragraph,
  13. :atx_header, :setext_header, :hrule, :reference_def,
  14. :str, :softbreak, :linebreak, :code, :inline_html,
  15. :emph, :strong, :link, :image]
  16. enum :list_type, [:no_list, :bullet_list, :ordered_list]
  17. attach_function :cmark_free_nodes, [:node], :void
  18. attach_function :cmark_node_unlink, [:node], :void
  19. attach_function :cmark_markdown_to_html, [:string, :int], :string
  20. attach_function :cmark_parse_document, [:string, :int], :node
  21. attach_function :cmark_node_first_child, [:node], :node
  22. attach_function :cmark_node_parent, [:node], :node
  23. attach_function :cmark_node_next, [:node], :node
  24. attach_function :cmark_node_previous, [:node], :node
  25. attach_function :cmark_node_get_type, [:node], :node_type
  26. attach_function :cmark_node_get_string_content, [:node], :string
  27. attach_function :cmark_node_get_url, [:node], :string
  28. attach_function :cmark_node_get_title, [:node], :string
  29. attach_function :cmark_node_get_header_level, [:node], :int
  30. attach_function :cmark_node_get_list_type, [:node], :list_type
  31. attach_function :cmark_node_get_list_start, [:node], :int
  32. attach_function :cmark_node_get_list_tight, [:node], :bool
  33. end
  34. class Node
  35. attr_accessor :type, :children, :string_content, :header_level,
  36. :list_type, :list_start, :list_tight, :url, :title
  37. def initialize(pointer)
  38. if pointer.null?
  39. return nil
  40. end
  41. @pointer = pointer
  42. @type = CMark::cmark_node_get_type(pointer)
  43. @children = []
  44. first_child = CMark::cmark_node_first_child(pointer)
  45. b = first_child
  46. while !b.null?
  47. @children << Node.new(b)
  48. b = CMark::cmark_node_next(b)
  49. end
  50. @string_content = CMark::cmark_node_get_string_content(pointer)
  51. if @type == :atx_header || @type == :setext_header
  52. @header_level = CMark::cmark_node_get_header_level(pointer)
  53. end
  54. if @type == :list
  55. @list_type = CMark::cmark_node_get_list_type(pointer)
  56. @list_start = CMark::cmark_node_get_list_start(pointer)
  57. @list_tight = CMark::cmark_node_get_list_tight(pointer)
  58. end
  59. if @type == :link || @type == :image
  60. @url = CMark::cmark_node_get_url(pointer)
  61. if !@url then @url = "" end
  62. @title = CMark::cmark_node_get_title(pointer)
  63. if !@title then @title = "" end
  64. end
  65. if @type == :document
  66. self.free
  67. end
  68. end
  69. def self.parse_string(s)
  70. Node.new(CMark::cmark_parse_document(s, s.bytesize))
  71. end
  72. def self.parse_file(f)
  73. s = f.read()
  74. self.parse_string(s)
  75. end
  76. def free
  77. CMark::cmark_free_nodes(@pointer)
  78. end
  79. end
  80. class Renderer
  81. attr_accessor :in_tight, :warnings, :in_plain
  82. def initialize(stream = nil)
  83. if stream
  84. @stream = stream
  85. @stringwriter = false
  86. else
  87. @stringwriter = true
  88. @stream = StringIO.new
  89. end
  90. @need_blocksep = false
  91. @warnings = Set.new []
  92. @in_tight = false
  93. @in_plain = false
  94. end
  95. def outf(format, *args)
  96. @stream.printf(format, *args)
  97. end
  98. def out(*args)
  99. args.each do |arg|
  100. if arg.kind_of?(String)
  101. @stream.write(arg)
  102. elsif arg.kind_of?(Node)
  103. self.render(arg)
  104. elsif arg.kind_of?(Array)
  105. arg.each { |x| self.out(x) }
  106. else
  107. @stream.write(arg)
  108. end
  109. end
  110. end
  111. def render(node)
  112. @node = node
  113. if node.type == :document
  114. self.document(node)
  115. self.out("\n")
  116. if @stringwriter
  117. return @stream.string
  118. end
  119. elsif self.in_plain && node.type != :str && node.type != :softbreak
  120. # pass through looking for str, softbreak
  121. node.children.each do |child|
  122. render(child)
  123. end
  124. else
  125. begin
  126. self.send(node.type, node)
  127. rescue NoMethodError => e
  128. @warnings.add("WARNING: " + node.type.to_s + " not implemented.")
  129. raise e
  130. end
  131. end
  132. end
  133. def document(node)
  134. self.out(node.children)
  135. end
  136. def indented_code(node)
  137. self.code_block(node)
  138. end
  139. def fenced_code(node)
  140. self.code_block(node)
  141. end
  142. def setext_header(node)
  143. self.header(node)
  144. end
  145. def atx_header(node)
  146. self.header(node)
  147. end
  148. def reference_def(node)
  149. end
  150. def blocksep
  151. self.out("\n\n")
  152. end
  153. def containersep
  154. if !self.in_tight
  155. self.out("\n")
  156. end
  157. end
  158. def block(&blk)
  159. if @need_blocksep
  160. self.blocksep
  161. end
  162. blk.call
  163. @need_blocksep = true
  164. end
  165. def container(starter, ender, &blk)
  166. self.out(starter)
  167. self.containersep
  168. @need_blocksep = false
  169. blk.call
  170. self.containersep
  171. self.out(ender)
  172. end
  173. def plain(&blk)
  174. old_in_plain = @in_plain
  175. @in_plain = true
  176. blk.call
  177. @in_plain = old_in_plain
  178. end
  179. end
  180. class HtmlRenderer < Renderer
  181. def header(node)
  182. block do
  183. self.out("<h", node.header_level, ">", node.children,
  184. "</h", node.header_level, ">")
  185. end
  186. end
  187. def paragraph(node)
  188. block do
  189. if self.in_tight
  190. self.out(node.children)
  191. else
  192. self.out("<p>", node.children, "</p>")
  193. end
  194. end
  195. end
  196. def list(node)
  197. old_in_tight = self.in_tight
  198. self.in_tight = node.list_tight
  199. block do
  200. if node.list_type == :bullet_list
  201. container("<ul>", "</ul>") do
  202. self.out(node.children)
  203. end
  204. else
  205. start = node.list_start == 1 ? '' :
  206. (' start="' + node.list_start.to_s + '"')
  207. container(start, "</ol>") do
  208. self.out(node.children)
  209. end
  210. end
  211. end
  212. self.in_tight = old_in_tight
  213. end
  214. def list_item(node)
  215. block do
  216. container("<li>", "</li>") do
  217. self.out(node.children)
  218. end
  219. end
  220. end
  221. def blockquote(node)
  222. block do
  223. container("<blockquote>", "</blockquote>") do
  224. self.out(node.children)
  225. end
  226. end
  227. end
  228. def hrule(node)
  229. block do
  230. self.out("<hr />")
  231. end
  232. end
  233. def code_block(node)
  234. block do
  235. self.out("<pre><code>")
  236. self.out(CGI.escapeHTML(node.string_content))
  237. self.out("</code></pre>")
  238. end
  239. end
  240. def html(node)
  241. block do
  242. self.out(node.string_content)
  243. end
  244. end
  245. def inline_html(node)
  246. self.out(node.string_content)
  247. end
  248. def emph(node)
  249. self.out("<em>", node.children, "</em>")
  250. end
  251. def strong(node)
  252. self.out("<strong>", node.children, "</strong>")
  253. end
  254. def link(node)
  255. self.out('<a href="', URI.escape(node.url), '"')
  256. if node.title
  257. self.out(' title="', CGI.escapeHTML(node.title), '"')
  258. end
  259. self.out('>', node.children, '</a>')
  260. end
  261. def image(node)
  262. self.out('<img src="', URI.escape(node.url), '"')
  263. if node.title
  264. self.out(' title="', CGI.escapeHTML(node.title), '"')
  265. end
  266. plain do
  267. self.out(' alt="', node.children, '" />')
  268. end
  269. end
  270. def str(node)
  271. self.out(CGI.escapeHTML(node.string_content))
  272. end
  273. def code(node)
  274. self.out("<code>")
  275. self.out(CGI.escapeHTML(node.string_content))
  276. self.out("</code>")
  277. end
  278. def linebreak(node)
  279. self.out("<br/>")
  280. self.softbreak(node)
  281. end
  282. def softbreak(node)
  283. self.out("\n")
  284. end
  285. end
  286. doc = Node.parse_file(ARGF)
  287. renderer = HtmlRenderer.new(STDOUT)
  288. renderer.render(doc)
  289. renderer.warnings.each do |w|
  290. STDERR.write(w)
  291. STDERR.write("\n")
  292. end
  293. # def markdown_to_html(s)
  294. # len = s.bytes.length
  295. # CMark::cmark_markdown_to_html(s, len)
  296. # end
  297. # print markdown_to_html(STDIN.read())