diff --git a/README.md b/README.md index 10de33c4..08094821 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ When it comes to modifying the document, you can perform the following operation - `insert_after` - `prepend_child` - `append_child` -- `detach` +- `delete` You can also get the source position of a node by calling `source_position`: @@ -109,12 +109,34 @@ end doc.walk do |node| if node.type == :link node.insert_before(node.first_child) - node.detach + node.delete end end # =>

The site

\n

GitHub

\n ``` +#### Example: Converting a document back into raw CommonMark + +You can use `to_commonmark` on a node to render it as raw text: + +```ruby +require 'commonmarker' + +# parse some string +doc = Commonmarker.parse("# The site\n\n [GitHub](https://www.github.com)") + +# Transform links to regular text +doc.walk do |node| + if node.type == :link + node.insert_before(node.first_child) + node.delete + end +end + +doc.to_commonmark +# => # The site\n\nGitHub\n +``` + ## Options and plugins ### Options diff --git a/ext/commonmarker/src/node.rs b/ext/commonmarker/src/node.rs index 79f5ea22..45ee869e 100644 --- a/ext/commonmarker/src/node.rs +++ b/ext/commonmarker/src/node.rs @@ -235,9 +235,7 @@ impl CommonmarkerNode { } fn replace_node(&self, new_node: &CommonmarkerNode) -> Result { - let node = new_node.inner.clone(); - - self.insert_node_after(&new_node)?; + self.insert_node_after(new_node)?; match self.detach_node() { Ok(_) => Ok(true), Err(e) => Err(e), @@ -560,6 +558,74 @@ impl CommonmarkerNode { )), } } + + fn to_commonmark(&self, args: &[Value]) -> Result { + let args = scan_args::scan_args::<(), (), (), (), _, ()>(args)?; + + let kwargs = scan_args::get_kwargs::<_, (), (Option, Option), ()>( + args.keywords, + &[], + &["options", "plugins"], + )?; + let (rb_options, _rb_plugins) = kwargs.optional; + + let mut comrak_options = ComrakOptions::default(); + + if let Some(rb_options) = rb_options { + rb_options.foreach(|key: Symbol, value: RHash| { + iterate_options_hash(&mut comrak_options, key, value)?; + Ok(ForEach::Continue) + })?; + } + + let arena: Arena = Arena::new(); + fn iter_nodes<'a>( + arena: &'a Arena>>, + node: &CommonmarkerNode, + ) -> &'a comrak::arena_tree::Node<'a, std::cell::RefCell> { + let comrak_node: &'a mut ComrakAstNode = arena.alloc(ComrakNode::new(RefCell::new( + node.inner.borrow().data.clone(), + ))); + + for c in node.inner.children() { + let child = CommonmarkerNode { inner: c }; + let child_node = iter_nodes(arena, &child); + comrak_node.append(child_node); + } + + comrak_node + } + + let comrak_root_node: ComrakNode> = + ComrakNode::new(RefCell::new(self.inner.borrow().data.clone())); + + for c in self.inner.children() { + let child = CommonmarkerNode { inner: c }; + + let new_child = iter_nodes(&arena, &child); + + comrak_root_node.append(new_child); + } + + let mut output = vec![]; + match comrak::format_commonmark(&comrak_root_node, &comrak_options, &mut output) { + Ok(_) => {} + Err(e) => { + return Err(magnus::Error::new( + magnus::exception::runtime_error(), + format!("cannot convert into html: {}", e), + )); + } + } + + match std::str::from_utf8(&output) { + Ok(s) => Ok(s.to_string()), + Err(_e) => Err(magnus::Error::new( + magnus::exception::runtime_error(), + "cannot convert into utf-8", + )), + } + } } pub fn init(m_commonmarker: RModule) -> Result<(), magnus::Error> { @@ -583,6 +649,10 @@ pub fn init(m_commonmarker: RModule) -> Result<(), magnus::Error> { )?; c_node.define_method("node_to_html", method!(CommonmarkerNode::to_html, -1))?; + c_node.define_method( + "node_to_commonmark", + method!(CommonmarkerNode::to_commonmark, -1), + )?; c_node.define_method("replace", method!(CommonmarkerNode::replace_node, 1))?; @@ -642,21 +712,6 @@ pub fn init(m_commonmarker: RModule) -> Result<(), magnus::Error> { c_node.define_method("fence_info", method!(CommonmarkerNode::get_fence_info, 0))?; c_node.define_method("fence_info=", method!(CommonmarkerNode::set_fence_info, 1))?; - c_node.define_method( - "table_alignments", - method!(CommonmarkerNode::get_table_alignments, 0), - )?; - - c_node.define_method( - "tasklist_item_checked?", - method!(CommonmarkerNode::get_tasklist_item_checked, 0), - )?; - - c_node.define_method( - "tasklist_item_checked=", - method!(CommonmarkerNode::set_tasklist_item_checked, 1), - )?; - c_node.define_method("fence_info", method!(CommonmarkerNode::get_fence_info, 0))?; c_node.define_method("fence_info=", method!(CommonmarkerNode::set_fence_info, 1))?; diff --git a/lib/commonmarker/node.rb b/lib/commonmarker/node.rb index 284fc899..50bd71e5 100644 --- a/lib/commonmarker/node.rb +++ b/lib/commonmarker/node.rb @@ -46,5 +46,20 @@ def to_html(options: Commonmarker::Config::OPTIONS, plugins: Commonmarker::Confi node_to_html(options: opts, plugins: plugins).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 + # plugins - A {Hash} of additional plugins. + # + # Returns a {String}. + def to_commonmark(options: Commonmarker::Config::OPTIONS, plugins: Commonmarker::Config::PLUGINS) + raise TypeError, "options must be a Hash; got a #{options.class}!" unless options.is_a?(Hash) + + opts = Config.process_options(options) + plugins = Config.process_plugins(plugins) + + node_to_commonmark(options: opts, plugins: plugins).force_encoding("utf-8") + end end end diff --git a/test/node/traversal_test.rb b/test/node/traversal_test.rb index 2e0cdabe..41335feb 100644 --- a/test/node/traversal_test.rb +++ b/test/node/traversal_test.rb @@ -63,7 +63,7 @@ def test_walk_and_delete_node @document.walk do |node| if node.type == :emph node.insert_before(node.first_child) - node.detach + node.delete end end diff --git a/test/node_test.rb b/test/node_test.rb index 452ea842..d4875d4c 100644 --- a/test/node_test.rb +++ b/test/node_test.rb @@ -65,6 +65,18 @@ def test_can_append_child assert_match(%r{!<\/strong><\/p>\n}, @document.to_html) end + def test_can_render_back_to_commonmark + strikethrough_node = Commonmarker::Node.new(:strikethrough) + text_node = Commonmarker::Node.new(:text) + text_node.string_content = "bazinga" + + strikethrough_node.append_child(text_node) + + assert(@document.first_child.first_child.replace(strikethrough_node)) + + assert_match(/~bazinga~\*there\*/, @document.to_commonmark) + end + def test_last_child assert_equal(:paragraph, @document.last_child.type) end @@ -81,9 +93,9 @@ def test_previous_sibling assert_equal(:text, @document.first_child.first_child.next_sibling.previous_sibling.type) end - def test_detach + def test_delete emph = @document.first_child.first_child.next_sibling - emph.detach + emph.delete assert_match(%r{

Hi . This has many nodes!

\n}, @document.to_html) end