Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -24,22 +27,27 @@ 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:
- name: Checkout
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:
Expand Down
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
github-markup (6.1.0)
github-markup (6.1.1)
rdoc (< 9)

GEM
Expand Down
14 changes: 12 additions & 2 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -82,6 +84,8 @@ cd script
./bootstrap
```

Set `COMMONMARKER_VERSION` to override the development bundle's default `~> 2.8.2` requirement. See [Testing](CONTRIBUTING.md#testing) for parser version commands.

Contributing
------------

Expand Down
2 changes: 1 addition & 1 deletion lib/github-markup.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module GitHub
module Markup
VERSION = '6.1.0'
VERSION = '6.1.1'
Version = VERSION
end
end
13 changes: 8 additions & 5 deletions lib/github/markup/markdown.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -60,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
Expand Down
16 changes: 2 additions & 14 deletions test/coverage_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
96 changes: 96 additions & 0 deletions test/markdown_test.rb
Original file line number Diff line number Diff line change
@@ -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|
"<p>legacy #{content}</p>\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 "<p>legacy payload</p>\n", actual
end
end
end

def test_renders_through_both_public_entry_points
assert_equal "<p>hello</p>\n", GitHub::Markup.render("raw.md", "hello")
assert_equal "<p>hello</p>\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 <b>world</b>[^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? ? "<p>first second</p>\n" : "<p>first\nsecond</p>\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? ? "<p>first<br />\nsecond</p>\n" : "<p>first\nsecond</p>\n"
assert_equal expected, actual

reverse = GitHub::Markup.render("raw.md", "first\nsecond\n", options: {commonmarker_opts: [:NOBREAKS, :HARDBREAKS]})
assert_equal "<p>first<br />\nsecond</p>\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? ? "<p>~one~ <del>two</del></p>\n" : "<p><del>one</del> <del>two</del></p>\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
18 changes: 18 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading