From 6dedddc545e2f1930bdc2256784eb1551bd4231d Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 1 Feb 2026 00:49:15 +0900 Subject: feat(nuldoc): rewrite nuldoc in Ruby --- .../trick-2025-most-ruby-on-ruby-award/index.html | 153 +++++++++++---------- 1 file changed, 80 insertions(+), 73 deletions(-) (limited to 'services/nuldoc/public/blog/posts/2025-04-20') diff --git a/services/nuldoc/public/blog/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/index.html b/services/nuldoc/public/blog/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/index.html index 52aef34c..74a1f48d 100644 --- a/services/nuldoc/public/blog/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/index.html +++ b/services/nuldoc/public/blog/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/index.html @@ -119,7 +119,7 @@

作品紹介

- 今回頂いたのは審査員賞の一つ eto award (公式の賞の名前に合わせて敬称略) で、“Most Ruby-on-Ruby” Award (『最もRuby on Ruby賞』) として受賞した (IOCCC と同じく、それぞれの賞に個別の名前が付く)。 + 今回頂いたのは審査員賞の一つ eto award (公式の賞の名前に合わせて敬称略) で、”Most Ruby-on-Ruby” Award (『最もRuby on Ruby賞』) として受賞した (IOCCC と同じく、それぞれの賞に個別の名前が付く)。

ソースコード等はこちら: https://github.com/tric/trick2025/tree/main/10-nsfisis @@ -161,7 +161,8 @@ 表示している。つまり、Ruby プログラムにルビを振った作品である。例えば、先頭の2行目の require は次のような HTML で構成されている。

-
<ruby class="IDENTIFIER">require<rp class="">(</rp><rt class="">リクワイア</rt><rp class="">)</rp></ruby>
+
<ruby class="IDENTIFIER">require<rp class="">(</rp><rt class="">リクワイア</rt><rp class="">)</rp></ruby>
+

順に使ったテクニックを解説していく。 @@ -172,11 +173,12 @@ 改めて quine について説明する。Quine とは、自身のソースコードを出力するようなプログラムである。Ruby では様々な方法で quine を書くことができるが、この作品で使っている基本形は以下のようなものである。

-
eval $s=<<'EOS'
-print "eval $s=<<'EOS'\n"
-print $s
-print "EOS\n"
-EOS
+
eval $s=<<'EOS'
+print "eval $s=<<'EOS'\n"
+print $s
+print "EOS\n"
+EOS
+

変数 $s に 2 行目、3 行目、4 行目が入っており、それに加えて 1 行目と 5 行目を出力すれば元のソースコードが得られる。実際には $s を加工してシンタックスハイライトや振り仮名を振ることになる。 @@ -191,23 +193,24 @@ トークナイズには Ruby 3.4 からデフォルトのパーサになった Prism を利用している。Prism.lex() を使うとトークナイズができるので、トークンに付いているソースコード位置の情報を使いつつ元のソースコードを復元する。

-
y = 1                 # 現在の行
-x = 0                 # 現在の列
-Prism.lex($s).value[..-2].each {|t, *|
-  l = t.location
-  r = l.start_line    # トークンの開始行
-  if y < r            # 改行が必要なら
-    p "\n" * (r - y)  #   改行を挿入して
-    x = 0             #   列の先頭へ戻る
-  end
-  c = l.start_column  # トークンの開始列
-  if x < c            # 空白が必要なら
-    p " " * (c - x)   #   空白を挿入
-  end
-  p ruby(t)           # トークン本体を出力
-  y = l.end_line      # 現在行を更新
-  x = l.end_column    # 現在列を更新
-}
+
y = 1                 # 現在の行
+x = 0                 # 現在の列
+Prism.lex($s).value[..-2].each {|t, *|
+  l = t.location
+  r = l.start_line    # トークンの開始行
+  if y < r            # 改行が必要なら
+    p "\n" * (r - y)  #   改行を挿入して
+    x = 0             #   列の先頭へ戻る
+  end
+  c = l.start_column  # トークンの開始列
+  if x < c            # 空白が必要なら
+    p " " * (c - x)   #   空白を挿入
+  end
+  p ruby(t)           # トークン本体を出力
+  y = l.end_line      # 現在行を更新
+  x = l.end_column    # 現在列を更新
+}
+

