@zonuexe/techbook-mcp 0.1.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.
Files changed (174) hide show
  1. package/.claude/settings.local.json +23 -0
  2. package/.github/workflows/test.yml +36 -0
  3. package/AGENTS.md +72 -0
  4. package/CLAUDE.md +2 -0
  5. package/LICENSE +661 -0
  6. package/README.md +154 -0
  7. package/dist/adapters/cache/memory-cache.d.ts +8 -0
  8. package/dist/adapters/cache/memory-cache.d.ts.map +1 -0
  9. package/dist/adapters/cache/memory-cache.js +23 -0
  10. package/dist/adapters/cache/memory-cache.js.map +1 -0
  11. package/dist/adapters/cache/null-cache.d.ts +8 -0
  12. package/dist/adapters/cache/null-cache.d.ts.map +1 -0
  13. package/dist/adapters/cache/null-cache.js +7 -0
  14. package/dist/adapters/cache/null-cache.js.map +1 -0
  15. package/dist/adapters/html/cheerio-parser.d.ts +5 -0
  16. package/dist/adapters/html/cheerio-parser.d.ts.map +1 -0
  17. package/dist/adapters/html/cheerio-parser.js +45 -0
  18. package/dist/adapters/html/cheerio-parser.js.map +1 -0
  19. package/dist/adapters/http/fetch-client.d.ts +6 -0
  20. package/dist/adapters/http/fetch-client.d.ts.map +1 -0
  21. package/dist/adapters/http/fetch-client.js +43 -0
  22. package/dist/adapters/http/fetch-client.js.map +1 -0
  23. package/dist/adapters/http/mock-client.d.ts +19 -0
  24. package/dist/adapters/http/mock-client.d.ts.map +1 -0
  25. package/dist/adapters/http/mock-client.js +59 -0
  26. package/dist/adapters/http/mock-client.js.map +1 -0
  27. package/dist/adapters/publishers/base.d.ts +24 -0
  28. package/dist/adapters/publishers/base.d.ts.map +1 -0
  29. package/dist/adapters/publishers/base.js +88 -0
  30. package/dist/adapters/publishers/base.js.map +1 -0
  31. package/dist/adapters/publishers/gihyo.d.ts +3 -0
  32. package/dist/adapters/publishers/gihyo.d.ts.map +1 -0
  33. package/dist/adapters/publishers/gihyo.js +75 -0
  34. package/dist/adapters/publishers/gihyo.js.map +1 -0
  35. package/dist/adapters/publishers/lambdanote.d.ts +3 -0
  36. package/dist/adapters/publishers/lambdanote.d.ts.map +1 -0
  37. package/dist/adapters/publishers/lambdanote.js +113 -0
  38. package/dist/adapters/publishers/lambdanote.js.map +1 -0
  39. package/dist/adapters/publishers/registry.d.ts +3 -0
  40. package/dist/adapters/publishers/registry.d.ts.map +1 -0
  41. package/dist/adapters/publishers/registry.js +11 -0
  42. package/dist/adapters/publishers/registry.js.map +1 -0
  43. package/dist/adapters/publishers/tatsu-zine.d.ts +3 -0
  44. package/dist/adapters/publishers/tatsu-zine.d.ts.map +1 -0
  45. package/dist/adapters/publishers/tatsu-zine.js +110 -0
  46. package/dist/adapters/publishers/tatsu-zine.js.map +1 -0
  47. package/dist/adapters/publishers/techbookfest.d.ts +3 -0
  48. package/dist/adapters/publishers/techbookfest.d.ts.map +1 -0
  49. package/dist/adapters/publishers/techbookfest.js +134 -0
  50. package/dist/adapters/publishers/techbookfest.js.map +1 -0
  51. package/dist/application/get-book-detail.d.ts +4 -0
  52. package/dist/application/get-book-detail.d.ts.map +1 -0
  53. package/dist/application/get-book-detail.js +9 -0
  54. package/dist/application/get-book-detail.js.map +1 -0
  55. package/dist/application/search-books.d.ts +11 -0
  56. package/dist/application/search-books.d.ts.map +1 -0
  57. package/dist/application/search-books.js +23 -0
  58. package/dist/application/search-books.js.map +1 -0
  59. package/dist/domain/book.d.ts +32 -0
  60. package/dist/domain/book.d.ts.map +1 -0
  61. package/dist/domain/book.js +2 -0
  62. package/dist/domain/book.js.map +1 -0
  63. package/dist/domain/publisher.d.ts +17 -0
  64. package/dist/domain/publisher.d.ts.map +1 -0
  65. package/dist/domain/publisher.js +2 -0
  66. package/dist/domain/publisher.js.map +1 -0
  67. package/dist/main.d.ts +2 -0
  68. package/dist/main.d.ts.map +1 -0
  69. package/dist/main.js +12 -0
  70. package/dist/main.js.map +1 -0
  71. package/dist/mcp/server.d.ts +5 -0
  72. package/dist/mcp/server.d.ts.map +1 -0
  73. package/dist/mcp/server.js +79 -0
  74. package/dist/mcp/server.js.map +1 -0
  75. package/dist/mcp/tools.d.ts +47 -0
  76. package/dist/mcp/tools.d.ts.map +1 -0
  77. package/dist/mcp/tools.js +53 -0
  78. package/dist/mcp/tools.js.map +1 -0
  79. package/dist/ports/cache.d.ts +6 -0
  80. package/dist/ports/cache.d.ts.map +1 -0
  81. package/dist/ports/cache.js +2 -0
  82. package/dist/ports/cache.js.map +1 -0
  83. package/dist/ports/html-parser.d.ts +14 -0
  84. package/dist/ports/html-parser.d.ts.map +1 -0
  85. package/dist/ports/html-parser.js +2 -0
  86. package/dist/ports/html-parser.js.map +1 -0
  87. package/dist/ports/http.d.ts +16 -0
  88. package/dist/ports/http.d.ts.map +1 -0
  89. package/dist/ports/http.js +2 -0
  90. package/dist/ports/http.js.map +1 -0
  91. package/docs/design-doc.md +365 -0
  92. package/flake.nix +50 -0
  93. package/package.json +29 -0
  94. package/src/adapters/cache/memory-cache.ts +31 -0
  95. package/src/adapters/cache/null-cache.ts +8 -0
  96. package/src/adapters/html/cheerio-parser.ts +49 -0
  97. package/src/adapters/http/fetch-client.ts +47 -0
  98. package/src/adapters/http/mock-client.ts +77 -0
  99. package/src/adapters/publishers/base.ts +129 -0
  100. package/src/adapters/publishers/book-tech.ts +117 -0
  101. package/src/adapters/publishers/born-digital.ts +158 -0
  102. package/src/adapters/publishers/coronasha.ts +139 -0
  103. package/src/adapters/publishers/gihyo.ts +120 -0
  104. package/src/adapters/publishers/lambdanote.ts +146 -0
  105. package/src/adapters/publishers/manatee.ts +112 -0
  106. package/src/adapters/publishers/maruzen-publishing.ts +141 -0
  107. package/src/adapters/publishers/optronics.ts +113 -0
  108. package/src/adapters/publishers/oreilly-japan.ts +138 -0
  109. package/src/adapters/publishers/peaks.ts +98 -0
  110. package/src/adapters/publishers/personal-media.ts +168 -0
  111. package/src/adapters/publishers/registry.ts +36 -0
  112. package/src/adapters/publishers/rutles.ts +161 -0
  113. package/src/adapters/publishers/saiensu.ts +149 -0
  114. package/src/adapters/publishers/seshop.ts +121 -0
  115. package/src/adapters/publishers/tatsu-zine.ts +129 -0
  116. package/src/adapters/publishers/techbookfest.ts +179 -0
  117. package/src/application/get-book-detail.ts +17 -0
  118. package/src/application/search-books.ts +39 -0
  119. package/src/domain/book.ts +35 -0
  120. package/src/domain/publisher.ts +18 -0
  121. package/src/main.ts +13 -0
  122. package/src/mcp/server.ts +103 -0
  123. package/src/mcp/tools.ts +54 -0
  124. package/src/ports/cache.ts +5 -0
  125. package/src/ports/html-parser.ts +15 -0
  126. package/src/ports/http.ts +17 -0
  127. package/tests/fixtures/book-tech-detail.html +51 -0
  128. package/tests/fixtures/book-tech-search.html +91 -0
  129. package/tests/fixtures/born-digital-detail.html +62 -0
  130. package/tests/fixtures/born-digital-search.html +51 -0
  131. package/tests/fixtures/coronasha-detail.html +41 -0
  132. package/tests/fixtures/coronasha-search.html +61 -0
  133. package/tests/fixtures/gihyo-detail.html +42 -0
  134. package/tests/fixtures/gihyo-search.json +54 -0
  135. package/tests/fixtures/lambdanote-search.html +66 -0
  136. package/tests/fixtures/manatee-detail.html +53 -0
  137. package/tests/fixtures/manatee-search.html +59 -0
  138. package/tests/fixtures/maruzen-detail.html +51 -0
  139. package/tests/fixtures/maruzen-search.html +60 -0
  140. package/tests/fixtures/optronics-detail.html +30 -0
  141. package/tests/fixtures/optronics-search.html +75 -0
  142. package/tests/fixtures/oreilly-detail.html +52 -0
  143. package/tests/fixtures/oreilly-ebook-list.html +53 -0
  144. package/tests/fixtures/peaks-detail.html +39 -0
  145. package/tests/fixtures/peaks-top.html +50 -0
  146. package/tests/fixtures/personal-media-detail.html +32 -0
  147. package/tests/fixtures/personal-media-search.html +39 -0
  148. package/tests/fixtures/rutles-detail.html +32 -0
  149. package/tests/fixtures/rutles-search.html +62 -0
  150. package/tests/fixtures/saiensu-detail.html +41 -0
  151. package/tests/fixtures/saiensu-search.html +65 -0
  152. package/tests/fixtures/seshop-detail.html +45 -0
  153. package/tests/fixtures/seshop-search.html +58 -0
  154. package/tests/fixtures/tatsu-zine-detail-free.html +22 -0
  155. package/tests/fixtures/tatsu-zine-search.html +24 -0
  156. package/tests/fixtures/techbookfest-search.json +73 -0
  157. package/tests/unit/adapters/publishers/book-tech.test.ts +183 -0
  158. package/tests/unit/adapters/publishers/born-digital.test.ts +191 -0
  159. package/tests/unit/adapters/publishers/coronasha.test.ts +201 -0
  160. package/tests/unit/adapters/publishers/gihyo.test.ts +135 -0
  161. package/tests/unit/adapters/publishers/lambdanote.test.ts +84 -0
  162. package/tests/unit/adapters/publishers/manatee.test.ts +163 -0
  163. package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +177 -0
  164. package/tests/unit/adapters/publishers/optronics.test.ts +205 -0
  165. package/tests/unit/adapters/publishers/oreilly-japan.test.ts +191 -0
  166. package/tests/unit/adapters/publishers/peaks.test.ts +174 -0
  167. package/tests/unit/adapters/publishers/personal-media.test.ts +196 -0
  168. package/tests/unit/adapters/publishers/rutles.test.ts +170 -0
  169. package/tests/unit/adapters/publishers/saiensu.test.ts +167 -0
  170. package/tests/unit/adapters/publishers/seshop.test.ts +171 -0
  171. package/tests/unit/adapters/publishers/tatsu-zine.test.ts +130 -0
  172. package/tests/unit/adapters/publishers/techbookfest.test.ts +93 -0
  173. package/tsconfig.json +17 -0
  174. package/vitest.config.ts +8 -0
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # techbook-mcp
2
+
3
+ [![CI](https://github.com/zonuexe/techbook-mcp/actions/workflows/test.yml/badge.svg)](https://github.com/zonuexe/techbook-mcp/actions/workflows/test.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@zonuexe/techbook-mcp)](https://www.npmjs.com/package/@zonuexe/techbook-mcp)
5
+ [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
6
+
7
+ 日本語技術書の書誌情報を出版社公式サイト・APIから取得する [MCP](https://modelcontextprotocol.io/) サーバー。
8
+
9
+ Claude などの AI アシスタントから、複数の出版社を横断して技術書を検索したり、書籍の詳細情報(著者・価格・ISBN・電子書籍ストアなど)を取得できます。
10
+
11
+ ## 使用例
12
+
13
+ ### 技術書を検索する
14
+
15
+ > 「Rustの入門書を探して」
16
+
17
+ Claude が `search_books` ツールを呼び出し、対応する全出版社から「Rust」を含む書籍を一覧で返します。
18
+
19
+ ```json
20
+ {
21
+ "books": [
22
+ {
23
+ "title": "実践Rustプログラミング入門",
24
+ "authors": ["山口聖弘", "吉川哲史", "扇谷陽", "尾崎嶺", "豊田優貴", "中村謙弘", "フレイム"],
25
+ "publisher": "秀和システム",
26
+ "publishedAt": "2020-08-22",
27
+ "isbn": "9784798061702",
28
+ "url": "https://www.seshop.com/product/detail/22342",
29
+ "price": 3740,
30
+ "ebookStores": [
31
+ {
32
+ "name": "SEshop",
33
+ "url": "https://www.seshop.com/product/detail/22342",
34
+ "drm": "social",
35
+ "drmLabel": "DRMフリー (ソーシャル)"
36
+ }
37
+ ]
38
+ }
39
+ ]
40
+ }
41
+ ```
42
+
43
+ ### 特定の出版社だけを検索する
44
+
45
+ > 「技術評論社のDocker本を調べて」
46
+
47
+ ```json
48
+ // search_books({ title: "Docker", publisher: "gihyo" })
49
+ ```
50
+
51
+ ### 書籍の詳細情報を取得する
52
+
53
+ > 「この URL の本の詳細を教えて: https://gihyo.jp/book/2024/978-4-297-14000-6」
54
+
55
+ Claude が `get_book_detail` ツールで著者・価格・ISBN・購入可能なストア情報をまとめて取得します。
56
+
57
+ ### 対応出版社を確認する
58
+
59
+ > 「どの出版社に対応していますか?」
60
+
61
+ Claude が `list_publishers` ツールを呼び出し、出版社名と ID の一覧を返します。
62
+
63
+ ## 対応出版社
64
+
65
+ | 出版社 | ID |
66
+ |--------|-----|
67
+ | BOOK TECH | `book-tech` |
68
+ | ボーンデジタル | `born-digital` |
69
+ | コロナ社 | `coronasha` |
70
+ | 技術評論社 | `gihyo` |
71
+ | ラムダノート | `lambdanote` |
72
+ | マナティ(マイナビ出版直販) | `manatee` |
73
+ | 丸善出版 | `maruzen-publishing` |
74
+ | オプトロニクス社 | `optronics` |
75
+ | オライリー・ジャパン | `oreilly-japan` |
76
+ | PEAKS | `peaks` |
77
+ | パーソナルメディア | `personal-media` |
78
+ | ラトルズ | `rutles` |
79
+ | サイエンス社 | `saiensu` |
80
+ | SEshop(翔泳社) | `seshop` |
81
+ | 達人出版会 | `tatsu-zine` |
82
+ | 技術書典オンラインマーケット | `techbookfest` |
83
+
84
+ ## MCPツール
85
+
86
+ ### `search_books`
87
+
88
+ 書名・著者名で複数の出版社を横断検索します。
89
+
90
+ | パラメータ | 型 | 説明 |
91
+ |------------|-----|------|
92
+ | `title` | string | 書名(部分一致) |
93
+ | `author` | string | 著者名(部分一致) |
94
+ | `publisher` | string | 出版社ID(省略時は全社検索) |
95
+ | `limit` | number | 1出版社あたりの最大件数(デフォルト: 10、最大: 50) |
96
+
97
+ ### `get_book_detail`
98
+
99
+ 書籍の公式ページ URL から詳細な書誌情報を取得します。
100
+
101
+ | パラメータ | 型 | 説明 |
102
+ |------------|-----|------|
103
+ | `url` | string | 書籍の公式ページ URL |
104
+
105
+ ### `list_publishers`
106
+
107
+ 対応出版社の一覧と ID を返します。
108
+
109
+ ## セットアップ
110
+
111
+ ### Claude Desktop
112
+
113
+ `claude_desktop_config.json` に以下を追加してください。
114
+
115
+ ```json
116
+ {
117
+ "mcpServers": {
118
+ "techbook-mcp": {
119
+ "command": "npx",
120
+ "args": ["-y", "@zonuexe/techbook-mcp"]
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ 設定ファイルの場所:
127
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
128
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
129
+
130
+ ### Claude Code
131
+
132
+ ```bash
133
+ claude mcp add techbook-mcp -- npx -y @zonuexe/techbook-mcp
134
+ ```
135
+
136
+ ## 開発
137
+
138
+ ```bash
139
+ npm install
140
+ npm test # ユニットテスト実行(Vitest)
141
+ npm run build # TypeScript コンパイル → dist/
142
+ ```
143
+
144
+ ### ランタイム
145
+
146
+ | ランタイム | 起動方法 |
147
+ |----------|---------|
148
+ | Node.js 22+ | `node dist/main.js` |
149
+ | Bun | `bun src/main.ts` |
150
+ | Deno | `deno run --allow-net src/main.ts` |
151
+
152
+ ## ライセンス
153
+
154
+ AGPL-3.0-or-later
@@ -0,0 +1,8 @@
1
+ import type { CacheStore } from "../../ports/cache.js";
2
+ export declare class MemoryCacheStore implements CacheStore {
3
+ private readonly store;
4
+ get(key: string): Promise<string | null>;
5
+ set(key: string, value: string, ttlSeconds?: number): Promise<void>;
6
+ delete(key: string): Promise<void>;
7
+ }
8
+ //# sourceMappingURL=memory-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-cache.d.ts","sourceRoot":"","sources":["../../../src/adapters/cache/memory-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAOvD,qBAAa,gBAAiB,YAAW,UAAU;IACjD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IAEjD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUxC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOnE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGzC"}
@@ -0,0 +1,23 @@
1
+ export class MemoryCacheStore {
2
+ store = new Map();
3
+ async get(key) {
4
+ const entry = this.store.get(key);
5
+ if (!entry)
6
+ return null;
7
+ if (entry.expiresAt !== undefined && Date.now() > entry.expiresAt) {
8
+ this.store.delete(key);
9
+ return null;
10
+ }
11
+ return entry.value;
12
+ }
13
+ async set(key, value, ttlSeconds) {
14
+ this.store.set(key, {
15
+ value,
16
+ expiresAt: ttlSeconds !== undefined ? Date.now() + ttlSeconds * 1000 : undefined,
17
+ });
18
+ }
19
+ async delete(key) {
20
+ this.store.delete(key);
21
+ }
22
+ }
23
+ //# sourceMappingURL=memory-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-cache.js","sourceRoot":"","sources":["../../../src/adapters/cache/memory-cache.ts"],"names":[],"mappings":"AAOA,MAAM,OAAO,gBAAgB;IACV,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAEvD,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;YAClE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAa,EAAE,UAAmB;QACvD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE;YAClB,KAAK;YACL,SAAS,EAAE,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS;SACjF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ import type { CacheStore } from "../../ports/cache.js";
2
+ /** テスト・デバッグ用。キャッシュを一切行わない。 */
3
+ export declare class NullCacheStore implements CacheStore {
4
+ get(_key: string): Promise<null>;
5
+ set(_key: string, _value: string, _ttlSeconds?: number): Promise<void>;
6
+ delete(_key: string): Promise<void>;
7
+ }
8
+ //# sourceMappingURL=null-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"null-cache.d.ts","sourceRoot":"","sources":["../../../src/adapters/cache/null-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,8BAA8B;AAC9B,qBAAa,cAAe,YAAW,UAAU;IACzC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAChC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IACtE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAC1C"}
@@ -0,0 +1,7 @@
1
+ /** テスト・デバッグ用。キャッシュを一切行わない。 */
2
+ export class NullCacheStore {
3
+ async get(_key) { return null; }
4
+ async set(_key, _value, _ttlSeconds) { }
5
+ async delete(_key) { }
6
+ }
7
+ //# sourceMappingURL=null-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"null-cache.js","sourceRoot":"","sources":["../../../src/adapters/cache/null-cache.ts"],"names":[],"mappings":"AAEA,8BAA8B;AAC9B,MAAM,OAAO,cAAc;IACzB,KAAK,CAAC,GAAG,CAAC,IAAY,IAAmB,OAAO,IAAI,CAAC,CAAC,CAAC;IACvD,KAAK,CAAC,GAAG,CAAC,IAAY,EAAE,MAAc,EAAE,WAAoB,IAAkB,CAAC;IAC/E,KAAK,CAAC,MAAM,CAAC,IAAY,IAAkB,CAAC;CAC7C"}
@@ -0,0 +1,5 @@
1
+ import type { HtmlParser, HtmlDocument } from "../../ports/html-parser.js";
2
+ export declare class CheerioHtmlParser implements HtmlParser {
3
+ parse(html: string): HtmlDocument;
4
+ }
5
+ //# sourceMappingURL=cheerio-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cheerio-parser.d.ts","sourceRoot":"","sources":["../../../src/adapters/html/cheerio-parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAe,MAAM,4BAA4B,CAAC;AA0CxF,qBAAa,iBAAkB,YAAW,UAAU;IAClD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY;CAIlC"}
@@ -0,0 +1,45 @@
1
+ import * as cheerio from "cheerio";
2
+ class CheerioElement {
3
+ $;
4
+ el;
5
+ constructor($, el) {
6
+ this.$ = $;
7
+ this.el = el;
8
+ }
9
+ text() {
10
+ return this.$(this.el).text().trim();
11
+ }
12
+ html() {
13
+ return this.$(this.el).html();
14
+ }
15
+ attr(name) {
16
+ return this.$(this.el).attr(name);
17
+ }
18
+ find(selector) {
19
+ return this.$(this.el)
20
+ .find(selector)
21
+ .toArray()
22
+ .map(el => new CheerioElement(this.$, el));
23
+ }
24
+ }
25
+ class CheerioDocument {
26
+ $;
27
+ constructor($) {
28
+ this.$ = $;
29
+ }
30
+ select(selector) {
31
+ return this.$(selector)
32
+ .toArray()
33
+ .map(el => new CheerioElement(this.$, el));
34
+ }
35
+ selectOne(selector) {
36
+ return this.select(selector)[0] ?? null;
37
+ }
38
+ }
39
+ export class CheerioHtmlParser {
40
+ parse(html) {
41
+ const $ = cheerio.load(html);
42
+ return new CheerioDocument($);
43
+ }
44
+ }
45
+ //# sourceMappingURL=cheerio-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cheerio-parser.js","sourceRoot":"","sources":["../../../src/adapters/html/cheerio-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAGnC,MAAM,cAAc;IAEC;IACA;IAFnB,YACmB,CAAqB,EACrB,EAAmB;QADnB,MAAC,GAAD,CAAC,CAAoB;QACrB,OAAE,GAAF,EAAE,CAAiB;IACnC,CAAC;IAEJ,IAAI;QACF,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;IACvC,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,CAAC,IAAY;QACf,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,IAAI,CAAC,QAAgB;QACnB,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;aACnB,IAAI,CAAC,QAAQ,CAAC;aACd,OAAO,EAAE;aACT,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC/C,CAAC;CACF;AAED,MAAM,eAAe;IACU;IAA7B,YAA6B,CAAqB;QAArB,MAAC,GAAD,CAAC,CAAoB;IAAG,CAAC;IAEtD,MAAM,CAAC,QAAgB;QACrB,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;aACpB,OAAO,EAAE;aACT,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,SAAS,CAAC,QAAgB;QACxB,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAC1C,CAAC;CACF;AAED,MAAM,OAAO,iBAAiB;IAC5B,KAAK,CAAC,IAAY;QAChB,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7B,OAAO,IAAI,eAAe,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ import type { HttpClient, RequestOptions, HttpResponse } from "../../ports/http.js";
2
+ export declare class FetchHttpClient implements HttpClient {
3
+ get(url: string, options?: RequestOptions): Promise<HttpResponse>;
4
+ post(url: string, body: string, options?: RequestOptions): Promise<HttpResponse>;
5
+ }
6
+ //# sourceMappingURL=fetch-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-client.d.ts","sourceRoot":"","sources":["../../../src/adapters/http/fetch-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAsBpF,qBAAa,eAAgB,YAAW,UAAU;IAC1C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAWjE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;CAYvF"}
@@ -0,0 +1,43 @@
1
+ class FetchHttpResponse {
2
+ response;
3
+ constructor(response) {
4
+ this.response = response;
5
+ }
6
+ get status() {
7
+ return this.response.status;
8
+ }
9
+ get url() {
10
+ return this.response.url;
11
+ }
12
+ text() {
13
+ return this.response.text();
14
+ }
15
+ header(name) {
16
+ return this.response.headers.get(name);
17
+ }
18
+ }
19
+ export class FetchHttpClient {
20
+ async get(url, options) {
21
+ const init = {
22
+ headers: options?.headers,
23
+ };
24
+ if (options?.timeout !== undefined) {
25
+ init.signal = AbortSignal.timeout(options.timeout);
26
+ }
27
+ const response = await fetch(url, init);
28
+ return new FetchHttpResponse(response);
29
+ }
30
+ async post(url, body, options) {
31
+ const init = {
32
+ method: "POST",
33
+ body,
34
+ headers: { "Content-Type": "application/json", ...options?.headers },
35
+ };
36
+ if (options?.timeout !== undefined) {
37
+ init.signal = AbortSignal.timeout(options.timeout);
38
+ }
39
+ const response = await fetch(url, init);
40
+ return new FetchHttpResponse(response);
41
+ }
42
+ }
43
+ //# sourceMappingURL=fetch-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-client.js","sourceRoot":"","sources":["../../../src/adapters/http/fetch-client.ts"],"names":[],"mappings":"AAEA,MAAM,iBAAiB;IACQ;IAA7B,YAA6B,QAAkB;QAAlB,aAAQ,GAAR,QAAQ,CAAU;IAAG,CAAC;IAEnD,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;IAC3B,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC9B,CAAC;IAED,MAAM,CAAC,IAAY;QACjB,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC;CACF;AAED,MAAM,OAAO,eAAe;IAC1B,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,OAAwB;QAC7C,MAAM,IAAI,GAAgB;YACxB,OAAO,EAAE,OAAO,EAAE,OAAO;SAC1B,CAAC;QACF,IAAI,OAAO,EAAE,OAAO,KAAK,SAAS,EAAE,CAAC;YACnC,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACrD,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACxC,OAAO,IAAI,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,IAAY,EAAE,OAAwB;QAC5D,MAAM,IAAI,GAAgB;YACxB,MAAM,EAAE,MAAM;YACd,IAAI;YACJ,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE;SACrE,CAAC;QACF,IAAI,OAAO,EAAE,OAAO,KAAK,SAAS,EAAE,CAAC;YACnC,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACrD,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACxC,OAAO,IAAI,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;CACF"}
@@ -0,0 +1,19 @@
1
+ import type { HttpClient, RequestOptions, HttpResponse } from "../../ports/http.js";
2
+ export interface MockResponseData {
3
+ status: number;
4
+ body: string;
5
+ headers?: Record<string, string>;
6
+ }
7
+ export declare class MockHttpClient implements HttpClient {
8
+ private readonly handlers;
9
+ private readonly postHandlers;
10
+ private readonly _calls;
11
+ /** GET: URL の前方一致でレスポンスを登録する */
12
+ addResponse(urlPrefix: string, data: MockResponseData): this;
13
+ /** POST: URL の前方一致でレスポンスを登録する */
14
+ addPostResponse(urlPrefix: string, data: MockResponseData): this;
15
+ get calls(): readonly string[];
16
+ get(url: string, _options?: RequestOptions): Promise<HttpResponse>;
17
+ post(url: string, _body: string, _options?: RequestOptions): Promise<HttpResponse>;
18
+ }
19
+ //# sourceMappingURL=mock-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-client.d.ts","sourceRoot":"","sources":["../../../src/adapters/http/mock-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEpF,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAgBD,qBAAa,cAAe,YAAW,UAAU;IAC/C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuC;IAChE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAuC;IACpE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IAEvC,gCAAgC;IAChC,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAK5D,iCAAiC;IACjC,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAKhE,IAAI,KAAK,IAAI,SAAS,MAAM,EAAE,CAE7B;IAEK,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAkBlE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;CAezF"}
@@ -0,0 +1,59 @@
1
+ class MockHttpResponse {
2
+ data;
3
+ requestUrl;
4
+ constructor(data, requestUrl) {
5
+ this.data = data;
6
+ this.requestUrl = requestUrl;
7
+ }
8
+ get status() { return this.data.status; }
9
+ get url() { return this.requestUrl; }
10
+ async text() { return this.data.body; }
11
+ header(name) {
12
+ return this.data.headers?.[name.toLowerCase()] ?? null;
13
+ }
14
+ }
15
+ export class MockHttpClient {
16
+ handlers = new Map();
17
+ postHandlers = new Map();
18
+ _calls = [];
19
+ /** GET: URL の前方一致でレスポンスを登録する */
20
+ addResponse(urlPrefix, data) {
21
+ this.handlers.set(urlPrefix, data);
22
+ return this;
23
+ }
24
+ /** POST: URL の前方一致でレスポンスを登録する */
25
+ addPostResponse(urlPrefix, data) {
26
+ this.postHandlers.set(urlPrefix, data);
27
+ return this;
28
+ }
29
+ get calls() {
30
+ return this._calls;
31
+ }
32
+ async get(url, _options) {
33
+ this._calls.push(url);
34
+ // 完全一致を優先
35
+ if (this.handlers.has(url)) {
36
+ return new MockHttpResponse(this.handlers.get(url), url);
37
+ }
38
+ // 前方一致
39
+ for (const [prefix, data] of this.handlers) {
40
+ if (url.startsWith(prefix)) {
41
+ return new MockHttpResponse(data, url);
42
+ }
43
+ }
44
+ throw new Error(`MockHttpClient: no handler for GET: ${url}`);
45
+ }
46
+ async post(url, _body, _options) {
47
+ this._calls.push(url);
48
+ if (this.postHandlers.has(url)) {
49
+ return new MockHttpResponse(this.postHandlers.get(url), url);
50
+ }
51
+ for (const [prefix, data] of this.postHandlers) {
52
+ if (url.startsWith(prefix)) {
53
+ return new MockHttpResponse(data, url);
54
+ }
55
+ }
56
+ throw new Error(`MockHttpClient: no handler for POST: ${url}`);
57
+ }
58
+ }
59
+ //# sourceMappingURL=mock-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-client.js","sourceRoot":"","sources":["../../../src/adapters/http/mock-client.ts"],"names":[],"mappings":"AAQA,MAAM,gBAAgB;IAED;IACA;IAFnB,YACmB,IAAsB,EACtB,UAAkB;QADlB,SAAI,GAAJ,IAAI,CAAkB;QACtB,eAAU,GAAV,UAAU,CAAQ;IAClC,CAAC;IAEJ,IAAI,MAAM,KAAa,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IACjD,IAAI,GAAG,KAAa,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;IAC7C,KAAK,CAAC,IAAI,KAAsB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACxD,MAAM,CAAC,IAAY;QACjB,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,IAAI,CAAC;IACzD,CAAC;CACF;AAED,MAAM,OAAO,cAAc;IACR,QAAQ,GAAG,IAAI,GAAG,EAA4B,CAAC;IAC/C,YAAY,GAAG,IAAI,GAAG,EAA4B,CAAC;IACnD,MAAM,GAAa,EAAE,CAAC;IAEvC,gCAAgC;IAChC,WAAW,CAAC,SAAiB,EAAE,IAAsB;QACnD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,iCAAiC;IACjC,eAAe,CAAC,SAAiB,EAAE,IAAsB;QACvD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACvC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,QAAyB;QAC9C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEtB,UAAU;QACV,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,OAAO,IAAI,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAE,EAAE,GAAG,CAAC,CAAC;QAC5D,CAAC;QAED,OAAO;QACP,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC3C,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3B,OAAO,IAAI,gBAAgB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACzC,CAAC;QACH,CAAC;QAED,MAAM,IAAI,KAAK,CAAC,uCAAuC,GAAG,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,KAAa,EAAE,QAAyB;QAC9D,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEtB,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/B,OAAO,IAAI,gBAAgB,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAE,EAAE,GAAG,CAAC,CAAC;QAChE,CAAC;QAED,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YAC/C,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3B,OAAO,IAAI,gBAAgB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACzC,CAAC;QACH,CAAC;QAED,MAAM,IAAI,KAAK,CAAC,wCAAwC,GAAG,EAAE,CAAC,CAAC;IACjE,CAAC;CACF"}
@@ -0,0 +1,24 @@
1
+ import type { PublisherDeps } from "../../domain/publisher.js";
2
+ import type { EbookStore } from "../../domain/book.js";
3
+ import type { HtmlDocument } from "../../ports/html-parser.js";
4
+ export declare const CACHE_TTL_SECONDS = 3600;
5
+ export declare function fetchText(url: string, deps: PublisherDeps): Promise<string>;
6
+ /** HTMLタグを除去する(gihyo APIのauthorフィールドのruby markup除去に使用) */
7
+ export declare function stripHtmlTags(html: string): string;
8
+ /** "¥3,960" や "3,300円(税込)" などから整数値を取り出す */
9
+ export declare function parseJapanesePrice(text: string): number | undefined;
10
+ /** 相対URLを絶対URLに解決する */
11
+ export declare function resolveUrl(base: string, path: string): string;
12
+ /**
13
+ * HTMLテキストから Amazon ASIN を抽出する。
14
+ * amazon.co.jp/dp/{ASIN}, /gp/product/{ASIN}, /o/ASIN/{ASIN} 形式に対応。
15
+ */
16
+ export declare function extractAsin(html: string): string | undefined;
17
+ /** URLから電子書籍ストア情報を返す。未知のストアは null。 */
18
+ export declare function classifyEbookStore(url: string): EbookStore | null;
19
+ /**
20
+ * HTMLドキュメント内の全リンクを走査して電子書籍ストアを抽出する。
21
+ * 同一ストアのURLが複数あれば最初の1件のみ返す。
22
+ */
23
+ export declare function extractEbookStoresFromDoc(doc: HtmlDocument): EbookStore[];
24
+ //# sourceMappingURL=base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../src/adapters/publishers/base.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAW,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAQ/D,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAYjF;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,2CAA2C;AAC3C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAInE;AAED,uBAAuB;AACvB,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG5D;AA6BD,sCAAsC;AACtC,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAOjE;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,YAAY,GAAG,UAAU,EAAE,CAgBzE"}
@@ -0,0 +1,88 @@
1
+ const DEFAULT_HEADERS = {
2
+ "User-Agent": "techbook-mcp/0.1.0 (+https://github.com/zonuexe/techbook-mcp; bibliographic search bot)",
3
+ "Accept": "text/html,application/xhtml+xml,application/json",
4
+ "Accept-Language": "ja,en;q=0.9",
5
+ };
6
+ export const CACHE_TTL_SECONDS = 3600; // 1時間
7
+ export async function fetchText(url, deps) {
8
+ const cached = await deps.cache.get(url);
9
+ if (cached !== null)
10
+ return cached;
11
+ const response = await deps.http.get(url, { headers: DEFAULT_HEADERS });
12
+ if (response.status !== 200) {
13
+ throw new Error(`HTTP ${response.status}: ${url}`);
14
+ }
15
+ const text = await response.text();
16
+ await deps.cache.set(url, text, CACHE_TTL_SECONDS);
17
+ return text;
18
+ }
19
+ /** HTMLタグを除去する(gihyo APIのauthorフィールドのruby markup除去に使用) */
20
+ export function stripHtmlTags(html) {
21
+ return html.replace(/<[^>]+>/g, "");
22
+ }
23
+ /** "¥3,960" や "3,300円(税込)" などから整数値を取り出す */
24
+ export function parseJapanesePrice(text) {
25
+ const match = text.match(/[\d,]+/);
26
+ if (!match)
27
+ return undefined;
28
+ return parseInt(match[0].replace(/,/g, ""), 10);
29
+ }
30
+ /** 相対URLを絶対URLに解決する */
31
+ export function resolveUrl(base, path) {
32
+ return new URL(path, base).toString();
33
+ }
34
+ /**
35
+ * HTMLテキストから Amazon ASIN を抽出する。
36
+ * amazon.co.jp/dp/{ASIN}, /gp/product/{ASIN}, /o/ASIN/{ASIN} 形式に対応。
37
+ */
38
+ export function extractAsin(html) {
39
+ const match = html.match(/amazon\.co\.jp\/(?:dp|gp\/product|o\/ASIN)\/([A-Z0-9]{10})/);
40
+ return match?.[1];
41
+ }
42
+ const EBOOK_STORE_PATTERNS = [
43
+ // DRM-free
44
+ { pattern: /techbookfest\.org\/product\//, name: "技術書典", drm: "free" },
45
+ { pattern: /gihyo\.jp\/dp\/ebook\//, name: "Gihyo Digital Publishing", drm: "social" },
46
+ // ソーシャルDRM (購入時生成IDまたは購入者情報を透かし刻印、技術的制限なし)
47
+ { pattern: /www\.lambdanote\.com\/products\//, name: "ラムダノート", drm: "social" },
48
+ { pattern: /tatsu-zine\.com\/books\/(?!pub\/)/, name: "達人出版会", drm: "social" },
49
+ // ソーシャルDRM (購入者情報透かし入りPDF、技術的制限なし)
50
+ { pattern: /book\.impress\.co\.jp\/books\//, name: "インプレスブックス", drm: "social" },
51
+ // DRM-attached
52
+ { pattern: /amazon\.co\.jp/, name: "Kindle", drm: "drm" },
53
+ { pattern: /books\.rakuten\.co\.jp|rakuten\.kobo\.com|kobo\.com/, name: "楽天Kobo", drm: "drm" },
54
+ { pattern: /booklive\.jp/, name: "BookLive", drm: "drm" },
55
+ { pattern: /honto\.jp/, name: "honto", drm: "drm" },
56
+ { pattern: /bookwalker\.jp/, name: "BOOK☆WALKER", drm: "drm" },
57
+ { pattern: /ebookjapan\.yahoo\.co\.jp/, name: "eBookJapan", drm: "drm" },
58
+ { pattern: /store\.line\.me/, name: "LINEマンガ", drm: "drm" },
59
+ ];
60
+ /** URLから電子書籍ストア情報を返す。未知のストアは null。 */
61
+ export function classifyEbookStore(url) {
62
+ for (const { pattern, name, drm } of EBOOK_STORE_PATTERNS) {
63
+ if (pattern.test(url)) {
64
+ return { name, url, drm };
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ /**
70
+ * HTMLドキュメント内の全リンクを走査して電子書籍ストアを抽出する。
71
+ * 同一ストアのURLが複数あれば最初の1件のみ返す。
72
+ */
73
+ export function extractEbookStoresFromDoc(doc) {
74
+ const stores = [];
75
+ const seenNames = new Set();
76
+ for (const link of doc.select("a[href]")) {
77
+ const href = link.attr("href");
78
+ if (!href)
79
+ continue;
80
+ const store = classifyEbookStore(href);
81
+ if (store && !seenNames.has(store.name)) {
82
+ seenNames.add(store.name);
83
+ stores.push(store);
84
+ }
85
+ }
86
+ return stores;
87
+ }
88
+ //# sourceMappingURL=base.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.js","sourceRoot":"","sources":["../../../src/adapters/publishers/base.ts"],"names":[],"mappings":"AAIA,MAAM,eAAe,GAAG;IACtB,YAAY,EAAE,yFAAyF;IACvG,QAAQ,EAAE,kDAAkD;IAC5D,iBAAiB,EAAE,aAAa;CACjC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC,CAAC,MAAM;AAE7C,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,IAAmB;IAC9D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAEnC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IACxE,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC;IACnD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AACtC,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AAClD,CAAC;AAED,uBAAuB;AACvB,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,IAAY;IACnD,OAAO,IAAI,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;AACxC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,4DAA4D,CAAC,CAAC;IACvF,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAUD,MAAM,oBAAoB,GAAmB;IAC3C,WAAW;IACX,EAAE,OAAO,EAAE,8BAA8B,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE;IACtE,EAAE,OAAO,EAAE,wBAAwB,EAAE,IAAI,EAAE,0BAA0B,EAAE,GAAG,EAAE,QAAQ,EAAE;IACtF,2CAA2C;IAC3C,EAAE,OAAO,EAAE,kCAAkC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE;IAC9E,EAAE,OAAO,EAAE,mCAAmC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE;IAC9E,mCAAmC;IACnC,EAAE,OAAO,EAAE,gCAAgC,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,EAAE;IAC/E,eAAe;IACf,EAAE,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,EAAE;IACzD,EAAE,OAAO,EAAE,qDAAqD,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,EAAE;IAC9F,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,EAAE;IACzD,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE;IACnD,EAAE,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,KAAK,EAAE;IAC9D,EAAE,OAAO,EAAE,2BAA2B,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,KAAK,EAAE;IACxE,EAAE,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE;CAC5D,CAAC;AAEF,sCAAsC;AACtC,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,oBAAoB,EAAE,CAAC;QAC1D,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAiB;IACzD,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IAEpC,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI;YAAE,SAAS;QAEpB,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,KAAK,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACxC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { PublisherAdapter } from "../../domain/publisher.js";
2
+ export declare const gihyoAdapter: PublisherAdapter;
3
+ //# sourceMappingURL=gihyo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gihyo.d.ts","sourceRoot":"","sources":["../../../src/adapters/publishers/gihyo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAiB,MAAM,2BAA2B,CAAC;AAwEjF,eAAO,MAAM,YAAY,EAAE,gBA+C1B,CAAC"}
@@ -0,0 +1,75 @@
1
+ import { fetchText, stripHtmlTags, resolveUrl, extractAsin, extractEbookStoresFromDoc } from "./base.js";
2
+ const BASE_URL = "https://gihyo.jp";
3
+ // --- 変換ヘルパー ---
4
+ function parseAuthors(authorField) {
5
+ return Object.values(authorField).flatMap(roleEntries => Object.keys(roleEntries).map(name => stripHtmlTags(name).trim()));
6
+ }
7
+ function parseReleaseDate(release) {
8
+ const raw = release[0];
9
+ if (!raw)
10
+ return undefined;
11
+ // "2025.9.29" → "2025-09-29"
12
+ const parts = raw.split(".");
13
+ if (parts.length !== 3)
14
+ return raw;
15
+ const [y, m, d] = parts;
16
+ return `${y}-${m.padStart(2, "0")}-${d.padStart(2, "0")}`;
17
+ }
18
+ function entryToBookRecord(isbn, entry) {
19
+ const title = entry.subtitle
20
+ ? `${entry.title} ${entry.subtitle}`
21
+ : entry.title;
22
+ return {
23
+ title,
24
+ authors: parseAuthors(entry.author),
25
+ publisher: "技術評論社",
26
+ publishedAt: parseReleaseDate(entry.release),
27
+ isbn: isbn.replace(/-/g, ""),
28
+ url: resolveUrl(BASE_URL, entry.url),
29
+ price: entry.price[0] > 0 ? entry.price[0] : undefined,
30
+ coverImageUrl: entry.cover[3] ? resolveUrl(BASE_URL, entry.cover[3]) : undefined,
31
+ };
32
+ }
33
+ // --- アダプター実装 ---
34
+ export const gihyoAdapter = {
35
+ id: "gihyo",
36
+ name: "技術評論社",
37
+ baseUrl: BASE_URL,
38
+ async search(query, deps) {
39
+ const word = [query.title, query.author].filter(Boolean).join(" ");
40
+ if (!word)
41
+ return [];
42
+ const limit = query.limit ?? 10;
43
+ const url = `${BASE_URL}/api_gh/site/search?search=${encodeURIComponent(word)}&limit=${limit}`;
44
+ const text = await fetchText(url, deps);
45
+ const data = JSON.parse(text);
46
+ return Object.entries(data.list).map(([isbn, entry]) => entryToBookRecord(isbn, entry));
47
+ },
48
+ async getDetail(url, deps) {
49
+ // URL例: https://gihyo.jp/book/2022/978-4-297-12815-2
50
+ const isbnMatch = url.match(/\/(978-[\d-]+)\s*$/);
51
+ if (!isbnMatch)
52
+ throw new Error(`URLからISBNを取得できません: ${url}`);
53
+ const isbn = isbnMatch[1];
54
+ const apiUrl = `${BASE_URL}/api_gh/site/search?search=${encodeURIComponent(isbn)}&limit=1`;
55
+ // JSONメタデータとebook store情報(HTML)を並列取得
56
+ const [apiText, htmlText] = await Promise.all([
57
+ fetchText(apiUrl, deps),
58
+ fetchText(url, deps),
59
+ ]);
60
+ const data = JSON.parse(apiText);
61
+ const entry = data.list[isbn];
62
+ if (!entry)
63
+ throw new Error(`書籍が見つかりません: ${isbn}`);
64
+ const base = entryToBookRecord(isbn, entry);
65
+ const doc = deps.parser.parse(htmlText);
66
+ const ebookStores = extractEbookStoresFromDoc(doc);
67
+ const asin = extractAsin(htmlText);
68
+ return {
69
+ ...base,
70
+ asin,
71
+ ebookStores: ebookStores.length > 0 ? ebookStores : undefined,
72
+ };
73
+ },
74
+ };
75
+ //# sourceMappingURL=gihyo.js.map