Skip to content

Commit

Permalink
it can walk!
Browse files Browse the repository at this point in the history
  • Loading branch information
gjtorikian committed Jan 30, 2024
1 parent ac47ad7 commit 5f21436
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 7 deletions.
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,75 @@ Commonmarker.to_html('"Hi *there*"', options: {
# <p>“Hi <em>there</em>”</p>\n
```

The second argument is optional--[see below](#options) for more information.
(The second argument is optional--[see below](#options-and-plugins) for more information.)

### Generating a document

You can also parse a string to receive a `:document` node. You can then print that node to HTML, iterate over the children, and do other fun node stuff. For example:

```ruby
require 'commonmarker'

doc = Commonmarker.parse("*Hello* world", options: {
parse: { smart: true }
})
puts(doc.to_html) # <p><em>Hello</em> world</p>\n

doc.walk do |node|
puts node.type # [:document, :paragraph, :emph, :text, :text]
end
```

(The second argument is optional--[see below](#options-and-plugins) for more information.)

When it comes to modifying the document, you can perform the following operations:

- `insert_before`
- `insert_after`
- `prepend_child`
- `append_child`
- `detach`

You can also modify the following attributes:

- `url`
- `title`
- `header_level`
- `list_type`
- `list_start`
- `list_tight`
- `fence_info`

#### Example: Walking the AST

You can use `walk` or `each` to iterate over nodes:

- `walk` will iterate on a node and recursively iterate on a node's children.
- `each` will iterate on a node and its children, but no further.

```ruby
require 'commonmarker'

# parse some string
doc = Commonmarker.parse("# The site\n\n [GitHub](https://www.github.com)")

# Walk tree and print out URLs for links
doc.walk do |node|
if node.type == :link
printf("URL = %s\n", node.url)
end
end
# => URL = https://www.github.com

# Transform links to regular text
doc.walk do |node|
if node.type == :link
node.insert_before(node.first_child)
node.detach
end
end
# => <h1><a href=\"#the-site\"></a>The site</h1>\n<p>GitHub</p>\n
```

## Options and plugins

Expand Down
24 changes: 18 additions & 6 deletions ext/commonmarker/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,31 +195,42 @@ impl CommonmarkerNode {
}

fn prepend_child_node(&self, new_child: &CommonmarkerNode) -> Result<bool, magnus::Error> {
self.inner.prepend(new_child.inner.clone());
let node = new_child.inner.clone();
node.detach();
self.inner.prepend(node);

Ok(true)
}

fn append_child_node(&self, new_child: &CommonmarkerNode) -> Result<bool, magnus::Error> {
self.inner.append(new_child.inner.clone());
let node = new_child.inner.clone();
node.detach();
self.inner.append(node);

Ok(true)
}

fn detach_node(&self) -> Result<bool, magnus::Error> {
fn detach_node(&self) -> Result<CommonmarkerNode, magnus::Error> {
let node = self.inner.make_copy().borrow().data.clone();
self.inner.detach();

Ok(true)
Ok(CommonmarkerNode {
inner: Node::new(CommonmarkerAst { data: node }),
})
}

fn insert_node_before(&self, new_sibling: &CommonmarkerNode) -> Result<bool, magnus::Error> {
self.inner.insert_before(new_sibling.inner.clone());
let node = new_sibling.inner.clone();
node.detach();
self.inner.insert_before(node);

Ok(true)
}

fn insert_node_after(&self, new_sibling: &CommonmarkerNode) -> Result<bool, magnus::Error> {
self.inner.insert_after(new_sibling.inner.clone());
let node = new_sibling.inner.clone();
node.detach();
self.inner.insert_after(node);

Ok(true)
}
Expand Down Expand Up @@ -423,6 +434,7 @@ impl CommonmarkerNode {
)),
}
}

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

Expand Down
76 changes: 76 additions & 0 deletions test/node/traversal_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2023, by Samuel Williams.

# require 'markly'
# require 'markly'

require "test_helper"

class NodeTraversalTest < Minitest::Test
def setup
@document = Commonmarker.parse("Hi *there*, I am mostly text!")
end

def test_it_can_walk_all_nodes
nodes = []
@document.walk do |node|
nodes << node.type
end

assert_equal([:document, :paragraph, :text, :emph, :text, :text], nodes)
end

def test_enumerate_nodes
nodes = []
@document.first_child.each do |node|
nodes << node.type
end

assert_equal([:text, :emph, :text], nodes)
end

def test_select_nodes
nodes = @document.first_child.select { |node| node.type == :text }

assert_instance_of(Commonmarker::Node, nodes.first)
assert_equal([:text, :text], nodes.map(&:type))
end

def test_map_nodes
nodes = @document.first_child.map(&:type)

assert_equal([:text, :emph, :text], nodes)
end

def test_will_not_allow_invalid_node_insertion
nodes = @document.first_child.map(&:type)

assert_equal([:text, :emph, :text], nodes)

@document.insert_before(Commonmarker::Node.new(:document))
nodes = @document.first_child.map(&:type)

assert_equal([:text, :emph, :text], nodes)
end

def test_generate_html
assert_equal("<p>Hi <em>there</em>, I am mostly text!</p>\n", @document.to_html)
end

def test_walk_and_delete_node
@document.walk do |node|
if node.type == :emph
node.insert_before(node.first_child)
node.detach
end
end

assert_equal("<p>Hi there, I am mostly text!</p>\n", @document.to_html)
end

def test_inspect_node
assert_includes(@document.inspect, "#<Commonmarker::Node(document)")
end
end

0 comments on commit 5f21436

Please sign in to comment.