-
-
Save ssig33/62bdcb313c92a12fbb01335e69f18b4c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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