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