├── .gitignore ├── test ├── test_helper.cr └── common_mark_test.cr ├── src ├── main.cr ├── lib_cmark.cr ├── common_mark.cr └── lib_cmark.cr.in ├── shard.lock ├── shard.yml ├── Makefile ├── ext └── Makefile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .crystal 2 | .shards 3 | /lib 4 | /ext 5 | /docs 6 | -------------------------------------------------------------------------------- /test/test_helper.cr: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "../src/common_mark" 3 | -------------------------------------------------------------------------------- /src/main.cr: -------------------------------------------------------------------------------- 1 | require "./common_mark" 2 | 3 | md = CommonMark.new(File.read(ARGV[0])) 4 | 5 | puts case ARGV[1]? 6 | when "xml" then md.to_xml 7 | when "man" then md.to_man 8 | when "commonmark" then md.to_commonmark 9 | when "latex" then md.to_latex 10 | else md.to_html 11 | end 12 | -------------------------------------------------------------------------------- /shard.lock: -------------------------------------------------------------------------------- 1 | version: 1.0 2 | shards: 3 | clang: 4 | github: crystal-lang/clang.cr 5 | commit: d019b9ff105cd652f31cca73e950adbf41037d52 6 | 7 | crystal_lib: 8 | github: manastech/crystal_lib 9 | commit: 85421997dd176e289d76a369b796fa96c42a3018 10 | 11 | minitest: 12 | github: ysbaddaden/minitest.cr 13 | version: 0.5.0 14 | 15 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: common_mark 2 | version: 0.3.0 3 | description: libcmark bindings, the reference CommonMark C library 4 | 5 | development_dependencies: 6 | crystal_lib: 7 | github: manastech/crystal_lib 8 | 9 | minitest: 10 | github: ysbaddaden/minitest.cr 11 | version: ">= 0.5.0" 12 | 13 | scripts: 14 | postinstall: cd ext && make 15 | 16 | crystal: 0.20.0 17 | 18 | license: BSD-2-Clause 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs clean test 2 | 3 | all: ext/libcmark.a src/lib_cmark.cr 4 | 5 | ext/libcmark.a: 6 | cd ext && make 7 | 8 | src/lib_cmark.cr: src/lib_cmark.cr.in ext/*.h 9 | crystal ./lib/crystal_lib/src/main.cr -- src/lib_cmark.cr.in > src/lib_cmark.cr 10 | sed -i 's/.\/..\/ext/#{__DIR__}\/..\/ext/' src/lib_cmark.cr 11 | 12 | docs: 13 | crystal docs src/common_mark.cr 14 | 15 | clean: 16 | cd ext && make clean 17 | 18 | test: 19 | crystal test/*_test.cr 20 | -------------------------------------------------------------------------------- /ext/Makefile: -------------------------------------------------------------------------------- 1 | VERSION ?= 0.30.2 2 | CMARK = cmark-$(VERSION) 3 | SOURCES = $(wildcard $(CMARK)/src/*.c) $(wildcard $(CMARK)/src/*.h) 4 | 5 | all: libcmark.a 6 | 7 | libcmark.a: cmark-$(VERSION) $(SOURCES) 8 | cd $(CMARK) && INSTALL_PREFIX=.. make 9 | cp $(CMARK)/src/*.h . 10 | cp $(CMARK)/build/src/*.h . 11 | cp $(CMARK)/build/src/*.a . 12 | 13 | $(CMARK): 14 | curl -L https://github.com/jgm/cmark/archive/$(VERSION).tar.gz | tar zx 15 | 16 | clean: 17 | rm -rf cmark-* 18 | 19 | distclean: clean 20 | rm -rf *.a *.h 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CommonMark 2 | 3 | Crystal wrapper for libcmark, the reference CommonMark C library. 4 | 5 | ## Install 6 | 7 | Add the dependency to `shard.yml` then run `shards install`: 8 | 9 | ```yaml 10 | dependencies: 11 | common_mark: 12 | github: ysbaddaden/crystal-cmark 13 | ``` 14 | 15 | This will automatically download and compile libcmark. 16 | 17 | ## Usage 18 | 19 | ```crystal 20 | require "common_mark" 21 | html = CommonMark.new(text).to_html 22 | ``` 23 | 24 | ## License 25 | 26 | Distributed under the [BSD 2 Clause](http://opensource.org/licenses/BSD-2-Clause) license. 27 | -------------------------------------------------------------------------------- /src/lib_cmark.cr: -------------------------------------------------------------------------------- 1 | @[Link(ldflags: "-L#{__DIR__}/../ext -lcmark")] 2 | lib LibCmark 3 | type CmarkNode = Void* 4 | fun node_free = cmark_node_free(node : CmarkNode) 5 | fun parse_document = cmark_parse_document(buffer : LibC::Char*, len : LibC::SizeT, options : LibC::Int) : CmarkNode 6 | fun render_xml = cmark_render_xml(root : CmarkNode, options : LibC::Int) : LibC::Char* 7 | fun render_html = cmark_render_html(root : CmarkNode, options : LibC::Int) : LibC::Char* 8 | fun render_man = cmark_render_man(root : CmarkNode, options : LibC::Int, width : LibC::Int) : LibC::Char* 9 | fun render_commonmark = cmark_render_commonmark(root : CmarkNode, options : LibC::Int, width : LibC::Int) : LibC::Char* 10 | fun render_latex = cmark_render_latex(root : CmarkNode, options : LibC::Int, width : LibC::Int) : LibC::Char* 11 | OPT_DEFAULT = 0 12 | OPT_SOURCEPOS = (1 << 1) 13 | OPT_HARDBREAKS = (1 << 2) 14 | OPT_NORMALIZE = (1 << 8) 15 | OPT_SMART = (1 << 10) 16 | OPT_VALIDATE_UTF8 = (1 << 9) 17 | OPT_UNSAFE = (1 << 17) 18 | VERSION = (((0 << 16) | (29 << 8)) | 0) 19 | VERSION_STRING = "0.29.0" 20 | end 21 | 22 | -------------------------------------------------------------------------------- /test/common_mark_test.cr: -------------------------------------------------------------------------------- 1 | require "./test_helper" 2 | 3 | class CommonMarkTest < Minitest::Test 4 | TEXT = <<-MARKDOWN 5 | # title 6 | 7 | some 8 | body 9 | 10 | * item 1 11 | * item 2 12 | MARKDOWN 13 | 14 | def test_options 15 | md = CommonMark.new("") 16 | refute md.sourcepos? 17 | refute md.hardbreaks? 18 | refute md.normalize? 19 | refute md.smart? 20 | refute md.unsafe? 21 | 22 | md = CommonMark.new("", sourcepos: true, hardbreaks: true, normalize: true, 23 | smart: true, unsafe: true, validate_utf8: true) 24 | assert md.sourcepos? 25 | assert md.hardbreaks? 26 | assert md.normalize? 27 | assert md.smart? 28 | assert md.unsafe? 29 | assert md.validate_utf8? 30 | end 31 | 32 | def test_to_commonmark 33 | commonmark = CommonMark.new(TEXT).to_commonmark 34 | assert_equal "# title\n\nsome body\n\n - item 1\n - item 2\n", commonmark 35 | end 36 | 37 | def test_to_html 38 | html = CommonMark.new(TEXT).to_html 39 | assert_match "

title

", html 40 | assert_match "

some\nbody

", html 41 | assert_match "
  • item 1
  • ", html 42 | assert_match "
  • item 2
  • ", html 43 | 44 | unsafe_html = CommonMark.new(TEXT, unsafe: true).to_html 45 | assert_match "
  • item 2
  • ", unsafe_html 46 | end 47 | 48 | def test_to_latex 49 | latex = CommonMark.new(TEXT).to_latex 50 | assert_match "\\section{title}", latex 51 | assert_match "some body", latex 52 | assert_match "begin{itemize}\n\\item item 1", latex 53 | end 54 | 55 | def test_to_man 56 | man = CommonMark.new(TEXT).to_man 57 | assert_match ".SH\ntitle", man 58 | assert_match ".PP\nsome body", man 59 | assert_match ".IP \\[bu] 2\nitem 1", man 60 | end 61 | 62 | def test_to_xml 63 | xml = CommonMark.new(TEXT, unsafe: true).to_xml 64 | assert_match %(\n title\n ), xml 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /src/common_mark.cr: -------------------------------------------------------------------------------- 1 | require "./lib_cmark" 2 | 3 | # Crystal wrapper for libcmark, the reference CommonMark C library. 4 | # 5 | # ``` 6 | # md = CommonMark.new(text) 7 | # ``` 8 | # 9 | # Render to another format: 10 | # ``` 11 | # md.to_html # => HTML 12 | # md.to_man # => man page 13 | # ``` 14 | class CommonMark 15 | # The original markdown text. 16 | getter text : String 17 | 18 | # Initializes a new CommonMark parser. 19 | # 20 | # ``` 21 | # md = CommonMark.new(text, smart: true) 22 | # md.to_html 23 | # ``` 24 | # 25 | # Options: 26 | # * `hardbreak` — render `softbreak` elements as hard line breaks. 27 | # * `unsafe` — keep raw HTML and unsafe links (`javascript:`, `vbscript:`, `file:`, and `data:`, except for `image/png`, `image/gif`, `image/jpeg`, or `image/webp` mime types). Raw HTML is replaced by a placeholder HTML comment. Unsafe links are replaced by empty strings. 28 | # * `sourcepos` — include a `data-sourcepos` attribute on all block elements. 29 | # * `smart` — convert straight quotes to curly, `---` to em dashes, `--` to en dashes. 30 | # * `normalize` — normalize tree by consolidating adjacent text nodes. 31 | # * `validate_utf8` — Validate UTF-8 in the input before parsing, replacing illegal sequences with the replacement character U+FFFD. 32 | def initialize(@text, sourcepos = false, hardbreaks = false, normalize = false, validate_utf8 = false, smart = false, unsafe = false) 33 | @options = LibCmark::OPT_DEFAULT 34 | @options |= LibCmark::OPT_SOURCEPOS if sourcepos 35 | @options |= LibCmark::OPT_HARDBREAKS if hardbreaks 36 | @options |= LibCmark::OPT_NORMALIZE if normalize 37 | @options |= LibCmark::OPT_VALIDATE_UTF8 if validate_utf8 38 | @options |= LibCmark::OPT_SMART if smart 39 | @options |= LibCmark::OPT_UNSAFE if unsafe 40 | end 41 | 42 | {% for option in %w(sourcepos hardbreaks normalize smart unsafe validate_utf8)%} 43 | # Returns true if the `{{ option.id }}` option was set. 44 | def {{ option.id }}? 45 | @options & LibCmark::OPT_{{ option.upcase.id }} == LibCmark::OPT_{{ option.upcase.id }} 46 | end 47 | {% end %} 48 | 49 | {% for export in %w(html xml) %} 50 | # Renders `#text` as {{ export.upcase.id }}. 51 | def to_{{ export.id }} 52 | parse do |document| 53 | result = LibCmark.render_{{ export.id }}(document, @options) 54 | String.new(result) 55 | end 56 | end 57 | {% end %} 58 | 59 | {% for export in %w(commonmark latex man) %} 60 | # Renders `#text` as a {{ export.id }} page. 61 | def to_{{ export.id }}(width = 80) 62 | parse do |document| 63 | result = LibCmark.render_{{ export.id }}(document, @options, width) 64 | String.new(result) 65 | end 66 | end 67 | {% end %} 68 | 69 | def parse 70 | document = LibCmark.parse_document(text, text.bytesize, @options) 71 | yield document 72 | ensure 73 | LibCmark.node_free(document) if document 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /src/lib_cmark.cr.in: -------------------------------------------------------------------------------- 1 | @[Include("cmark.h", flags: "-Iext")] 2 | @[Link(ldflags: "-L#{__DIR__}/../ext -lcmark")] 3 | lib LibCmark 4 | #fun markdown_to_html = cmark_markdown_to_html 5 | 6 | #fun node_new = cmark_node_new 7 | fun node_free = cmark_node_free 8 | 9 | #fun node_next = cmark_node_next 10 | #fun node_previous = cmark_node_previous 11 | #fun node_parent = cmark_node_parent 12 | #fun node_first_child = cmark_node_first_child 13 | #fun node_last_child = cmark_node_last_child 14 | 15 | #fun iter_new = cmark_iter_new 16 | #fun iter_free = cmark_iter_free 17 | #fun iter_next = cmark_iter_next 18 | #fun iter_get_node = cmark_iter_get_node 19 | #fun iter_get_event_type = cmark_iter_get_event_type 20 | #fun iter_get_root = cmark_iter_get_root 21 | #fun iter_reset = cmark_iter_reset 22 | 23 | #fun node_get_user_data = cmark_node_get_user_data 24 | #fun node_set_user_data = cmark_node_set_user_data 25 | #fun node_get_type = cmark_node_get_type 26 | #fun node_get_type_string = cmark_node_get_type_string 27 | #fun node_get_literal = cmark_node_get_literal 28 | #fun node_set_literal = cmark_node_set_literal 29 | #fun node_get_header_level = cmark_node_get_header_level 30 | #fun node_set_header_level = cmark_node_set_header_level 31 | #fun node_get_list_type = cmark_node_get_list_type 32 | #fun node_set_list_type = cmark_node_set_list_type 33 | #fun node_get_list_delim = cmark_node_get_list_delim 34 | #fun node_set_list_delim = cmark_node_set_list_delim 35 | #fun node_get_list_start = cmark_node_get_list_start 36 | #fun node_set_list_start = cmark_node_set_list_start 37 | #fun node_get_list_tight = cmark_node_get_list_tight 38 | #fun node_set_list_tight = cmark_node_set_list_tight 39 | #fun node_get_fence_info = cmark_node_get_fence_info 40 | #fun node_set_fence_info = cmark_node_set_fence_info 41 | #fun node_get_url = cmark_node_get_url 42 | #fun node_set_url = cmark_node_set_url 43 | #fun node_get_title = cmark_node_get_title 44 | #fun node_set_title = cmark_node_set_title 45 | #fun node_get_start_line = cmark_node_get_start_line 46 | #fun node_get_start_column = cmark_node_get_start_column 47 | #fun node_get_end_line = cmark_node_get_end_line 48 | #fun node_get_end_column = cmark_node_get_end_column 49 | 50 | #fun node_unlink = cmark_node_unlink 51 | #fun node_insert_before = cmark_node_insert_before 52 | #fun node_insert_after = cmark_node_insert_after 53 | #fun node_prepend_child = cmark_node_prepend_child 54 | #fun node_append_child = cmark_node_append_child 55 | #fun consolidate_text_nodes = cmark_consolidate_text_nodes 56 | 57 | #fun parser_new = cmark_parser_new 58 | #fun parser_free = cmark_parser_free 59 | #fun parser_feed = cmark_parser_feed 60 | #fun parser_finish = cmark_parser_finish 61 | fun parse_document = cmark_parse_document 62 | #fun parse_file = cmark_parse_file 63 | 64 | fun render_xml = cmark_render_xml 65 | fun render_html = cmark_render_html 66 | fun render_man = cmark_render_man 67 | fun render_commonmark = cmark_render_commonmark 68 | fun render_latex = cmark_render_latex 69 | 70 | OPT_DEFAULT = CMARK_OPT_DEFAULT 71 | OPT_SOURCEPOS = CMARK_OPT_SOURCEPOS 72 | OPT_HARDBREAKS = CMARK_OPT_HARDBREAKS 73 | OPT_NORMALIZE = CMARK_OPT_NORMALIZE 74 | OPT_SMART = CMARK_OPT_SMART 75 | OPT_VALIDATE_UTF8 = CMARK_OPT_VALIDATE_UTF8 76 | # OPT_SAFE = CMARK_OPT_SAFE 77 | OPT_UNSAFE = CMARK_OPT_UNSAFE 78 | 79 | VERSION = CMARK_VERSION 80 | VERSION_STRING = CMARK_VERSION_STRING 81 | end 82 | --------------------------------------------------------------------------------