diff --git a/.gitignore b/.gitignore index 537be1c1..4e736633 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /.bundle /.config /coverage/ +/.idea /InstalledFiles /pkg/ /spec/reports/ diff --git a/bin/commonmarker b/bin/commonmarker index 0c9688d0..a38488a0 100755 --- a/bin/commonmarker +++ b/bin/commonmarker @@ -15,13 +15,16 @@ def parse_options extensions = CommonMarker.extensions parse_options = CommonMarker::Config::OPTS.fetch(:parse) render_options = CommonMarker::Config::OPTS.fetch(:render) + format_options = CommonMarker::Config::OPTS.fetch(:format) options.active_extensions = [] options.active_parse_options = [:DEFAULT] options.active_render_options = [:DEFAULT] + options.output_format = :html option_parser = OptionParser.new do |opts| opts.banner = 'Usage: commonmarker [--html-renderer] [--extension=EXTENSION]' + opts.separator ' [--to=FORMAT]' opts.separator ' [--parse-option=OPTION]' opts.separator ' [--render-option=OPTION]' opts.separator ' [FILE..]' @@ -43,6 +46,7 @@ def parse_options opts.on('-h', '--help', 'Prints this help') do puts opts puts + puts "Available formats: #{format_options.join(', ')}" puts "Available extentions: #{extensions.join(', ')}" puts "Available parse options: #{parse_options.keys.join(', ')}" puts "Available render options: #{render_options.keys.join(', ')}" @@ -51,7 +55,12 @@ def parse_options exit end - opts.on('--html-renderer', 'Use the HtmlRenderer renderer rather than the native C renderer') do + opts.on('-tFORMAT', '--to=FORMAT', String, 'Specify output format (html, xml)') do |value| + value = value.to_sym + options.output_format = value if format_options.include?(value) + end + + opts.on('--html-renderer', 'Use the HtmlRenderer renderer rather than the native C renderer (only valid when format is html)') do options.renderer = true end @@ -87,12 +96,16 @@ def parse_options end options = parse_options - doc = CommonMarker.render_doc(ARGF.read, options.active_parse_options, options.active_extensions) -if options.renderer - renderer = CommonMarker::HtmlRenderer.new(extensions: options.active_extensions) - $stdout.write(renderer.render(doc)) -else - $stdout.write(doc.to_html(options.active_render_options, options.active_extensions)) +case options.output_format +when :html + if options.renderer + renderer = CommonMarker::HtmlRenderer.new(options: options.active_render_options, extensions: options.active_extensions) + $stdout.write(renderer.render(doc)) + else + $stdout.write(doc.to_html(options.active_render_options, options.active_extensions)) + end +when :xml + $stdout.write(doc.to_xml(options.active_render_options)) end diff --git a/ext/commonmarker/commonmarker.c b/ext/commonmarker/commonmarker.c index 785dc1dd..d6b9967c 100644 --- a/ext/commonmarker/commonmarker.c +++ b/ext/commonmarker/commonmarker.c @@ -186,6 +186,40 @@ static VALUE rb_markdown_to_html(VALUE self, VALUE rb_text, VALUE rb_options, VA return ruby_html; } +/* + * Internal: Parses a Markdown string into an HTML string. + * + */ +static VALUE rb_markdown_to_xml(VALUE self, VALUE rb_text, VALUE rb_options, VALUE rb_extensions) { + char *str, *xml; + int len; + cmark_parser *parser; + cmark_node *doc; + Check_Type(rb_text, T_STRING); + Check_Type(rb_options, T_FIXNUM); + + parser = prepare_parser(rb_options, rb_extensions, cmark_get_arena_mem_allocator()); + + str = (char *)RSTRING_PTR(rb_text); + len = RSTRING_LEN(rb_text); + + cmark_parser_feed(parser, str, len); + doc = cmark_parser_finish(parser); + if (doc == NULL) { + cmark_arena_reset(); + rb_raise(rb_eNodeError, "error parsing document"); + } + + cmark_mem *default_mem = cmark_get_default_mem_allocator(); + xml = cmark_render_xml_with_mem(doc, FIX2INT(rb_options), default_mem); + cmark_arena_reset(); + + VALUE ruby_xml = rb_str_new2(xml); + default_mem->free(xml); + + return ruby_xml; +} + /* * Internal: Creates a node based on a node type. * @@ -574,6 +608,28 @@ static VALUE rb_render_html(VALUE self, VALUE rb_options, VALUE rb_extensions) { return ruby_html; } +/* Internal: Convert the node to an XML string. + * + * Returns a {String}. + */ +static VALUE rb_render_xml(VALUE self, VALUE rb_options) { + int options; + int i; + cmark_node *node; + Check_Type(rb_options, T_FIXNUM); + + options = FIX2INT(rb_options); + + Data_Get_Struct(self, cmark_node, node); + + char *xml = cmark_render_xml(node, options); + VALUE ruby_xml = rb_str_new2(xml); + + free(xml); + + return ruby_xml; +} + /* Internal: Convert the node to a CommonMark string. * * Returns a {String}. @@ -1216,6 +1272,8 @@ __attribute__((visibility("default"))) void Init_commonmarker() { rb_cNode = rb_define_class_under(module, "Node", rb_cObject); rb_define_singleton_method(rb_cNode, "markdown_to_html", rb_markdown_to_html, 3); + rb_define_singleton_method(rb_cNode, "markdown_to_xml", rb_markdown_to_xml, + 3); rb_define_singleton_method(rb_cNode, "new", rb_node_new, 1); rb_define_singleton_method(rb_cNode, "parse_document", rb_parse_document, 4); rb_define_method(rb_cNode, "string_content", rb_node_get_string_content, 0); @@ -1228,6 +1286,7 @@ __attribute__((visibility("default"))) void Init_commonmarker() { rb_define_method(rb_cNode, "next", rb_node_next, 0); rb_define_method(rb_cNode, "insert_before", rb_node_insert_before, 1); rb_define_method(rb_cNode, "_render_html", rb_render_html, 2); + rb_define_method(rb_cNode, "_render_xml", rb_render_xml, 1); rb_define_method(rb_cNode, "_render_commonmark", rb_render_commonmark, -1); rb_define_method(rb_cNode, "_render_plaintext", rb_render_plaintext, -1); rb_define_method(rb_cNode, "insert_after", rb_node_insert_after, 1); diff --git a/lib/commonmarker/config.rb b/lib/commonmarker/config.rb index 6563aaa1..c7f3347b 100644 --- a/lib/commonmarker/config.rb +++ b/lib/commonmarker/config.rb @@ -24,7 +24,8 @@ module Config FULL_INFO_STRING: (1 << 16), UNSAFE: (1 << 17), FOOTNOTES: (1 << 13) - }.freeze + }.freeze, + format: %i[html xml].freeze }.freeze def self.process_options(option, type) diff --git a/lib/commonmarker/node.rb b/lib/commonmarker/node.rb index 131334a4..c4c0dcc4 100644 --- a/lib/commonmarker/node.rb +++ b/lib/commonmarker/node.rb @@ -30,6 +30,16 @@ def to_html(options = :DEFAULT, extensions = []) _render_html(opts, extensions).force_encoding('utf-8') end + # Public: Convert the node to an XML string. + # + # options - A {Symbol} or {Array of Symbol}s indicating the render options + # + # Returns a {String}. + def to_xml(options = :DEFAULT) + opts = Config.process_options(options, :render) + _render_xml(opts).force_encoding('utf-8') + end + # Public: Convert the node to a CommonMark string. # # options - A {Symbol} or {Array of Symbol}s indicating the render options diff --git a/test/test_commands.rb b/test/test_commands.rb index 1a9e3b23..9038799e 100644 --- a/test/test_commands.rb +++ b/test/test_commands.rb @@ -28,4 +28,10 @@ def test_understands_multiple_extensions assert_includes out, '

hi' %w[].each { |html| assert_includes out, html } end + + def test_understands_format + out = make_bin('strong.md', '--to=xml') + assert_includes out, '' + assert_includes out, 'strong' + end end diff --git a/test/test_xml.rb b/test/test_xml.rb new file mode 100644 index 00000000..df24291b --- /dev/null +++ b/test/test_xml.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'test_helper' + +class TestXml < Minitest::Test + def setup + @markdown = <<~MD + Hi *there*! + + 1. I am a numeric list. + 2. I continue the list. + * Suddenly, an unordered list! + * What fun! + + Okay, _enough_. + + | a | b | + | --- | --- | + | c | d | + MD + end + + def render_doc(doc) + CommonMarker.render_doc(doc, :DEFAULT, [:table]) + end + + def test_to_commonmark + compare = render_doc(@markdown).to_xml(:SOURCEPOS) + + assert_equal <<~XML, compare + + + + + Hi + + there + + ! + + + + + I am a numeric list. + + + + + I continue the list. + + + + + + + Suddenly, an unordered list! + + + + + What fun! + + + + + Okay, + + enough + + . + +
a c
+ + + a + + + b + + + + + c + + + d + + +
+ + XML + end +end