From c126c01c12af0581aaacbca7126d47e29f811348 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Sun, 18 Oct 2026 13:14:18 -0700 Subject: [PATCH 1/2] fix: restore legacy CommonMarker compatibility I retain the existing CommonMarker 2.x adapter and delegate to the original render_html API when the legacy parser is installed. Existing installations keep their parser and its option behavior. I prepare version `6.1.1` with real-parser regression tests and a legacy full-suite CI job. The incompatible `6.1.0` release has been withdrawn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6fe372b1-3ff6-4129-9d33-be564fb6a5f0 Signed-off-by: Zack Koppert --- .github/workflows/ci.yml | 20 +++++--- CONTRIBUTING.md | 7 +++ Gemfile | 2 +- Gemfile.lock | 2 +- HISTORY.md | 14 ++++- README.md | 4 ++ lib/github-markup.rb | 2 +- lib/github/markup/markdown.rb | 4 ++ test/coverage_test.rb | 16 +----- test/markdown_test.rb | 96 +++++++++++++++++++++++++++++++++++ test/test_helper.rb | 18 +++++++ 11 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 test/markdown_test.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3b6ca10..291b73ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,12 +9,15 @@ permissions: jobs: build: - name: "Test / Ruby ${{ matrix.ruby }} / RDoc ${{ matrix.rdoc }}" + name: "Test / Ruby ${{ matrix.ruby }} / RDoc ${{ matrix.rdoc }} / CommonMarker ${{ matrix.commonmarker }}" runs-on: ubuntu-latest env: RDOC_VERSION: "~> ${{ matrix.rdoc }}" + COMMONMARKER_VERSION: "${{ matrix.commonmarker }}" strategy: matrix: + commonmarker: + - "~> 2.8.2" rdoc: - "7.2.0" ruby: @@ -24,6 +27,10 @@ jobs: include: - ruby: "4.0" rdoc: "8.0" + commonmarker: "~> 2.8.2" + - ruby: "3.4" + rdoc: "7.2.0" + commonmarker: "0.23.12" fail-fast: false steps: @@ -31,15 +38,16 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 10 + persist-credentials: false - - uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1 + - uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1.329.0 with: ruby-version: ${{ matrix.ruby }} - bundler-cache: ${{ matrix.rdoc == '7.2.0' }} + bundler-cache: ${{ matrix.rdoc == '7.2.0' && matrix.commonmarker == '~> 2.8.2' }} - - name: Install RDoc 8 dependencies - if: matrix.rdoc == '8.0' - run: bundle update rdoc + - name: Resolve overridden parser dependencies + if: matrix.rdoc != '7.2.0' || matrix.commonmarker != '~> 2.8.2' + run: bundle update commonmarker rdoc - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e2b9886..239e852d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,6 +42,13 @@ CI tests RDoc 7.2 and 8.x. To test RDoc 8.x locally: $ RDOC_VERSION='~> 8.0' bundle update rdoc $ RDOC_VERSION='~> 8.0' bundle exec rake +CI also tests CommonMarker 0.23.12. To run the full suite with the legacy parser: + + $ COMMONMARKER_VERSION=0.23.12 bundle update commonmarker + $ COMMONMARKER_VERSION=0.23.12 bundle exec rake + +Run these commands in a separate checkout to keep the default lockfile unchanged. You can combine `COMMONMARKER_VERSION` and `RDOC_VERSION` to test both overrides. + ## Releasing a new version If you are the current maintainer of this gem: diff --git a/Gemfile b/Gemfile index c111f6f1..a29dd0f5 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ gemspec gem "redcarpet", :platforms => :ruby gem "kramdown", :platforms => :jruby gem "RedCloth" -gem "commonmarker", "~> 2.8.2" +gem "commonmarker", ENV.fetch("COMMONMARKER_VERSION", "~> 2.8.2") gem "rdoc", ENV.fetch("RDOC_VERSION", "~> 7.2.0") gem "org-ruby", "0.9.12" gem "creole", "~>0.5.0" diff --git a/Gemfile.lock b/Gemfile.lock index dc0e5ee2..61ce481a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - github-markup (6.1.0) + github-markup (6.1.1) rdoc (< 9) GEM diff --git a/HISTORY.md b/HISTORY.md index e6cb831e..52216674 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,16 @@ -## 6.1.0 - 2026-10-16 +## 6.1.1 (unreleased) -Version `6.1.0` replaces the withdrawn `7.0.0` release with the same runtime behavior and dependency requirements. The CommonMarker compatibility changes below remain in effect. +### Bug Fixes + +* I restored the CommonMarker 0.x API without requiring a parser upgrade. Existing 0.x installations retain their rendering flags, extension validation, and output behavior. +* I retained CommonMarker 2.x support with its existing option behavior. +* I added full-suite CI coverage for CommonMarker 0.23.12 alongside the default 2.8.2 dependency. + +The RDoc 8 and reStructuredText fixes listed below remain included. + +## 6.1.0 - 2026-10-16 (withdrawn) + +I withdrew this release because it required a CommonMarker upgrade for existing 0.x installations. The `v6.1.0` tag remains available for traceability. ### Compatibility Changes diff --git a/README.md b/README.md index 018f54cc..2e82a89b 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ from this directory. Usage ----- +Markdown rendering supports CommonMarker 0.x and 2.x. Keep the installed version to retain that parser's option behavior. Extensions follow the installed parser; 2.x-only extensions remain unavailable with 0.x. + Basic form: ```ruby @@ -82,6 +84,8 @@ cd script ./bootstrap ``` +The development bundle uses CommonMarker 2.8.2 by default. Set `COMMONMARKER_VERSION` to test another version, then resolve dependencies with `bundle update commonmarker`. See [Testing](CONTRIBUTING.md#testing) for commands. + Contributing ------------ diff --git a/lib/github-markup.rb b/lib/github-markup.rb index 646c3954..d5f680c0 100644 --- a/lib/github-markup.rb +++ b/lib/github-markup.rb @@ -1,6 +1,6 @@ module GitHub module Markup - VERSION = '6.1.0' + VERSION = '6.1.1' Version = VERSION end end diff --git a/lib/github/markup/markdown.rb b/lib/github/markup/markdown.rb index 5b17afb7..855bc8ff 100644 --- a/lib/github/markup/markdown.rb +++ b/lib/github/markup/markdown.rb @@ -11,6 +11,10 @@ class Markdown < Implementation [:tagfilter, :autolink, :table, :strikethrough], ) + unless defined?(::Commonmarker) && ::Commonmarker.respond_to?(:to_html) + next ::CommonMarker.render_html(content, [:GITHUB_PRE_LANG] + legacy_opts, legacy_exts) + end + parse_options = {} # commonmarker 2.x changes several render defaults that diverge from cmark-gfm 0.x: # - hardbreaks defaults to true in 2.x but was false in 0.x. diff --git a/test/coverage_test.rb b/test/coverage_test.rb index c971d67d..c922a80e 100644 --- a/test/coverage_test.rb +++ b/test/coverage_test.rb @@ -22,6 +22,8 @@ require 'minitest/autorun' class CoverageTest < Minitest::Test + include ConstantStubbing + def test_version_constant_is_defined assert_kind_of String, GitHub::Markup::VERSION assert_equal GitHub::Markup::VERSION, GitHub::Markup::Version @@ -342,20 +344,6 @@ def capture_commonmarker_call(options) captured end - def with_stub_const(path, value) - parts = path.split("::") - name = parts.pop - parent = parts.inject(Object) { |mod, part| mod.const_get(part) } - had_const = parent.const_defined?(name, false) - original = parent.const_get(name) if had_const - parent.send(:remove_const, name) if had_const - parent.const_set(name, value) - yield - ensure - parent.send(:remove_const, name) if parent.const_defined?(name, false) - parent.const_set(name, original) if had_const - end - def without_linguist had_const = Object.const_defined?(:Linguist, false) original = Object.const_get(:Linguist) if had_const diff --git a/test/markdown_test.rb b/test/markdown_test.rb new file mode 100644 index 00000000..3983bca8 --- /dev/null +++ b/test/markdown_test.rb @@ -0,0 +1,96 @@ +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) + +require_relative "test_helper" +require "github/markup" +require "commonmarker" +require "minitest/autorun" +require "nokogiri" + +class MarkdownTest < Minitest::Test + include ConstantStubbing + + def test_uses_legacy_api_when_modern_interface_is_unavailable + legacy = Module.new + legacy.define_singleton_method(:render_html) do |content, _options, _extensions| + "

