Posted at

SATySFi で Ruby のソースコードをシンタックス ハイライトする


結論

前処理でRubyのコードをパースしてSATySFiのインラインコマンドに変換し、SATySFiのインラインコマンドを定義して色付けをする感じで実装した。


結果

Screenshot from 2018-10-09 12-44-24.png



https://github.com/hanachin/satysfi-syntax-highlight-sample/releases/tag/v0.0.1

ソースコードはここにあります。

https://github.com/hanachin/satysfi-syntax-highlight-sample


実装方針

以下のような方法が考えられます。


  • SATySFi(言語の方)で字句解析する

  • SATySFi(言語実装の方)で字句解析する

  • SATySFiでは字句解析をせず前処理として外部で字句解析を行い結果を.satyに変換する

ここでは一番最後の方針で実装します。


理由

SATySFi(言語の方)で字句解析を実装するにはまだまだSATySFiでのプログラミング経験が足りません。

SATySFi(言語実装の方)に手を加えるにもOCamlを書いた経験がなく学習に時間がかかりそうです。

一番最後の前処理を挟む方針だと書き慣れたRubyで実装することが出来ます。


前処理でやること


  • Rubyのプログラムのソースコードを受けとり字句解析する

  • 字句解析した結果をSATySFiのインラインコマンドに変換しsatyに埋め込む

字句解析にはiroを使います。

埋め込みにはerbを使います。


実装

こんな感じでRubyのコードをパースしてSATySFiのコマンドに変換して書き出す


highlight.rb

require "iro"

using Module.new {
refine(Array) do
def type
self[0]
end

def lineno
self[1] - 1
end

def column
self[2] - 1
end

def length
self[3]
end
end
}

INLINE_CMD_MAP =
{
"String" => "code-string",
"Charactor" => "code-character",
"Number" => "code-number",
"Float" => "code-float",
"Comment" => "code-comment",
"Type" => "code-type",
"Delimiter" => "code-delimiter",
"rubyDefine" => "code-define",
"keyword" => "code-keyword",
"rubyFunction" => "code-function",
"rubyLocalVariable" => "code-lvar"
}

def highlight_positions(code)
tokens = Iro::Ruby::Parser.tokens(code)
tokens.flat_map {|group, poss|
poss.map {|pos|
[INLINE_CMD_MAP.fetch(group), *pos]
}
}.sort_by {|pos|
[pos.lineno, pos.column]
}
end

def highlight_code(cmd, code)
code.each_char.chunk {|c| c == " " }.map do |space, cs|
if space
"\\code-space;" * cs.size
elsif cs.empty?
""
else
"\\#{cmd}(`#{cs.join}`);"
end
end.join
end

def highlight(code)
hs = highlight_positions(code)

saty = ''
code.each_line.with_index do |line, lineno|
line_hs = hs.select {|h| h.lineno == lineno }

saty << highlight_code("code-other", line) and next if line_hs.empty?

last_column = 0
line_hs.each do |h|
saty << highlight_code("code-other", line[last_column...h.column])
last_column = h.column + h.length
saty << highlight_code(h.type, line[h.column...last_column])
end
saty << highlight_code("code-other", line[last_column..-1])
saty << "\\code-new-line;"
end
saty
end


こういう感じでerbからhighlightメソッドを呼び出します。

Rubyのソースコードをヒアドキュメントで書いてhighlightメソッドに渡しています。


sample.saty.erb

    +code-highlight {

<%=
highlight(<<~'RUBY')
# Greet to you!
def hello(name)
puts "Hello, #{name}"
end

hello('your name')
RUBY
%>
}


こういう感じに変換されます。


