Skip to content

Conversation

@takahashim
Copy link
Collaborator

概要

現行のRe:VIEWはそのままに、別途抽象構文木(AST)ベースのレンダリングアーキテクチャーを導入します。
これにより、従来のCompiler/Builderアーキテクチャから独立した処理フローが実現できます。

主な変更内容

AST/Rendererアーキテクチャの実装

BookやChapterはほぼ現行のものを使う形ですが、CompilerとBuilderは新たに作り直しました。
前者はAST::Compiler、後者はRendererになります。

  • AST Compiler (lib/review/ast/compiler.rb): Re:VIEW構文を抽象構文木(AST)に変換
  • Renderer基盤 (lib/review/renderer/base.rb): 各出力形式に対応するRendererの基底クラス
  • 複数のRenderer実装:
    • HtmlRenderer: HTML出力
    • LatexRenderer: LaTeX/PDF出力
    • IdgxmlRenderer: InDesign XML出力
    • MarkdownRenderer: Markdown出力
    • PlaintextRenderer: プレーンテキスト出力
    • TopRenderer: TOP形式出力
graph LR
    A[Re:VIEW文書] --> B[AST::Compiler]
    B --> C[AST]
    C --> D[HTMLRenderer]
    C --> E[LaTeXRenderer]
    C --> F[IDGXMLRenderer]
    C --> G[JSON出力]
    C --> H[カスタムツール]

    style C fill:#ccffcc
Loading

新規コマンドラインツールの追加

review-ast-という共通prefixを使い、既存のコマンドとは干渉しないようにしています。

  • review-ast-compile: AST経由での単一ファイルコンパイル
  • review-ast-pdfmaker: AST経由でのPDF生成
  • review-ast-epubmaker: AST経由でのEPUB生成
  • review-ast-idgxmlmaker: AST経由でのIDGXML生成
  • review-ast-dump: AST構造のダンプ(デバッグ用)
  • review-ast-dump2re: ASTからRe:VIEW形式への逆変換(デバッグ用)

整理された実行プロセス

AST生成処理とASTは各Rendererには依存しない、フォーマット中立なデータ構造とプロセスになっています。
一方、各RendererもAST生成には直接関与せず、完成したASTに対しレンダリングを行うようにしています。

flowchart TB
    A[Re:VIEW文書] --> B[AST::Compiler]
    B --> C[AST構築]
    C --> D[参照解決]
    D --> E[後処理]
    E --> F[AST生成完了]

    F --> G[HTMLRenderer]
    F --> H[LaTeXRenderer]
    F --> I[IDGXMLRenderer]
    F --> J[JSONSerializer]

    G --> K[HTML出力]
    H --> L[LaTeX出力]
    I --> M[IDGXML出力]
    J --> N[JSON出力]

    subgraph "1. AST生成フェーズ"
        B
        C
        D
        E
        F
    end

    subgraph "2. レンダリングフェーズ"
        G
        H
        I
        J
    end
Loading

参照解決システムの強化

参照解決はAST構築とは別に専用のフェーズを設けて解決します。

  • ReferenceResolver: コンパイル時の参照解決
  • ResolvedData: 参照データ管理
  • クロスチャプター参照の完全サポート
  • 12種類の参照タイプ(図表、リスト、数式、章節、コラム、脚注等)

インデックス生成システム

ASTに対してインデックスを生成する、Builder/Compilerに依存しない独立したインデックス処理機構を導入しています。

  • AST::Indexer: 章レベルのインデックス生成
  • AST::BookIndexer: 書籍全体のインデックス生成

リスト処理・ブロック処理・テーブル処理機構

parse段階で複雑な処理が必要なリスト、ブロック、テーブルについて、通常のAST::Compilerとは別にAST::ListProcessor、AST::BlockProcessor、AST::BlockProcessor::TableProcessorを導入し、処理を委譲します。

Post-Processor機構

これまでは様々な場所で行われていた複数ノードが関わるような処理を、AST構築後にまとめて実行します。
そのために後処理専用ベースクラスAST::Compiler::PostProcessorを導入し、そのサブクラスとして各後処理機能を実装しています。

  • AutoIdProcessor: 自動ID生成
  • NoindentProcessor: //noindent処理
  • TsizeProcessor: テーブル幅指定処理
  • OlnumProcessor: リスト開始番号処理
  • FirstlinenumProcessor: コードブロック開始行番号処理
  • ListStructureNormalizer: リスト構造の正規化

Markdown入力サポート(experimental)

Re:VIEW記法のみではなく、Markdownで書かれたファイルも直接扱えるようになります。
catalog.ymlにfoo.md等と書けばMarkdownとして解釈されます。

  • GitHub Flavored Markdown (GFM)のサポート
  • Markdownファイル内でのRe:VIEWコラム記法のサポート
  • HTMLコメントベースのコラム記法

包括的なテストスイート

BuilderとRendererでの出力に差がないことを検証するため、専用の差分出力クラスReVIEW::AST::Diff::(Html|Latex|Idgxml)を導入し、出力が(ほぼ)同一であるかもテストしています。

  • 199個のファイル変更、45,931行追加
  • 2,226個のテストケース(成功率100%)
  • Builder/Renderer出力比較テスト
  • AST JSONシリアライゼーションテスト
  • 各種エッジケースのテスト

ドキュメント整備

  • doc/ast.md: AST概要とAPI
  • doc/ast_architecture.md: アーキテクチャ設計
  • doc/ast_node.md: ノード仕様
  • doc/ast_list_processing.md: リスト処理詳細
  • doc/ast_markdown.md: Markdownサポート詳細

アーキテクチャ上の利点

  1. 関心の分離: パース処理(AST Compiler)と出力生成(Renderer)の明確な分離
  2. 拡張性: 新しい出力形式のRendererを容易に追加可能
  3. テスタビリティ: AST層での検証により、より細かいテストが可能
  4. 保守性: コードの重複削減、モジュール化の推進
  5. 柔軟性: JSON経由のAST共有、他ツールとの連携が容易

後方互換性

  • 従来のCompiler/Builderシステムはほぼ完全に保持しており、変わらず利用できます。
  • review-compile, review-pdfmaker等の既存コマンドも変更は一切ありません。
  • 新しいAST/Rendererシステムはreview-ast-*コマンドとして、新規に追加しています。
  • 既存プロジェクトへの影響は、従来のコマンドを使っている分にはゼロのはずです。

テスト結果

2226 tests, 6225 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

30.28 tests/s, 84.68 assertions/s
Coverage report generated for Unit Tests to /Users/maki/git/review/coverage. 34195 / 38227 LOC (89.45%) covered.

関連

@takahashim takahashim marked this pull request as ready for review December 1, 2025 05:48
@takahashim
Copy link
Collaborator Author

このPRによるコミット済みの現行ファイルに対する変更は以下で、それ以外はすべて新規追加になります(なので既存機能への影響は最小限のはずです):

  • Gemfile
    Ruby 3.0ではmarklyがサポートされないため
  • lib/review/book/book_unit.rb
    AST用のインデックス処理対応のためsetterを追加
  • lib/review/book/chapter.rb
    on_file? で.reと.mdに対応するため
  • lib/review/book/index/item.rb
    キャプション用のASTノードを保持できるようにするため
  • lib/review/snapshot_location.rb
    Hashの変換と、エラーメッセージの再現のため
  • lib/review/textutils.rb
    unicode/eawをすでに使っていたため
  • review.gemspec
    テストでの差分検出用にdiff-lcs と nokogiri を追加
  • samples/debug-book/edge_cases_test.re
    //blankline のデバッグ用
  • test/test_helper.rb
    AST用のヘルパー追加
diff --git a/Gemfile b/Gemfile
index fce680bb..f22f26df 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,3 +4,10 @@ source 'https://rubygems.org'
 
 # Specify your gem's dependencies in review.gemspec
 gemspec
+
+# Development dependencies
+group :development do
+  # markly gem (for Markdown support) requires Ruby >= 3.1
+  # On Ruby 3.0, tests will be skipped but Re:VIEW will work with .re files
+  gem 'markly', '~> 0.13' if Gem.ruby_version >= Gem::Version.new('3.1.0')
+end
diff --git a/lib/review/book/book_unit.rb b/lib/review/book/book_unit.rb
index db3e93fb..357f7ecb 100644
--- a/lib/review/book/book_unit.rb
+++ b/lib/review/book/book_unit.rb
@@ -68,6 +68,22 @@ def generate_indexes(use_bib: false)
         end
       end
 
