From 3432fd2c2dcc1a367732e16404f5cd1fee895c43 Mon Sep 17 00:00:00 2001 From: Jordon Bedwell Date: Tue, 3 Nov 2015 19:48:29 -0600 Subject: [PATCH] Modernize Kramdown for Markdown converter. --- Gemfile | 3 +- lib/jekyll/configuration.rb | 12 +- .../converters/markdown/kramdown_parser.rb | 93 +++++++++++--- lib/jekyll/utils.rb | 4 + test/helper.rb | 14 +- test/test_kramdown.rb | 121 ++++++++++++------ 6 files changed, 173 insertions(+), 74 deletions(-) diff --git a/Gemfile b/Gemfile index 9b64809e..19548e81 100644 --- a/Gemfile +++ b/Gemfile @@ -18,8 +18,9 @@ group :test do gem 'jekyll_test_plugin_malicious' gem 'minitest-reporters' gem 'minitest-profile' - gem 'minitest' gem 'rspec-mocks' + gem 'minitest' + gem "nokogiri" if RUBY_PLATFORM =~ /cygwin/ || RUBY_VERSION.start_with?("2.2") gem 'test-unit' diff --git a/lib/jekyll/configuration.rb b/lib/jekyll/configuration.rb index f57b3ae4..a3e80c7f 100644 --- a/lib/jekyll/configuration.rb +++ b/lib/jekyll/configuration.rb @@ -68,17 +68,7 @@ module Jekyll 'footnote_nr' => 1, 'entity_output' => 'as_char', 'toc_levels' => '1..6', - 'smart_quotes' => 'lsquo,rsquo,ldquo,rdquo', - 'enable_coderay' => false, - - 'coderay' => { - 'coderay_wrap' => 'div', - 'coderay_line_numbers' => 'inline', - 'coderay_line_number_start' => 1, - 'coderay_tab_width' => 4, - 'coderay_bold_every' => 10, - 'coderay_css' => 'style' - } + 'smart_quotes' => 'lsquo,rsquo,ldquo,rdquo' } }] diff --git a/lib/jekyll/converters/markdown/kramdown_parser.rb b/lib/jekyll/converters/markdown/kramdown_parser.rb index 40dfa87e..d1fa7865 100644 --- a/lib/jekyll/converters/markdown/kramdown_parser.rb +++ b/lib/jekyll/converters/markdown/kramdown_parser.rb @@ -1,33 +1,86 @@ +# Frozen-string-literal: true +# Encoding: utf-8 + module Jekyll module Converters class Markdown class KramdownParser + CODERAY_DEFAULTS = { + "css" => "style", + "bold_every" => 10, + "line_numbers" => "inline", + "line_number_start" => 1, + "tab_width" => 4, + "wrap" => "div" + } + def initialize(config) - require 'kramdown' - @config = config - # If kramdown supported highlighter enabled, use that - highlighter = @config['highlighter'] - if highlighter == 'rouge' || highlighter == 'coderay' - @config['kramdown']['syntax_highlighter'] ||= highlighter - end - rescue LoadError - STDERR.puts 'You are missing a library required for Markdown. Please run:' - STDERR.puts ' $ [sudo] gem install kramdown' - raise Errors::FatalException.new("Missing dependency: kramdown") + Jekyll::External.require_with_graceful_fail "kramdown" + @main_fallback_highlighter = config["highlighter"] || "rogue" + @config = config["kramdown"] || {} + setup + end + + # Setup and normalize the configuration: + # * Create Kramdown if it doesn't exist. + # * Set syntax_highlighter, detecting enable_coderay and merging highlighter if none. + # * Merge kramdown[coderay] into syntax_highlighter_opts stripping coderay_. + # * Make sure `syntax_highlighter_opts` exists. + + def setup + @config["syntax_highlighter"] ||= highlighter + @config["syntax_highlighter_opts"] ||= {} + @config["coderay"] ||= {} # XXX: Legacy. + modernize_coderay_config end def convert(content) - # Check for use of coderay - if @config['kramdown']['enable_coderay'] - %w[wrap line_numbers line_numbers_start tab_width bold_every css default_lang].each do |opt| - key = "coderay_#{opt}" - @config['kramdown'][key] = @config['kramdown']['coderay'][key] unless @config['kramdown'].key?(key) - end - end - - Kramdown::Document.new(content, Utils.symbolize_hash_keys(@config['kramdown'])).to_html + Kramdown::Document.new(content, @config).to_html end + # config[kramdown][syntax_higlighter] > config[kramdown][enable_coderay] > config[highlighter] + # Where `enable_coderay` is now deprecated because Kramdown + # supports Rouge now too. + + private + def highlighter + @highlighter ||= begin + if highlighter = @config["syntax_highlighter"] then highlighter + elsif @config.key?("enable_coderay") && @config["enable_coderay"] + Jekyll.logger.warn "DEPRECATION: You are using 'enable_coderay', use syntax_highlighter: coderay." + "coderay" + else + @main_fallback_highlighter + end + end + end + + private + def strip_coderay_prefix(hash) + hash.each_with_object({}) do |(key, val), hsh| + cleaned_key = key.gsub(/\Acoderay_/, "") + Jekyll.logger.warn "You are using '#{key}'. Normalizing to #{cleaned_key}." if key != cleaned_key + hsh[cleaned_key] = val + end + end + + # If our highlighter is CodeRay we go in to merge the CodeRay defaults + # with your "coderay" key if it's there, deprecating it in the + # process of you using it. + + private + def modernize_coderay_config + if highlighter == "coderay" + Jekyll.logger.warn "DEPRECATION: kramdown.coderay is deprecated use syntax_highlighter_opts." + @config["syntax_highlighter_opts"] = begin + strip_coderay_prefix( + @config["syntax_highlighter_opts"] \ + .merge(CODERAY_DEFAULTS) \ + .merge(@config["coderay"]) + ) + end + end + end end end end diff --git a/lib/jekyll/utils.rb b/lib/jekyll/utils.rb index 7d2490a6..0b67911b 100644 --- a/lib/jekyll/utils.rb +++ b/lib/jekyll/utils.rb @@ -8,6 +8,10 @@ module Jekyll SLUGIFY_DEFAULT_REGEXP = Regexp.new('[^[:alnum:]]+').freeze SLUGIFY_PRETTY_REGEXP = Regexp.new("[^[:alnum:]._~!$&'()+,;=@]+").freeze + def strip_heredoc(str) + str.gsub(/^[ \t]{#{(str.scan(/^[ \t]*(?=\S)/).min || "").size}}/, "") + end + # Non-destructive version of deep_merge_hashes! See that method. # # Returns the merged hashes. diff --git a/test/helper.rb b/test/helper.rb index dc021767..281f6871 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -11,13 +11,13 @@ unless ENV['TRAVIS'] end end +require "nokogiri" require 'rubygems' require 'ostruct' require 'minitest/autorun' require 'minitest/reporters' require 'minitest/profile' require 'rspec/mocks' - require 'jekyll' Jekyll.logger = Logger.new(StringIO.new) @@ -51,15 +51,15 @@ class JekyllUnitTest < Minitest::Test end def before_setup - ::RSpec::Mocks.setup + RSpec::Mocks.setup super end def after_teardown super - ::RSpec::Mocks.verify + RSpec::Mocks.verify ensure - ::RSpec::Mocks.teardown + RSpec::Mocks.teardown end def fixture_site(overrides = {}) @@ -120,4 +120,10 @@ class JekyllUnitTest < Minitest::Test end alias_method :capture_stdout, :capture_output alias_method :capture_stderr, :capture_output + + def nokogiri_fragment(str) + Nokogiri::HTML.fragment( + str + ) + end end diff --git a/test/test_kramdown.rb b/test/test_kramdown.rb index b7f44925..21ba5764 100644 --- a/test/test_kramdown.rb +++ b/test/test_kramdown.rb @@ -8,65 +8,110 @@ class TestKramdown < JekyllUnitTest @config = { 'markdown' => 'kramdown', 'kramdown' => { - 'auto_ids' => false, - 'footnote_nr' => 1, + 'smart_quotes' => 'lsquo,rsquo,ldquo,rdquo', 'entity_output' => 'as_char', - 'toc_levels' => '1..6', - 'smart_quotes' => 'lsquo,rsquo,ldquo,rdquo', + 'toc_levels' => '1..6', + 'auto_ids' => false, + 'footnote_nr' => 1, - 'enable_coderay' => true, - 'coderay_bold_every'=> 12, - 'coderay' => { - 'coderay_css' => :style, - 'coderay_bold_every' => 8 + 'syntax_highlighter_opts' => { + 'bold_every' => 8, 'css' => :class } } } + @config = Jekyll.configuration(@config) - @markdown = Converters::Markdown.new(@config) + @markdown = Converters::Markdown.new( + @config + ) end - # http://kramdown.gettalong.org/converter/html.html#options - should "pass kramdown options" do + should "run Kramdown" do assert_equal "

