Module: VivlioStarter::CLI::Lint::Tokenizer

Defined in:
lib/vivlio_starter/cli/lint/tokenizer.rb

Overview

Markdownファイルから英単語トークンを抽出する

Constant Summary collapse

FRONTMATTER_SEP =
/^---\s*$/
VS_LINT_DISABLE =

vs-lint コメント記法の定義

/^\s*<!--\s*vs-lint-disable\s*-->\s*$/
VS_LINT_ENABLE =
/^\s*<!--\s*vs-lint-enable\s*-->\s*$/
VS_LINT_DISABLE_NEXT_LINE =
/^\s*<!--\s*vs-lint-disable-next-line\s*-->\s*$/

Class Method Summary collapse

Class Method Details

.build_excluded_lines(content) ⇒ Array(Set<Integer>, Integer?)

vs-lint コメントに基づいて除外すべき行番号のセットを構築する

コード領域の中は見ない。 校正の使い方を解説する原稿は、フェンスの中へ <!-- vs-lint-disable --> を書き写す。それを本物の指示として解釈すると、 例示のつもりの 1 行がファイル末尾までの抑止になる(実測: ```markdown の中に 閉じない disable を置くと、その下の本文の綴り誤りが検出されなくなった)。 記法かどうかの判定は Masking へ委ね、ProseChecker と同じ扱いに揃える。

Parameters:

  • content (String)

    Markdownファイル全体の内容

Returns:

  • (Array(Set<Integer>, Integer?))

    除外行番号セットと、 未クローズ disable ブロックの開始行番号(クローズ済みなら nil)



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/vivlio_starter/cli/lint/tokenizer.rb', line 90

def build_excluded_lines(content)
  excluded_lines = Set.new
  disable_opened_at = nil
  prose = Set.new
  Masking.each_prose_line(content) { |_line, lineno| prose << lineno }
  line_no = 0

  content.each_line do |line|
    line_no += 1
    next unless prose.include?(line_no)

    # vs-lint-disable コメント行自体を除外
    if line.match?(VS_LINT_DISABLE)
      disable_opened_at ||= line_no
      excluded_lines.add(line_no)
      next
    end

    # vs-lint-enable コメント行自体を除外
    if line.match?(VS_LINT_ENABLE)
      disable_opened_at = nil
      excluded_lines.add(line_no)
      next
    end

    # vs-lint-disable-next-line コメント行自体を除外し、次の行も除外
    if line.match?(VS_LINT_DISABLE_NEXT_LINE)
      excluded_lines.add(line_no)
      excluded_lines.add(line_no + 1)
      next
    end

    # disable ブロック内の行を除外
    excluded_lines.add(line_no) if disable_opened_at
  end

  [excluded_lines, disable_opened_at]
end

.code_fence_lines(content) ⇒ Object

コード(フェンス区切り行・内容行)とみなす行番号の集合を Masking で判定する。



72
73
74
75
76
77
# File 'lib/vivlio_starter/cli/lint/tokenizer.rb', line 72

def code_fence_lines(content)
  prose = Set.new
  Masking.each_prose_line(content) { |_line, lineno| prose << lineno }
  total = content.each_line.count
  (1..total).reject { prose.include?(it) }.to_set
end

.extract_words(line) ⇒ Array<String>

Returns 抽出された英単語の配列.

Parameters:

  • line (String)

    1行のMarkdownテキスト

Returns:

  • (Array<String>)

    抽出された英単語の配列



148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/vivlio_starter/cli/lint/tokenizer.rb', line 148

def extract_words(line)
  cleaned = line.dup
  cleaned.gsub!(/`[^`]*`/, ' ')                          # インラインコードを除去
  cleaned.gsub!(/<[^>]+>/, ' ')                          # HTMLタグを除去
  cleaned.gsub!(/\{[^}]*\}/, ' ')                        # Vivliostyle拡張記法 {.aki} 等
  cleaned.gsub!(/(?<![A-Za-z0-9_])@[A-Za-z][A-Za-z0-9-]*/, ' ') # 相互参照ラベル @id(メールは除外)
  cleaned.gsub!(/!?\[([^\]]*)\]\([^)]*\)/, '\1') # Markdownリンク・画像
  cleaned.gsub!(/!?\[([^\]]*)\]\[[^\]]*\]/, '\1')        # 参照リンク
  cleaned.gsub!(%r{https?://\S+}, ' ')                   # URLを除去
  cleaned.gsub!(/^#+\s*/, '')                            # 見出し記号を除去

  cleaned.scan(/[a-zA-Z]+(?:-[a-zA-Z]+)*/).select { it.length >= 2 }
end

.tokenize(content, check_code_blocks: false, path: nil) ⇒ Array<[String, Integer]>

Returns [word, line_no] のペア配列.

Parameters:

  • content (String)

    Markdownファイル全体の内容

  • check_code_blocks (Boolean) (defaults to: false)

    コードブロック内もチェックするか

  • path (String, nil) (defaults to: nil)

    警告メッセージに含めるファイルパス(省略可)

Returns:

  • (Array<[String, Integer]>)

    [word, line_no] のペア配列



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/vivlio_starter/cli/lint/tokenizer.rb', line 25

def tokenize(content, check_code_blocks: false, path: nil)
  # VFM 記法(showcase の座標行・クラス属性など)を先に中和する。textlint 側と
  # 同じ NotationGuard を通すことで、記法の判定が lint 全体で一本化される。
  # 行数は保存されるため、以降の行番号は原稿のものと一致したままになる。
  content = NotationGuard.strip_notation(content)

  tokens           = []
  in_frontmatter   = false
  line_no          = 0

  # vs-lint コメントによる除外行番号セットを構築
  excluded_lines, unclosed_disable_at = build_excluded_lines(content)
  warn_unclosed_disable(path, unclosed_disable_at) if unclosed_disable_at

  # コードフェンス行の集合を Masking(唯一の実装)で判定する。
  # 従来の /^```/ 単純トグルと異なり、可変長フェンス(```/````/~~~)と
  # 入れ子・```include: 除外に追従するため、入れ子フェンスで本文を誤って
  # コード扱いしなくなる。check_code_blocks 時はコードも検査対象に含める。
  code_lines = check_code_blocks ? Set.new : code_fence_lines(content)

  content.each_line do |line|
    line_no += 1

    # YAMLフロントマター(先頭 --- 〜 --- )をスキップ
    if line_no == 1 && line.match?(FRONTMATTER_SEP)
      in_frontmatter = true
      next
    end

    if in_frontmatter
      in_frontmatter = false if line.match?(FRONTMATTER_SEP)
      next
    end

    # コードブロック内のスキップ
    next if code_lines.include?(line_no)

    # vs-lint コメントによる除外
    next if excluded_lines.include?(line_no)

    extract_words(line).each { |word| tokens << [word, line_no] }
  end

  tokens
end

.warn_unclosed_disable(path, opened_at) ⇒ Object

vs-lint-disable が閉じられないままファイル末尾に達した場合に警告を出す。 著者が誤って enable を書き忘れたケースを検知するためのガード。

Kernel#warn を使ってはならない。 bin/vs は起動時に RUBYOPT=-W0 を 付けて自身を再実行しており、-W0Kernel#warn の出力を丸ごと捨てる。 そのため vs lint 経由ではこの警告が一度も著者へ届いていなかった (2026-08-18 に判明。ライブラリを直接叩くテストでは出るので気づきにくい)。 Common.log_warn を通すことで 🟡 の体裁とログレベル制御が他の警告と揃う (cli-warning-delivery-spec.md §5.1)。

Parameters:

  • path (String, nil)

    ファイルパス(警告メッセージ用)

  • opened_at (Integer)

    disable が開始された行番号



140
141
142
143
144
# File 'lib/vivlio_starter/cli/lint/tokenizer.rb', line 140

def warn_unclosed_disable(path, opened_at)
  location = path ? "#{path}:#{opened_at}" : "line #{opened_at}"
  Common.log_warn("[vs-lint] #{location} の <!-- vs-lint-disable --> が " \
                  '<!-- vs-lint-enable --> で閉じられていません。ファイル末尾まで lint が無効化されます。')
end