├── ext ├── .keep └── Makefile ├── spec ├── spec_helper.cr └── common_marker_spec.cr ├── src ├── common_marker │ ├── version.cr │ └── parser.cr ├── common_marker.cr ├── lib_cmark.cr.in └── lib_cmark.cr ├── .editorconfig ├── .gitignore ├── shard.yml ├── .github ├── dependabot.yml └── workflows │ └── crystal.yml ├── LICENSE └── README.md /ext/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/common_marker" 3 | -------------------------------------------------------------------------------- /src/common_marker/version.cr: -------------------------------------------------------------------------------- 1 | class CommonMarker 2 | VERSION = {{ `shards version #{__DIR__}`.chomp.stringify }} 3 | end 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | *.dwarf 6 | /ext 7 | /.vscode/ 8 | 9 | # Libraries don't need dependency lock 10 | # Dependencies will be locked in applications that use them 11 | /shard.lock 12 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: common_marker 2 | version: 0.7.1 3 | 4 | development_dependencies: 5 | crystal_lib: 6 | github: crystal-lang/crystal_lib 7 | ameba: 8 | github: crystal-ameba/ameba 9 | 10 | scripts: 11 | postinstall: cd ext && make 12 | 13 | authors: 14 | - Anton Maminov 15 | 16 | crystal: ">= 1.0.0" 17 | 18 | license: MIT 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Maintain dependencies for GitHub Actions 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /ext/Makefile: -------------------------------------------------------------------------------- 1 | CMARK = cmark-gfm 2 | SOURCES = $(wildcard $(CMARK)/src/*.c) $(wildcard $(CMARK)/src/*.h) 3 | 4 | all: libcmark-gfm.a 5 | 6 | libcmark-gfm.a: $(CMARK) $(SOURCES) 7 | mkdir -p $(CMARK)/build 8 | cd $(CMARK)/build && cmake .. \ 9 | -DCMAKE_BUILD_TYPE=Release \ 10 | -DCMAKE_INSTALL_PREFIX=.. \ 11 | -DCMAKE_POLICY_VERSION_MINIMUM=3.5 12 | cd $(CMARK)/build && make 13 | 14 | cp $(CMARK)/build/src/libcmark-gfm.a . 15 | cp $(CMARK)/build/extensions/libcmark-gfm-extensions.a . 16 | 17 | $(CMARK): 18 | git clone --depth 1 https://github.com/github/cmark-gfm.git 19 | 20 | clean: 21 | rm -rf cmark-gfm 22 | 23 | distclean: clean 24 | rm -f *.a 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2025 Anton Maminov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/common_marker/parser.cr: -------------------------------------------------------------------------------- 1 | class Parser 2 | getter parser : LibCmark::CmarkParser 3 | 4 | # Creates a new cmark-gfm's parser abstraction with the defined options. 5 | def initialize(options : Int32) 6 | LibCmark.cmark_gfm_core_extensions_ensure_registered 7 | 8 | @parser = LibCmark.cmark_parser_new(options) 9 | end 10 | 11 | # Feeds a string to parser. 12 | def parse!(text : String) 13 | LibCmark.parser_feed(parser, text, text.bytesize) 14 | end 15 | 16 | # Returns a list containing the extensions of the current parser. 17 | def extensions 18 | LibCmark.cmark_parser_get_syntax_extensions(parser) 19 | end 20 | 21 | def add_extension(name : String) 22 | extension = LibCmark.cmark_find_syntax_extension(name) 23 | 24 | if extension.null? 25 | raise "Unknown extension #{name}" 26 | end 27 | 28 | result = LibCmark.cmark_parser_attach_syntax_extension(parser, extension) 29 | 30 | if result == 0 31 | raise "Unable to attach #{name}" 32 | end 33 | 34 | true 35 | end 36 | 37 | def add_extensions(extensions = [] of String) 38 | extensions.each { |name| add_extension(name) } 39 | end 40 | 41 | # Finish parsing and return a pointer to a tree of nodes. 42 | def finish 43 | LibCmark.parser_finish(parser) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /src/common_marker.cr: -------------------------------------------------------------------------------- 1 | require "./lib_cmark" 2 | require "./common_marker/*" 3 | 4 | class CommonMarker 5 | getter text : String 6 | getter parser : Parser 7 | 8 | CMARK_VERSION = LibCmark::VERSION_STRING 9 | 10 | def initialize(@text, options = [] of String, @extensions = [] of String) 11 | @options = LibCmark::OPT_DEFAULT 12 | parse_options!(options) 13 | 14 | @parser = Parser.new(@options) 15 | @parser.add_extensions(@extensions) 16 | end 17 | 18 | # Parses a Markdown string into an HTML string. 19 | def to_html : String 20 | extensions = parser.extensions 21 | 22 | parser.parse!(text) 23 | 24 | document = parser.finish 25 | 26 | result = LibCmark.render_html(document, @options, extensions) 27 | 28 | LibCmark.node_free(document) 29 | 30 | String.new(result) 31 | end 32 | 33 | private def parse_options!(options) 34 | @options |= LibCmark::OPT_SOURCEPOS if options.includes?("sourcepos") 35 | @options |= LibCmark::OPT_HARDBREAKS if options.includes?("hardbreaks") 36 | @options |= LibCmark::OPT_NORMALIZE if options.includes?("normalize") 37 | @options |= LibCmark::OPT_VALIDATE_UTF8 if options.includes?("validate_utf8") 38 | @options |= LibCmark::OPT_SMART if options.includes?("smart") 39 | @options |= LibCmark::OPT_UNSAFE if options.includes?("unsafe") 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/lib_cmark.cr.in: -------------------------------------------------------------------------------- 1 | @[Include("cmark-gfm.h", flags: "-Iext")] 2 | @[Include("cmark-gfm-extension_api.h", flags: "-Iext")] 3 | @[Include("cmark-gfm-core-extensions.h", flags: "-Iext")] 4 | 5 | @[Link(ldflags: "\#{__DIR__}/../ext/*.a")] 6 | lib LibCmark 7 | fun cmark_parser_new = cmark_parser_new 8 | fun cmark_parser_get_syntax_extensions = cmark_parser_get_syntax_extensions 9 | fun node_free = cmark_node_free 10 | fun cmark_find_syntax_extension = cmark_find_syntax_extension 11 | fun cmark_parser_attach_syntax_extension = cmark_parser_attach_syntax_extension 12 | fun cmark_gfm_core_extensions_ensure_registered = cmark_gfm_core_extensions_ensure_registered 13 | fun parser_feed = cmark_parser_feed 14 | fun parser_finish = cmark_parser_finish 15 | fun render_html = cmark_render_html 16 | 17 | OPT_DEFAULT = CMARK_OPT_DEFAULT 18 | OPT_SOURCEPOS = CMARK_OPT_SOURCEPOS 19 | OPT_HARDBREAKS = CMARK_OPT_HARDBREAKS 20 | OPT_NORMALIZE = CMARK_OPT_NORMALIZE 21 | OPT_UNSAFE = CMARK_OPT_UNSAFE 22 | OPT_NOBREAKS = CMARK_OPT_NOBREAKS 23 | OPT_VALIDATE_UTF8 = CMARK_OPT_VALIDATE_UTF8 24 | OPT_SMART = CMARK_OPT_SMART 25 | OPT_GITHUB_PRE_LANG = CMARK_OPT_GITHUB_PRE_LANG 26 | OPT_LIBERAL_HTML_TAG = CMARK_OPT_LIBERAL_HTML_TAG 27 | OPT_FOOTNOTES = CMARK_OPT_FOOTNOTES 28 | OPT_STRIKETHROUGH_DOUBLE_TILDE = CMARK_OPT_STRIKETHROUGH_DOUBLE_TILDE 29 | OPT_TABLE_PREFER_STYLE_ATTRIBUTES = CMARK_OPT_TABLE_PREFER_STYLE_ATTRIBUTES 30 | 31 | VERSION = CMARK_GFM_VERSION 32 | VERSION_STRING = CMARK_GFM_VERSION_STRING 33 | end 34 | -------------------------------------------------------------------------------- /.github/workflows/crystal.yml: -------------------------------------------------------------------------------- 1 | name: Crystal CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | check_format: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Install Crystal 14 | uses: crystal-lang/install-crystal@v1 15 | - name: Check out repository code 16 | uses: actions/checkout@v5 17 | - name: Install dependencies 18 | run: shards install 19 | - name: Check format 20 | run: crystal tool format --check 21 | check_ameba: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Install Crystal 25 | uses: crystal-lang/install-crystal@v1 26 | - name: Check out repository code 27 | uses: actions/checkout@v5 28 | - name: Install dependencies 29 | run: shards install 30 | - name: Check ameba 31 | run: ./bin/ameba 32 | test: 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | include: 37 | - {os: ubuntu-latest, crystal: latest} 38 | - {os: ubuntu-latest, crystal: nightly} 39 | - {os: macos-latest} 40 | - {os: macos-14} 41 | runs-on: ${{ matrix.os }} 42 | steps: 43 | - name: Install Crystal 44 | uses: crystal-lang/install-crystal@v1 45 | with: 46 | crystal: ${{ matrix.crystal }} 47 | - name: Check out repository code 48 | uses: actions/checkout@v5 49 | - name: Install dependencies 50 | run: shards install 51 | - name: Build dependencies 52 | run: cd ext && make && cd - 53 | - name: Run tests 54 | run: crystal spec 55 | -------------------------------------------------------------------------------- /src/lib_cmark.cr: -------------------------------------------------------------------------------- 1 | @[Link(ldflags: "#{__DIR__}/../ext/libcmark-gfm-extensions.a #{__DIR__}/../ext/libcmark-gfm.a")] 2 | lib LibCmark 3 | type CmarkParser = Void* 4 | fun cmark_parser_new(options : LibC::Int) : CmarkParser 5 | fun cmark_parser_get_syntax_extensions(parser : CmarkParser) : CmarkParser 6 | fun node_free = cmark_node_free(node : CmarkParser) 7 | fun cmark_find_syntax_extension(name : LibC::Char*) : CmarkParser 8 | fun cmark_parser_attach_syntax_extension(parser : CmarkParser, extension : CmarkParser) : LibC::Int 9 | fun cmark_gfm_core_extensions_ensure_registered 10 | fun parser_feed = cmark_parser_feed(parser : CmarkParser, buffer : LibC::Char*, len : LibC::SizeT) 11 | fun parser_finish = cmark_parser_finish(parser : CmarkParser) : CmarkParser 12 | fun render_html = cmark_render_html(root : CmarkParser, options : LibC::Int, extensions : CmarkParser) : LibC::Char* 13 | OPT_DEFAULT = 0 14 | OPT_SOURCEPOS = (1 << 1) 15 | OPT_HARDBREAKS = (1 << 2) 16 | OPT_NORMALIZE = (1 << 8) 17 | OPT_UNSAFE = (1 << 17) 18 | OPT_NOBREAKS = (1 << 4) 19 | OPT_VALIDATE_UTF8 = (1 << 9) 20 | OPT_SMART = (1 << 10) 21 | OPT_GITHUB_PRE_LANG = (1 << 11) 22 | OPT_LIBERAL_HTML_TAG = (1 << 12) 23 | OPT_FOOTNOTES = (1 << 13) 24 | OPT_STRIKETHROUGH_DOUBLE_TILDE = (1 << 14) 25 | OPT_TABLE_PREFER_STYLE_ATTRIBUTES = (1 << 15) 26 | VERSION = ((((0 << 24) | (29 << 16)) | (0 << 8)) | 2) 27 | VERSION_STRING = "0.29.0.gfm.2" 28 | end 29 | -------------------------------------------------------------------------------- /spec/common_marker_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe CommonMarker do 4 | it "have cmark version" do 5 | (CommonMarker::CMARK_VERSION).should_not be_nil 6 | end 7 | 8 | describe "#initialize" do 9 | it "raise exception with unknown extensions" do 10 | expect_raises Exception, "Unknown extension pipeline" do 11 | text = "_Hello_ **world**" 12 | extensions = ["pipeline"] 13 | CommonMarker.new(text, extensions: extensions) 14 | end 15 | end 16 | end 17 | 18 | describe "#to_html" do 19 | it "should parse markdown" do 20 | text = "_Hello_ **world**" 21 | md = CommonMarker.new(text) 22 | html = md.to_html 23 | html.should eq("

Hello world

\n") 24 | end 25 | 26 | it "should parse markdown with options and extensions" do 27 | text = "_Hello_ **world**" 28 | extensions = ["table", "strikethrough", "autolink", "tagfilter", "tasklist"] 29 | options = ["unsafe"] 30 | md = CommonMarker.new(text, options: options, extensions: extensions) 31 | html = md.to_html 32 | html.should eq("

Hello world

\n") 33 | end 34 | 35 | it "should parse markdown tables" do 36 | text = <<-TXT 37 | | Month | Savings | 38 | | -------- | ------- | 39 | | January | $250 | 40 | | February | $80 | 41 | | March | $420 | 42 | TXT 43 | 44 | markdown = <<-TXT 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
MonthSavings
January$250
February$80
March$420
67 | 68 | TXT 69 | 70 | extensions = ["table", "strikethrough", "autolink", "tagfilter", "tasklist"] 71 | options = ["unsafe"] 72 | md = CommonMarker.new(text, options: options, extensions: extensions) 73 | html = md.to_html 74 | 75 | html.should eq(markdown) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CommonMarker 2 | 3 | [![Crystal CI](https://github.com/mamantoha/crystal-cmark-gfm/actions/workflows/crystal.yml/badge.svg)](https://github.com/mamantoha/crystal-cmark-gfm/actions/workflows/crystal.yml) 4 | [![GitHub release](https://img.shields.io/github/release/mamantoha/crystal-cmark-gfm.svg)](https://github.com/mamantoha/crystal-cmark-gfm/releases) 5 | [![License](https://img.shields.io/github/license/mamantoha/crystal-cmark-gfm.svg)](https://github.com/mamantoha/crystal-cmark-gfm/blob/master/LICENSE) 6 | 7 | [Crystal](https://crystal-lang.org/) wrapper for [cmark-gfm](https://github.com/github/cmark-gfm), GitHub's fork of the reference parser for CommonMark. 8 | 9 | This binding is statically linked with a specific version of cmark-gfm. 10 | 11 | cmark-gfm supports simple parsing and rendering of markdown content. 12 | 13 | If you want more features please check [cr-cmark-gfm](https://github.com/amauryt/cr-cmark-gfm). 14 | 15 | ## Installation 16 | 17 | 1. Add the dependency to your `shard.yml`: 18 | 19 | ```yaml 20 | dependencies: 21 | common_marker: 22 | github: mamantoha/crystal-cmark-gfm 23 | ``` 24 | 25 | 2. Run `shards install` 26 | 27 | ## Usage 28 | 29 | ```crystal 30 | require "common_marker" 31 | 32 | text = File.read("README.md") 33 | extensions = ["table", "strikethrough", "autolink", "tagfilter", "tasklist"] 34 | options = ["unsafe"] 35 | md = CommonMarker.new(text, options: options, extensions: extensions) 36 | html = md.to_html 37 | ``` 38 | 39 | ## Extensions 40 | 41 | `CommonMarker` initializer takes an optional third argument defining the extensions you want enabled as your CommonMark document is being processed. The documentation for these extensions are [defined in this spec](https://github.github.com/gfm/). 42 | 43 | The available extensions are: 44 | 45 | - `table` - This provides support for tables. 46 | - `strikethrough` - This provides support for strikethroughs. 47 | - `autolink` - This provides support for automatically converting URLs to anchor tags. 48 | - `tagfilter` - This escapes [several "unsafe" HTML tags](https://github.github.com/gfm/#disallowed-raw-html-extension-), causing them to not have any effect. 49 | - `tasklist` - This provides support for task list items. 50 | 51 | ## Development 52 | 53 | ```console 54 | cd ext && make && cd .. 55 | ``` 56 | 57 | ```console 58 | crystal ./lib/crystal_lib/src/main.cr src/lib_cmark.cr.in > ./src/lib_cmark.cr 59 | ``` 60 | 61 | ## Contributing 62 | 63 | 1. Fork it () 64 | 2. Create your feature branch (`git checkout -b my-new-feature`) 65 | 3. Commit your changes (`git commit -am 'Add some feature'`) 66 | 4. Push to the branch (`git push origin my-new-feature`) 67 | 5. Create a new Pull Request 68 | 69 | ## Contributors 70 | 71 | - [Anton Maminov](https://github.com/mamantoha) - creator and maintainer 72 | --------------------------------------------------------------------------------