Some Header

", @markdown.convert('# Some Header #').strip end - should "convert quotes to smart quotes" do - assert_match /

(“|“)Pit(’|’)hy(”|”)<\/p>/, @markdown.convert(%{"Pit'hy"}).strip + context "when asked to convert smart quotes" do + should "convert" do + assert_match %r!

(“|“)Pit(’|’)hy(”|”)<\/p>!, @markdown.convert(%{"Pit'hy"}).strip + end - override = { 'kramdown' => { 'smart_quotes' => 'lsaquo,rsaquo,laquo,raquo' } } - markdown = Converters::Markdown.new(Utils.deep_merge_hashes(@config, override)) - assert_match /

(«|«)Pit(›|›)hy(»|»)<\/p>/, markdown.convert(%{"Pit'hy"}).strip + should "support custom types" do + override = { + 'kramdown' => { + 'smart_quotes' => 'lsaquo,rsaquo,laquo,raquo' + } + } + + markdown = Converters::Markdown.new(Utils.deep_merge_hashes(@config, override)) + assert_match %r!

(«|«)Pit(›|›)hy(»|»)<\/p>!, \ + markdown.convert(%{"Pit'hy"}).strip + end end should "render fenced code blocks with syntax highlighting" do - assert_equal "

puts \"Hello world\"\n
\n
", @markdown.convert( - <<-EOS -~~~ruby -puts "Hello world" -~~~ - EOS - ).strip + result = nokogiri_fragment(@markdown.convert(Utils.strip_heredoc <<-MARKDOWN)) + ~~~ruby + puts "Hello World" + ~~~ + MARKDOWN + + selector = "div.highlighter-rouge>pre.highlight>code" + refute result.css(selector).empty? end - context "moving up nested coderay options" do - setup do - @markdown.convert('some markup') - @converter_config = @markdown.instance_variable_get(:@config)['kramdown'] + context "when a custom highlighter is chosen" do + should "use the chosen highlighter if it's available" do + override = { "markdown" => "kramdown", "kramdown" => { "syntax_highlighter" => :coderay }} + markdown = Converters::Markdown.new(Utils.deep_merge_hashes(@config, override)) + result = nokogiri_fragment(markdown.convert(Utils.strip_heredoc <<-MARKDOWN)) + ~~~ruby + puts "Hello World" + ~~~ + MARKDOWN + + selector = "div.highlighter-coderay>div.CodeRay>div.code>pre" + refute result.css(selector).empty? end - should "work correctly" do - assert_equal :style, @converter_config['coderay_css'] + should "support legacy enable_coderay... for now" do + override = { + "markdown" => "kramdown", + "kramdown" => { + "syntax_highlighter" => nil, + "enable_coderay" => true + } + } + + markdown = Converters::Markdown.new(Utils.deep_merge_hashes(@config, override)) + result = nokogiri_fragment(markdown.convert(Utils.strip_heredoc <<-MARKDOWN)) + ~~~ruby + puts "Hello World" + ~~~ + MARKDOWN + + selector = "div.highlighter-coderay>div.CodeRay>div.code>pre" + refute result.css(selector).empty? + end + end + + should "move coderay to syntax_highlighter_opts" do + original = Kramdown::Document.method(:new) + markdown = Converters::Markdown.new(Utils.deep_merge_hashes(@config, { + "markdown" => "kramdown", + "kramdown" => { + "syntax_highlighter" => "coderay", + "coderay" => { + "hello" => "world" + } + } + })) + + expect(Kramdown::Document).to receive(:new) do |arg1, hash| + assert_equal hash["syntax_highlighter_opts"]["hello"], "world" + original.call(arg1, hash) end - should "also work for defaults" do - default = Jekyll::Configuration::DEFAULTS['kramdown']['coderay']['coderay_tab_width'] - assert_equal default, @converter_config['coderay_tab_width'] - end - - should "not overwrite" do - assert_equal 12, @converter_config['coderay_bold_every'] - end + markdown.convert("hello world") end end end