+      # Set indexes using AST-based indexing
+      def ast_indexes=(indexes)
+        @footnote_index = indexes[:footnote_index] if indexes[:footnote_index]
+        @endnote_index = indexes[:endnote_index] if indexes[:endnote_index]
+        @list_index = indexes[:list_index] if indexes[:list_index]
+        @table_index = indexes[:table_index] if indexes[:table_index]
+        @equation_index = indexes[:equation_index] if indexes[:equation_index]
+        @image_index = indexes[:image_index] if indexes[:image_index]
+        @icon_index = indexes[:icon_index] if indexes[:icon_index]
+        @numberless_image_index = indexes[:numberless_image_index] if indexes[:numberless_image_index]
+        @indepimage_index = indexes[:indepimage_index] if indexes[:indepimage_index]
+        @headline_index = indexes[:headline_index] if indexes[:headline_index]
+        @column_index = indexes[:column_index] if indexes[:column_index]
+        @book.bibpaper_index = indexes[:bibpaper_index] if @book.present? && indexes[:bibpaper_index]
+      end
+
       def dirname
         @path && File.dirname(@path)
       end
diff --git a/lib/review/book/chapter.rb b/lib/review/book/chapter.rb
index 9f6a9d23..56128400 100644
--- a/lib/review/book/chapter.rb
+++ b/lib/review/book/chapter.rb
@@ -149,7 +149,9 @@ def on_postdef?
       private
 
       def on_file?(contents)
-        contents.map(&:strip).include?("#{id}#{@book.ext}")
+        contents.map(&:strip).include?("#{id}#{@book.ext}") ||
+          contents.map(&:strip).include?("#{id}.re") ||
+          contents.map(&:strip).include?("#{id}.md")
       end
 
       # backward compatibility
diff --git a/lib/review/book/index/item.rb b/lib/review/book/index/item.rb
index 595a364a..c02f01ca 100644
--- a/lib/review/book/index/item.rb
+++ b/lib/review/book/index/item.rb
@@ -18,10 +18,11 @@ module ReVIEW
   module Book
     class Index
       class Item
-        def initialize(id, number, caption = nil)
+        def initialize(id, number, caption = nil, caption_node: nil)
           @id = id
           @number = number
           @caption = caption
+          @caption_node = caption_node
           @path = nil
           @index = nil
         end
@@ -29,6 +30,7 @@ def initialize(id, number, caption = nil)
         attr_reader :id
         attr_reader :number
         attr_reader :caption
+        attr_accessor :caption_node
         attr_accessor :index # internal use only
 
         alias_method :content, :caption
diff --git a/lib/review/snapshot_location.rb b/lib/review/snapshot_location.rb
index 17eb22a3..e9d971ed 100644
--- a/lib/review/snapshot_location.rb
+++ b/lib/review/snapshot_location.rb
@@ -21,6 +21,21 @@ def string
       "#{@filename}:#{@lineno}"
     end
 
+    def to_h
+      {
+        filename: filename,
+        lineno: lineno
+      }
+    end
+
+    # Format location information for error messages
+    # Returns a string like " at line 42 in chapter01.re"
+    def format_for_error
+      info = " at line #{@lineno}"
+      info += " in #{@filename}" if @filename
+      info
+    end
+
     alias_method :to_s, :string
 
     def snapshot
diff --git a/lib/review/textutils.rb b/lib/review/textutils.rb
index 34904dfe..bb2bc045 100644
--- a/lib/review/textutils.rb
+++ b/lib/review/textutils.rb
@@ -10,6 +10,7 @@
 #
 require 'nkf'
 require 'digest'
+require 'unicode/eaw'
 
 module ReVIEW
   module TextUtils
diff --git a/review.gemspec b/review.gemspec
index 31d6d451..c6b75c44 100644
--- a/review.gemspec
+++ b/review.gemspec
@@ -31,7 +31,9 @@ Gem::Specification.new do |gem|
   gem.add_dependency('rubyzip')
   gem.add_dependency('tty-logger')
   gem.add_development_dependency('chunky_png')
+  gem.add_development_dependency('diff-lcs')
   gem.add_development_dependency('math_ml')
+  gem.add_development_dependency('nokogiri')
   gem.add_development_dependency('playwright-runner')
   gem.add_development_dependency('pygments.rb')
   gem.add_development_dependency('rake')
diff --git a/samples/debug-book/edge_cases_test.re b/samples/debug-book/edge_cases_test.re
index 15f69173..151468b2 100644
--- a/samples/debug-book/edge_cases_test.re
+++ b/samples/debug-book/edge_cases_test.re
@@ -277,6 +277,8 @@ class ValidationError extends Error {
 対策として@<list>{empty_and_special_cases}で示したnullチェックが重要。
 //}
 
+//blankline
+
 == まとめ
 
 このエッジケーステストでは以下を検証:
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 7409904e..79f8becb 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -7,6 +7,8 @@
 require 'review/yamlloader'
 require 'review/extentions'
 
+require_relative 'ast/caption_parser_helper'
+
 def touch_file(path)
   FileUtils.touch(path)
 end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants