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 +7 -0
- data/CLAUDE.md +188 -0
- data/LICENSE.txt +13 -0
- data/README.md +110 -0
- data/Rakefile +12 -0
- data/lib/textbringer/rouge/version.rb +7 -0
- data/lib/textbringer/rouge_adapter.rb +167 -0
- data/lib/textbringer/rouge_config.rb +76 -0
- data/lib/textbringer/rouge_mode.rb +36 -0
- data/lib/textbringer/rouge_mode_factory.rb +102 -0
- data/lib/textbringer_plugin.rb +5 -0
- data/test/test_helper.rb +120 -0
- data/test/textbringer/rouge_adapter_test.rb +96 -0
- data/test/textbringer/rouge_mode_test.rb +59 -0
- metadata +88 -0
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,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
|
data/test/test_helper.rb
ADDED
|
@@ -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: []
|