query-stream 1.2.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +181 -0
- data/README.md +62 -2
- data/RELEASE_NOTE.md +61 -0
- data/lib/query_stream/command.rb +14 -6
- data/lib/query_stream/configuration.rb +9 -6
- data/lib/query_stream/data_resolver.rb +25 -2
- data/lib/query_stream/errors.rb +26 -1
- data/lib/query_stream/template_compiler.rb +7 -11
- data/lib/query_stream/version.rb +1 -1
- data/lib/query_stream.rb +78 -15
- data/query-stream.gemspec +4 -3
- metadata +5 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 399e9fb8aea90ac45fa0d7837c9891c7db5785b2bd81f6976c697eaa1d92f35c
|
|
4
|
+
data.tar.gz: 6587887f665b0a6ffde78dc029e2988bd647debcd92f0740766805b273f2ea61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 47693f041e99aa59784a2d50a55fca8d1aa50c18798a16862a946a5c6ed1faedeafbc89a4fa2113d80fb793d2ef97d031a2e578ba64ab695f42e2ae3c946013a
|
|
7
|
+
data.tar.gz: '095ed49dca369ea7420c037ddf95d77bbf2833b85d5b9a312393f31694632ee9744b6bfc319dfdabd55b5025fcd82f9921d0d2e80b3d988a407145e2902b3132'
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Planned
|
|
10
|
+
- パフォーマンス最適化
|
|
11
|
+
- 追加のテンプレートスタイル
|
|
12
|
+
|
|
13
|
+
## [1.4.0] - 2026-08-07
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
- **`QueryStream.logger` と `Configuration#logger` を削除した** (`query_stream.rb`, `configuration.rb`, `query-stream.gemspec`): 下記の `logger.error` 撤去により、gem 内でロガーを読む箇所がゼロになった。設定できるのに何も起きない項目は、利用者に「ログ出力を制御できる」という誤った期待を持たせるだけなので残さない。`logger` gem への依存も外した。
|
|
17
|
+
- **破壊的変更**: `config.logger = ...` を書いているコードは削除が必要(設定しても何も起きないため、消すだけでよい)。
|
|
18
|
+
- 唯一の利用者である vivlio-starter は `QueryStream.configure` 自体を使っておらず、影響は無い。
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **`UnknownKeyError` を構造化例外にし、gem 内に最後まで残っていた `logger.error` を撤去した** (`errors.rb`, `template_compiler.rb`): 1.1.0 で `logger.error`、1.2.0 で `logger.warn` を廃したが、`TemplateCompiler` がテンプレートのキーを検証する 2 箇所にだけ `logger.error` が残っており、「gem 内のログ出力を全廃した」という設計が最後まで通っていなかった。`UnknownKeyError` は属性を 1 つも持たなかったため、**呼び出し元は「利用できるキーの一覧」を受け取れず**、gem が直接吐くログに頼るしかなかったのが原因である。`key_path` / `available_keys` / `template_path` / `location` を持たせ、ログ出力を削除した。これで呼び出し元は「もしかして `=title`?」のような案内を自前で組み立てられる。
|
|
22
|
+
- `location`(記法が書かれた原稿の位置)と `template_path`(打ち間違いがある雛形の位置)は別物として持つ。**直すのは雛形**なので、記法の位置だけでは著者はファイルを開けない。
|
|
23
|
+
- `TemplateCompiler.render` に `template_path:` キーワードを追加した(任意・既定 `nil`)。
|
|
24
|
+
- **必要な Ruby を 4.0 以上から 3.4 以上へ緩めた** (`query-stream.gemspec`): Ruby 4.0 固有の機能は使っていない。Prism のバージョン指定パースで全 14 ファイルを解析したところ 4.0.0 / 3.4.0 / 3.3.0 いずれも構文エラー 0 件で、実際の下限を決めていたのは `it`(暗黙ブロック引数・16 箇所)**単独**だった。`it` は 3.3 でも構文エラーにならず「`it` というメソッドの呼び出し」として通り、実行時に `NameError` になる——静的解析では見つからないので、Ruby 3.4.10 で実走して全テスト通過を確認している。コード変更はゼロ(gemspec 1 行のみ)。
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **壊れた入力で生の例外が外へ漏れていたのを直した**: いずれも「著者が用意した原稿・データが壊れている」という正当に起こりうる状況で、`QueryStream::Error` 系ではない例外が呼び出し元へ届いていた。呼び出し元は例外型で扱いを分けるため、生の `ArgumentError` / `NoMethodError` が出ると「どのファイルのどこが悪いか」を著者へ案内できない。
|
|
28
|
+
- **空のデータファイルで `NoMethodError`** (`data_resolver.rb`): YAML が `nil` を返すのを素通ししており、`TemplateCompiler` が `undefined method 'any?' for nil` で落ちていた。`load_records` は**常に配列を返す**契約に正規化した(`@return [Array<Hash>]` という既存の記述に実装を合わせた形)。レコードの配列でもハッシュでもない中身(スカラーだけの YAML)は、何が入っていたかを添えて `DataLoadError` にする。
|
|
29
|
+
- **不正な UTF-8 を含む原稿で `ArgumentError`** (`query_stream.rb`): `String#match?` も `lstrip` も不正バイト列に対して `invalid byte sequence in UTF-8` を投げるため、原稿に 1 バイトの壊れが混ざっただけで展開が丸ごと止まっていた。読めない行は**本文としてそのまま返す**。scrub して通す設計は採らない——gem が黙ってバイト列を書き換えると、著者が気付けない形で本文が変わる。
|
|
30
|
+
- **`scan` に本文を渡すと `ArgumentError` / `Errno::EISDIR`** (`query_stream.rb`): `scan` はパスと本文の両方を受け取る API なのに、引数を無条件で `File.exist?` へ渡していた。NUL バイトを含む本文で `path name contains null byte`、ディレクトリ名で `File.exist?` が true を返し `File.read` が `Errno::EISDIR` になっていた。パスとして成立しうる形(NUL・改行を含まず `MAX_PATH_BYTES` 以下で、かつ `File.file?`)だけを読み、判定自体の失敗も本文扱いに倒す。
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- **堅牢性テストを整備した** (`test/robustness/`): 「正しく使ったとき正しく動くか」を見る既存のユニットテストとは別に、**想定外の入力・環境に対する振る舞い**を固定する 27 件を追加。守る不変条件は「gem の外へ出てよい例外は `QueryStream::Error` の系統だけ」。上記 3 件の修正はいずれもこのテストを書く過程で見つかったもの。
|
|
34
|
+
- `data_file_safety_test.rb` — `!ruby/object` 等の危険なタグ・壊れた YAML/JSON・空ファイル・レコードでない中身
|
|
35
|
+
- `broken_encoding_test.rb` — 不正な UTF-8 を含む原稿。**バイト列を書き換えずに返す**ことまで固定
|
|
36
|
+
- `scan_input_test.rb` — `scan` のパス/本文判定(NUL・改行・超長入力・ディレクトリ)
|
|
37
|
+
- `missing_source_test.rb` — データ/テンプレート不在時の例外属性と、1 行の失敗で全体を止めないこと。**当てずっぽうの hint を返さない**ことも固定
|
|
38
|
+
- `unreadable_file_test.rb` — 権限エラーは `DataLoadError` へ包まず `Errno::EACCES` のまま伝える(環境の問題とデータの問題は直し方が違うため)
|
|
39
|
+
|
|
40
|
+
## [1.3.0] - 2026-07-12
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- **`post_render` 後段フィルタコールバックを追加** (`lib/query_stream.rb`, `lib/query_stream/configuration.rb`): 1 記法の展開結果を、コンテキスト付きで呼び出し元へ通す汎用フックを新設。`QueryStream.render` / `render_query` に `post_render:` キーワードを追加し、`Configuration#post_render` からも指定できる。コールバックは `(text, context)` を受け取り、`context` は `source`(記法の論理名)・`data_file`(単複解決後の実パス)・`data_dir`・`template_path`・`query`・`location` を含む。戻り値が String ならそれを、String 以外(nil 含む)なら元の展開結果を採用する。gem 自身は用途を規定せず、画像パス解決などの呼び出し元固有の後処理を委譲できる。コールバック内の例外は握り潰さず伝播する(`render` の既存 rescue は `QueryStream::Error` のみ捕捉するため、それ以外の後処理例外の扱いは呼び出し元の責務)。
|
|
44
|
+
|
|
45
|
+
## [1.2.2] - 2026-04-26
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- **アンダースコア付き複合名の単数形変換が誤る問題を修正** (`lib/query_stream/singularize.rb`): `physics_books` が `physic_books` に変換されていた問題を修正。アンダースコアで分割し、末尾から複数形セグメントを探して単数化するよう変更。`people` は不規則変化として優先処理。`physics_books` → `physics_book`、`books_nested` → `book_nested` が正しく変換されるようになった。
|
|
49
|
+
|
|
50
|
+
## [1.2.1] - 2026-04-20
|
|
51
|
+
|
|
52
|
+
### Security
|
|
53
|
+
- **`DataResolver.load_records` を `YAML.safe_load_file` に移行** (`lib/query_stream/data_resolver.rb`):
|
|
54
|
+
従来は `YAML.load_file(file_path, symbolize_names: true)` を使用しており、Psych のバージョンや将来のアップデートで `!ruby/object` などの Ruby オブジェクトタグが受理されてしまう可能性があった。
|
|
55
|
+
`permitted_classes: [Symbol, Time, Date, DateTime]` と `aliases: true` を明示的に指定する `YAML.safe_load_file` に置き換え、安全性を Psych バージョン非依存にした。
|
|
56
|
+
これにより悪意のあるデータファイル(`!ruby/object:Kernel {}` 等)を読み込ませて任意コード実行を試みる攻撃ベクトルを明示的に塞いだ。
|
|
57
|
+
なお Symbol が許可されているため `symbolize_names: true` の挙動は従来どおり維持される。
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
- **`QueryStream::DataLoadError` 新規例外クラス** (`lib/query_stream/errors.rb`):
|
|
61
|
+
`Psych::DisallowedClass` / `Psych::SyntaxError` / `JSON::ParserError` を呼び出し元に優しいメッセージ付きの `DataLoadError` に変換する。
|
|
62
|
+
`file_path` と `cause_error` 属性を持ち、呼び出し元で詳細なログ出力や i18n が可能。
|
|
63
|
+
|
|
64
|
+
### Tests
|
|
65
|
+
- **セキュリティテスト 7 件を追加** (`test/query_stream_test.rb`):
|
|
66
|
+
- `!ruby/object:Object {}` を含む YAML は `DataLoadError` で拒否
|
|
67
|
+
- `!ruby/struct:Point` / `!ruby/hash:MyCustomHash` も同様に拒否
|
|
68
|
+
- Symbol / Time / Date / DateTime は正常に読み込める
|
|
69
|
+
- YAML / JSON 構文エラーは `DataLoadError` に変換される
|
|
70
|
+
- 通常の YAML anchor / alias は正常に展開される(DoS 耐性の副次確認)
|
|
71
|
+
|
|
72
|
+
## [1.2.0] - 2026-04-19
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
- **`NoResultWarning` / `AmbiguousQueryWarning` を構造化例外に変更し、gem 内の `logger.warn` 呼び出しを全廃** (`lib/query_stream.rb`, `lib/query_stream/errors.rb`): 1.1.0 で `logger.error` は廃止されていたが、`render_query` の一件検索分岐に `logger.warn("一件検索で該当なし(…): …")` / `logger.warn("一件検索で複数件ヒット(…): …")` が残存しており、呼び出し元(vivlio-starter 等)が `⚠️` プレフィックスや i18n を付与できない問題があった。`NoResultWarning` / `AmbiguousQueryWarning` に `query` / `location` (ambiguous は `count` も)属性を追加し、`logger.warn` 呼び出しを削除。新たに `QueryStream.render` / `render_query` に `on_warning:` コールバックを追加し、警告情報を構造化例外として呼び出し元へ委譲する。これにより gem 内のログ出力が完全になくなり、メッセージ構成・ログ出力・i18n はすべて呼び出し元の責務となった。
|
|
76
|
+
|
|
77
|
+
## [1.1.0] - 2026-04-13
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- **`TemplateNotFoundError` / `DataNotFoundError` を構造化例外に変更** (`lib/query_stream.rb`, `lib/query_stream/errors.rb`):
|
|
82
|
+
従来は `render_query` 内で `logger.error` によるログ出力を行ってから例外を raise していた。
|
|
83
|
+
これでは呼び出し元がメッセージフォーマットや言語(i18n)を制御できないため、
|
|
84
|
+
gem 内のログ出力を廃止し、例外クラスに `template_path`, `query`, `location`, `hint`
|
|
85
|
+
(`DataNotFoundError` は `expected_path`, `query`, `location`)の属性を追加。
|
|
86
|
+
メッセージ構成・ログ出力・i18n はすべて呼び出し元の責務とした。
|
|
87
|
+
|
|
88
|
+
- **`render` 内で展開エラーをキャッチして後続の記法を継続展開するよう変更** (`lib/query_stream.rb`):
|
|
89
|
+
従来は1行の展開失敗で `render` 全体が中断されていた。
|
|
90
|
+
`render_query` の呼び出しを `rescue Error` で囲み、失敗した行は元の記法のまま残して
|
|
91
|
+
後続の QueryStream 記法の展開を継続するよう修正。
|
|
92
|
+
`on_error` コールバックを追加し、エラー情報を呼び出し元に通知できるようにした。
|
|
93
|
+
|
|
94
|
+
- **`Singularize` で `People`(大文字)が `person` に変換されない問題を修正** (`lib/query_stream/singularize.rb`):
|
|
95
|
+
`people` パターンの正規表現に `/i` フラグを追加し、大文字小文字を問わず変換できるようにした。
|
|
96
|
+
|
|
97
|
+
## [1.0.0] - 2026-03-21
|
|
98
|
+
|
|
99
|
+
### 🎉 最初のメジャーリリース
|
|
100
|
+
|
|
101
|
+
QueryStream 1.0.0 としてリリース!YAML/JSON データファイルとテンプレートファイルを組み合わせて、テキストコンテンツ内の QueryStream 記法を展開する汎用 Ruby ライブラリが完成しました。
|
|
102
|
+
|
|
103
|
+
### ✅ 実績と品質保証
|
|
104
|
+
- **vivlio-starter** プロジェクトで実稼働実績
|
|
105
|
+
- **109 テストケース**、**395 アサーション**で網羅的品質保証
|
|
106
|
+
- **VFM フェンス記法** 完全対応と実用性検証
|
|
107
|
+
|
|
108
|
+
### 🚀 主要機能
|
|
109
|
+
- QueryStream 記法の完全実装(源泉・抽出・ソート・件数・スタイル)
|
|
110
|
+
- VFM フェンス記法対応(`:::{.class-name}` 〜 `:::`)
|
|
111
|
+
- テンプレートコンパイラ(変数展開、画像記法、ネスト値アクセス)
|
|
112
|
+
- 高度なデータ処理(フィルタリング、ソート、単数形化)
|
|
113
|
+
- 柔軟な設定システム
|
|
114
|
+
|
|
115
|
+
### 📚 ドキュメント整備
|
|
116
|
+
- 詳細な README と具体例
|
|
117
|
+
- 完全な CHANGELOG
|
|
118
|
+
- vivlio-style での実装例
|
|
119
|
+
|
|
120
|
+
### 🔧 技術的特徴
|
|
121
|
+
- **Ruby 4.0+** モダン開発標準準拠
|
|
122
|
+
- **Semantic Versioning** 準拠
|
|
123
|
+
- 後方互換性保証
|
|
124
|
+
|
|
125
|
+
## [0.3.0] - 2026-03-21
|
|
126
|
+
|
|
127
|
+
### Added
|
|
128
|
+
- VFM フェンス記法対応 (`:::{.class-name}` 〜 `:::`)
|
|
129
|
+
- フェンス行が動的行を囲んでいる場合、フェンス行も repeating 範囲に含めて各レコードごとに反復出力
|
|
130
|
+
- `{.book-card}`, `{.person-card}` 等の任意 VFM クラス名に汎用的に対応
|
|
131
|
+
- フェンス記法関連のテストケースを追加 (12件)
|
|
132
|
+
- ファイルドキュメントに VFM フェンス記法対応の説明を追加
|
|
133
|
+
|
|
134
|
+
### Changed
|
|
135
|
+
- `TemplateCompiler` に `FENCE_OPEN_PATTERN` / `FENCE_CLOSE_PATTERN` 定数を追加
|
|
136
|
+
- `classify_lines` で `:fence_open` / `:fence_close` タイプを認識
|
|
137
|
+
- `expand_fence_range` メソッドを新設し、repeating 範囲の拡張ロジックを実装
|
|
138
|
+
|
|
139
|
+
### Fixed
|
|
140
|
+
- QueryStream と vivlio-starter の VFM フェンス記法の相性問題を解決
|
|
141
|
+
- フェンス行が一度しか出力されず、全レコードが1つのフェンスに押し込まれる問題を修正
|
|
142
|
+
|
|
143
|
+
## [0.2.0] - Previous Release
|
|
144
|
+
|
|
145
|
+
### Added
|
|
146
|
+
- 複数スタイル対応 (`:full`, `:table` 等)
|
|
147
|
+
- 高度なフィルタリング機能(範囲指定、不等値比較)
|
|
148
|
+
- パフォーマンス最適化
|
|
149
|
+
- エラーハンドリングの改善
|
|
150
|
+
|
|
151
|
+
## [0.1.0] - Initial Release
|
|
152
|
+
|
|
153
|
+
### Added
|
|
154
|
+
- 基本的な QueryStream 記法の実装
|
|
155
|
+
- 源泉指定 (`= books`)
|
|
156
|
+
- フィルタリング (`tags=ruby`, `condition=晴`)
|
|
157
|
+
- ソート (`-title`, `+date`)
|
|
158
|
+
- 件数制限 (`5`)
|
|
159
|
+
- スタイル指定 (`:full`)
|
|
160
|
+
- テンプレートコンパイラ機能
|
|
161
|
+
- 変数展開 (`= title`, `= author`)
|
|
162
|
+
- 画像記法の展開 (``)
|
|
163
|
+
- ネストされた値へのアクセス (`= author.name`)
|
|
164
|
+
- テーブル記法対応
|
|
165
|
+
- データフィルタリングとソート機能
|
|
166
|
+
- 等値フィルタ (`tags=ruby`)
|
|
167
|
+
- AND/OR 条件 (`tags=ruby && beginner`, `tags=ruby, javascript`)
|
|
168
|
+
- 比較演算子 (`>=`, `<=`, `>`, `<`, `!=`)
|
|
169
|
+
- データリゾルバー
|
|
170
|
+
- YAML/JSON ファイルの自動探索
|
|
171
|
+
- 複数形→単数形の自動テンプレート解決
|
|
172
|
+
- 単数形化ユーティリティ (Singularize)
|
|
173
|
+
- 英語の複数形パターン対応
|
|
174
|
+
- 不規則名詞 (people → person)
|
|
175
|
+
- 設定システム
|
|
176
|
+
- データディレクトリ、テンプレートディレクトリの設定
|
|
177
|
+
- ロガー設定
|
|
178
|
+
- エラー処理
|
|
179
|
+
- データファイル不在エラー
|
|
180
|
+
- テンプレートファイル不在エラー
|
|
181
|
+
- 不明キーエラー
|
data/README.md
CHANGED
|
@@ -13,6 +13,8 @@ gem 'query-stream'
|
|
|
13
13
|
bundle install
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
Ruby 3.4 以上が必要です。
|
|
17
|
+
|
|
16
18
|
## 基本的な使い方
|
|
17
19
|
|
|
18
20
|
```ruby
|
|
@@ -20,9 +22,11 @@ require 'query_stream'
|
|
|
20
22
|
|
|
21
23
|
# テキスト内の QueryStream 記法を展開
|
|
22
24
|
source = File.read('contents/05-references.md')
|
|
23
|
-
result = QueryStream.render(source)
|
|
25
|
+
result = QueryStream.render(source, data_dir: 'data', templates_dir: 'templates')
|
|
24
26
|
```
|
|
25
27
|
|
|
28
|
+
`data_dir` / `templates_dir` は後述の `configure` で既定値を決めておけば省略できます。
|
|
29
|
+
|
|
26
30
|
## QueryStream 記法
|
|
27
31
|
|
|
28
32
|
```
|
|
@@ -106,10 +110,66 @@ QueryStream.configure do |config|
|
|
|
106
110
|
config.data_dir = 'data'
|
|
107
111
|
config.templates_dir = 'templates'
|
|
108
112
|
config.default_format = :md
|
|
109
|
-
config.
|
|
113
|
+
config.post_render = ->(text, context) { text }
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## エラー・警告の扱い
|
|
118
|
+
|
|
119
|
+
**この gem はメッセージを組み立てません。** どう表示するか、どの言語で出すか、そもそもログに残すかは、すべて呼び出し元が決めます。そのため展開時の出来事は、構造化された例外としてコールバックへ渡されます。
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
QueryStream.render(
|
|
123
|
+
content,
|
|
124
|
+
data_dir: 'data',
|
|
125
|
+
templates_dir: 'templates',
|
|
126
|
+
source_filename: 'chapter.md', # 位置情報(location)の表示に使います
|
|
127
|
+
on_error: ->(e) { }, # 展開失敗。その行は元の記法のまま残ります
|
|
128
|
+
on_warning: ->(w) { }, # 一件検索の該当なし/複数ヒット
|
|
129
|
+
post_render: ->(text, ctx) { } # 展開結果の後処理
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**1 行の失敗で全体は止まりません。** 失敗した行は元の記法のまま残り、後続の記法は展開され続けます。
|
|
134
|
+
|
|
135
|
+
| 例外クラス | 属性 |
|
|
136
|
+
|:---|:---|
|
|
137
|
+
| `TemplateNotFoundError` | `template_path`, `query`, `location`, `hint` |
|
|
138
|
+
| `DataNotFoundError` | `expected_path`, `query`, `location` |
|
|
139
|
+
| `DataLoadError` | `file_path`, `cause_error` |
|
|
140
|
+
| `UnknownKeyError` | `key_path`, `available_keys`, `template_path`, `location` |
|
|
141
|
+
| `NoResultWarning` | `query`, `location` |
|
|
142
|
+
| `AmbiguousQueryWarning` | `query`, `location`, `count` |
|
|
143
|
+
|
|
144
|
+
前 4 つは `QueryStream::Error`、後 2 つは `QueryStream::Warning` を継承します。**gem の外へ出る例外はこの 2 系統だけ**です。壊れた YAML も、不正な UTF-8 を含む原稿も、生の `ArgumentError` や `NoMethodError` にはなりません。
|
|
145
|
+
|
|
146
|
+
たとえば `UnknownKeyError` は「使えるキーの一覧」と「直すべき雛形のパス」まで持って届くので、呼び出し元で打ち間違いの案内を組み立てられます。`location` は記法が書かれた原稿の位置、`template_path` は直す雛形の位置で、**直すのは雛形のほう**です。
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
on_error: lambda do |e|
|
|
150
|
+
next unless e.is_a?(QueryStream::UnknownKeyError)
|
|
151
|
+
|
|
152
|
+
keys = e.available_keys.map(&:to_s)
|
|
153
|
+
near = DidYouMean::SpellChecker.new(dictionary: keys).correct(e.key_path.split('.').first).first
|
|
154
|
+
puts "#{e.location} もしかして =#{near}?(#{e.template_path} / 使えるキー: #{keys.join(', ')})"
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
例外はファイル権限のみ扱いが異なり、`Errno::EACCES` のまま伝わります。環境の問題(`chmod` で直る)とデータの問題(中身を直す)は、対処が違うためです。
|
|
159
|
+
|
|
160
|
+
### `post_render`
|
|
161
|
+
|
|
162
|
+
展開結果を、その出どころの情報つきで受け取れます。gem 側は用途を規定しないので、画像パスの解決など固有の後処理を差し込めます。
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
post_render: lambda do |text, context|
|
|
166
|
+
# context: source, data_file, data_dir, template_path, query, location
|
|
167
|
+
MyImageResolver.rewrite(text, context)
|
|
110
168
|
end
|
|
111
169
|
```
|
|
112
170
|
|
|
171
|
+
戻り値が `String` ならそれを、そうでなければ元の展開結果を採用します。
|
|
172
|
+
|
|
113
173
|
## ライセンス
|
|
114
174
|
|
|
115
175
|
MIT License
|
data/RELEASE_NOTE.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# QueryStream リリースノート
|
|
2
|
+
|
|
3
|
+
YAML / JSON のデータファイルとテンプレートを組み合わせて、文章中の 1 行記法を展開する Ruby ライブラリです。使い方は [README](README.md)、網羅的な変更履歴は [CHANGELOG](CHANGELOG.md) をご覧ください。
|
|
4
|
+
|
|
5
|
+
新しい版から順に並べています。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1.4.0 — 2026-08-07
|
|
10
|
+
|
|
11
|
+
**細かな不具合修正が中心のリリースです。** いずれも「著者が用意した原稿やデータが壊れている」という、実際に起こりうる場面で想定外の例外が漏れていた問題です。
|
|
12
|
+
|
|
13
|
+
- **空のデータファイルで落ちなくなりました。** 空 YAML は例外ではなく空配列として扱います。レコードでない中身(スカラーだけの YAML など)は、何が入っていたかを添えて `DataLoadError` で知らせます
|
|
14
|
+
- **壊れた文字が 1 バイト混ざっただけで展開が止まらなくなりました。** 読めない行は本文としてそのまま返します。gem が黙ってバイト列を書き換える(scrub する)設計は採っていません
|
|
15
|
+
- **`scan` に本文を渡しても落ちなくなりました。** パスと本文の両方を受け取る API なのに、引数を無条件でファイルとして開こうとしていました
|
|
16
|
+
- **`UnknownKeyError` が「使えるキーの一覧」と「直すべき雛形のパス」を持って届くようになりました。** これまでこの例外だけ属性が無く、キーの候補は gem が直接吐くログにしか出ていませんでした。`key_path` / `available_keys` / `template_path` / `location` を持たせ、**gem 内に最後まで残っていた `logger.error` を撤去**しています。呼び出し元は「もしかして `=title`?」のような案内を自前で組み立てられます
|
|
17
|
+
- **必要な Ruby を 4.0 以上から 3.4 以上へ緩めました**(コード変更はゼロ、gemspec 1 行のみ)
|
|
18
|
+
|
|
19
|
+
**破壊的変更が 1 つあります。** `QueryStream.logger` と `config.logger` を削除しました。上記の撤去で gem 内にロガーを読む箇所が無くなり、設定できるのに何も起きない項目になったためです。`config.logger = ...` と書いている箇所があれば、消してください(設定しても何も起きないので、消すだけで済みます)。`logger` gem への依存も外しました。
|
|
20
|
+
|
|
21
|
+
あわせて、これらを二度と起こさないための**堅牢性テスト 27 件**を `test/robustness/` に追加しました。守る約束は 1 つ、**gem の外へ出てよい例外は `QueryStream::Error` の系統だけ**です。呼び出し元は例外の型で扱いを分けるため、生の `ArgumentError` が出ると「どのファイルのどこが悪いか」を著者へ案内できません。上の 3 件は、このテストを書く過程で見つかりました。
|
|
22
|
+
|
|
23
|
+
ただし権限エラーだけは `Errno::EACCES` のまま伝えます。**環境の問題(`chmod` で直る)とデータの問題(中身を直す)は対処が違う**ためです。
|
|
24
|
+
|
|
25
|
+
## 1.3.0 — 2026-07-12
|
|
26
|
+
|
|
27
|
+
- **`post_render` 後段フィルタを追加しました。** 1 記法の展開結果を、コンテキスト付きで呼び出し元へ渡す汎用フックです。`context` には `source` / `data_file` / `data_dir` / `template_path` / `query` / `location` が入ります
|
|
28
|
+
- gem 自身は用途を規定しません。画像パス解決などの呼び出し元固有の後処理を、gem に固有知識を持ち込まずに差し込めます
|
|
29
|
+
|
|
30
|
+
## 1.2.2 — 2026-04-26
|
|
31
|
+
|
|
32
|
+
- **`physics_books` が `physic_books` になる単数形変換の誤りを修正しました。** アンダースコアで分割し、末尾から複数形セグメントを探して単数化します(`physics_books` → `physics_book`、`books_nested` → `book_nested`)
|
|
33
|
+
|
|
34
|
+
## 1.2.1 — 2026-04-20
|
|
35
|
+
|
|
36
|
+
- **データファイルの読み込みを `YAML.safe_load_file` へ移行しました(セキュリティ)。** `permitted_classes: [Symbol, Time, Date, DateTime]` を明示し、`!ruby/object:Kernel {}` のような Ruby オブジェクトタグを読ませて任意コード実行を狙う経路を塞いでいます。安全性が Psych のバージョンに依存しなくなりました
|
|
37
|
+
- **`DataLoadError` を追加しました。** `Psych::DisallowedClass` / `Psych::SyntaxError` / `JSON::ParserError` を、`file_path` と `cause_error` を持つ例外に変換します
|
|
38
|
+
|
|
39
|
+
## 1.2.0 — 2026-04-19
|
|
40
|
+
|
|
41
|
+
- **警告の `logger.warn` を全廃し、`on_warning:` コールバックで呼び出し元へ委譲しました。** `NoResultWarning` / `AmbiguousQueryWarning` に `query` / `location`(後者は `count` も)を持たせています
|
|
42
|
+
- これにより、`⚠️` の付け方も i18n も呼び出し元で決められるようになりました
|
|
43
|
+
|
|
44
|
+
> **補足**: このとき `TemplateCompiler` が `UnknownKeyError` を投げる経路にだけ `logger.error` が残っていました。1.4.0 で撤去し、**gem 内のログ出力はここで本当にゼロになりました**(設定項目 `config.logger` も同時に削除)。
|
|
45
|
+
|
|
46
|
+
## 1.1.0 — 2026-04-13
|
|
47
|
+
|
|
48
|
+
- **エラーを構造化例外へ移行しました。** `TemplateNotFoundError` に `template_path` / `query` / `location` / `hint`、`DataNotFoundError` に `expected_path` / `query` / `location` を持たせ、gem 内の `logger.error` を廃止しました
|
|
49
|
+
- **1 行の展開失敗で全体が止まる問題を修正しました。** 失敗した行は元の記法のまま残し、後続の展開を続けます。`on_error:` コールバックで通知します
|
|
50
|
+
- **`People`(大文字)が `person` に変換されない問題を修正しました**
|
|
51
|
+
|
|
52
|
+
## 1.0.0 — 2026-03-21
|
|
53
|
+
|
|
54
|
+
最初のメジャーリリースです。
|
|
55
|
+
|
|
56
|
+
- QueryStream 記法の完全実装(源泉・抽出条件・ソート・件数・スタイル)
|
|
57
|
+
- VFM フェンス記法対応(`:::{.book-card}` 〜 `:::`)。各レコードが個別のフェンスで囲まれて展開されます
|
|
58
|
+
- テンプレートコンパイラ(変数展開・画像記法・ネスト値アクセス・表記法)
|
|
59
|
+
- データリゾルバ(YAML / JSON の自動探索、複数形→単数形のテンプレート解決)
|
|
60
|
+
- フィルタリング(等値・AND / OR・比較演算子)とソート
|
|
61
|
+
- 設定システム(`data_dir` / `templates_dir` / `default_format` / `logger`)
|
data/lib/query_stream/command.rb
CHANGED
|
@@ -17,6 +17,7 @@ module QueryStream
|
|
|
17
17
|
|
|
18
18
|
options do
|
|
19
19
|
option '--version', 'Print version and exit'
|
|
20
|
+
option '--help', 'Print this message and exit'
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
# コマンド実行
|
|
@@ -28,12 +29,19 @@ module QueryStream
|
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
# 使い方を表示する。
|
|
33
|
+
#
|
|
34
|
+
# public かつ `output:` を受ける形でなければならない。Samovar は引数の解析に
|
|
35
|
+
# 失敗すると、外から `error.command.print_usage(output: output)` を呼んで
|
|
36
|
+
# 使い方を出そうとする。private に置いていた頃は、そこで
|
|
37
|
+
# `private method 'print_usage' called` の NoMethodError になり、
|
|
38
|
+
# **`--help` と未知の引数がスタックトレースで落ちていた**。
|
|
39
|
+
# @param output [IO] 出力先(Samovar からはエラー出力が渡される)
|
|
40
|
+
def print_usage(output: $stdout)
|
|
41
|
+
output.puts self.class.description
|
|
42
|
+
output.puts 'Usage: query-stream [options]'
|
|
43
|
+
output.puts ' --version Print version and exit'
|
|
44
|
+
output.puts ' --help Print this message and exit'
|
|
37
45
|
end
|
|
38
46
|
end
|
|
39
47
|
end
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'logger'
|
|
4
|
-
|
|
5
3
|
# ================================================================
|
|
6
4
|
# File: lib/query_stream/configuration.rb
|
|
7
5
|
# ================================================================
|
|
8
6
|
# 責務:
|
|
9
7
|
# QueryStream のグローバル設定を管理する。
|
|
10
|
-
# data_dir, templates_dir, default_format,
|
|
8
|
+
# data_dir, templates_dir, default_format, post_render の4項目。
|
|
9
|
+
#
|
|
10
|
+
# ログ出力の設定は持たない。gem 自身がログを出さないためである
|
|
11
|
+
# (失敗はすべて構造化例外として on_error / on_warning へ渡す)。
|
|
11
12
|
# ================================================================
|
|
12
13
|
|
|
13
14
|
module QueryStream
|
|
@@ -22,14 +23,16 @@ module QueryStream
|
|
|
22
23
|
# @return [Symbol] スタイル省略時のデフォルト出力形式(:md / :html / :json)
|
|
23
24
|
attr_accessor :default_format
|
|
24
25
|
|
|
25
|
-
#
|
|
26
|
-
|
|
26
|
+
# 1 記法の展開結果を、コンテキスト付きで呼び出し元へ通す後段フィルタ。
|
|
27
|
+
# gem 自身は用途を規定しない(画像パス解決など呼び出し元固有の後処理を担わせる)。
|
|
28
|
+
# @return [Proc, nil] (text, context) -> String。String 以外を返した場合は元の展開結果を採用する
|
|
29
|
+
attr_accessor :post_render
|
|
27
30
|
|
|
28
31
|
def initialize
|
|
29
32
|
@data_dir = 'data'
|
|
30
33
|
@templates_dir = 'templates'
|
|
31
34
|
@default_format = :md
|
|
32
|
-
@
|
|
35
|
+
@post_render = nil
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
end
|
|
@@ -77,8 +77,7 @@ module QueryStream
|
|
|
77
77
|
symbolize_names: true)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
records
|
|
81
|
-
records
|
|
80
|
+
normalize_records(records, file_path)
|
|
82
81
|
rescue Psych::DisallowedClass => e
|
|
83
82
|
raise DataLoadError.new(
|
|
84
83
|
"データファイルに許可されていないクラス/タグが含まれています: #{e.message} (#{file_path})",
|
|
@@ -96,6 +95,30 @@ module QueryStream
|
|
|
96
95
|
)
|
|
97
96
|
end
|
|
98
97
|
|
|
98
|
+
# 読み込み結果をレコード配列へ正規化する。
|
|
99
|
+
#
|
|
100
|
+
# **常に配列を返すのが本メソッドの契約である。** 空のデータファイルは YAML が
|
|
101
|
+
# `nil` を返すため、素通しすると呼び出し先(TemplateCompiler)が
|
|
102
|
+
# `undefined method 'any?' for nil` で落ちる——データを空にしただけの著者に
|
|
103
|
+
# NoMethodError を見せることになる。
|
|
104
|
+
#
|
|
105
|
+
# 配列でもハッシュでもない中身(先頭が文字列や数値だけの YAML)は、
|
|
106
|
+
# レコード群として解釈しようがないので DataLoadError で理由を伝える。
|
|
107
|
+
# @return [Array<Hash>]
|
|
108
|
+
def normalize_records(records, file_path)
|
|
109
|
+
case records
|
|
110
|
+
when nil then []
|
|
111
|
+
when Hash then [records]
|
|
112
|
+
when Array then records
|
|
113
|
+
else
|
|
114
|
+
raise DataLoadError.new(
|
|
115
|
+
"データファイルはレコードの配列(またはハッシュ 1 件)である必要があります" \
|
|
116
|
+
"(#{records.class} が入っています): #{file_path}",
|
|
117
|
+
file_path: file_path
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
99
122
|
# 指定名ですべての拡張子を試行する
|
|
100
123
|
# @param base_name [String] 拡張子なしファイル名
|
|
101
124
|
# @param data_dir [String] データディレクトリ
|
data/lib/query_stream/errors.rb
CHANGED
|
@@ -51,7 +51,32 @@ module QueryStream
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# テンプレートに、データレコードが持たないキーが書かれている
|
|
55
|
+
#
|
|
56
|
+
# 「どのキーが無いか」だけでなく「代わりに何が使えるか」「どのファイルを直すか」まで
|
|
57
|
+
# 属性で渡す。呼び出し元はこれを使って「もしかして =author?」のような案内を組み立てられる。
|
|
58
|
+
# 属性が無かった頃はこのクラスだけが gem 内で logger.error を呼んでおり、
|
|
59
|
+
# 利用できるキーの一覧を呼び出し元が受け取れなかった。
|
|
60
|
+
#
|
|
61
|
+
# location(記法の位置)と template_path(雛形の位置)は別物である。
|
|
62
|
+
# 直すのは雛形なので、記法の位置だけでは著者はファイルを開けない。
|
|
63
|
+
#
|
|
64
|
+
# @attr_reader key_path [String] テンプレートに書かれていたキー("author.name" 等)
|
|
65
|
+
# @attr_reader available_keys [Array<Symbol>] レコードが実際に持つキー
|
|
66
|
+
# @attr_reader template_path [String, nil] 打ち間違いがあるテンプレートのパス
|
|
67
|
+
# @attr_reader location [String] 記法が書かれていたソースファイル名と行番号
|
|
68
|
+
class UnknownKeyError < Error
|
|
69
|
+
attr_reader :key_path, :available_keys, :template_path, :location
|
|
70
|
+
|
|
71
|
+
def initialize(msg = nil, key_path: nil, available_keys: [], template_path: nil, location: nil)
|
|
72
|
+
super(msg || "テンプレートに存在しないキーが記述されています: #{key_path}")
|
|
73
|
+
@key_path = key_path
|
|
74
|
+
@available_keys = Array(available_keys)
|
|
75
|
+
@template_path = template_path
|
|
76
|
+
@location = location
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
55
80
|
class InvalidDateError < Error; end # 無効な日付
|
|
56
81
|
|
|
57
82
|
# データファイルの読み込みに失敗した
|
|
@@ -51,10 +51,11 @@ module QueryStream
|
|
|
51
51
|
# @param records [Array<Hash>] データレコード群
|
|
52
52
|
# @param source_filename [String, nil] エラー報告用ファイル名
|
|
53
53
|
# @param line_number [Integer, nil] エラー報告用行番号
|
|
54
|
+
# @param template_path [String, nil] エラー報告用テンプレートパス(キーの打ち間違いはここを直す)
|
|
54
55
|
# @return [String] 展開後のテキスト
|
|
55
|
-
def render(template, records, source_filename: nil, line_number: nil)
|
|
56
|
+
def render(template, records, source_filename: nil, line_number: nil, template_path: nil)
|
|
56
57
|
lines = template.lines
|
|
57
|
-
validate_template_keys!(lines, records.first, source_filename:, line_number:) if records.any?
|
|
58
|
+
validate_template_keys!(lines, records.first, source_filename:, line_number:, template_path:) if records.any?
|
|
58
59
|
|
|
59
60
|
parts = classify_lines(lines)
|
|
60
61
|
return '' if records.empty?
|
|
@@ -306,7 +307,8 @@ module QueryStream
|
|
|
306
307
|
# @param sample_record [Hash] サンプルレコード(最初の1件)
|
|
307
308
|
# @param source_filename [String, nil] エラー報告用ファイル名
|
|
308
309
|
# @param line_number [Integer, nil] エラー報告用行番号
|
|
309
|
-
|
|
310
|
+
# @param template_path [String, nil] エラー報告用テンプレートパス
|
|
311
|
+
def validate_template_keys!(lines, sample_record, source_filename: nil, line_number: nil, template_path: nil)
|
|
310
312
|
return unless sample_record
|
|
311
313
|
|
|
312
314
|
location = source_filename ? "#{source_filename}:#{line_number}" : ''
|
|
@@ -327,10 +329,7 @@ module QueryStream
|
|
|
327
329
|
line.scan(VARIABLE_PATTERN).each do |(key_path)|
|
|
328
330
|
root_key = key_path.split('.').first.to_sym
|
|
329
331
|
unless available_keys.include?(root_key)
|
|
330
|
-
|
|
331
|
-
QueryStream.logger.error("#{msg}(#{location})")
|
|
332
|
-
QueryStream.logger.error(" 利用可能なキー: #{available_keys.join(', ')}")
|
|
333
|
-
raise UnknownKeyError, msg
|
|
332
|
+
raise UnknownKeyError.new(key_path:, available_keys:, template_path:, location:)
|
|
334
333
|
end
|
|
335
334
|
end
|
|
336
335
|
|
|
@@ -341,10 +340,7 @@ module QueryStream
|
|
|
341
340
|
|
|
342
341
|
root_key = src.split('.').first.to_sym
|
|
343
342
|
unless available_keys.include?(root_key)
|
|
344
|
-
|
|
345
|
-
QueryStream.logger.error("#{msg}(#{location})")
|
|
346
|
-
QueryStream.logger.error(" 利用可能なキー: #{available_keys.join(', ')}")
|
|
347
|
-
raise UnknownKeyError, msg
|
|
343
|
+
raise UnknownKeyError.new(key_path: src, available_keys:, template_path:, location:)
|
|
348
344
|
end
|
|
349
345
|
end
|
|
350
346
|
end
|
data/lib/query_stream/version.rb
CHANGED
data/lib/query_stream.rb
CHANGED
|
@@ -29,6 +29,10 @@ module QueryStream
|
|
|
29
29
|
# 行頭 = の直後に英数字/ハイフン/アンダースコアのデータ名(スペースは任意)
|
|
30
30
|
QUERY_STREAM_PATTERN = /^=\s*([a-zA-Z][a-zA-Z0-9_-]*)(?:\s*\|.*)?$/
|
|
31
31
|
|
|
32
|
+
# これを超える長さの引数はパスとみなさず本文として扱う(scan の path/content 判定)。
|
|
33
|
+
# 主要な OS の PATH_MAX は 4096 前後で、それより長い「パス」は実在しない。
|
|
34
|
+
MAX_PATH_BYTES = 4096
|
|
35
|
+
|
|
32
36
|
class << self
|
|
33
37
|
# グローバル設定を返す
|
|
34
38
|
# @return [Configuration]
|
|
@@ -42,12 +46,6 @@ module QueryStream
|
|
|
42
46
|
yield(configuration)
|
|
43
47
|
end
|
|
44
48
|
|
|
45
|
-
# ロガーへのショートカット
|
|
46
|
-
# @return [Logger]
|
|
47
|
-
def logger
|
|
48
|
-
configuration.logger
|
|
49
|
-
end
|
|
50
|
-
|
|
51
49
|
# テキストコンテンツ内の QueryStream 記法をすべて展開する
|
|
52
50
|
# 1行の展開に失敗しても残りの行の処理を継続する。
|
|
53
51
|
# エラー情報は例外の属性として呼び出し元に委ねる(gem 内ではログ出力しない)。
|
|
@@ -57,10 +55,13 @@ module QueryStream
|
|
|
57
55
|
# @param templates_dir [String, nil] テンプレートディレクトリ(nil時はconfigを使用)
|
|
58
56
|
# @param on_error [Proc, nil] エラー時コールバック。|exception| を受け取る。省略時は何もしない。
|
|
59
57
|
# @param on_warning [Proc, nil] 警告時コールバック。|warning| を受け取る。省略時は何もしない。
|
|
58
|
+
# @param post_render [Proc, nil] 展開結果の後段フィルタ。(text, context) -> String。省略時は configuration.post_render を使う。
|
|
60
59
|
# @return [String] 展開後のテキストコンテンツ
|
|
61
|
-
def render(content, source_filename: nil, data_dir: nil, templates_dir: nil, on_error: nil, on_warning: nil
|
|
60
|
+
def render(content, source_filename: nil, data_dir: nil, templates_dir: nil, on_error: nil, on_warning: nil,
|
|
61
|
+
post_render: nil)
|
|
62
62
|
data_dir ||= configuration.data_dir
|
|
63
63
|
templates_dir ||= configuration.templates_dir
|
|
64
|
+
post_render ||= configuration.post_render
|
|
64
65
|
|
|
65
66
|
lines = content.lines
|
|
66
67
|
result = []
|
|
@@ -70,7 +71,7 @@ module QueryStream
|
|
|
70
71
|
line_number = idx + 1
|
|
71
72
|
|
|
72
73
|
# コードブロック内はスキップ
|
|
73
|
-
if
|
|
74
|
+
if code_fence?(line)
|
|
74
75
|
in_code_block = !in_code_block
|
|
75
76
|
result << line
|
|
76
77
|
next
|
|
@@ -82,10 +83,10 @@ module QueryStream
|
|
|
82
83
|
end
|
|
83
84
|
|
|
84
85
|
# QueryStream 記法の検出
|
|
85
|
-
if
|
|
86
|
+
if query_line?(line)
|
|
86
87
|
begin
|
|
87
88
|
expanded = render_query(
|
|
88
|
-
line.chomp, line_number:, source_filename:, data_dir:, templates_dir:, on_warning:
|
|
89
|
+
line.chomp, line_number:, source_filename:, data_dir:, templates_dir:, on_warning:, post_render:
|
|
89
90
|
)
|
|
90
91
|
result << expanded << "\n"
|
|
91
92
|
rescue Error => e
|
|
@@ -107,8 +108,10 @@ module QueryStream
|
|
|
107
108
|
# @param source_filename [String, nil] ソースファイル名
|
|
108
109
|
# @param data_dir [String, nil] データディレクトリ
|
|
109
110
|
# @param templates_dir [String, nil] テンプレートディレクトリ
|
|
111
|
+
# @param post_render [Proc, nil] 展開結果の後段フィルタ。(text, context) -> String
|
|
110
112
|
# @return [String] 展開後のテキスト
|
|
111
|
-
def render_query(query, line_number: nil, source_filename: nil, data_dir: nil, templates_dir: nil, on_warning: nil
|
|
113
|
+
def render_query(query, line_number: nil, source_filename: nil, data_dir: nil, templates_dir: nil, on_warning: nil,
|
|
114
|
+
post_render: nil)
|
|
112
115
|
data_dir ||= configuration.data_dir
|
|
113
116
|
templates_dir ||= configuration.templates_dir
|
|
114
117
|
location = source_filename ? "#{source_filename}:#{line_number}" : "行#{line_number}"
|
|
@@ -178,26 +181,42 @@ module QueryStream
|
|
|
178
181
|
template_content = File.read(template_path, encoding: 'utf-8')
|
|
179
182
|
|
|
180
183
|
# --- Phase: Render ---
|
|
181
|
-
TemplateCompiler.render(template_content, records, source_filename:, line_number:)
|
|
184
|
+
rendered = TemplateCompiler.render(template_content, records, source_filename:, line_number:, template_path:)
|
|
185
|
+
|
|
186
|
+
# --- Phase: Post-render filter ---
|
|
187
|
+
# 展開結果を呼び出し元の後段フィルタへ通す(画像パス解決など gem 外の後処理)。
|
|
188
|
+
# コールバックが String 以外を返した場合は安全側に倒して元の展開結果を採用する。
|
|
189
|
+
return rendered unless post_render
|
|
190
|
+
|
|
191
|
+
context = {
|
|
192
|
+
source: parsed[:source], # 記法に書かれた論理名(例: "physics_book")
|
|
193
|
+
data_file: data_file, # 単複解決後の実パス(例: "data/physics_books.yml")
|
|
194
|
+
data_dir: data_dir,
|
|
195
|
+
template_path: template_path,
|
|
196
|
+
query: query,
|
|
197
|
+
location: location # "filename:line" 形式
|
|
198
|
+
}
|
|
199
|
+
result = post_render.call(rendered, context)
|
|
200
|
+
result.is_a?(String) ? result : rendered
|
|
182
201
|
end
|
|
183
202
|
|
|
184
203
|
# テキスト内の QueryStream 記法を検出してリストを返す
|
|
185
204
|
# @param path_or_content [String] ファイルパスまたはテキストコンテンツ
|
|
186
205
|
# @return [Array<String>] 検出された QueryStream 記法のリスト
|
|
187
206
|
def scan(path_or_content)
|
|
188
|
-
content =
|
|
207
|
+
content = readable_file?(path_or_content) ? File.read(path_or_content, encoding: 'utf-8') : path_or_content
|
|
189
208
|
lines = content.lines
|
|
190
209
|
queries = []
|
|
191
210
|
in_code_block = false
|
|
192
211
|
|
|
193
212
|
lines.each do |line|
|
|
194
|
-
if
|
|
213
|
+
if code_fence?(line)
|
|
195
214
|
in_code_block = !in_code_block
|
|
196
215
|
next
|
|
197
216
|
end
|
|
198
217
|
next if in_code_block
|
|
199
218
|
|
|
200
|
-
queries << line.chomp if
|
|
219
|
+
queries << line.chomp if query_line?(line)
|
|
201
220
|
end
|
|
202
221
|
|
|
203
222
|
queries
|
|
@@ -205,6 +224,50 @@ module QueryStream
|
|
|
205
224
|
|
|
206
225
|
private
|
|
207
226
|
|
|
227
|
+
# その行を QueryStream 記法として扱うか。
|
|
228
|
+
#
|
|
229
|
+
# **不正な UTF-8 を含む行は記法として解釈しない。** `match?` はそういう行に
|
|
230
|
+
# `ArgumentError: invalid byte sequence in UTF-8` を投げるので、素通しにすると
|
|
231
|
+
# 原稿に 1 バイトの壊れが混ざっただけで展開が丸ごと止まる。
|
|
232
|
+
# ここでバイト列を書き換えて(scrub して)通す手もあるが採らない——gem が黙って
|
|
233
|
+
# 原稿のバイト列を変えると、著者が気付けない形で本文が変わる。
|
|
234
|
+
# 記法として読めない行は本文として、そのまま返すのが正しい。
|
|
235
|
+
#
|
|
236
|
+
# EncodingError と ArgumentError の両方を捕まえるのは意図的である。壊れたバイト列に
|
|
237
|
+
# Ruby がどちらを投げるかはメソッド次第で(match? / 先頭破損の lstrip は ArgumentError、
|
|
238
|
+
# strip は Encoding::CompatibilityError)、その差で展開が止まってはならない。
|
|
239
|
+
def query_line?(line)
|
|
240
|
+
line.match?(QUERY_STREAM_PATTERN)
|
|
241
|
+
rescue EncodingError, ArgumentError
|
|
242
|
+
false
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# コードフェンスの開始/終了行か。
|
|
246
|
+
# `lstrip` も不正な UTF-8 で ArgumentError を投げるので、query_line? と同じく
|
|
247
|
+
# 「読めない行は本文」へ倒す。ここを素通しにすると、記法判定だけ守っても
|
|
248
|
+
# 手前のフェンス判定で落ちる。
|
|
249
|
+
def code_fence?(line)
|
|
250
|
+
line.lstrip.start_with?('```')
|
|
251
|
+
rescue EncodingError, ArgumentError
|
|
252
|
+
false
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# 引数をファイルパスとして読むべきか。
|
|
256
|
+
#
|
|
257
|
+
# `scan` はパスと本文のどちらも受け取る API なので、本文をそのまま
|
|
258
|
+
# `File.exist?` へ渡すと壊れる——NUL バイトを含めば `ArgumentError`、
|
|
259
|
+
# ディレクトリなら「存在する」と判定されて `File.read` が `Errno::EISDIR` を出す。
|
|
260
|
+
# パスとして成立しうる形だけを先に選別し、判定自体の失敗も本文扱いに倒す。
|
|
261
|
+
def readable_file?(str)
|
|
262
|
+
s = str.to_s
|
|
263
|
+
return false if s.empty? || s.include?("\0") || s.include?("\n")
|
|
264
|
+
return false if s.bytesize > MAX_PATH_BYTES
|
|
265
|
+
|
|
266
|
+
File.file?(s)
|
|
267
|
+
rescue ArgumentError, SystemCallError
|
|
268
|
+
false
|
|
269
|
+
end
|
|
270
|
+
|
|
208
271
|
# テンプレートファイルパスを解決する
|
|
209
272
|
# @param singular_name [String] 単数形のデータ名
|
|
210
273
|
# @param style [String, nil] スタイル名
|
data/query-stream.gemspec
CHANGED
|
@@ -17,15 +17,16 @@ Gem::Specification.new do |spec|
|
|
|
17
17
|
spec.metadata['source_code_uri'] = 'https://github.com/Atelier-Mirai/query-stream'
|
|
18
18
|
spec.metadata['changelog_uri'] = 'https://github.com/Atelier-Mirai/query-stream/blob/master/CHANGELOG.md'
|
|
19
19
|
|
|
20
|
-
spec.required_ruby_version = '>= 4
|
|
20
|
+
spec.required_ruby_version = '>= 3.4'
|
|
21
21
|
|
|
22
|
-
spec.files = Dir.glob('{lib,bin}/**/*') +
|
|
22
|
+
spec.files = Dir.glob('{lib,bin}/**/*') +
|
|
23
|
+
%w[README.md CHANGELOG.md RELEASE_NOTE.md LICENSE Gemfile query-stream.gemspec]
|
|
23
24
|
spec.bindir = 'bin'
|
|
24
25
|
spec.executables = ['query-stream']
|
|
25
26
|
spec.require_paths = ['lib']
|
|
26
27
|
|
|
27
28
|
# Runtime dependencies
|
|
28
|
-
|
|
29
|
+
# logger への依存は 1.4.0 で外した。gem 自身はログを出さず、失敗は構造化例外で伝える。
|
|
29
30
|
spec.add_dependency 'samovar', '~> 2.1'
|
|
30
31
|
|
|
31
32
|
# Development dependencies
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: query-stream
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Atelier Mirai
|
|
@@ -9,20 +9,6 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: logger
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - ">="
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0'
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - ">="
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0'
|
|
26
12
|
- !ruby/object:Gem::Dependency
|
|
27
13
|
name: samovar
|
|
28
14
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -88,9 +74,11 @@ executables:
|
|
|
88
74
|
extensions: []
|
|
89
75
|
extra_rdoc_files: []
|
|
90
76
|
files:
|
|
77
|
+
- CHANGELOG.md
|
|
91
78
|
- Gemfile
|
|
92
79
|
- LICENSE
|
|
93
80
|
- README.md
|
|
81
|
+
- RELEASE_NOTE.md
|
|
94
82
|
- bin/query-stream
|
|
95
83
|
- lib/query_stream.rb
|
|
96
84
|
- lib/query_stream/command.rb
|
|
@@ -117,14 +105,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
117
105
|
requirements:
|
|
118
106
|
- - ">="
|
|
119
107
|
- !ruby/object:Gem::Version
|
|
120
|
-
version: '4
|
|
108
|
+
version: '3.4'
|
|
121
109
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
110
|
requirements:
|
|
123
111
|
- - ">="
|
|
124
112
|
- !ruby/object:Gem::Version
|
|
125
113
|
version: '0'
|
|
126
114
|
requirements: []
|
|
127
|
-
rubygems_version: 4.0.
|
|
115
|
+
rubygems_version: 4.0.16
|
|
128
116
|
specification_version: 4
|
|
129
117
|
summary: QueryStream - YAML/JSON data renderer with template expansion
|
|
130
118
|
test_files: []
|