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
\nGitHub
\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