diff --git a/lib/markly/node.rb b/lib/markly/node.rb index 2987e85..0a46571 100644 --- a/lib/markly/node.rb +++ b/lib/markly/node.rb @@ -139,19 +139,22 @@ def replace_section(new_node, replace_header: true, remove_subsections: true) self.delete if replace_header end - # Finds the next sibling heading. + # Finds the next sibling header. # - # @returns [Markly::Node | Nil] The next heading, if present. - def next_heading + # @returns [Markly::Node | Nil] The next header, if present. + def next_header current = self.next while current - if current.type == :heading + if current.type == :header return current end current = current.next end end + # An alias for {ruby Markly::Node#next_header}. + alias next_heading next_header + # Append the given node after the current node. # # It's okay to provide a document node, its children will be appended. diff --git a/lib/markly/renderer/generic.rb b/lib/markly/renderer/generic.rb index d17b1bd..6c0103e 100644 --- a/lib/markly/renderer/generic.rb +++ b/lib/markly/renderer/generic.rb @@ -20,7 +20,6 @@ class Generic def initialize(flags: DEFAULT, extensions: []) @flags = flags @stream = StringIO.new(+"") - @need_blocksep = false @in_tight = false @in_plain = false @tagfilter = extensions.include?(:tagfilter) @@ -75,9 +74,9 @@ def document(_node) # Renders a code block node. # # Subclasses should override this callback. - # @parameter node [Markly::Node] The code block node. - def code_block(node) - code_block(node) + # @parameter _node [Markly::Node] The code block node. + def code_block(_node) + raise NotImplementedError, "#{self.class} must implement #code_block" end # Ignores reference-definition nodes, which have no direct output. @@ -93,18 +92,6 @@ def cr out("\n") end - # Writes a separator between block-level nodes. - # - def blocksep - out("\n") - end - - # Writes a container separator unless tight rendering is enabled. - # - def containersep - cr unless @in_tight - end - # Renders a block surrounded by normalized newlines. # # @yields {|| ...} The block content to render. diff --git a/lib/markly/renderer/html.rb b/lib/markly/renderer/html.rb index 29487ca..2ebe43d 100644 --- a/lib/markly/renderer/html.rb +++ b/lib/markly/renderer/html.rb @@ -13,11 +13,6 @@ require "cgi/escape" -# Compatibility for older Ruby versions where escape_html alias doesn't exist: -unless CGI.respond_to?(:escape_html) - require "cgi" -end - module Markly module Renderer # Renders Markdown node trees as HTML. @@ -58,7 +53,8 @@ def document(_) def id_for(node) if @headings anchor = @headings.anchor_for(node) - return " id=\"#{CGI.escape_html anchor}\"" + # `CGI.escape_html` is not exposed by `cgi/escape` on Ruby 3.4: + return " id=\"#{CGI.escapeHTML anchor}\"" end end diff --git a/releases.md b/releases.md index d8d1dd2..228f3e4 100644 --- a/releases.md +++ b/releases.md @@ -4,6 +4,9 @@ - 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`. + - Add `Node#next_header`, retain `Node#next_heading` as an alias, and correctly recognize header nodes. + - Raise `NotImplementedError` from the base renderer's unimplemented code-block callback instead of recursing indefinitely. + - Remove unused legacy separator helpers from the Ruby renderer. ## v0.16.0 diff --git a/test/markly/node.rb b/test/markly/node.rb index a43434d..8ae47c4 100644 --- a/test/markly/node.rb +++ b/test/markly/node.rb @@ -262,6 +262,28 @@ end end + with "#next_header" do + let(:document) {Markly.parse("# Heading\n\nParagraph\n\n## Subheading")} + + it "finds the next sibling heading" do + heading = document.first_child + expect(heading.next_header).to be == heading.next.next + end + + it "returns nil when there is no subsequent heading" do + expect(document.last_child.next_header).to be_nil + end + end + + with "#next_heading" do + it "is an alias for #next_header" do + document = Markly.parse("# Heading\n\n## Subheading") + heading = document.first_child + + expect(heading.next_heading).to be == heading.next_header + end + end + with "#replace_section" do let(:document) {Markly.parse("# Heading\n\n## Subheading")} let(:new_document) {Markly.parse("### New Heading")} diff --git a/test/markly/renderer/generic.rb b/test/markly/renderer/generic.rb new file mode 100644 index 0000000..079cf16 --- /dev/null +++ b/test/markly/renderer/generic.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "markly" + +describe Markly::Renderer::Generic do + let(:renderer_class) do + Class.new(subject) do + def text(node) + out(node.string_content) + end + + def output + @stream.string + end + end + end + + let(:renderer) {renderer_class.new} + + with "#out" do + it "renders arrays of nodes" do + text = Markly.parse("Hello").first_child.first_child + renderer.out([text]) + + expect(renderer.output).to be == "Hello" + end + end + + with "#code_block" do + it "must be implemented by subclasses" do + code_block = Markly.parse(" Hello").first_child + + expect do + renderer.render(code_block) + end.to raise_exception(NotImplementedError) + end + end + +end diff --git a/test/markly/renderer/html.rb b/test/markly/renderer/html.rb index eb23e68..9376bf2 100644 --- a/test/markly/renderer/html.rb +++ b/test/markly/renderer/html.rb @@ -70,6 +70,11 @@ expect(Markly::Renderer::HTML.anchor_for(header)).to be == "hello-world" end + it "works as an instance method" do + header = document.first_child + expect(renderer.anchor_for(header)).to be == "hello-world" + end + it "handles edge cases properly" do test_cases = [ ["# Multiple Spaces", "multiple-spaces"], @@ -162,4 +167,39 @@ expect(renderer.render(document)).to be == document.to_html end end + + with "render flags" do + it "renders fenced code languages on the pre element" do + document = Markly.parse("```ruby\nputs 'Hello'\n```") + renderer = subject.new(flags: Markly::GITHUB_PRE_LANG) + + expect(renderer.render(document)).to be == "
puts 'Hello'\n\n"
+ end
+
+ it "omits unsafe block HTML" do
+ document = Markly.parse("Before unsafe after
\n" + end + + it "renders soft breaks as hard breaks" do + document = Markly.parse("One\nTwo") + renderer = subject.new(flags: Markly::HARD_BREAKS) + + expect(renderer.render(document)).to be == "One
\nTwo
One Two
\n" + end + end end