LoginSignup
5
2

More than 5 years have passed since last update.

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

Posted at

結論

前処理で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

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2