legacy #{content}

\n" + end + + with_stub_const("Commonmarker", Module.new) do + with_stub_const("CommonMarker", legacy) do + actual = GitHub::Markup::Markdown::MARKDOWN_GEMS.fetch("commonmarker").call("payload") + assert_equal "

legacy payload

\n", actual + end + end + end + + def test_renders_through_both_public_entry_points + assert_equal "

hello

\n", GitHub::Markup.render("raw.md", "hello") + assert_equal "

hello

\n", GitHub::Markup.render_s(GitHub::Markups::MARKUP_MARKDOWN, "hello") + end + + def test_preserves_code_language + document = Nokogiri::HTML.fragment(GitHub::Markup.render("raw.md", "```ruby\nputs 1\n```\n")) + assert_equal "ruby", document.at_css("pre")["lang"] + assert_equal "puts 1\n", document.at_css("code").text + end + + def test_preserves_unsafe_html_and_footnotes_without_mutating_frozen_options + options = {commonmarker_opts: [:UNSAFE, :FOOTNOTES].freeze}.freeze + html = GitHub::Markup.render("raw.md", "hello world[^1]\n\n[^1]: note\n", options: options) + document = Nokogiri::HTML.fragment(html) + + assert_equal "world", document.at_css("b").text + assert_equal "1", document.at_css("sup a").text + assert_includes document.at_css("li p").text, "note" + end + + def test_nobreaks_preserves_the_installed_backends_behavior + actual = GitHub::Markup.render("raw.md", "first\nsecond\n", options: {commonmarker_opts: [:NOBREAKS]}) + expected = legacy_commonmarker? ? "

