From db0bc83fde9c761f5224e3eb5791258b32815e67 Mon Sep 17 00:00:00 2001 From: kristoph Date: Wed, 18 Feb 2026 20:36:34 -0800 Subject: [PATCH] Add Node#fence for code block metadata Expose the parsed fence character, length, and indentation as a Node::Fence structure for fenced code blocks. --- context/getting-started.md | 12 ++++++++++++ ext/markly/markly.c | 27 +++++++++++++++++++++++++++ guides/getting-started/readme.md | 12 ++++++++++++ releases.md | 1 + test/markly/node.rb | 28 ++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+) diff --git a/context/getting-started.md b/context/getting-started.md index 69bfccb..975fdd8 100644 --- a/context/getting-started.md +++ b/context/getting-started.md @@ -100,6 +100,18 @@ blocks and front matter. `Node#code_language` returns the first token of that info string, while `Node#fence_info` remains available for compatibility on those block nodes. +For a fenced code block, `Node#fence` returns a `Node::Fence` structure with +the fence character, length, and indentation: + +``` ruby +block = Markly.parse(" ~~~~ ruby\n Object.new\n ~~~~").first_child + +block.fence +# => # +``` + +Indented code blocks and other node types return `nil`. + ## Extensions Both `render_html` and `parse` take an optional `extensions:` argument defining the extensions you want enabled as your CommonMark document is being processed: diff --git a/ext/markly/markly.c b/ext/markly/markly.c index 0af0ee2..b1571ca 100644 --- a/ext/markly/markly.c +++ b/ext/markly/markly.c @@ -11,6 +11,7 @@ static VALUE rb_Markly; static VALUE rb_Markly_Error; static VALUE rb_Markly_Node; +static VALUE rb_Markly_Node_Fence; static VALUE rb_Markly_Parser; static VALUE sym_document; @@ -1080,6 +1081,30 @@ static VALUE rb_node_set_code_info(VALUE self, VALUE info) { return Qnil; } +/* + * Public: Gets fencing details for the current node. + * + * Returns a {Markly::Node::Fence} for fenced code blocks, `nil` otherwise. + */ +static VALUE rb_node_get_fence(VALUE self) { + int fence_length = 0; + int fence_offset = 0; + char fence_character = '\0'; + cmark_node *node; + TypedData_Get_Struct(self, cmark_node, &rb_Markly_Node_Type, node); + + if (!cmark_node_get_fenced(node, &fence_length, &fence_offset, &fence_character)) { + return Qnil; + } + + return rb_struct_new( + rb_Markly_Node_Fence, + rb_str_new(&fence_character, 1), + INT2NUM(fence_length), + INT2NUM(fence_offset) + ); +} + static VALUE rb_node_get_tasklist_item_checked(VALUE self) { int tasklist_state; cmark_node *node; @@ -1260,6 +1285,7 @@ __attribute__((visibility("default"))) void Init_markly(void) { rb_Markly_Node = rb_define_class_under(rb_Markly, "Node", rb_cObject); rb_undef_alloc_func(rb_Markly_Node); rb_define_singleton_method(rb_Markly_Node, "new", rb_node_new, 1); + rb_Markly_Node_Fence = rb_struct_define_under(rb_Markly_Node, "Fence", "character", "length", "indent", NULL); rb_define_method(rb_Markly_Node, "replace", rb_node_replace, 1); @@ -1297,6 +1323,7 @@ __attribute__((visibility("default"))) void Init_markly(void) { rb_define_method(rb_Markly_Node, "fence_info=", rb_node_set_fence_info, 1); rb_define_method(rb_Markly_Node, "code_info", rb_node_get_code_info, 0); rb_define_method(rb_Markly_Node, "code_info=", rb_node_set_code_info, 1); + rb_define_method(rb_Markly_Node, "fence", rb_node_get_fence, 0); rb_define_method(rb_Markly_Node, "table_alignments", rb_node_get_table_alignments, 0); rb_define_method(rb_Markly_Node, "tasklist_item_checked?", rb_node_get_tasklist_item_checked, 0); rb_define_method(rb_Markly_Node, "tasklist_item_checked=", rb_node_set_tasklist_item_checked, 1); diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index 69bfccb..975fdd8 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -100,6 +100,18 @@ blocks and front matter. `Node#code_language` returns the first token of that info string, while `Node#fence_info` remains available for compatibility on those block nodes. +For a fenced code block, `Node#fence` returns a `Node::Fence` structure with +the fence character, length, and indentation: + +``` ruby +block = Markly.parse(" ~~~~ ruby\n Object.new\n ~~~~").first_child + +block.fence +# => # +``` + +Indented code blocks and other node types return `nil`. + ## Extensions Both `render_html` and `parse` take an optional `extensions:` argument defining the extensions you want enabled as your CommonMark document is being processed: diff --git a/releases.md b/releases.md index b218c00..d8d1dd2 100644 --- a/releases.md +++ b/releases.md @@ -3,6 +3,7 @@ ## Unreleased - Add opt-in language prefixes for inline code spans with `Markly::INLINE_CODE_INFO`, expose code metadata through `Node#code_info`, and provide `Node#code_language` as a convenient language accessor. + - Expose fenced code-block metadata through `Node#fence` and `Node::Fence`. ## v0.16.0 diff --git a/test/markly/node.rb b/test/markly/node.rb index 4551d5f..a43434d 100644 --- a/test/markly/node.rb +++ b/test/markly/node.rb @@ -225,6 +225,34 @@ end end + with "#fence" do + let(:fenced_backtick_node) {Markly.parse("``` ruby\nputs 'wow'\n```").first_child} + let(:fenced_tilde_node) {Markly.parse(" ~~~~ ruby\n puts 'wow'\n ~~~~").first_child} + let(:indented_code_node) {Markly.parse(" puts 'wow'").first_child} + let(:paragraph_node) {Markly.parse("hello").first_child} + + it "returns fenced metadata for backtick fences" do + expect(fenced_backtick_node.fence).to be == Markly::Node::Fence.new("`", 3, 0) + end + + it "returns fenced metadata for tilde fences" do + fence = fenced_tilde_node.fence + + expect(fence).to be_a(Markly::Node::Fence) + expect(fence.character).to be == "~" + expect(fence.length).to be == 4 + expect(fence.indent).to be == 2 + end + + it "returns nil for non-fenced code blocks" do + expect(indented_code_node.fence).to be_nil + end + + it "returns nil for non-code nodes" do + expect(paragraph_node.fence).to be_nil + end + end + with "#find_header" do let(:document) {Markly.parse("# Heading\n\n## Subheading")}