Skip to content

Commit

Permalink
permit rendering back to raw CommonMark
Browse files Browse the repository at this point in the history
  • Loading branch information
gjtorikian committed Mar 12, 2024
1 parent c28939d commit b8b7e61
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 23 deletions.
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down Expand Up @@ -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
# => <h1><a href=\"#the-site\"></a>The site</h1>\n<p>GitHub</p>\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
Expand Down
91 changes: 73 additions & 18 deletions ext/commonmarker/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,7 @@ impl CommonmarkerNode {
}

fn replace_node(&self, new_node: &CommonmarkerNode) -> Result<bool, magnus::Error> {
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),
Expand Down Expand Up @@ -560,6 +558,74 @@ impl CommonmarkerNode {
)),
}
}

fn to_commonmark(&self, args: &[Value]) -> Result<String, magnus::Error> {
let args = scan_args::scan_args::<(), (), (), (), _, ()>(args)?;

let kwargs = scan_args::get_kwargs::<_, (), (Option<RHash>, Option<RHash>), ()>(
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<ComrakAstNode> = Arena::new();
fn iter_nodes<'a>(
arena: &'a Arena<comrak::arena_tree::Node<'a, RefCell<ComrakAst>>>,
node: &CommonmarkerNode,
) -> &'a comrak::arena_tree::Node<'a, std::cell::RefCell<comrak::nodes::Ast>> {
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<RefCell<ComrakAst>> =
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> {
Expand All @@ -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))?;

Expand Down Expand Up @@ -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))?;

Expand Down
15 changes: 15 additions & 0 deletions lib/commonmarker/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion test/node/traversal_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 14 additions & 2 deletions test/node_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ def test_can_append_child
assert_match(%r{!<strong><\/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
Expand All @@ -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{<p>Hi . This has <strong>many nodes</strong>!</p>\n}, @document.to_html)
end
Expand Down

0 comments on commit b8b7e61

Please sign in to comment.