Skip to content

Instantly share code, notes, and snippets.

@ssig33
Created March 25, 2026 05:40
Show Gist options
  • Select an option

  • Save ssig33/62bdcb313c92a12fbb01335e69f18b4c to your computer and use it in GitHub Desktop.

Select an option

Save ssig33/62bdcb313c92a12fbb01335e69f18b4c to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# frozen_string_literal: true
# CommonMark の emphasis 規則と CJK (分かち書きなし) の組み合わせで
# 意図通りに強調が解釈されないケースを検出する CLI
if ARGV.empty?
warn "Usage: #{$0} <markdown_file> [...]"
exit 1
end
# CommonMark の Unicode punctuation = \p{P} (Pc|Pd|Pe|Pf|Pi|Po|Ps)
# right-flanking にならないパターン:
# 閉じ delimiter の直前が Unicode punctuation で、直後が punctuation でも whitespace でもない
# left-flanking にならないパターン:
# 開始 delimiter の直後が Unicode punctuation で、直前が punctuation でも whitespace でもない
# \p{P} から * と _ を除外した punctuation キャラクタクラス
P = /[^\s\w*_]/.freeze # delimiter 以外の punctuation 相当(簡易判定)
PUNCT = /(?:\p{P}(?<![*_]))/.freeze
CLOSE_PATTERN = /(#{PUNCT})(\*{1,3}|_{1,3})([^\p{P}\s])/
OPEN_PATTERN = /([^\p{P}\s])(\*{1,3}|_{1,3})(#{PUNCT})/
def in_code_block?(lines, line_index)
fence_count = 0
lines[0...line_index].each do |l|
fence_count += 1 if l.match?(/\A\s*(```|~~~)/)
end
fence_count.odd?
end
def strip_inline_code(line)
line.gsub(/`[^`]*`/, ' ' * 1)
end
issues = []
ARGV.each do |file|
unless File.exist?(file)
warn "File not found: #{file}"
next
end
lines = File.readlines(file)
lines.each_with_index do |line, idx|
next if in_code_block?(lines, idx)
next if line.match?(/\A\s*(```|~~~)/)
cleaned = strip_inline_code(line)
cleaned.scan(CLOSE_PATTERN) do |punct, delim, after|
col = cleaned.index("#{punct}#{delim}#{after}")
issues << {
file: file,
line: idx + 1,
col: col,
text: line.chomp,
msg: "#{delim} の直前に約物「#{punct}」があるため emphasis が正しく解釈されない可能性がある"
}
end
cleaned.scan(OPEN_PATTERN) do |before, delim, punct|
col = cleaned.index("#{before}#{delim}#{punct}")
issues << {
file: file,
line: idx + 1,
col: col,
text: line.chomp,
msg: "#{delim} の直後に約物「#{punct}」があるため emphasis が正しく解釈されない可能性がある"
}
end
end
end
if issues.empty?
puts "No issues found."
exit 0
end
GUIDANCE = <<~'MSG'
=== CommonMark emphasis と CJK テキストの問題について ===
CommonMark の emphasis (強調) は delimiter run (* や **) が "left-flanking" または
"right-flanking" であるかどうかで開始・終了が決まる。この判定に Unicode punctuation
カテゴリ (\p{P}) が使われる。
日本語・中国語・韓国語 (CJK) は分かち書きをしないため、emphasis delimiter と約物
(句読点、括弧など)が空白を挟まず隣接するケースが頻繁に発生する。このとき
CommonMark の規則により delimiter が left-flanking / right-flanking の条件を
満たさなくなり、emphasis が意図通りに解釈されない。
■ 問題が起きる例:
✗ これは**私のやりたかったこと。**だからするの。
→ 閉じ ** の直前に「。」(punctuation) があり、直後が通常文字のため
right-flanking にならず ** がそのまま表示される
✗ **利益剰余金4,624億円**(2019年3月期時点)
→ 閉じ ** の直後に「(」(punctuation) があり正しく解釈されない
■ 安全な書き方:
○ emphasis delimiter の直前・直後に約物を置かない
○ 約物は emphasis の外に出す: **テスト**。 **テスト**(補足)
○ どうしても中に入れる場合は前後に半角スペースを入れる: **テスト。** だからするの
■ 参考:
- CommonMark Spec 0.31.2 Section 6.2 (Emphasis and strong emphasis)
- https://github.com/commonmark/commonmark-spec/issues/650
MSG
puts GUIDANCE
puts
issues.each do |i|
puts "#{i[:file]}:#{i[:line]}: #{i[:msg]}"
puts " #{i[:text]}"
puts
end
exit 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment