Skip to content

Commit e15ff61

Browse files
znzclaude
andcommitted
Rouge を導入して ruby 以外の言語のシンタックスハイライトに対応する
コードフェンスの言語指定は ruby だけがハイライトされ、他の言語は 素通し(しかも HTML エスケープなし)だった。 - Rouge を bitclust-core の依存に追加。lang 指定付きコードブロックの ハイライトを共通ヘルパ highlight_source に集約(rd の //emlist[][lang] と md のコードフェンスの両方) - 言語の解決は Rouge::Lexer.find に委ね、解決先が Ruby lexer の場合 (rb などの alias を含む)は従来どおり構文チェックを兼ねる Ripper ベースの SyntaxHighlighter を使う - その他の言語は Rouge::Formatters::HTML でハイライト。出力クラス名は Pygments 互換なので既存の syntax-highlight.css がそのまま効く (不足していた cpf / pi のみ追記) - Rouge が知らない言語はエスケープのみで出力。従来この経路と ruby の 構文エラー時フォールバックはエスケープされずに出力されていたのを修正 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1b5feac commit e15ff61

7 files changed

Lines changed: 88 additions & 30 deletions

File tree

Gemfile.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ PATH
77
nkf
88
progressbar (>= 1.9.0, < 2.0)
99
rack
10+
rouge
1011
webrick
1112
bitclust-dev (1.4.0)
1213
bitclust-core (= 1.4.0)
@@ -78,6 +79,8 @@ GEM
7879
rbs (3.10.3)
7980
logger
8081
tsort
82+
rouge (5.0.0)
83+
strscan (~> 3.1)
8184
rr (3.1.2)
8285
securerandom (0.4.1)
8386
steep (1.9.4)
@@ -181,6 +184,7 @@ CHECKSUMS
181184
rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
182185
rbs (3.10.3) sha256=70627f3919016134d554e6c99195552ae3ef6020fe034c8e983facc9c192daa6
183186
refe2 (1.4.0)
187+
rouge (5.0.0) sha256=e2de9bba2f9cb88f8a73bca3643b560b2b398f32f91bf716f584477bc0175ebc
184188
rr (3.1.2) sha256=cc4a5cc91f012327d317dc661154419f9d36d8f9c05031ac46accd89ceb0ff1b
185189
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
186190
steep (1.9.4) sha256=09a941469b55d31cdc70a3812b5c1d4102d361f628f5fe5497b6a9aabc3cee05

bitclust-core.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ EOD
3131
s.add_development_dependency "test-unit-notify"
3232
s.add_development_dependency "test-unit-rr"
3333
s.add_runtime_dependency "rack"
34+
s.add_runtime_dependency "rouge"
3435
s.add_runtime_dependency "progressbar", ">= 1.9.0", "< 2.0"
3536
s.add_runtime_dependency "webrick"
3637
s.add_runtime_dependency "drb"

lib/bitclust/mdcompiler.rb

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -347,21 +347,7 @@ def code_fence
347347
@f.until_terminator(terminator) do |code_line|
348348
src << code_line
349349
end
350-
if lang == "ruby"
351-
begin
352-
filename = (caption&.size || 0) > 2 ? caption : @f.name or raise
353-
string BitClust::SyntaxHighlighter.new(src, filename).highlight
354-
rescue BitClust::SyntaxHighlighter::Error => ex
355-
$stderr.puts ex.message
356-
if stop_on_syntax_error?
357-
exit(false)
358-
else
359-
string src
360-
end
361-
end
362-
else
363-
string src
364-
end
350+
string highlight_source(src, lang, caption)
365351
line '</code></pre>'
366352
elsif fence.size == 3
367353
line '<pre>'

lib/bitclust/rdcompiler.rb

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
require 'bitclust/textutils'
1515
require 'bitclust/messagecatalog'
1616
require 'bitclust/syntax_highlighter'
17+
require 'rouge'
1718
require 'stringio'
1819

1920
module BitClust
@@ -271,6 +272,29 @@ def stop_on_syntax_error?
271272
@option[:stop_on_syntax_error]
272273
end
273274

275+
# lang 指定付きコードブロックのハイライト HTML を返す。
276+
# ruby(rb などの Rouge 上の alias を含む)は構文チェックを兼ねる
277+
# Ripper ベースの SyntaxHighlighter、その他の言語は Rouge を使う。
278+
# Rouge が知らない言語はエスケープのみ(従来この経路と ruby の構文
279+
# エラー時フォールバックはエスケープされずに出力されていたのを修正)
280+
def highlight_source(src, lang, caption)
281+
lexer = ::Rouge::Lexer.find(lang)
282+
if lexer && lexer.tag == 'ruby'
283+
begin
284+
filename = (caption&.size || 0) > 2 ? caption : @f.name or raise
285+
BitClust::SyntaxHighlighter.new(src, filename).highlight
286+
rescue BitClust::SyntaxHighlighter::Error => ex
287+
$stderr.puts ex.message
288+
exit(false) if stop_on_syntax_error?
289+
escape_html(src)
290+
end
291+
elsif lexer
292+
::Rouge::Formatters::HTML.new.format(lexer.lex(src))
293+
else
294+
escape_html(src)
295+
end
296+
end
297+
274298
def emlist
275299
command = @f.gets
276300
if %r!\A//emlist\[(?<caption>[^\[\]]+?)?\]\[(?<lang>\w+?)\]! =~ command
@@ -283,21 +307,7 @@ def emlist
283307
@f.until_terminator(%r<\A//\}>) do |line|
284308
src << line
285309
end
286-
if lang == "ruby"
287-
begin
288-
filename = (caption&.size || 0) > 2 ? caption : @f.name or raise
289-
string BitClust::SyntaxHighlighter.new(src, filename).highlight
290-
rescue BitClust::SyntaxHighlighter::Error => ex
291-
$stderr.puts ex.message
292-
if stop_on_syntax_error?
293-
exit(false)
294-
else
295-
string src
296-
end
297-
end
298-
else
299-
string src
300-
end
310+
string highlight_source(src, lang, caption)
301311
line '</code></pre>'
302312
else
303313
line '<pre>'

sig/bitclust/rdcompiler.rbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ module BitClust
8282

8383
def stop_on_syntax_error?: () -> bool
8484

85+
def highlight_source: (String src, String lang, String? caption) -> String
86+
8587
def emlist: () -> void
8688

8789
def list: () -> void

test/test_mdcompiler.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,3 +610,50 @@ def test_doc_dlist
610610
RD
611611
end
612612
end
613+
614+
# lang 指定付きコードブロックのハイライト(ruby は Ripper ベースの
615+
# SyntaxHighlighter、その他の言語は Rouge、未知の言語はエスケープのみ)
616+
class TestMDCompilerRougeHighlight < Test::Unit::TestCase
617+
def setup
618+
@dummy = 'dummy'
619+
@u = BitClust::URLMapper.new(Hash.new { @dummy })
620+
@db = BitClust::MethodDatabase.dummy("version" => "2.0.0")
621+
@md = BitClust::MDCompiler.new(@u, 1, { :database => @db, :gfm => true })
622+
@rd = BitClust::RDCompiler.new(@u, 1, { :database => @db })
623+
end
624+
625+
def test_c_fence_is_highlighted_with_rouge
626+
html = @md.compile("```c\nint x = 1; /* comment */\n```\n")
627+
assert_match(/<pre class="highlight c">/, html)
628+
assert_match(%r{<span class="kt">int</span>}, html)
629+
assert_match(%r{<span class="cm">/\* comment \*/</span>}, html)
630+
end
631+
632+
def test_ruby_alias_rb_uses_bitclust_highlighter
633+
ruby_html = @md.compile("```ruby\nputs 1\n```\n")
634+
rb_html = @md.compile("```rb\nputs 1\n```\n")
635+
assert_equal(ruby_html.sub('highlight ruby', 'highlight rb'), rb_html)
636+
# bitclust の SyntaxHighlighter 由来のマークアップ(Rouge の Ruby lexer ではなく)
637+
assert_match(%r{<span class="nb">puts</span>}, rb_html)
638+
end
639+
640+
def test_unknown_lang_fence_is_escaped
641+
html = @md.compile("```nosuchlang\n<b>&raw</b>\n```\n")
642+
assert_match(/<pre class="highlight nosuchlang">/, html)
643+
assert_match(/&lt;b&gt;&amp;raw&lt;\/b&gt;/, html)
644+
assert_not_match(/<b>/, html)
645+
end
646+
647+
def test_text_fence_is_escaped_without_highlight
648+
html = @md.compile("```text\n<b>plain</b>\n```\n")
649+
assert_match(/&lt;b&gt;plain&lt;\/b&gt;/, html)
650+
assert_not_match(/<b>plain/, html)
651+
end
652+
653+
def test_rd_emlist_with_c_lang_is_equivalent
654+
rd_src = "//emlist[キャプション][c]{\nint x = 1;\n//}\n"
655+
md_src = BitClust::RRDToMarkdown.convert(rd_src)
656+
assert_equal(@rd.compile(rd_src), @md.compile(md_src), "md source:\n#{md_src}")
657+
assert_match(%r{<span class="kt">int</span>}, @rd.compile(rd_src))
658+
end
659+
end

theme/default/syntax-highlight.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
color: #999999;
99
font-weight: bold;
1010
}
11+
.highlight .cpf { /* preprocessor file (C の #include <...> など。Rouge 用) */
12+
color: #999999;
13+
font-weight: bold;
14+
}
15+
.highlight .pi { /* YAML などの indicator (Rouge 用) */
16+
color: #999999;
17+
font-weight: bold;
18+
}
1119
.highlight .c1 {
1220
color: #999988;
1321
font-style: italic;

0 commit comments

Comments
 (0)