first second

\n" : "

first\nsecond

\n" + assert_equal expected, actual + end + + def test_conflicting_break_flags_preserve_the_installed_backends_precedence + actual = GitHub::Markup.render("raw.md", "first\nsecond\n", options: {commonmarker_opts: [:HARDBREAKS, :NOBREAKS]}) + expected = legacy_commonmarker? ? "

first
\nsecond

\n" : "

first\nsecond

\n" + assert_equal expected, actual + + reverse = GitHub::Markup.render("raw.md", "first\nsecond\n", options: {commonmarker_opts: [:NOBREAKS, :HARDBREAKS]}) + assert_equal "

first
\nsecond

\n", reverse + end + + def test_table_style_option_and_frozen_extension_list + content = "| left | right |\n| :--- | ---: |\n| a | b |\n" + options = {commonmarker_opts: [:TABLE_PREFER_STYLE_ATTRIBUTES], commonmarker_exts: [:table].freeze} + document = Nokogiri::HTML.fragment(GitHub::Markup.render("raw.md", content, options: options)) + + assert_equal ["left", "right"], document.css("th").map(&:text) + attribute = legacy_commonmarker? ? "style" : "align" + expected = legacy_commonmarker? ? "text-align: left" : "left" + assert_equal expected, document.at_css("th")[attribute] + end + + def test_double_tilde_option_preserves_the_installed_backends_behavior + options = {commonmarker_opts: [:STRIKETHROUGH_DOUBLE_TILDE], commonmarker_exts: [:strikethrough]} + actual = GitHub::Markup.render("raw.md", "~one~ ~~two~~\n", options: options) + expected = legacy_commonmarker? ? "

~one~ two

\n" : "

one two

\n" + assert_equal expected, actual + end + + def test_extensions_follow_the_installed_parsers_capabilities + options = {commonmarker_exts: [:header_ids]} + if legacy_commonmarker? + error = assert_raises(ArgumentError) { GitHub::Markup.render("raw.md", "# Title", options: options) } + assert_match(/extension header_ids not found/, error.message) + else + document = Nokogiri::HTML.fragment(GitHub::Markup.render("raw.md", "# Title", options: options)) + assert_equal "title", document.at_css("a[id]")["id"] + end + end + + private + + def legacy_commonmarker? + Gem.loaded_specs.fetch("commonmarker").version < Gem::Version.new("1") + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index e04c9fdd..44d88457 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -9,3 +9,21 @@ command_name "MarkupTests" minimum_coverage line: 100, branch: 100 end + +module ConstantStubbing + private + + def with_stub_const(path, value) + parts = path.split("::") + name = parts.pop + parent = parts.inject(Object) { |mod, part| mod.const_get(part) } + had_const = parent.const_defined?(name, false) + original = parent.const_get(name) if had_const + parent.send(:remove_const, name) if had_const + parent.const_set(name, value) + yield + ensure + parent.send(:remove_const, name) if parent.const_defined?(name, false) + parent.const_set(name, original) if had_const + end +end From 2041987ab9b05a6d5ab2148cfa08ff845fe50e85 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Sun, 18 Oct 2026 21:56:46 -0700 Subject: [PATCH 2/2] docs: clarify CommonMarker testing and extension behavior I keep the development version override in the README and place full test commands in CONTRIBUTING. I correct comments about modern-only extensions without changing rendering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6fe372b1-3ff6-4129-9d33-be564fb6a5f0 Signed-off-by: Zack Koppert --- README.md | 2 +- lib/github/markup/markdown.rb | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2e82a89b..14cacfa7 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ cd script ./bootstrap ``` -The development bundle uses CommonMarker 2.8.2 by default. Set `COMMONMARKER_VERSION` to test another version, then resolve dependencies with `bundle update commonmarker`. See [Testing](CONTRIBUTING.md#testing) for commands. +Set `COMMONMARKER_VERSION` to override the development bundle's default `~> 2.8.2` requirement. See [Testing](CONTRIBUTING.md#testing) for parser version commands. Contributing ------------ diff --git a/lib/github/markup/markdown.rb b/lib/github/markup/markdown.rb index 855bc8ff..1714cd21 100644 --- a/lib/github/markup/markdown.rb +++ b/lib/github/markup/markdown.rb @@ -64,17 +64,16 @@ class Markdown < Implementation :greentext, :alerts, :description_lists extension_options[ext] = true when :header_ids - # header_ids takes a string prefix in 2.x rather than a boolean. The legacy contract - # only passed it as a symbol, so use an empty prefix to enable anchor generation. + # header_ids takes a string prefix in 2.x rather than a boolean. An empty prefix + # enables anchor generation for the symbol-based commonmarker_exts option. extension_options[:header_ids] = "" else raise ArgumentError, "unknown commonmarker extension: #{ext.inspect}" end end - # Several extensions (tagfilter, autolink, table, strikethrough, tasklist, shortcodes) - # are enabled by default in commonmarker 2.x but were strictly opt-in in 0.x. Explicitly - # disable any extension the caller did not request so behavior matches the legacy contract. + # CommonMarker 2.x enables more extensions by default than this wrapper does. + # Disable unrequested defaults to preserve the wrapper's extension choices. [:strikethrough, :tagfilter, :autolink, :table, :tasklist, :shortcodes].each do |ext| extension_options[ext] = false unless extension_options[ext] end