query-stream 1.4.0 → 1.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 399e9fb8aea90ac45fa0d7837c9891c7db5785b2bd81f6976c697eaa1d92f35c
4
- data.tar.gz: 6587887f665b0a6ffde78dc029e2988bd647debcd92f0740766805b273f2ea61
3
+ metadata.gz: a2079c750ea911328b6974a0fbf39b5a4059583fb62a25c78ea4c3e24aad9a01
4
+ data.tar.gz: 9c8ae592f404a39291d57c7fa9772ae16c4e4aaba071d21905c8991db004ee17
5
5
  SHA512:
6
- metadata.gz: 47693f041e99aa59784a2d50a55fca8d1aa50c18798a16862a946a5c6ed1faedeafbc89a4fa2113d80fb793d2ef97d031a2e578ba64ab695f42e2ae3c946013a
7
- data.tar.gz: '095ed49dca369ea7420c037ddf95d77bbf2833b85d5b9a312393f31694632ee9744b6bfc319dfdabd55b5025fcd82f9921d0d2e80b3d988a407145e2902b3132'
6
+ metadata.gz: f71a3c87d420d35b80c40ea30f71df4abc4c9093db212daa40b0235584f3e218fb765637698c74da4f5ae84ba7b7349497162aaeb30e9439e2683667b574e49a
7
+ data.tar.gz: ff141bb1169aaa52b8c48b2df55b2b1dcc6cd6817bacdfc4c9443f06b21009d97561a7ed707aea95d0e951e50f3b51d068dbebe7ceea8a805ecf15cc6b2a6d94
data/CHANGELOG.md CHANGED
@@ -10,6 +10,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
  - パフォーマンス最適化
11
11
  - 追加のテンプレートスタイル
12
12
 
13
+ ## [Unreleased]
14
+
15
+ ## [1.6.0] - 2026-11-04
16
+
17
+ ### Added
18
+ - **テンプレートで配列の値を表示できるようにした** (`template_compiler.rb` / `configuration.rb`): `= tags` が `tags: [ruby, beginner]` のとき、`ruby、beginner` と出す(1.5 までは Ruby の表記 `["ruby", "beginner"]` が出ていた)。区切りは `config.list_separator`(既定は「、」)。空の要素は落とし、空の配列は値が無いのと同じに扱う(行・部品を消す)。カンマ区切りの文字列は書いたまま出す。
19
+ - **抽出条件と並べ替えで、ドットでつないだ項目名を使えるようにした** (`query_stream_parser.rb` / `filter_engine.rb`): `= books | author.name=高橋征義` や `= books | -author.name` のように、ネストした値で絞り込み、並べ替えられる。テンプレートの `= author.name` と同じ書き方である。項目名には数字も使える(`temp2>=20`)。
20
+
21
+ ### Fixed
22
+ - **テンプレートでハッシュの値を出すと、Ruby の表記がそのまま出ていた** (`template_compiler.rb` / `errors.rb`): `= author` で author が `{name:, bio:}` のとき、`{name: "高橋征義", bio: "…"}` が紙面に出た。どれをどう出すかは決められないので、新しい `NestedValueError` を投げ、その行は記法のまま残す。`available_keys` にハッシュの中のキーを入れ、`= author.name` への書き直しを案内できるようにした。ハッシュや配列を含む配列も同じエラーにする(`list: true`、書き直す先のキーは無い)。
23
+ - **読めない抽出条件が無視され、全件が出力されていた** (`query_stream_parser.rb` / `errors.rb`): `年=2019`(英字でない項目名)・`tags=`(値が空)・`ruby AND go`(項目名が無い)・`author.name=…`(1.5 まではドットを読めなかった)のような条件は、空の条件として捨てられ、エラーも警告も無いまま絞り込みが外れていた。新しい `InvalidQueryError`(`reason: :unparseable`)を投げ、その行は記法のまま残す。項目名を省いた節でも、演算子を含む節(`tags=ruby AND 年=2019` の `年=2019`)は値として扱わずエラーにする。並べ替えの項目名として読めない綴り(`-title foo`)も同じ。
24
+ - **どのレコードにも無い項目で絞り込むと 0 件、並べ替えると定義順のまま、何も知らせずに展開していた** (`filter_engine.rb`): `tag=ruby`(`tags` の打ち間違い)や `-yaer` が該当する。`InvalidQueryError`(`reason: :unknown_field`、`available_keys` 付き)を投げる。テンプレートのキーと同じく一番上の階層だけを見て、どれか 1 件にあれば正しい項目とみなす。
25
+
26
+ ## [1.5.0] - 2026-11-01
27
+
28
+ ### Added
29
+ - **値の無い項目の部分だけを消せるようにした** (`template_compiler.rb`): 変数が 2 つ以上ある行は、区切り(`/`・`・`・`、`・`,`・`,` と、前後に空白のある ` / `)と、変数を含む括弧(`(…)`・`(…)`)で部品に分け、値の無い変数を含む部品だけを、区切りを詰めて消す。`= author.name 著/= publisher/= year 年` は、発行年の分からない本でも `高橋征義 著/SBクリエイティブ` と出る(これまでは著者まで行ごと消えていた)。`= author.name 著(= year年)` も、発行年が無ければ `高橋征義 著` になる。部品が 1 つも残らなければ、今までどおり行ごと消す。すべての値がそろった行の出力は変わらない。
30
+ - 変数が 1 つだけの行と、表の行(`|` で始まる行)は今までどおり行ごと消す(セルを消すと列がずれる)。
31
+ - 行頭の Markdown の目印(`- `・`1. `・`> `・`# `)は、先頭の部品が消えても残す。
32
+ - 空白なしの半角の `/` は区切りにせず(URL を割らない)、`]` の直後の括弧(リンク先)も部品にしない。
33
+ - **抽出条件の数を、アンダースコアで桁区切りして書けるようにした** (`query_stream_parser.rb` / `filter_engine.rb`): `population>=9_000_000` や `population=900_0000..` のように書ける。Ruby の数値リテラルと同じく区切りは数字と数字のあいだにだけ置け、区切る位置は問わない(万の位で区切る `900_0000` も書ける)。比較(`>=` など)・範囲(`..`)・主キー検索に加え、等値(`=`)でも区切りを外して比べる。等値ではデータ側の値からも区切りを外すので、データに `"2024_01"` のような文字列があっても同じ綴りの条件で一致する。`snake_case` のような数でない値のアンダースコアはそのまま残す。
34
+ - **日付を和暦(`R06-01-01`)でも書けるようにした** (`filter_engine.rb`): データでも抽出条件でも、西暦の `2024-01-01` と同じ日付として比べ、並べ替える。元号は頭文字(M・T・S・H・R)で書き、年は 1 桁でもよい(`R6-01-01`)。等値(`=`)でも和暦と西暦を同じ日として扱う。変換は `Date.jisx0301` に任せた——`Date.parse` は `"R06-01-01"` を 2006-01-01 と読み違えるため使えない。元号の境目は検査しない(`H31-05-01` も 2019-05-01 として読む)。存在しない日(`R06-02-30`)は、西暦と同じく `InvalidDateError` になる。
35
+
36
+ - **抽出条件に `OR` / `or` を足した** (`query_stream_parser.rb` / `filter_engine.rb`): `tags=ruby OR year>=2020` のように、別の項目の条件もつなげる。AND は OR より強く結び付き、`a=1 AND b=2 OR c=3` は「a かつ b」または「c」になる。2 件目以降の項目名は省ける(`tags=ruby OR go`)。`||` は記法の区切りの `|` と衝突するので扱わない。前後に空白のある `OR` / `or` は区切りになるので、抽出条件の値に書けなくなった(`title=Rock or Pop`)。値だけの主キー検索(`= book | Rock or Pop`)は今までどおり 1 つの検索語として扱う。
37
+
38
+ ### Fixed
39
+ - **項目名を省いた AND が OR として動いていた** (`query_stream_parser.rb`): `tags=ruby AND beginner` は、`ruby` と `beginner` の両方を含むレコードを選ぶ仕様だが、値を前の条件の値リストへ足していたため、どちらかを含むレコードを選んでいた。別の条件として組み立てる。誤った動きを期待していたテストも直した。
40
+ - **比較とソートの対象の項目に値が無いレコードや比較できない値があると、`ArgumentError` が `render` の外へ出ていた** (`filter_engine.rb`): `on_error` にも渡らず、展開全体が止まった。比較では該当のレコードを除き、ソートでは最も多い種類の値を比べ、それ以外の値と値が無いレコードを昇順でも降順でも末尾に置く。
41
+ - **両端を比較できない範囲で `ArgumentError` が出ていた** (`query_stream_parser.rb` / `filter_engine.rb`): `year=2020..abc` は `Range` を作れない。文字列の値として扱い、どのレコードにも一致しない。
42
+ - **省ける項目が最初の 1 件に無いと、打ち間違いとして止まっていた** (`template_compiler.rb`): テンプレートのキーを確かめるとき、最初の 1 件のキーしか見ていなかった。1 冊目に `year` が無いだけで `UnknownKeyError` になり、値の無い項目を消す仕組みが働く前に止まった。どれか 1 件にあるキーは正しいキーとみなす。どの件にも無いキーは、今までどおり打ち間違いとして知らせる。
43
+ - **日付の範囲指定が 1 件も返さなかった** (`filter_engine.rb`): `date=2024-06-01..2024-06-30` のような範囲は、両端が文字列のまま比べられ、日付に直したデータの値を含まなかった。両端も比較用にそろえてから判定する。数の範囲は両端が最初から数なので影響を受けていなかった。
44
+ - **昇順ソートの書き方が README に無かった** (`README.md`): 例に並ぶのは降順(`-title`)だけで、昇順の `+title` はどこにも出てこない。実装は昇順を持っている——出口が塞がっていただけである。
45
+
46
+ 自然に思いつくのは符号を付けない `= books | title` のほうだが、これは**主キー検索**として解釈され、該当なしの警告とともに何も展開されない(`= book | 楽しいRuby` と綴りが同じで、符号の有無だけが見分ける手掛かりになっている)。警告は出るので黙って壊れはしないが、書いた本人は「ソートしたつもり」なので、読んで気づける場所に書いておく必要があった。例へ `+title` / `-year` を足し、符号が必須である理由も添えた。
47
+
48
+ - **一度も実行されない分岐が残っていた** (`query_stream_parser.rb`): `parse_sort` は符号を省いた `title` を昇順として扱う分岐(`/\A?(.+)\z/`)を持ち、コメントも受け付ける綴りとして `"title"` を挙げていた。しかし振り分け側(`parse_segment`)は `/\A[+-][a-zA-Z_]/` に一致した綴りしか渡さないため、**符号なしはここへ届かない**。
49
+
50
+ 素の名前を昇順にすると主キー検索と区別が付かなくなるので、**いまの仕様のほうが正しい**。分岐とコメントが古かったので、符号が必須であることを前提に書き直した。挙動は変えていない(`+` / `-` の両方を実測で確認)。
51
+
52
+ ### Changed
53
+ - **対応 Ruby を 3.4.10 / 4.0.9 にした**。4.0.9 は 2026-10-23 公開。
54
+ - **依存を最新へ上げた**: Minitest → 6.0.6、samovar 2.4.1 → 2.5.1、rake 13.3.1 → 13.4.2。gemspec の minitest は `~> 5.22` から `>= 5.22` へ緩めた——5 系に固定する理由が無く、6 でも 158 件すべて通る(`stub` / `mock` を使っていないため、6 での `minitest/mock` 分離の影響も受けない)。
55
+ - **rubocop を入れた**(`.rubocop.yml`・`rake rubocop`・CI)。見るのは `Lint` と `Security` だけで、引用符や行数は見ない。他の 2 つの gem と同じ構えである。現状 0 件。
56
+ - **README と CHANGELOG の「vivlio-style」を「Vivlio Starter」にした**。製品名を崩した綴りだった。
57
+
13
58
  ## [1.4.0] - 2026-08-07
14
59
 
15
60
  ### Removed
@@ -115,7 +160,7 @@ QueryStream 1.0.0 としてリリース!YAML/JSON データファイルとテ
115
160
  ### 📚 ドキュメント整備
116
161
  - 詳細な README と具体例
117
162
  - 完全な CHANGELOG
118
- - vivlio-style での実装例
163
+ - Vivlio Starter での実装例
119
164
 
120
165
  ### 🔧 技術的特徴
121
166
  - **Ruby 4.0+** モダン開発標準準拠
data/README.md CHANGED
@@ -38,17 +38,131 @@ result = QueryStream.render(source, data_dir: 'data', templates_dir: 'templates'
38
38
  ```
39
39
  = books # 全件展開
40
40
  = books | tags=ruby # タグで絞り込み
41
+ = books | +title # タイトルの昇順
42
+ = books | -year # 発行年の降順
41
43
  = books | tags=ruby | -title | 5 # 絞り込み+降順ソート+5件
42
- = book | 楽しいRuby # 主キーで一件検索
44
+ = prefectures | population>=9_000_000 # 数は _ で桁を区切れる(900_0000 も可)
45
+ = weather_reports | date>=R06-01-01 # 日付は西暦(2024-01-01)でも和暦でも書ける
46
+ = book | たのしいRuby # 主キーで一件検索
43
47
  = books | :full # fullスタイルで展開
44
48
  ```
45
49
 
46
- ### 具体例:vivlio-style での書籍データ展開
50
+ **ソートには符号を必ず付けてください**(`+` が昇順、`-` が降順)。符号を省いた
51
+ `= books | title` はソートではなく**主キー検索**として扱われ、該当なしの警告とともに
52
+ 何も展開されません。`= book | たのしいRuby` のような一件検索と綴りが同じになるため、
53
+ 符号の有無で見分けています。
54
+
55
+ ソートでは、最も多い種類の値(数・日付・文字列)を比べます。値が無いレコードと、それ以外の
56
+ 種類の値のレコードは、昇順でも降順でも末尾に置きます。
57
+
58
+ 日付は、データでも抽出条件でも `2024-01-01`(西暦)か `R06-01-01`(和暦)で書きます。
59
+ 和暦の元号は頭文字で書き(`M` 明治・`T` 大正・`S` 昭和・`H` 平成・`R` 令和)、年は 1 桁でも
60
+ 構いません(`R6-01-01`)。どちらで書いても同じ日付として比べ、並べ替えます。
61
+
62
+ ### 指定の順序
63
+
64
+ `|` で区切った指定は、省略しても、順序を入れ替えても構いません。形で種類を判別します。
65
+
66
+ | 形 | 種類 |
67
+ |---|---|
68
+ | `:full`・`:card.html` | スタイル |
69
+ | `+title`・`-title` | ソート |
70
+ | `tags=ruby`・`year>=2020` のように演算子を含むもの | 抽出条件 |
71
+ | 正の整数 | 件数 |
72
+ | それ以外の値 | 主キー検索 |
73
+
74
+ 単数形のデータ名に続く最初の指定は、数でも主キー検索として扱います(`= prefecture | 13`)。
75
+
76
+ ### 抽出条件
77
+
78
+ | 書き方 | 意味 |
79
+ |---|---|
80
+ | `tags=ruby`・`tags==ruby` | 等しい |
81
+ | `tags=ruby, go` | どちらかに等しい(OR) |
82
+ | `tags!=ruby` | 等しくない |
83
+ | `tags!=ruby, go` | どちらにも等しくない |
84
+ | `year>2020`・`year>=2020` | より大きい・以上 |
85
+ | `year<2020`・`year<=2020` | より小さい・以下 |
86
+ | `year=2020..2024` | 2020 以上 2024 以下 |
87
+ | `year=2020...2024` | 2020 以上 2024 未満 |
88
+ | `year=2020..` | 2020 以上 |
89
+ | `year=..2024` | 2024 以下 |
90
+ | `year=...2024` | 2024 未満 |
91
+ | `tags=ruby AND year>=2020` | 両方を満たす(`and`・`&&` も同じ) |
92
+ | `tags=ruby AND beginner` | `ruby` と `beginner` の両方を含む(2 件目以降の項目名は省ける) |
93
+ | `tags=ruby OR year>=2020` | どちらかを満たす(`or` も同じ) |
94
+ | `tags=ruby OR go` | どちらかを含む(2 件目以降の項目名は省ける) |
95
+
96
+ 等しいかどうかを調べるとき、データの値が配列やカンマ区切りの文字列なら、要素のどれかが一致すれば条件を満たします。`tags: [ruby, web]` は `tags=ruby` に一致します。
97
+
98
+ AND は OR より強く結び付きます。`tags=ruby AND year>=2020 OR tags=go` は、「`ruby` で 2020 年以降」または「`go`」を選びます。
99
+
100
+ 前後に空白のある `AND`・`and`・`OR`・`or`・`&&` は区切りとして扱うので、値には書けません。`||` は記法の区切りの `|` と重なるので使えません。
101
+
102
+ 比較(`>`・`>=`・`<`・`<=`)と範囲では、値が無いレコードと、比較できない値のレコード(数と比べるときの `不明` など)を除きます。
103
+
104
+ 項目名は半角英数字と `_` で書きます。`author.name` のようにドットでつなぐと、ネストした値で絞り込めます(並べ替えの `-author.name` も同じ)。
105
+
106
+ 条件として読めない指定(`年=2019`・`tags=`・項目名の無い `ruby AND go`)と、どのレコードにも無い項目(`tag=ruby` の打ち間違い)は `InvalidQueryError` になり、その行は展開されません。条件を無視して全件を出したり、0 件のまま気づかずに済ませたりはしません。項目があるかは、テンプレートのキーと同じく一番上の階層だけを見て、どれか 1 件にあれば正しい項目とみなします。
107
+
108
+ ### 一件検索
109
+
110
+ 値だけを書くと、主キーで検索します。主キーとして調べるのは `id`・`no`・`code`・`slug`・`name`・`title` です。
111
+
112
+ ```
113
+ = book | たのしいRuby # title が一致するレコード
114
+ = post | hello-world # slug が一致するレコード
115
+ ```
116
+
117
+ 一致するレコードが無ければ `NoResultWarning` を通知し、何も展開しません。複数あれば `AmbiguousQueryWarning` を通知し、一致したレコードをすべて展開します。
118
+
119
+ ### ファイルの探し方
120
+
121
+ データファイルは `data_dir` から次の順に探します。拡張子は `.yml`・`.yaml`・`.json` の順に試します。
122
+
123
+ 1. 書いたままの名前(`books`)
124
+ 2. 末尾に `s` を付けた名前(`book` → `books`)
125
+ 3. 単数形にした名前(`books` → `book`)
126
+
127
+ テンプレートは `templates_dir` にある、単数形の名前のファイルを使います。
128
+
129
+ | 記法 | テンプレート |
130
+ |---|---|
131
+ | `= books` | `_book.md` |
132
+ | `= books \| :full` | `_book.full.md` |
133
+ | `= books \| :card.html` | `_book.card.html` |
134
+
135
+ 拡張子を書かないときの形式は、`config.default_format` で変更できます。
136
+
137
+ ### テンプレートの書き方
138
+
139
+ | 書き方 | 意味 |
140
+ |---|---|
141
+ | `= key`・`=key` | キーの値を展開する |
142
+ | `= author.name` | ネストした値を展開する |
143
+ | `![](cover)`・`![](= cover)` | 画像のパスをキーの値で展開する |
144
+ | `![](logo.png)` | 拡張子のあるパスはそのまま出力する |
145
+
146
+ 値が配列(`tags: [ruby, beginner]`)なら、要素を `config.list_separator`(既定は「、」)でつないで出力します(`ruby、beginner`)。カンマ区切りの文字列(`tags: ruby, beginner`)は、抽出条件では配列と同じに扱いますが、表示では書いたまま出力します。空の配列は値が無いのと同じです。
147
+
148
+ 値がハッシュ(`author: {name: …, bio: …}`)や、ハッシュ・配列を含む配列のときは、どれをどう出すかを決められないので `NestedValueError` になります。`= author` は `= author.name` の書き間違いであることがほとんどなので、エラーの `available_keys` にハッシュの中のキーを入れて渡します。
149
+
150
+ 変数を含む行はレコードごとに繰り返し、含まない行は 1 回だけ出力します。表は次のように書くと、見出しが 1 回だけ出力されます。
151
+
152
+ ```markdown
153
+ | 書名 | 著者 |
154
+ |---|---|
155
+ | = title | = author.name |
156
+ ```
157
+
158
+ コードブロック(` ``` `)の中の記法は展開しません。
159
+
160
+ ### 具体例:Vivlio Starter での書籍データ展開
47
161
 
48
162
  #### データファイル例 (`data/books.yml`)
49
163
 
50
164
  ```yaml
51
- - title: 楽しいRuby
165
+ - title: たのしいRuby
52
166
  author:
53
167
  name: 高橋征義
54
168
  desc: Rubyを楽しく学べる入門書。
@@ -88,7 +202,7 @@ result = QueryStream.render(source, data_dir: 'data', templates_dir: 'templates'
88
202
 
89
203
  :::{.book-card}
90
204
  ![](ruby.webp)
91
- **楽しいRuby**
205
+ **たのしいRuby**
92
206
  Rubyを楽しく学べる入門書。
93
207
  :::
94
208
 
@@ -98,11 +212,55 @@ Rubyを楽しく学べる入門書。
98
212
  C言語の定番入門書。
99
213
  :::
100
214
 
215
+
101
216
  ---
102
217
  ```
103
218
 
104
219
  VFMフェンス記法(`:::{.book-card}`)にも対応しており、各レコードが個別のフェンスで囲まれて展開されます。
105
220
 
221
+ ### 値の無い項目
222
+
223
+ 値が無い(キーが無い・空の)項目を含む行は、行ごと出力しません。項目によって情報量の
224
+ 違うデータにも、同じテンプレートが使えます。
225
+
226
+ 1 行に変数が 2 つ以上ある行では、**値の無い項目の部分だけ**を消します。行は区切り
227
+ (`/`・`・`・`、`・`,`・`,` と、前後に空白のある ` / `)と、変数を含む括弧(`(…)`・`(…)`)で
228
+ 部品に分かれ、値の無い変数を含む部品だけが、区切りを詰めて消えます。変数に付いた言葉
229
+ (`著`・`年`)は、変数と一緒に残るか、一緒に消えます。
230
+
231
+ ```markdown
232
+ = author.name 著/= publisher/= year 年
233
+ = author.name 著(= year年)
234
+ ```
235
+
236
+ | データ | 1 行目 | 2 行目 |
237
+ |---|---|---|
238
+ | 全部ある | 高橋征義 著/SBクリエイティブ/2019 年 | 高橋征義 著(2019年) |
239
+ | 発行年が無い | 高橋征義 著/SBクリエイティブ | 高橋征義 著 |
240
+ | 全部無い | (行ごと消える) | (行ごと消える) |
241
+
242
+ - 行頭の箇条書きの `- ` や見出しの `# ` は、先頭の部品が消えても残ります。
243
+ - 表の行(`|` で始まる行)は部品に分けず、値の無い項目があれば行ごと消えます(セルを消すと列がずれるため)。
244
+ - 空白なしの半角の `/` と、リンク先の括弧(`[= title](= url)`)は区切りになりません。
245
+ - テンプレートのキーは、どれか 1 件のデータにあれば正しいキーとみなします。どの件にも無いキーは打ち間違いとして `UnknownKeyError` になります。
246
+
247
+ ## メソッド
248
+
249
+ | メソッド | 役割 |
250
+ |---|---|
251
+ | `QueryStream.render(text, **options)` | テキスト内の記法をすべて展開し、結果を返す |
252
+ | `QueryStream.render_query(query, **options)` | 1 つの記法を展開し、結果を返す |
253
+ | `QueryStream.scan(path_or_text)` | ファイルパスまたはテキストから記法を探し、一覧を返す |
254
+
255
+ ```ruby
256
+ QueryStream.render_query('= books | tags=ruby | :full', data_dir: 'data', templates_dir: 'templates')
257
+
258
+ QueryStream.scan('contents/05-references.md')
259
+ # => ["= books", "= books | :full"]
260
+ ```
261
+
262
+ `render_query` は `on_error` を受け取りません。展開に失敗すると、例外をそのまま投げます。`scan` はコードブロックの中の記法を含めません。
263
+
106
264
  ## 設定
107
265
 
108
266
  ```ruby
@@ -110,13 +268,14 @@ QueryStream.configure do |config|
110
268
  config.data_dir = 'data'
111
269
  config.templates_dir = 'templates'
112
270
  config.default_format = :md
271
+ config.list_separator = '、' # 配列の値をつなぐ区切り
113
272
  config.post_render = ->(text, context) { text }
114
273
  end
115
274
  ```
116
275
 
117
276
  ## エラー・警告の扱い
118
277
 
119
- **この gem はメッセージを組み立てません。** どう表示するか、どの言語で出すか、そもそもログに残すかは、すべて呼び出し元が決めます。そのため展開時の出来事は、構造化された例外としてコールバックへ渡されます。
278
+ **この gem はログを出力しません。** どう表示するか、どの言語で出すか、ログに残すかは、呼び出し元が決めます。そのため展開時の出来事は、構造化された例外としてコールバックへ渡されます。例外の `message` には日本語の既定の文が入っています。
120
279
 
121
280
  ```ruby
122
281
  QueryStream.render(
@@ -138,12 +297,15 @@ QueryStream.render(
138
297
  | `DataNotFoundError` | `expected_path`, `query`, `location` |
139
298
  | `DataLoadError` | `file_path`, `cause_error` |
140
299
  | `UnknownKeyError` | `key_path`, `available_keys`, `template_path`, `location` |
300
+ | `NestedValueError` | `key_path`, `available_keys`, `list`, `template_path`, `location` |
301
+ | `InvalidQueryError` | `reason`(`:unparseable`・`:unknown_field`), `condition`, `available_keys`, `query`, `location` |
302
+ | `InvalidDateError` | なし(`message` に日付を含みます) |
141
303
  | `NoResultWarning` | `query`, `location` |
142
304
  | `AmbiguousQueryWarning` | `query`, `location`, `count` |
143
305
 
144
- 前 4 つは `QueryStream::Error`、後 2 つは `QueryStream::Warning` を継承します。**gem の外へ出る例外はこの 2 系統だけ**です。壊れた YAML も、不正な UTF-8 を含む原稿も、生の `ArgumentError` や `NoMethodError` にはなりません。
306
+ 前 7 つは `QueryStream::Error`、後 2 つは `QueryStream::Warning` を継承します。**gem の外へ出る例外はこの 2 系統だけ**です。壊れた YAML も、不正な UTF-8 を含む原稿も、生の `ArgumentError` や `NoMethodError` にはなりません。
145
307
 
146
- たとえば `UnknownKeyError` は「使えるキーの一覧」と「直すべき雛形のパス」まで持って届くので、呼び出し元で打ち間違いの案内を組み立てられます。`location` は記法が書かれた原稿の位置、`template_path` は直す雛形の位置で、**直すのは雛形のほう**です。
308
+ たとえば `UnknownKeyError` は「使えるキーの一覧」と「直すべき雛形のパス」を属性に持つので、呼び出し元で打ち間違いの案内を組み立てられます。`location` は記法が書かれた原稿の位置、`template_path` は直す雛形の位置で、**直すのは雛形のほう**です。
147
309
 
148
310
  ```ruby
149
311
  on_error: lambda do |e|
data/RELEASE_NOTE.md CHANGED
@@ -6,6 +6,60 @@ YAML / JSON のデータファイルとテンプレートを組み合わせて
6
6
 
7
7
  ---
8
8
 
9
+ ## 1.6.0 — 2026-11-04
10
+
11
+ **抽出条件と並べ替えで、ネストした値を使えるようになりました。** テンプレートと同じく、項目名をドットでつなぎます。
12
+
13
+ ```
14
+ = books | author.name=高橋征義
15
+ = books | -author.name
16
+ ```
17
+
18
+ **テンプレートで配列の値を表示できるようになりました。** `tags: [ruby, beginner]` を `= tags` と書くと `ruby、beginner` と出ます(これまでは `["ruby", "beginner"]` と出ていました)。区切りは `config.list_separator` で変えられます。ハッシュの値(`= author` で author が `{name:, bio:}`)は出し方を決められないので、新しい `NestedValueError` になります。中のキーが `available_keys` に入るので、`= author.name` への書き直しを案内できます。
19
+
20
+ **使えない抽出条件を、エラーとして知らせるようになりました。** これまでは、読めない条件(`年=2019`・`tags=` など)は無視されて全件が出力され、どのレコードにも無い項目(`tag=ruby` の打ち間違い)では 0 件のまま展開されていました。どちらも新しい `InvalidQueryError` になり、その行は記法のまま残ります。`reason` が `:unparseable`(読めない)か `:unknown_field`(無い項目)かを示し、無い項目のときは `available_keys` に使える項目が入ります。
21
+
22
+ ---
23
+
24
+ ## 1.5.0 — 2026-11-01
25
+
26
+ **値の無い項目があっても、行ごと消さずに、その部分だけを消すようになりました。** 書誌のように一行へ複数の値を並べるテンプレートで、どれか一つが欠けると、これまでは行ごと消えていました。
27
+
28
+ ```
29
+ = author.name 著/= publisher/= year 年
30
+ ```
31
+
32
+ 発行年の分からない本でも、`高橋征義 著/SBクリエイティブ` と出ます(1.4 では著者と出版社まで消えていました)。区切り(`/`・`・`・`、` など)と、変数を含む括弧(`(= year 年)`)で行を部品に分け、値の無い部品だけを、区切りを詰めて消します。すべての値がそろった行の出力は変わりません。変数が一つだけの行と表の行は、これまでどおり行ごと消します(表のセルを消すと列がずれるためです)。
33
+
34
+ **抽出条件の書き方を三つ足しました。**
35
+
36
+ - **`OR` で条件をつなげます。** `tags=ruby OR year>=2020` のように、別の項目の条件も書けます。AND は OR より強く結び付きます。前後に空白のある `OR` / `or` は区切りになるので、`title=Rock or Pop` のような値は抽出条件に書けなくなりました
37
+ - **数をアンダースコアで桁区切りして書けます。** `population>=9_000_000` や、万の位で区切る `900_0000` も書けます。比較・範囲・主キー検索・等値のどれでも使えます
38
+ - **日付を和暦(`R06-01-01`)でも書けます。** データでも抽出条件でも、西暦の `2024-01-01` と同じ日として比べ、並べ替えます。元号は頭文字(M・T・S・H・R)で書き、年は 1 桁でも構いません
39
+
40
+ **昇順の並べ替えの書き方を README に追加しました。** 機能は最初からありましたが、README の例は降順(`-title`)だけでした。
41
+
42
+ ```
43
+ = books | +title # タイトルの昇順
44
+ = books | -year # 発行年の降順
45
+ ```
46
+
47
+ **符号は必ず付けてください。** 符号を省いた `= books | title` は、並べ替えではなく**一件検索**として解釈され、「該当なし」の警告を出して何も展開しません。`= book | 楽しいRuby` と同じ形なので、パーサーは符号の有無だけで両者を区別します。README にも理由を記載しました。
48
+
49
+ あわせて、内部に一度も実行されない分岐が残っていたのを整理しました。動きは変わりません。
50
+
51
+ - **対応 Ruby を 3.4.10 / 4.0.9 に更新しました**(4.0.9 は 2026 年 10 月 23 日公開)
52
+ - **Minitest を 6 系へ、samovar を 2.5.1 へ更新しました**
53
+ - **コード検査(rubocop)を導入しました。** 有効にしているのは `Lint` と `Security` の規則だけで、引用符の種類などの様式は検査しません
54
+ - README と CHANGELOG にあった「vivlio-style」という綴りを「Vivlio Starter」に直しました
55
+
56
+ ### 不具合の修正
57
+
58
+ - **項目名を省いた AND が OR として動いていたのを直しました。** `tags=ruby AND beginner` は、`ruby` と `beginner` の両方を含むレコードを選ぶ記法ですが、どちらかを含むレコードを選んでいました。この書き方を使っている場合は、結果の件数が変わります
59
+ - **比較とソートで `ArgumentError` が発生していたのを直しました。** 対象の項目に値が無いレコードや比較できない値のレコードが 1 件でもあると、例外が `render` の外へ出ていました。比較では該当のレコードを除き、ソートでは昇順でも降順でも末尾に置きます。`year=2020..abc` のように両端を比較できない範囲も、例外にならなくなりました
60
+ - **省ける項目が最初の 1 件に無いと、`UnknownKeyError` が発生していたのを直しました。** テンプレートのキーを最初の 1 件だけで確認していました。修正後は、どれか 1 件にあるキーを正しいキーとして扱い、どの件にも無いキーだけを `UnknownKeyError` にします
61
+ - **日付の範囲指定が 1 件も返さなかったのを直しました。** `date=2024-06-01..2024-06-30` のような範囲は、両端が文字列のまま比較され、どのデータにも一致しませんでした。数の範囲は影響を受けていません
62
+
9
63
  ## 1.4.0 — 2026-08-07
10
64
 
11
65
  **細かな不具合修正が中心のリリースです。** いずれも「著者が用意した原稿やデータが壊れている」という、実際に起こりうる場面で想定外の例外が漏れていた問題です。
@@ -2,8 +2,6 @@
2
2
 
3
3
  require 'samovar'
4
4
 
5
- # ================================================================
6
- # File: lib/query_stream/command.rb
7
5
  # ================================================================
8
6
  # 責務:
9
7
  # QueryStream の CLI コマンド。--version のみ提供する。
@@ -31,11 +29,8 @@ module QueryStream
31
29
 
32
30
  # 使い方を表示する。
33
31
  #
34
- # public かつ `output:` を受ける形でなければならない。Samovar は引数の解析に
35
- # 失敗すると、外から `error.command.print_usage(output: output)` を呼んで
36
- # 使い方を出そうとする。private に置いていた頃は、そこで
37
- # `private method 'print_usage' called` の NoMethodError になり、
38
- # **`--help` と未知の引数がスタックトレースで落ちていた**。
32
+ # public かつ `output:` を受ける形にする。Samovar は引数の解析に失敗すると、外から
33
+ # `error.command.print_usage(output: output)` を呼ぶ。
39
34
  # @param output [IO] 出力先(Samovar からはエラー出力が渡される)
40
35
  def print_usage(output: $stdout)
41
36
  output.puts self.class.description
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # ================================================================
4
- # File: lib/query_stream/configuration.rb
5
3
  # ================================================================
6
4
  # 責務:
7
5
  # QueryStream のグローバル設定を管理する。
8
- # data_dir, templates_dir, default_format, post_render の4項目。
6
+ # data_dir, templates_dir, default_format, list_separator, post_render の5項目。
9
7
  #
10
8
  # ログ出力の設定は持たない。gem 自身がログを出さないためである
11
9
  # (失敗はすべて構造化例外として on_error / on_warning へ渡す)。
@@ -23,6 +21,11 @@ module QueryStream
23
21
  # @return [Symbol] スタイル省略時のデフォルト出力形式(:md / :html / :json)
24
22
  attr_accessor :default_format
25
23
 
24
+ # テンプレートで配列の値(tags: [ruby, beginner])を表示するときの区切り。
25
+ # 既定は日本語の文章に合わせた「、」。英語の文章なら ", " にする。
26
+ # @return [String]
27
+ attr_accessor :list_separator
28
+
26
29
  # 1 記法の展開結果を、コンテキスト付きで呼び出し元へ通す後段フィルタ。
27
30
  # gem 自身は用途を規定しない(画像パス解決など呼び出し元固有の後処理を担わせる)。
28
31
  # @return [Proc, nil] (text, context) -> String。String 以外を返した場合は元の展開結果を採用する
@@ -32,6 +35,7 @@ module QueryStream
32
35
  @data_dir = 'data'
33
36
  @templates_dir = 'templates'
34
37
  @default_format = :md
38
+ @list_separator = "、"
35
39
  @post_render = nil
36
40
  end
37
41
  end
@@ -3,8 +3,6 @@
3
3
  require 'yaml'
4
4
  require 'json'
5
5
 
6
- # ================================================================
7
- # File: lib/query_stream/data_resolver.rb
8
6
  # ================================================================
9
7
  # 責務:
10
8
  # データファイルの探索・単数形/複数形の自動解決を行う。
@@ -97,10 +95,8 @@ module QueryStream
97
95
 
98
96
  # 読み込み結果をレコード配列へ正規化する。
99
97
  #
100
- # **常に配列を返すのが本メソッドの契約である。** 空のデータファイルは YAML が
101
- # `nil` を返すため、素通しすると呼び出し先(TemplateCompiler)が
102
- # `undefined method 'any?' for nil` で落ちる——データを空にしただけの著者に
103
- # NoMethodError を見せることになる。
98
+ # **常に配列を返す。** 空のデータファイルは YAML が `nil` を返すので、空配列に変換する
99
+ # (呼び出し先の TemplateCompiler は配列を前提にしている)。
104
100
  #
105
101
  # 配列でもハッシュでもない中身(先頭が文字列や数値だけの YAML)は、
106
102
  # レコード群として解釈しようがないので DataLoadError で理由を伝える。
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # ================================================================
4
- # File: lib/query_stream/errors.rb
5
3
  # ================================================================
6
4
  # 責務:
7
5
  # QueryStream の例外クラス体系を定義する。
@@ -55,8 +53,6 @@ module QueryStream
55
53
  #
56
54
  # 「どのキーが無いか」だけでなく「代わりに何が使えるか」「どのファイルを直すか」まで
57
55
  # 属性で渡す。呼び出し元はこれを使って「もしかして =author?」のような案内を組み立てられる。
58
- # 属性が無かった頃はこのクラスだけが gem 内で logger.error を呼んでおり、
59
- # 利用できるキーの一覧を呼び出し元が受け取れなかった。
60
56
  #
61
57
  # location(記法の位置)と template_path(雛形の位置)は別物である。
62
58
  # 直すのは雛形なので、記法の位置だけでは著者はファイルを開けない。
@@ -77,6 +73,74 @@ module QueryStream
77
73
  end
78
74
  end
79
75
 
76
+ # テンプレートで、そのままでは表示できない値(ハッシュ、またはハッシュや配列を含む配列)を出そうとした
77
+ #
78
+ # `= author` と書いて author が `{name:, bio:}` のとき、どれを出すかを gem は決められない。
79
+ # Ruby の表記({name: "…"})を紙面に出さず、直し方を示せるだけの素材を渡す。
80
+ # 文字列や数の配列は表示できる(list_separator でつなぐ)ので、このエラーにならない。
81
+ #
82
+ # @attr_reader key_path [String] テンプレートに書かれたキー("author" 等)
83
+ # @attr_reader available_keys [Array<Symbol>] ハッシュの中のキー(`= author.name` と書き直す候補)。配列のときは空
84
+ # @attr_reader list [Boolean] ハッシュや配列を含む配列か(書き直す先のキーが無い)
85
+ # @attr_reader template_path [String, nil] 直すテンプレートのパス
86
+ # @attr_reader location [String] 記法が書かれていたソースファイル名と行番号
87
+ class NestedValueError < Error
88
+ attr_reader :key_path, :available_keys, :list, :template_path, :location
89
+
90
+ def initialize(msg = nil, key_path: nil, available_keys: [], list: false, template_path: nil, location: nil)
91
+ super(msg || "そのままでは表示できない値です: #{key_path}")
92
+ @key_path = key_path
93
+ @available_keys = Array(available_keys)
94
+ @list = list
95
+ @template_path = template_path
96
+ @location = location
97
+ end
98
+
99
+ # テンプレートの位置と記法の位置を付け足して投げ直すときに使う(値を表示する箇所は位置を知らない)
100
+ def with_context(template_path:, location:)
101
+ self.class.new(message, key_path:, available_keys:, list:, template_path:, location:)
102
+ end
103
+ end
104
+
105
+ # 抽出条件・並べ替えの指定が使えない
106
+ #
107
+ # 条件を無視して全件を出すと、書いた本人は絞り込んだつもりのまま気づけない。
108
+ # 理由は 2 通りで、reason で見分ける。
109
+ # - :unparseable — 条件として読めない(`年=2019`・`tags=`・項目名の無い `ruby AND go`)
110
+ # - :unknown_field — どのレコードにも無い項目を指している(`tag=ruby` の打ち間違い)
111
+ #
112
+ # @attr_reader reason [Symbol] :unparseable / :unknown_field
113
+ # @attr_reader condition [String] 問題の条件(読めない綴り、または無い項目名)
114
+ # @attr_reader available_keys [Array<Symbol>] レコードが実際に持つキー(:unknown_field のとき)
115
+ # @attr_reader query [String] 元の QueryStream 記法
116
+ # @attr_reader location [String] ソースファイル名と行番号
117
+ class InvalidQueryError < Error
118
+ attr_reader :reason, :condition, :available_keys, :query, :location
119
+
120
+ def initialize(msg = nil, reason: :unparseable, condition: nil, available_keys: [], query: nil, location: nil)
121
+ super(msg || default_message(reason, condition))
122
+ @reason = reason
123
+ @condition = condition
124
+ @available_keys = Array(available_keys)
125
+ @query = query
126
+ @location = location
127
+ end
128
+
129
+ # 記法の位置を付け足して投げ直すときに使う(パーサーは位置を知らない)
130
+ def with_context(query:, location:)
131
+ self.class.new(message, reason:, condition:, available_keys:, query:, location:)
132
+ end
133
+
134
+ private
135
+
136
+ def default_message(reason, condition)
137
+ case reason
138
+ when :unknown_field then "どのレコードにも無い項目です: #{condition}"
139
+ else "抽出条件を読めません: #{condition}"
140
+ end
141
+ end
142
+ end
143
+
80
144
  class InvalidDateError < Error; end # 無効な日付
81
145
 
82
146
  # データファイルの読み込みに失敗した