3行
- https://github.com/pocke/rouge-opal
- 色々hackを書いた
- まだ動いていない
Opalとは?
ブラウザで動くRuby
Rougeとは?
RougeはRubyで実装されたSyntax Highlightライブラリです。
つまり、これがOpalで動けばブラウザのみでSyntax Highlightが行えるようになります。面白そうですね。1
とはいえOpalのことがなにも考えられていないライブラリは、一筋縄では動きません。
というわけでどういうhackをしていったのかを紹介していきます。
hack
rougeをrequireするようにする
rougeでは、requireの代わりにload_relative
やload_lexer
というメソッドが定義されています。
これらはOpalから使う上で都合が悪いので、代わりにrequireで置き換えます。
具体的には、rougeのリポジトリのlib/rouge.rb
をコピーしてきて、load_relative
の変わりに単にrequireを使うようにしました。
https://github.com/pocke/rouge-opal/blob/0c82e8ca67f9622a62d3a38a0e323f975606bc2e/lib/rouge.rb
なんでrougeがload_relative
をわざわざ定義して使っているのかはよく分かっていません。これはいらないんじゃなかろうか。
lexerの依存関係を注意してrequireするようにする
rougeではlexerが各言語のSyntax定義を持っています。つまりサポートしている言語の数だけlexerが定義されています。
これらをrougeは次のコードでrequire
しています。
# in lib/rouge.rb
Dir.glob(lexer_dir('*rb')).each { |f| Rouge::Lexers.load_lexer(f.sub(lexer_dir, '')) }
当然これもOpalではうまく動かないため、requireを書き下す必要があります。
ただし、単にファイル一覧を適当な順番でrequireしたのではうまく動きません。なぜならばこれらには依存関係があるためです。
たとえばTypeScriptのLexerを見てみましょう。
# lib/rouge/lexers/typescript.rb
module Rouge
module Lexers
load_lexer 'javascript.rb'
load_lexer 'typescript/common.rb'
class Typescript < Javascript
# ...
end
end
end
TypeScriptはだいたいJavaScriptなので、JavaScriptのLexerを継承しています。
つまりTypeScriptのLexerを読み込む前にJavaScriptのLexerを読み込む必要があるのです。
というわけで、まずは邪魔なRouge::Lexers.load_lexer
メソッドを無効化しましょう。
次のモンキーパッチをrouge/lexer
をrequireした後に入れます。
# https://github.com/pocke/rouge-opal/blob/2fd9b6257d96f7a004de71ef15913477e7f18b78/lib/rouge.rb#L45-L51
module Rouge
module Lexers
def self.load_lexer(_)
# ignore
end
end
end
次に、lexerを依存関係を考慮してrequireします。これにはトポロジカルソートを使います。
Rubyではtsortライブラリでトポロジカルソートを行えます。
https://docs.ruby-lang.org/ja/latest/class/TSort.html
今回はソートされたrequireを列挙するファイルを生成するRake taskを作成しました。2
https://github.com/pocke/rouge-opal/blob/2fd9b6257d96f7a004de71ef15913477e7f18b78/Rakefile#L8-L47
Opalに存在しないライブラリのスタブを作る
OpalにはRubyの標準ライブラリがいくらか含まれていますが、とはいえすべての標準ライブラリが含まれているわけではありません。
そのため今回は使用する範囲で標準ライブラリのスタブを作成しました。
今回作成したのはcgiとthreadです。どちらもOpalは提供していないライブラリです。
cgiは今回必要とする部分では使われていないようだったので、単にrequireが通るよう空のファイルをLOAD_PATH
が通ったところに置きました。
https://github.com/pocke/rouge-opal/blob/2fd9b6257d96f7a004de71ef15913477e7f18b78/lib/cgi.rb
threadは、スレッドローカルな値を保存するkey-value storeとして使われていました。
そのため、それだけが動くようなthreadライブラリを作成して、それを使用するようにしています。
# https://github.com/pocke/rouge-opal/blob/2fd9b6257d96f7a004de71ef15913477e7f18b78/lib/thread.rb
class Thread
def self.current
@current ||= self.new
end
def initialize
@values = {}
end
def [](k)
@values[k]
end
def []=(k, v)
@values[k] = v
end
end
Encoding
ブラウザでのJavaScriptのencodingはUTF-16です。
ところがRougeはUTF-8を要求していて、UTF-16の文字列を渡すとエラーになってしまいます。
そのため、String#force_encoding
を使って文字列のencodingを変換します。
https://github.com/pocke/rouge-opal/blob/2fd9b6257d96f7a004de71ef15913477e7f18b78/lib/main.rb#L8
respond_to? :format
Rouge.highlight
メソッドは、フォーマッタとして渡された引数がformat
メソッドを受け付ける場合にはそのまま使い、受け付けない場合にはそれをフォーマッタ名とみなしてフォーマッタを探索するようになっています。
# lib/rouge.rb
formatter = Formatter.find(formatter) unless formatter.respond_to? :format
ところが、Opalではこれがうまく動きませんでした。
具体的には、フォーマッタとして"html"
のような文字列を指定した時に、formatter.respond_to? :format
がtrue
を返してしまいます。
一方MRIではこれはfalseを返します。
$ opal-repl
>> "".respond_to? :format
=> true
$ irb
irb(main):001:0> "".respond_to? :format
=> false
これはOpalのバグなんじゃないかなあと思っています。Kernel.#format
が存在するからかな?
今回はRouge.highlight
メソッドにモンキーパッチを当てることで対応しました。
まだ動いていない
さて、これだけのhackを当ててもまだOpalでRougeは動きません。
次のようなエラーを出して死んでしまいました。
"empty?: undefined method `empty?' for nil
at constructor.$method_missing (http://127.0.0.1:43881/index.js:3907:56)
at constructor.method_missing_stub (http://127.0.0.1:43881/index.js:1310:35)
at $$49 (http://127.0.0.1:43881/index.js:26064:37)
at Object.Opal.yieldX (http://127.0.0.1:43881/index.js:1475:18)
at Function.$call (http://127.0.0.1:43881/index.js:17636:25)
at constructor.$$16 (http://127.0.0.1:43881/index.js:26351:39)
at constructor.$instance_exec (http://127.0.0.1:43881/index.js:3815:24)
at Opal.send (http://127.0.0.1:43881/index.js:1671:19)
at $$35 (http://127.0.0.1:43881/index.js:26561:15)
at Object.Opal.yield1 (http://127.0.0.1:43881/index.js:1452:14)"
見ていくと、どうやらRouge::RegexLexer#stream_tokens
メソッドの呼び出しで意図せずnilが発生していそうです。
https://github.com/rouge-ruby/rouge/blob/7e04388a1222bf26ddb660778744eab82a6ea847/lib/rouge/regex_lexer.rb#L256-L279
見たところOpalが実装したStringScannerを使っています。
このStringScannerの実装がバグっているのじゃないかなあと予想しています。
そのうち気が向いたらこのエラーの原因を追ってみようと思います。