textbringer-rouge 0.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a83149141e91d7c108286ded2187d565fdaeacc7fa005cae1545b7f5751dd0a1
4
+ data.tar.gz: 297fad68a36842e58348ecd573c7add6f8af0aef27238dfda95bbd29df150c8e
5
+ SHA512:
6
+ metadata.gz: 904d5ec8c63b8a4930e0a62cfb46edce29cf6a67d902dde342dd4c6522a77106f3b8a7457c028aba55a7f0283580b1123014cc3af3e86c3a5ffb33af280165e1
7
+ data.tar.gz: 9d849aa8beb0b3629a7b5199875bcaf9329b23bb1fa103b65a3cafdbe04402667455f05d8571ee2f696a4ee13bb417bb9b5f0c9e6428621747663ccdf3a44161
data/CLAUDE.md ADDED
@@ -0,0 +1,188 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## プロジェクト概要
6
+
7
+ **textbringer-rouge** は、Textbringer エディタに Rouge シンタックスハイライトライブラリを統合し、200+ の言語に自動でシンタックスハイライトを提供する Ruby gem。
8
+
9
+ ### 背景と目的
10
+
11
+ - 各言語ごとに正規表現ベースのシンタックスルールを手書きするのはメンテコストが高い
12
+ - Rouge(デファクトスタンダードなシンタックスハイライター)を統合することで、上流のメンテに任せられる
13
+ - 一つの gem で全言語をサポートする方が、言語ごとに個別の gem を作るよりも有用
14
+
15
+ ## 開発コマンド
16
+
17
+ ```bash
18
+ # 依存関係のインストール
19
+ bundle install
20
+
21
+ # テスト実行
22
+ bundle exec rake test
23
+
24
+ # 単一テストファイルの実行
25
+ bundle exec ruby -I lib:test test/textbringer/rouge_adapter_test.rb
26
+
27
+ # gem のビルド
28
+ bundle exec rake build
29
+
30
+ # ローカルインストール
31
+ bundle exec rake install
32
+ ```
33
+
34
+ ## アーキテクチャ
35
+
36
+ ### コア設計原則
37
+
38
+ Textbringer のシンタックスハイライトは、**Window クラス**が Mode クラスの `syntax_table` を直接参照して処理する。Mode の `highlight` メソッドは呼ばれない。そのため、Rouge を使ったカスタムハイライトを実現するには、**Window クラスのモンキーパッチ**が必要。
39
+
40
+ ### Window モンキーパッチの仕組み
41
+
42
+ ```ruby
43
+ # Window#highlight をオーバーライド
44
+ class Window
45
+ alias_method :original_highlight, :highlight
46
+
47
+ def highlight
48
+ if @buffer.mode.respond_to?(:custom_highlight)
49
+ @buffer.mode.custom_highlight(self) # Mode に委譲
50
+ else
51
+ original_highlight # デフォルトの正規表現ベース
52
+ end
53
+ end
54
+ end
55
+ ```
56
+
57
+ Mode が `custom_highlight` メソッドを実装していれば、それを使う。そうでなければ従来の正規表現ベースのハイライトにフォールバック。
58
+
59
+ ### RougeAdapter モジュール
60
+
61
+ Mode に include することで、Rouge を使ったハイライトを提供する:
62
+
63
+ ```ruby
64
+ module RougeAdapter
65
+ # Window から呼ばれる
66
+ def custom_highlight(window)
67
+ lexer = self.class.rouge_lexer.new
68
+ text = @buffer.to_s
69
+
70
+ position = @buffer.point_min
71
+ lexer.lex(text).each do |token, value|
72
+ face_name = token_type_to_face(token)
73
+ # face_name に対応する Face の attributes を適用
74
+ position += value.bytesize
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def token_type_to_face(token)
81
+ # Rouge のトークン階層を利用してマッピング
82
+ # Literal.String.Double → Literal.String → Literal
83
+ # 完全一致がなければ親トークンタイプへフォールバック
84
+ end
85
+ end
86
+ ```
87
+
88
+ ### トークンマッピングの階層
89
+
90
+ Rouge のトークンは階層構造を持つ(例: `Literal.String.Double` → `Literal.String` → `Literal`)。`token_type_to_face` メソッドは、完全一致がない場合に親トークンタイプへフォールバックすることで、すべてのトークンを適切にマッピングする。
91
+
92
+ ## 技術的な重要ポイント
93
+
94
+ ### 1. パフォーマンス考慮
95
+
96
+ - Textbringer は `CONFIG[:highlight_buffer_size_limit]` でハイライトするバッファサイズを制限(デフォルト 1024 バイト)
97
+ - Rouge のトークナイズは正規表現より遅い可能性があるため、パフォーマンステストが必要
98
+ - 大きいファイルでは部分的にハイライト
99
+
100
+ ### 2. エラーハンドリング
101
+
102
+ - Rouge が失敗した場合は、デフォルトの正規表現ベースにフォールバック
103
+ - 不正な構文のファイルでもクラッシュしないようにする
104
+
105
+ ### 3. デフォルトトークンマッピング
106
+
107
+ 標準的な Rouge トークンを Textbringer のフェイスにマッピング:
108
+
109
+ ```ruby
110
+ DEFAULT_TOKEN_MAP = {
111
+ 'Literal.String' => :string,
112
+ 'Literal.Number' => :number,
113
+ 'Keyword' => :keyword,
114
+ 'Comment' => :comment,
115
+ 'Name.Function' => :function_name,
116
+ 'Name.Class' => :type,
117
+ }
118
+ ```
119
+
120
+ ### 4. デフォルトフェイス定義
121
+
122
+ ```ruby
123
+ Face.define :string, foreground: "green"
124
+ Face.define :number, foreground: "magenta"
125
+ Face.define :keyword, foreground: "cyan", bold: true
126
+ Face.define :comment, foreground: "brightblack"
127
+ Face.define :function_name, foreground: "blue", bold: true
128
+ Face.define :type, foreground: "yellow", bold: true
129
+ ```
130
+
131
+ ## PoC 実装の参照先
132
+
133
+ 完全動作版の PoC 実装は `/Users/yancya/.ghq/github.com/yancya/textbringer-json/` にある:
134
+
135
+ - `lib/textbringer/rouge_adapter.rb` - RougeAdapter の完全実装
136
+ - `test/rouge_adapter_test.rb` - テストコード(全部通過)
137
+ - 実際に JSON ファイルでハイライトが動作確認済み(79 トークン処理、50 ハイライトエントリ適用)
138
+
139
+ 新しいコードを書く際は、この PoC 実装を参考にすること。
140
+
141
+ ## プロジェクト構造(予定)
142
+
143
+ ```
144
+ textbringer-rouge/
145
+ ├── lib/
146
+ │ ├── textbringer/
147
+ │ │ ├── rouge_adapter.rb # Window モンキーパッチ + RougeAdapter
148
+ │ │ ├── rouge_mode_factory.rb # 動的 Mode 生成
149
+ │ │ └── rouge_config.rb # デフォルトトークンマッピング
150
+ │ └── textbringer_plugin.rb # プラグインエントリポイント
151
+ ├── test/
152
+ ├── README.md
153
+ └── CLAUDE.md
154
+ ```
155
+
156
+ ## 自動言語検出の実装方針
157
+
158
+ ファイル拡張子や shebang から適切な Rouge lexer を選択し、動的に Mode を生成:
159
+
160
+ ```ruby
161
+ class RougeModeFactory
162
+ def self.create_mode_for_file(filename)
163
+ lexer = Rouge::Lexer.guess(filename: filename)
164
+ create_mode_for_lexer(lexer)
165
+ end
166
+
167
+ def self.create_mode_for_lexer(lexer_class)
168
+ mode_class = Class.new(Textbringer::Mode) do
169
+ include RougeAdapter
170
+ use_rouge lexer_class, default_token_map
171
+ end
172
+ mode_class
173
+ end
174
+ end
175
+ ```
176
+
177
+ ## テストの方針
178
+
179
+ - RougeAdapter 自体の単体テスト
180
+ - 複数言語(Ruby, Python, JavaScript など)のハイライトテスト
181
+ - パフォーマンステスト(大きいファイル、トークン数の多いファイル)
182
+ - エラーハンドリングのテスト(不正な構文のファイル)
183
+
184
+ ## 参考リンク
185
+
186
+ - [Rouge GitHub](https://github.com/rouge-ruby/rouge)
187
+ - [Textbringer GitHub](https://github.com/shugo/textbringer)
188
+ - [Rouge Lexers 一覧](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers)
data/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2
+ Version 2, December 2004
3
+
4
+ Copyright (C) 2026 Shinta Koyanagi <yancya@upec.jp>
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # Textbringer Rouge
2
+
3
+ Rouge syntax highlighting integration for Textbringer.
4
+
5
+ This plugin integrates the [Rouge](https://github.com/rouge-ruby/rouge) syntax highlighter into [Textbringer](https://github.com/shugo/textbringer), automatically providing syntax highlighting for 200+ programming languages without needing to write regex patterns for each language.
6
+
7
+ ## Features
8
+
9
+ - **200+ Languages**: Automatic syntax highlighting for all languages supported by Rouge
10
+ - **Zero Configuration**: Works out of the box - just install and open any supported file
11
+ - **Smart Detection**: Automatically detects the language from file extensions
12
+ - **Fallback Support**: Falls back to regex-based highlighting if Rouge fails
13
+ - **Customizable**: Override default token mappings and colors for any language
14
+
15
+ ## Installation
16
+
17
+ Install the gem by executing:
18
+
19
+ ```bash
20
+ gem install textbringer-rouge
21
+ ```
22
+
23
+ Or add it to your Gemfile:
24
+
25
+ ```ruby
26
+ gem 'textbringer-rouge'
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ The plugin is automatically loaded when you start Textbringer. Simply open any supported file and syntax highlighting will be applied automatically.
32
+
33
+ ### Supported Languages
34
+
35
+ Rouge supports 200+ languages including:
36
+
37
+ - **Web**: HTML, CSS, JavaScript, TypeScript, JSX, Vue, Svelte
38
+ - **Programming**: Ruby, Python, Java, C, C++, C#, Go, Rust, Swift, Kotlin
39
+ - **Scripting**: Bash, PowerShell, Perl, Lua, R
40
+ - **Data**: JSON, YAML, TOML, XML, CSV
41
+ - **Markup**: Markdown, reStructuredText, AsciiDoc
42
+ - **Config**: Nginx, Apache, Dockerfile, .gitignore
43
+ - **And many more...**
44
+
45
+ See the [full list of supported languages](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers).
46
+
47
+ ### Customization
48
+
49
+ You can customize the colors by modifying Textbringer faces in your `~/.textbringer.rb`:
50
+
51
+ ```ruby
52
+ # Change string color
53
+ Textbringer::Face.define :string, foreground: "lightgreen"
54
+
55
+ # Change keyword color
56
+ Textbringer::Face.define :keyword, foreground: "lightblue", bold: true
57
+
58
+ # Change comment color
59
+ Textbringer::Face.define :comment, foreground: "gray", italic: true
60
+ ```
61
+
62
+ ### Debug Mode
63
+
64
+ Enable debug logging to troubleshoot issues:
65
+
66
+ ```bash
67
+ TEXTBRINGER_ROUGE_DEBUG=1 txtb your_file.rb
68
+ ```
69
+
70
+ Debug logs will be written to `/tmp/rouge_adapter_debug.log`.
71
+
72
+ ## How It Works
73
+
74
+ Textbringer Rouge uses a clever technique to integrate Rouge with Textbringer:
75
+
76
+ 1. **Window Monkey Patch**: Extends `Window#highlight` to support custom highlighting methods
77
+ 2. **RougeAdapter**: A mixin module that provides Rouge-based highlighting for any Mode
78
+ 3. **Token Mapping**: Maps Rouge's semantic tokens to Textbringer's Face system
79
+ 4. **Fallback**: Falls back to regex-based highlighting if Rouge encounters errors
80
+
81
+ This approach maintains compatibility with existing Textbringer modes while adding powerful language support.
82
+
83
+ ## Development
84
+
85
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests.
86
+
87
+ To install this gem onto your local machine, run `bundle exec rake install`.
88
+
89
+ ### Running Tests
90
+
91
+ ```bash
92
+ # Run all tests
93
+ bundle exec rake test
94
+
95
+ # Run a specific test file
96
+ bundle exec ruby -I lib:test test/textbringer/rouge_adapter_test.rb
97
+ ```
98
+
99
+ ## Contributing
100
+
101
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yancya/textbringer-rouge.
102
+
103
+ ## License
104
+
105
+ The gem is available as open source under the terms of the [WTFPL](http://www.wtfpl.net/).
106
+
107
+ ## Credits
108
+
109
+ - [Textbringer](https://github.com/shugo/textbringer) - The Emacs-like text editor in Ruby
110
+ - [Rouge](https://github.com/rouge-ruby/rouge) - The pure Ruby syntax highlighter
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textbringer
4
+ module Rouge
5
+ VERSION = "0.99.0"
6
+ end
7
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rouge"
4
+
5
+ module Textbringer
6
+ # Monkey patch Window to support custom highlight methods in modes
7
+ class Window
8
+ unless method_defined?(:original_highlight)
9
+ alias_method :original_highlight, :highlight
10
+
11
+ def highlight
12
+ # If the mode has a custom highlight method, use it
13
+ if @buffer.mode.respond_to?(:custom_highlight)
14
+ @buffer.mode.custom_highlight(self)
15
+ else
16
+ # Otherwise use the default regex-based highlighting
17
+ original_highlight
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # Adapter module to use Rouge lexers for syntax highlighting in Textbringer.
24
+ #
25
+ # This allows modes to leverage Rouge's extensive language support (200+ languages)
26
+ # instead of manually writing regex patterns.
27
+ #
28
+ # Usage:
29
+ # class MyMode < Mode
30
+ # include RougeAdapter
31
+ # use_rouge Rouge::Lexers::JSON, {
32
+ # 'Literal.String' => :string,
33
+ # 'Literal.Number' => :number,
34
+ # }
35
+ # end
36
+ module RougeAdapter
37
+ DEBUG = ENV["TEXTBRINGER_ROUGE_DEBUG"] == "1"
38
+
39
+ attr_accessor :rouge_lexer, :token_map
40
+
41
+ def custom_highlight(window)
42
+ window.instance_variable_set(:@highlight_on, {})
43
+ window.instance_variable_set(:@highlight_off, {})
44
+
45
+ if DEBUG
46
+ File.open("/tmp/rouge_adapter_debug.log", "a") do |f|
47
+ f.puts "[#{Time.now}] RougeAdapter#custom_highlight called"
48
+ f.puts " has_colors: #{Window.class_variable_get(:@@has_colors)}"
49
+ f.puts " syntax_highlight: #{CONFIG[:syntax_highlight]}"
50
+ f.puts " binary: #{@buffer.binary?}"
51
+ f.puts " buffer: #{@buffer.name}"
52
+ end
53
+ end
54
+
55
+ return if !Window.class_variable_get(:@@has_colors) || !CONFIG[:syntax_highlight] || @buffer.binary?
56
+
57
+ # Use instance-level lexer if available, otherwise use class-level
58
+ lexer_class = @rouge_lexer || self.class.rouge_lexer
59
+ lexer = lexer_class&.new
60
+
61
+ if DEBUG
62
+ File.open("/tmp/rouge_adapter_debug.log", "a") do |f|
63
+ f.puts " lexer: #{lexer ? lexer.class : 'nil'}"
64
+ end
65
+ end
66
+
67
+ unless lexer
68
+ # Fallback to regex-based highlighting
69
+ window.send(:original_highlight)
70
+ return
71
+ end
72
+
73
+ # Get text to highlight (same logic as original)
74
+ if @buffer.bytesize < CONFIG[:highlight_buffer_size_limit]
75
+ base_pos = @buffer.point_min
76
+ text = @buffer.to_s
77
+ else
78
+ base_pos = @buffer.point
79
+ len = window.columns * (window.lines - 1) / 2 * 3
80
+ text = @buffer.substring(@buffer.point, @buffer.point + len).scrub("")
81
+ end
82
+
83
+ return unless text.valid_encoding?
84
+
85
+ # Tokenize using Rouge
86
+ highlight_on = {}
87
+ position = base_pos
88
+ token_count = 0
89
+ lexer.lex(text).each do |token, value|
90
+ token_count += 1
91
+ face_name = token_type_to_face(token)
92
+
93
+ # Apply highlight at token start position
94
+ if face_name && (attributes = Face[face_name]&.attributes)
95
+ # Skip if position is before buffer point (same logic as original)
96
+ token_end = position + value.bytesize
97
+ if position < @buffer.point && @buffer.point < token_end
98
+ position = @buffer.point
99
+ end
100
+
101
+ highlight_on[position] = attributes
102
+ end
103
+
104
+ position += value.bytesize
105
+ end
106
+
107
+ window.instance_variable_set(:@highlight_on, highlight_on)
108
+
109
+ if DEBUG
110
+ File.open("/tmp/rouge_adapter_debug.log", "a") do |f|
111
+ f.puts " tokens processed: #{token_count}"
112
+ f.puts " highlight_on entries: #{highlight_on.size}"
113
+ end
114
+ end
115
+ rescue ::Rouge::Guesser::Ambiguous, StandardError => e
116
+ # Fallback to regex-based highlighting if Rouge fails
117
+ if DEBUG
118
+ File.open("/tmp/rouge_adapter_debug.log", "a") do |f|
119
+ f.puts " ERROR: #{e.class}: #{e.message}"
120
+ f.puts " Falling back to regex-based highlighting"
121
+ end
122
+ end
123
+ window.send(:original_highlight)
124
+ end
125
+
126
+ private
127
+
128
+ def token_type_to_face(token)
129
+ # Convert Rouge token to face name using the configured mapping
130
+ # Use instance-level token_map if available, otherwise use class-level
131
+ token_map = @token_map || self.class.token_map || {}
132
+ token_qualname = token.qualname
133
+
134
+ # Try exact match first
135
+ return token_map[token_qualname] if token_map[token_qualname]
136
+
137
+ # Try parent token types (e.g., Literal.String.Double -> Literal.String -> Literal)
138
+ parent = token.token_chain.find { |t| token_map[t.qualname] }
139
+ token_map[parent&.qualname]
140
+ end
141
+
142
+ module ClassMethods
143
+ attr_accessor :rouge_lexer, :token_map
144
+
145
+ # Configure this mode to use a Rouge lexer for syntax highlighting.
146
+ #
147
+ # @param lexer_class [Class] Rouge lexer class (e.g., Rouge::Lexers::JSON)
148
+ # @param token_map [Hash] Mapping from Rouge token qualnames to Textbringer face names
149
+ #
150
+ # Example:
151
+ # use_rouge Rouge::Lexers::JSON, {
152
+ # 'Literal.String' => :string,
153
+ # 'Literal.Number' => :number,
154
+ # 'Keyword.Constant' => :keyword,
155
+ # }
156
+ def use_rouge(lexer_class, token_map = {})
157
+ include RougeAdapter unless included_modules.include?(RougeAdapter)
158
+ self.rouge_lexer = lexer_class
159
+ self.token_map = token_map
160
+ end
161
+ end
162
+
163
+ def self.included(base)
164
+ base.extend(ClassMethods)
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textbringer
4
+ module RougeConfig
5
+ # Default mapping from Rouge token types to Textbringer faces
6
+ DEFAULT_TOKEN_MAP = {
7
+ # String literals
8
+ "Literal.String" => :string,
9
+ "Literal.String.Double" => :string,
10
+ "Literal.String.Single" => :string,
11
+ "Literal.String.Backtick" => :string,
12
+ "Literal.String.Heredoc" => :string,
13
+ "Literal.String.Regex" => :string,
14
+ "Literal.String.Symbol" => :string,
15
+
16
+ # Numeric literals
17
+ "Literal.Number" => :number,
18
+ "Literal.Number.Integer" => :number,
19
+ "Literal.Number.Float" => :number,
20
+ "Literal.Number.Hex" => :number,
21
+ "Literal.Number.Oct" => :number,
22
+ "Literal.Number.Bin" => :number,
23
+
24
+ # Keywords
25
+ "Keyword" => :keyword,
26
+ "Keyword.Constant" => :keyword,
27
+ "Keyword.Declaration" => :keyword,
28
+ "Keyword.Namespace" => :keyword,
29
+ "Keyword.Pseudo" => :keyword,
30
+ "Keyword.Reserved" => :keyword,
31
+ "Keyword.Type" => :keyword,
32
+
33
+ # Comments
34
+ "Comment" => :comment,
35
+ "Comment.Single" => :comment,
36
+ "Comment.Multiline" => :comment,
37
+ "Comment.Doc" => :comment,
38
+ "Comment.Preproc" => :comment,
39
+ "Comment.PreprocFile" => :comment,
40
+
41
+ # Names (functions, classes, variables)
42
+ "Name.Function" => :function_name,
43
+ "Name.Class" => :type,
44
+ "Name.Constant" => :constant,
45
+ "Name.Variable" => :variable,
46
+ "Name.Variable.Instance" => :variable,
47
+ "Name.Variable.Class" => :variable,
48
+ "Name.Variable.Global" => :variable,
49
+ "Name.Builtin" => :builtin,
50
+ "Name.Label" => :label,
51
+
52
+ # Operators and punctuation
53
+ "Operator" => :operator,
54
+ "Punctuation" => :punctuation,
55
+ }.freeze
56
+
57
+ # Define default faces for syntax highlighting
58
+ def self.define_default_faces
59
+ Face.define :string, foreground: "green"
60
+ Face.define :number, foreground: "magenta"
61
+ Face.define :keyword, foreground: "cyan", bold: true
62
+ Face.define :comment, foreground: "brightblack"
63
+ Face.define :function_name, foreground: "blue", bold: true
64
+ Face.define :type, foreground: "yellow", bold: true
65
+ Face.define :constant, foreground: "yellow"
66
+ Face.define :variable, foreground: "default"
67
+ Face.define :builtin, foreground: "cyan"
68
+ Face.define :label, foreground: "yellow", bold: true
69
+ Face.define :operator, foreground: "default"
70
+ Face.define :punctuation, foreground: "default"
71
+ end
72
+ end
73
+
74
+ # Define default faces when this module is loaded
75
+ RougeConfig.define_default_faces
76
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rouge_adapter"
4
+ require_relative "rouge_config"
5
+
6
+ module Textbringer
7
+ # Universal Rouge mode that supports all languages
8
+ class RougeMode < Mode
9
+ include RougeAdapter
10
+
11
+ # Match common source file extensions
12
+ # This pattern will match before Fundamental mode but after specific modes like RubyMode
13
+ self.file_name_pattern = /\.(py|js|ts|jsx|tsx|json|yaml|yml|toml|xml|html|css|scss|sass|java|c|cpp|h|hpp|rs|go|php|rb|sh|bash|sql|md|txt)\z/i
14
+
15
+ def initialize(buffer)
16
+ super(buffer)
17
+
18
+ # Auto-detect lexer from filename
19
+ begin
20
+ lexer_class = ::Rouge::Lexer.guess(filename: buffer.name)
21
+ @rouge_lexer = lexer_class
22
+ @token_map = RougeConfig::DEFAULT_TOKEN_MAP
23
+ rescue ::Rouge::Guesser::Ambiguous => e
24
+ # If multiple lexers match, use the first one
25
+ @rouge_lexer = e.alternatives.first
26
+ @token_map = RougeConfig::DEFAULT_TOKEN_MAP
27
+ rescue
28
+ # No lexer found, don't set lexer (will fallback to default highlighting)
29
+ @rouge_lexer = nil
30
+ end
31
+
32
+ @buffer[:indent_tabs_mode] = false
33
+ @buffer[:tab_width] = 2
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rouge_adapter"
4
+ require_relative "rouge_config"
5
+ require_relative "rouge/version"
6
+
7
+ module Textbringer
8
+ module RougeModeFactory
9
+ @mode_cache = {}
10
+
11
+ class << self
12
+ # Create a Mode class for a specific Rouge lexer
13
+ #
14
+ # @param lexer_class [Class] Rouge lexer class (e.g., ::Rouge::Lexers::JSON)
15
+ # @return [Class] A Mode subclass configured for that lexer
16
+ def create_mode_for_lexer(lexer_class)
17
+ # Cache modes to avoid recreating them
18
+ @mode_cache[lexer_class] ||= begin
19
+ # Create class name for the mode
20
+ mode_class_name = "Rouge#{lexer_class.tag.capitalize}Mode"
21
+
22
+ # Check if already defined
23
+ if Textbringer.const_defined?(mode_class_name)
24
+ return Textbringer.const_get(mode_class_name)
25
+ end
26
+
27
+ # Build file name pattern
28
+ file_pattern_code = if lexer_class.filenames && !lexer_class.filenames.empty?
29
+ patterns_code = lexer_class.filenames.map do |pattern|
30
+ if pattern.start_with?("*.")
31
+ ext = Regexp.escape(pattern[2..-1])
32
+ "/\\.#{ext}\\z/i"
33
+ elsif pattern.include?("*")
34
+ regex_pattern = Regexp.escape(pattern).gsub('\*', '.*')
35
+ "/#{regex_pattern}\\z/i"
36
+ else
37
+ "/#{Regexp.escape(pattern)}\\z/"
38
+ end
39
+ end.join(", ")
40
+ "self.file_name_pattern = Regexp.union(#{patterns_code})"
41
+ else
42
+ ""
43
+ end
44
+
45
+ # Define the mode class using class_eval with a named class
46
+ # This ensures Mode.inherited is called with a properly named class
47
+ Textbringer.class_eval <<-RUBY, __FILE__, __LINE__ + 1
48
+ class #{mode_class_name} < Mode
49
+ include RougeAdapter
50
+ use_rouge #{lexer_class.inspect}, RougeConfig::DEFAULT_TOKEN_MAP
51
+
52
+ #{file_pattern_code}
53
+
54
+ def initialize(buffer)
55
+ super(buffer)
56
+ @buffer[:indent_tabs_mode] = false
57
+ @buffer[:tab_width] = 2
58
+ end
59
+ end
60
+ RUBY
61
+
62
+ Textbringer.const_get(mode_class_name)
63
+ end
64
+ end
65
+
66
+ # Create a Mode for a file based on its filename
67
+ #
68
+ # @param filename [String] The filename to guess the lexer for
69
+ # @return [Class, nil] A Mode subclass, or nil if no lexer found
70
+ def create_mode_for_file(filename)
71
+ lexer_class = ::Rouge::Lexer.guess(filename: filename)
72
+ create_mode_for_lexer(lexer_class)
73
+ rescue ::Rouge::Guesser::Ambiguous => e
74
+ # If multiple lexers match, pick the first one
75
+ create_mode_for_lexer(e.alternatives.first)
76
+ rescue
77
+ # No lexer found
78
+ nil
79
+ end
80
+
81
+ # Register all Rouge lexers with Textbringer
82
+ #
83
+ # This should be called when the plugin is loaded
84
+ def register_all_lexers
85
+ ::Rouge::Lexer.all.each do |lexer_class|
86
+ # Skip lexers without file patterns
87
+ next if lexer_class.filenames.nil? || lexer_class.filenames.empty?
88
+
89
+ begin
90
+ create_mode_for_lexer(lexer_class)
91
+ rescue => e
92
+ # Skip lexers that fail to initialize
93
+ warn "Failed to register lexer #{lexer_class}: #{e.message}" if ENV["TEXTBRINGER_ROUGE_DEBUG"] == "1"
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ # Auto-register all lexers when the module is loaded
101
+ RougeModeFactory.register_all_lexers
102
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "textbringer/rouge_adapter"
4
+ require "textbringer/rouge_config"
5
+ require "textbringer/rouge_mode"
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ # Mock Textbringer for testing without the actual dependency
6
+ module Textbringer
7
+ class Face
8
+ @faces = {}
9
+
10
+ def self.define(name, **options)
11
+ @faces[name] = new(name, options)
12
+ end
13
+
14
+ def self.[](name)
15
+ @faces[name]
16
+ end
17
+
18
+ attr_reader :name, :attributes
19
+
20
+ def initialize(name, attributes)
21
+ @name = name
22
+ @attributes = attributes
23
+ end
24
+ end
25
+
26
+ class Mode
27
+ attr_reader :buffer
28
+
29
+ def initialize(buffer)
30
+ @buffer = buffer
31
+ end
32
+
33
+ def self.define_syntax(face, pattern)
34
+ # Mock define_syntax
35
+ end
36
+
37
+ def self.file_name_pattern
38
+ @file_name_pattern
39
+ end
40
+
41
+ def self.file_name_pattern=(pattern)
42
+ @file_name_pattern = pattern
43
+ end
44
+ end
45
+
46
+ class Window
47
+ @@has_colors = true
48
+
49
+ def self.class_variable_get(name)
50
+ @@has_colors if name == :@@has_colors
51
+ end
52
+
53
+ attr_reader :buffer, :columns, :lines
54
+
55
+ def initialize(buffer, columns: 80, lines: 24)
56
+ @buffer = buffer
57
+ @columns = columns
58
+ @lines = lines
59
+ end
60
+
61
+ def highlight
62
+ # Mock highlight method (will be overridden by RougeAdapter)
63
+ end
64
+
65
+ def instance_variable_set(name, value)
66
+ super(name, value)
67
+ end
68
+
69
+ def instance_variable_get(name)
70
+ super(name)
71
+ end
72
+ end
73
+
74
+ class Buffer
75
+ attr_reader :name, :bytesize, :point_min, :point
76
+
77
+ def initialize(name:, content: "", bytesize: nil)
78
+ @name = name
79
+ @content = content
80
+ @bytesize = bytesize || content.bytesize
81
+ @point_min = 0
82
+ @point = 0
83
+ @vars = {}
84
+ end
85
+
86
+ def to_s
87
+ @content
88
+ end
89
+
90
+ def binary?
91
+ false
92
+ end
93
+
94
+ def substring(start_pos, end_pos)
95
+ @content[start_pos...end_pos]
96
+ end
97
+
98
+ def []=(key, value)
99
+ @vars[key] = value
100
+ end
101
+
102
+ def [](key)
103
+ @vars[key]
104
+ end
105
+ end
106
+
107
+ CONFIG = {
108
+ syntax_highlight: true,
109
+ highlight_buffer_size_limit: 1024 * 1024,
110
+ }
111
+ end
112
+
113
+ # Load Rouge before loading our code
114
+ require "rouge"
115
+
116
+ # Now load our code
117
+ require "textbringer/rouge_adapter"
118
+ require "textbringer/rouge_config"
119
+
120
+ require "test/unit"
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module Textbringer
6
+ class RougeAdapterTest < Test::Unit::TestCase
7
+ test "VERSION is defined" do
8
+ assert do
9
+ ::Textbringer::Rouge.const_defined?(:VERSION)
10
+ end
11
+ end
12
+
13
+ test "RougeAdapter module exists" do
14
+ assert do
15
+ defined?(Textbringer::RougeAdapter)
16
+ end
17
+ end
18
+
19
+ test "RougeAdapter provides use_rouge class method" do
20
+ mode_class = Class.new(Mode) do
21
+ include RougeAdapter
22
+ end
23
+
24
+ assert_respond_to mode_class, :use_rouge
25
+ end
26
+
27
+ test "RougeAdapter::use_rouge configures lexer and token_map" do
28
+ mode_class = Class.new(Mode) do
29
+ include RougeAdapter
30
+ use_rouge ::Rouge::Lexers::Ruby, {
31
+ "Keyword" => :keyword,
32
+ "Literal.String" => :string,
33
+ }
34
+ end
35
+
36
+ assert_equal ::Rouge::Lexers::Ruby, mode_class.rouge_lexer
37
+ assert_equal({ "Keyword" => :keyword, "Literal.String" => :string }, mode_class.token_map)
38
+ end
39
+
40
+ test "RougeAdapter::custom_highlight tokenizes simple Ruby code" do
41
+ mode_class = Class.new(Mode) do
42
+ include RougeAdapter
43
+ use_rouge ::Rouge::Lexers::Ruby, {
44
+ "Keyword" => :keyword,
45
+ "Literal.String" => :string,
46
+ }
47
+ end
48
+
49
+ buffer = Buffer.new(name: "test.rb", content: 'puts "hello"')
50
+ mode = mode_class.new(buffer)
51
+ window = Window.new(buffer)
52
+
53
+ mode.custom_highlight(window)
54
+
55
+ highlight_on = window.instance_variable_get(:@highlight_on)
56
+ assert_not_nil highlight_on
57
+ assert_operator highlight_on.size, :>, 0
58
+ end
59
+
60
+ test "RougeAdapter::token_type_to_face maps tokens with parent fallback" do
61
+ mode_class = Class.new(Mode) do
62
+ include RougeAdapter
63
+ use_rouge ::Rouge::Lexers::Ruby, {
64
+ "Literal.String" => :string,
65
+ "Keyword" => :keyword,
66
+ }
67
+ end
68
+
69
+ buffer = Buffer.new(name: "test.rb")
70
+ mode = mode_class.new(buffer)
71
+
72
+ # Test exact match
73
+ token = ::Rouge::Token["Keyword"]
74
+ face = mode.send(:token_type_to_face, token)
75
+ assert_equal :keyword, face
76
+
77
+ # Test parent fallback (Literal.String.Double -> Literal.String)
78
+ token = ::Rouge::Token["Literal.String.Double"]
79
+ face = mode.send(:token_type_to_face, token)
80
+ assert_equal :string, face
81
+ end
82
+
83
+ test "RougeConfig::DEFAULT_TOKEN_MAP is defined" do
84
+ assert do
85
+ RougeConfig::DEFAULT_TOKEN_MAP.is_a?(Hash)
86
+ end
87
+ end
88
+
89
+ test "RougeConfig defines default faces" do
90
+ assert_not_nil Face[:string]
91
+ assert_not_nil Face[:number]
92
+ assert_not_nil Face[:keyword]
93
+ assert_not_nil Face[:comment]
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "textbringer/rouge_mode"
5
+
6
+ module Textbringer
7
+ class RougeModeTest < Test::Unit::TestCase
8
+ test "RougeMode class exists" do
9
+ assert do
10
+ defined?(Textbringer::RougeMode)
11
+ end
12
+ end
13
+
14
+ test "RougeMode has file_name_pattern" do
15
+ assert_not_nil RougeMode.file_name_pattern
16
+ end
17
+
18
+ test "RougeMode file_name_pattern matches Python files" do
19
+ assert_match RougeMode.file_name_pattern, "test.py"
20
+ end
21
+
22
+ test "RougeMode file_name_pattern matches JSON files" do
23
+ assert_match RougeMode.file_name_pattern, "test.json"
24
+ end
25
+
26
+ test "RougeMode file_name_pattern matches JavaScript files" do
27
+ assert_match RougeMode.file_name_pattern, "test.js"
28
+ end
29
+
30
+ test "RougeMode auto-detects Python lexer" do
31
+ buffer = Buffer.new(name: "test.py")
32
+ mode = RougeMode.new(buffer)
33
+
34
+ assert_equal ::Rouge::Lexers::Python, mode.rouge_lexer
35
+ end
36
+
37
+ test "RougeMode auto-detects JSON lexer" do
38
+ buffer = Buffer.new(name: "test.json")
39
+ mode = RougeMode.new(buffer)
40
+
41
+ assert_equal ::Rouge::Lexers::JSON, mode.rouge_lexer
42
+ end
43
+
44
+ test "RougeMode auto-detects Ruby lexer" do
45
+ buffer = Buffer.new(name: "test.rb")
46
+ mode = RougeMode.new(buffer)
47
+
48
+ assert_equal ::Rouge::Lexers::Ruby, mode.rouge_lexer
49
+ end
50
+
51
+ test "RougeMode has default token_map" do
52
+ buffer = Buffer.new(name: "test.py")
53
+ mode = RougeMode.new(buffer)
54
+
55
+ assert_not_nil mode.token_map
56
+ assert_kind_of Hash, mode.token_map
57
+ end
58
+ end
59
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: textbringer-rouge
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.99.0
5
+ platform: ruby
6
+ authors:
7
+ - yancya
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-02-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: textbringer
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rouge
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ description: A Textbringer plugin that integrates Rouge syntax highlighter to provide
42
+ automatic syntax highlighting for 200+ languages.
43
+ email:
44
+ - yancya@upec.jp
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - CLAUDE.md
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - lib/textbringer/rouge/version.rb
54
+ - lib/textbringer/rouge_adapter.rb
55
+ - lib/textbringer/rouge_config.rb
56
+ - lib/textbringer/rouge_mode.rb
57
+ - lib/textbringer/rouge_mode_factory.rb
58
+ - lib/textbringer_plugin.rb
59
+ - test/test_helper.rb
60
+ - test/textbringer/rouge_adapter_test.rb
61
+ - test/textbringer/rouge_mode_test.rb
62
+ homepage: https://github.com/yancya/textbringer-rouge
63
+ licenses:
64
+ - WTFPL
65
+ metadata:
66
+ allowed_push_host: https://rubygems.org
67
+ homepage_uri: https://github.com/yancya/textbringer-rouge
68
+ source_code_uri: https://github.com/yancya/textbringer-rouge
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 3.2.0
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 3.4.19
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: Rouge syntax highlighting integration for Textbringer
88
+ test_files: []