補足: 変数名がやたら短いのは、このあとの振り仮名データの量を削減するため。 @@ -216,20 +219,21 @@ トークン種別に応じた色付けは CSS でおこなっている。出力する HTML のクラス名に Prism::Token#type を指定しておいて、index.html でそれぞれのクラスにスタイルを当てた。

-
    <style>
-      /* ... */
-
-      .COMMENT {
-        color: #777;
-        font-style: italic;
-      }
-
-      .CONSTANT, .GLOBAL_VARIABLE, .INSTANCE_VARIABLE, .IDENTIFIER {
-        color: #088;
-      }
-
-      /* ... */
-    </style>
+
    <style>
+      /* ... */
+
+      .COMMENT {
+        color: #777;
+        font-style: italic;
+      }
+
+      .CONSTANT, .GLOBAL_VARIABLE, .INSTANCE_VARIABLE, .IDENTIFIER {
+        color: #088;
+      }
+
+      /* ... */
+    </style>
+

トークン種別の列挙にはそれなりに文字数を使ってしまうのだが、今回の TRICK のレギュレーションでは index.html にサイズ制限がなかったので好きに色を付けることができた。 @@ -241,27 +245,28 @@ それぞれの英単語や記号に対応した振り仮名のデータは、プログラム中に埋め込まれている。

-
def rt(t)
-  r = {
-    :"&&" => "1136",
-    :"=" => "04199275",
-    :"||" => "623147",
-    :$s => "41750825",
-    :* => "111775",
-    # ...
-    type: "310455",
-    utf_8: "70923803920853080440",
-    value: "48746992",
-    x: "08351525",
-    y: "7904",
-  }
-  kana(
-    r[:"#{t.type}"] ||
-    r[s = :"#{t.value.downcase}"] ||
-    s.end_with?(":") && r[:"#{s[..-2]}"] ||
-    nil
-  )
-end
+
def rt(t)
+  r = {
+    :"&&" => "1136",
+    :"=" => "04199275",
+    :"||" => "623147",
+    :$s => "41750825",
+    :* => "111775",
+    # ...
+    type: "310455",
+    utf_8: "70923803920853080440",
+    value: "48746992",
+    x: "08351525",
+    y: "7904",
+  }
+  kana(
+    r[:"#{t.type}"] ||
+    r[s = :"#{t.value.downcase}"] ||
+    s.end_with?(":") && r[:"#{s[..-2]}"] ||
+    nil
+  )
+end
+

トークンの種類 (t.type) またはトークンの文字列表現そのもの (t.value.downcase) を使ってテーブルを引いて振り仮名へ変換している。このテーブルのキー部分そのものにも振り仮名を振るために、トークンが : で終わっていれば : を取り除いて振り仮名を得ている (例: "value:""value""48746992")。 @@ -270,25 +275,27 @@ このテーブルはサイズ制限を突破するために圧縮されており、kana() 関数で展開される。

-
def kana(s)
-  s
-    &.scan(/.{2}/)
-    &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)}
-    &.*("")
-end
+
def kana(s)
+  s
+    &.scan(/.{2}/)
+    &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)}
+    &.*("")
+end
+

例えば value に対応する振り仮名データ "48746992" であれば、次のような変換を経て振り仮名へと展開される。

-
  s
-    # => "48746992"
-    &.scan(/.{2}/)
-    # => ["48", "74", "69", "92"]
-    &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)}
-    # => ["バ", "リ", "ュ", "ー"]
-    &.*("")
-    # => "バリュー"
+
  s
+    # => "48746992"
+    &.scan(/.{2}/)
+    # => ["48", "74", "69", "92"]
+    &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)}
+    # => ["バ", "リ", "ュ", "ー"]
+    &.*("")
+    # => "バリュー"
+

これは後で気付いたのだが、Ruby は多倍長整数が扱えるので "48746992" のようなデータは単に 48746992 と書けばよかった。kana() 関数が多少長くはなるが、振り仮名データの数 x 2 バイト分サイズが減るのでこちらの方が短くなる。サイズ制限の都合で振り仮名を振るのを諦めた記号もあったのでもったいない。 -- cgit v1.3-1-g0d28