sample.saty

    +code-highlight {

\code-comment(`#`);\code-space;\code-comment(`Greet`);\code-space;\code-comment(`to`);\code-space;\code-comment(`you!`);\code-other(`
`);\code-new-line;\code-define(`def`);\code-space;\code-function(`hello`);\code-other(`(name)
`);\code-new-line;\code-space;\code-space;\code-other(`puts`);\code-space;\code-delimiter(`"`);\code-string(`Hello,`);\code-space;\code-delimiter(`#{`);\code-lvar(`name`);\code-delimiter(`}`);\code-delimiter(`"`);\code-other(`
`);\code-new-line;\code-define(`end`);\code-other(`
`);\code-new-line;\code-other(`
`);\code-other(`hello(`);\code-delimiter(`'`);\code-string(`your`);\code-space;\code-string(`name`);\code-delimiter(`'`);\code-other(`)
`);\code-new-line;
}

あとは対応するインラインコマンドをSATySFiで実装して、色とかつければ完成。


local.satyh

@require: color

@require: gr

module Highlight : sig
direct \code-string : [string] inline-cmd
direct \code-character : [string] inline-cmd
direct \code-number : [string] inline-cmd
direct \code-float : [string] inline-cmd
direct \code-comment : [string] inline-cmd
direct \code-type : [string] inline-cmd
direct \code-delimiter : [string] inline-cmd
direct \code-define : [string] inline-cmd
direct \code-keyword : [string] inline-cmd
direct \code-function : [string] inline-cmd
direct \code-lvar : [string] inline-cmd
direct \code-other : [string] inline-cmd

direct \code-space : [] inline-cmd
direct \code-new-line : [] inline-cmd
direct +code-highlight : [inline-text] block-cmd
end = struct
let highlight ctx color code =
read-inline (ctx |> set-text-color color) (embed-string code)

let-inline ctx \code-string code = highlight ctx (RGB (1.0, 0.43, 0.43)) code
let-inline ctx \code-character code = highlight ctx (RGB (1.0, 0.43, 0.43)) code
let-inline ctx \code-number code = highlight ctx (RGB (1.0, 0.43, 0.43)) code
let-inline ctx \code-float code = highlight ctx (RGB (1.0, 0.43, 0.43)) code
let-inline ctx \code-comment code = highlight ctx (RGB (0.6, 0.60, 0.60)) code
let-inline ctx \code-type code = highlight ctx (RGB (1.0, 0.86, 0.42)) code
let-inline ctx \code-delimiter code = highlight ctx (RGB (0.78, 0.29, 1.00)) code
let-inline ctx \code-define code = highlight ctx (RGB (0.28, 1.00, 0.65)) code
let-inline ctx \code-keyword code = highlight ctx (RGB (0.24, 0.75, 1.00)) code
let-inline ctx \code-function code = highlight ctx (RGB (0.24, 0.75, 1.00)) code
let-inline ctx \code-lvar code = highlight ctx (RGB (0.24, 0.75, 1.00)) code
let-inline ctx \code-other code = highlight ctx (RGB (0.87, 0.87, 0.87)) code

let-inline ctx \code-space =
inline-skip (get-natural-width (read-inline ctx {0}))

let-inline ctx \code-new-line =
inline-fil ++ discretionary 0 (inline-skip((get-text-width ctx) *' 2.)) inline-nil inline-nil

let code-decoset =
let deco (x, y) w h d =
[ fill (RGB (0.2, 0.2, 0.2)) (Gr.rectangle (x, y -' d) (x +' w, y +' h)) ]
in
(deco, deco, deco, deco)
let code-fill-color = (RGB(0.975, 0.975, 1.))
let code-stroke-color = (RGB(0., 0., 0.25))
let code-pads = (5pt, 5pt, 5pt, 5pt)
let code-ctx ctx =
let mono-ctx = ctx |> set-font Latin (`lmmono`, 1., 0.) in
let fontsize = get-font-size mono-ctx in
let charwid = get-natural-width (read-inline mono-ctx {0}) in
mono-ctx |> set-space-ratio (charwid /' fontsize) 0. 0.
let-block ctx +code-highlight inner =
block-frame-breakable ctx code-pads code-decoset (fun ctx -> (
line-break true true (code-ctx ctx) (read-inline (code-ctx ctx) inner)
))
end



今後

Rubyで実装した部分を徐々にSATySFi実装に置き換えていけば前処理としてerbを実行する必要がなくなる、はず。

c10d39a462b1931ddf8dc6566e51818c.jpg