bindery-cli 0.2.1 → 0.3.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/README.md +3 -4
- data/lib/bindery/assets/cover.html +892 -0
- data/lib/bindery/assets/index.html +165 -618
- data/lib/bindery/cli.rb +11 -27
- data/lib/bindery/generators/templates/project/README.md.erb +1 -4
- data/lib/bindery/server.rb +21 -63
- data/lib/bindery/version.rb +1 -1
- metadata +2 -1
data/lib/bindery/cli.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "rbconfig"
|
|
|
4
4
|
|
|
5
5
|
module Bindery
|
|
6
6
|
# 命令行入口:解析子命令并分发
|
|
7
|
-
# 支持: init / new book / build / clean / validate / watch / serve / status / version / help
|
|
7
|
+
# 支持: init / new book / build / clean / validate / watch / serve / cover / status / version / help
|
|
8
8
|
class CLI
|
|
9
9
|
def self.start(argv)
|
|
10
10
|
new.run(argv)
|
|
@@ -23,16 +23,14 @@ module Bindery
|
|
|
23
23
|
cmd_clean(rest)
|
|
24
24
|
when "validate"
|
|
25
25
|
cmd_validate(rest)
|
|
26
|
-
when "cover"
|
|
27
|
-
cmd_cover(rest)
|
|
28
26
|
when "rename"
|
|
29
27
|
cmd_rename(rest)
|
|
30
28
|
when "watch"
|
|
31
29
|
cmd_watch(rest)
|
|
32
30
|
when "serve"
|
|
33
31
|
cmd_serve(rest)
|
|
34
|
-
when "
|
|
35
|
-
|
|
32
|
+
when "cover"
|
|
33
|
+
cmd_cover(rest)
|
|
36
34
|
when "status"
|
|
37
35
|
cmd_status(rest)
|
|
38
36
|
when "version", "-v", "--version"
|
|
@@ -109,19 +107,6 @@ module Bindery
|
|
|
109
107
|
Generators::VolumeGenerator.call(book_id, volume_name, chapters: options[:chapters])
|
|
110
108
|
end
|
|
111
109
|
|
|
112
|
-
# ---------- bindery cover <book-id> ----------
|
|
113
|
-
def cmd_cover(args)
|
|
114
|
-
id = args.first
|
|
115
|
-
if id.nil?
|
|
116
|
-
warn "用法: bindery cover <book-id>"
|
|
117
|
-
exit 1
|
|
118
|
-
end
|
|
119
|
-
project_root = Project.root!
|
|
120
|
-
book = Bindery::Book.find(id, project_root)
|
|
121
|
-
path = Bindery::Cover.generate(book)
|
|
122
|
-
puts "已生成封面: #{path}"
|
|
123
|
-
end
|
|
124
|
-
|
|
125
110
|
# ---------- bindery rename chapters <book-id> ----------
|
|
126
111
|
def cmd_rename(args)
|
|
127
112
|
type, book_id = args
|
|
@@ -351,7 +336,7 @@ module Bindery
|
|
|
351
336
|
|
|
352
337
|
# ---------- bindery serve [--port N] [--host H] ----------
|
|
353
338
|
def cmd_serve(args)
|
|
354
|
-
options = { port:
|
|
339
|
+
options = { port: nil, host: "127.0.0.1" }
|
|
355
340
|
parser = OptionParser.new do |o|
|
|
356
341
|
o.on("--port N", Integer) { |v| options[:port] = v }
|
|
357
342
|
o.on("--host H") { |v| options[:host] = v }
|
|
@@ -363,9 +348,9 @@ module Bindery
|
|
|
363
348
|
Bindery::Server.new(project_root, host: options[:host], port: options[:port]).start
|
|
364
349
|
end
|
|
365
350
|
|
|
366
|
-
# ---------- bindery
|
|
367
|
-
def
|
|
368
|
-
options = { port:
|
|
351
|
+
# ---------- bindery cover [--port N] [--host H] ----------
|
|
352
|
+
def cmd_cover(args)
|
|
353
|
+
options = { port: nil, host: "127.0.0.1" }
|
|
369
354
|
parser = OptionParser.new do |o|
|
|
370
355
|
o.on("--port N", Integer) { |v| options[:port] = v }
|
|
371
356
|
o.on("--host H") { |v| options[:host] = v }
|
|
@@ -375,8 +360,9 @@ module Bindery
|
|
|
375
360
|
project_root = Project.root!
|
|
376
361
|
Index.rebuild(project_root)
|
|
377
362
|
|
|
378
|
-
url = "http://#{options[:host]}:#{options[:port]}/cover"
|
|
379
363
|
server = Bindery::Server.new(project_root, host: options[:host], port: options[:port])
|
|
364
|
+
server.listen # 先绑定端口(未指定 --port 时由系统自动分配),再拿到实际端口拼 URL
|
|
365
|
+
url = "http://#{options[:host]}:#{server.port}/cover"
|
|
380
366
|
thread = Thread.new do
|
|
381
367
|
begin
|
|
382
368
|
server.start
|
|
@@ -384,7 +370,6 @@ module Bindery
|
|
|
384
370
|
warn "服务启动失败: #{e.message}"
|
|
385
371
|
end
|
|
386
372
|
end
|
|
387
|
-
sleep 0.5 # 等服务就绪
|
|
388
373
|
open_browser(url)
|
|
389
374
|
puts "封面生成器已打开: #{url}(Ctrl-C 退出)"
|
|
390
375
|
thread.join
|
|
@@ -439,7 +424,6 @@ module Bindery
|
|
|
439
424
|
--translator 译者 --isbn ISBN --cover 自动生成封面
|
|
440
425
|
bindery new chapter <book-id> <数量> 批量创建章节(编号自动接续)
|
|
441
426
|
bindery new volume <book-id> <卷名> [--chapters N] 新建卷(平铺 md,h2=卷名/h3=章名)
|
|
442
|
-
bindery cover <book-id> 自动生成装饰性封面
|
|
443
427
|
bindery rename chapters <book-id> 按标题自动更新章节文件名
|
|
444
428
|
bindery build <id> 构建单本书的 epub
|
|
445
429
|
bindery build --all 批量构建所有书
|
|
@@ -451,8 +435,8 @@ module Bindery
|
|
|
451
435
|
bindery validate --all 校验所有书
|
|
452
436
|
bindery clean [--all] 清理构建产物(--all 连 books.json 一起删)
|
|
453
437
|
bindery watch [--interval 秒] 监听 books/ 自动重建
|
|
454
|
-
bindery serve [--port N] [--host H]
|
|
455
|
-
bindery
|
|
438
|
+
bindery serve [--port N] [--host H] 启动本地书库站点(不加 --port 时自动选空闲端口)
|
|
439
|
+
bindery cover [--port N] [--host H] 打开 Web 封面生成器(不加 --port 时自动选空闲端口)
|
|
456
440
|
bindery status 查看所有书籍状态
|
|
457
441
|
bindery version 查看版本号
|
|
458
442
|
HELP
|
|
@@ -19,9 +19,6 @@ bindery new chapter <书籍ID> <数量>
|
|
|
19
19
|
# 按标题自动更新章节文件名(chapter-0001-标题.md)
|
|
20
20
|
bindery rename chapters <书籍ID>
|
|
21
21
|
|
|
22
|
-
# 单独生成封面
|
|
23
|
-
bindery cover <书籍ID>
|
|
24
|
-
|
|
25
22
|
# 构建一本书
|
|
26
23
|
bindery build <书籍ID>
|
|
27
24
|
|
|
@@ -45,7 +42,7 @@ bindery watch
|
|
|
45
42
|
bindery serve
|
|
46
43
|
|
|
47
44
|
# 打开 Web 封面生成器(自动填充书籍信息)
|
|
48
|
-
bindery
|
|
45
|
+
bindery cover
|
|
49
46
|
|
|
50
47
|
# 清理构建产物
|
|
51
48
|
bindery clean --all
|
data/lib/bindery/server.rb
CHANGED
|
@@ -20,20 +20,33 @@ module Bindery
|
|
|
20
20
|
".svg" => "image/svg+xml",
|
|
21
21
|
}.freeze
|
|
22
22
|
|
|
23
|
-
#
|
|
23
|
+
# 书库与封面编辑器页面(随 gem 一起打包)
|
|
24
24
|
ASSETS_DIR = Pathname.new(__dir__) + "assets"
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
attr_reader :port
|
|
27
|
+
|
|
28
|
+
# port 为 nil 时不预先指定端口,由操作系统自动分配一个空闲端口
|
|
29
|
+
def initialize(project_root, host: "127.0.0.1", port: nil)
|
|
27
30
|
@project_root = project_root
|
|
28
31
|
@host = host
|
|
29
32
|
@port = port
|
|
30
33
|
end
|
|
31
34
|
|
|
35
|
+
# 绑定监听地址,并返回实际监听的端口号(未指定 --port 时可用于提前拿到自动分配的端口)
|
|
36
|
+
def listen
|
|
37
|
+
return @port if @server
|
|
38
|
+
|
|
39
|
+
@server = TCPServer.new(@host, @port || 0)
|
|
40
|
+
@port = @server.addr[1]
|
|
41
|
+
rescue Errno::EADDRINUSE
|
|
42
|
+
raise Bindery::Error, "端口 #{@port} 已被占用,换个端口试试(--port N),或不指定 --port 让系统自动分配"
|
|
43
|
+
end
|
|
44
|
+
|
|
32
45
|
def start
|
|
33
|
-
|
|
46
|
+
listen
|
|
34
47
|
puts "服务已启动: http://#{@host}:#{@port} (Ctrl-C 退出)"
|
|
35
48
|
loop do
|
|
36
|
-
client = server.accept
|
|
49
|
+
client = @server.accept
|
|
37
50
|
begin
|
|
38
51
|
respond(client)
|
|
39
52
|
rescue StandardError => e
|
|
@@ -50,7 +63,7 @@ module Bindery
|
|
|
50
63
|
puts "\n服务已停止"
|
|
51
64
|
ensure
|
|
52
65
|
begin
|
|
53
|
-
server
|
|
66
|
+
@server&.close
|
|
54
67
|
rescue StandardError
|
|
55
68
|
nil
|
|
56
69
|
end
|
|
@@ -72,7 +85,7 @@ module Bindery
|
|
|
72
85
|
end
|
|
73
86
|
end
|
|
74
87
|
|
|
75
|
-
path = (URI::
|
|
88
|
+
path = (URI::RFC2396_PARSER.unescape(raw_path.split("?").first) rescue raw_path)
|
|
76
89
|
path = "/" if path.nil? || path.empty?
|
|
77
90
|
path = path.gsub("..", "") # 防目录穿越
|
|
78
91
|
|
|
@@ -84,12 +97,11 @@ module Bindery
|
|
|
84
97
|
|
|
85
98
|
case path
|
|
86
99
|
when "/", "/index.html"
|
|
87
|
-
|
|
88
|
-
send_response(client, 200, MIME[".html"], body, method)
|
|
100
|
+
serve_file(client, ASSETS_DIR + "index.html", MIME[".html"], method)
|
|
89
101
|
when "/books.json"
|
|
90
102
|
serve_file(client, Project.index_file(@project_root), MIME[".json"], method)
|
|
91
103
|
when "/cover"
|
|
92
|
-
serve_file(client, ASSETS_DIR + "
|
|
104
|
+
serve_file(client, ASSETS_DIR + "cover.html", MIME[".html"], method)
|
|
93
105
|
when %r{\A/epub/([\w\-.]+\.epub)\z}
|
|
94
106
|
serve_file(client, Project.output_dir(@project_root) + "epub" + Regexp.last_match(1), MIME[".epub"], method)
|
|
95
107
|
when %r{\A/cover/([\w\-]+)\z}
|
|
@@ -99,56 +111,6 @@ module Bindery
|
|
|
99
111
|
end
|
|
100
112
|
end
|
|
101
113
|
|
|
102
|
-
def index_html
|
|
103
|
-
index_file = Project.index_file(@project_root)
|
|
104
|
-
data = if index_file.file?
|
|
105
|
-
JSON.parse(index_file.read)
|
|
106
|
-
else
|
|
107
|
-
Bindery::Index.rebuild(@project_root)
|
|
108
|
-
end
|
|
109
|
-
books = data["books"] || []
|
|
110
|
-
|
|
111
|
-
rows = books.map do |b|
|
|
112
|
-
id = escape_html(b["id"].to_s)
|
|
113
|
-
title = escape_html(b["title"].to_s)
|
|
114
|
-
author = escape_html(b["author"].to_s)
|
|
115
|
-
epub_link = if b["epub"]
|
|
116
|
-
%(<a href="/epub/#{id}.epub" download>下载 EPUB</a>)
|
|
117
|
-
else
|
|
118
|
-
"<em>未构建</em>"
|
|
119
|
-
end
|
|
120
|
-
cover = %(<img src="/cover/#{id}" class="cover" alt="">)
|
|
121
|
-
"<li class=\"book\">#{cover}<div class=\"info\"><h2>#{title}</h2><p class=\"author\">#{author}</p>#{epub_link}</div></li>"
|
|
122
|
-
end.join("\n")
|
|
123
|
-
|
|
124
|
-
<<~HTML
|
|
125
|
-
<!doctype html>
|
|
126
|
-
<html lang="zh-CN">
|
|
127
|
-
<head>
|
|
128
|
-
<meta charset="utf-8">
|
|
129
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
130
|
-
<title>#{escape_html(@project_root.basename.to_s)} · 书库</title>
|
|
131
|
-
<style>
|
|
132
|
-
body { font-family: system-ui, -apple-system, sans-serif; max-width: 720px; margin: 2em auto; padding: 0 1em; color: #222; }
|
|
133
|
-
h1 { font-size: 1.6em; border-bottom: 1px solid #eee; padding-bottom: .4em; }
|
|
134
|
-
ul { list-style: none; padding: 0; }
|
|
135
|
-
.book { display: flex; gap: 1em; padding: 1em 0; border-bottom: 1px solid #f0f0f0; }
|
|
136
|
-
.cover { width: 64px; height: 102px; object-fit: cover; background: #f5f5f5; border-radius: 4px; }
|
|
137
|
-
.info h2 { margin: 0; font-size: 1.1em; }
|
|
138
|
-
.author { color: #666; margin: .3em 0 .5em; }
|
|
139
|
-
a { color: #b45309; }
|
|
140
|
-
em { color: #999; }
|
|
141
|
-
</style>
|
|
142
|
-
</head>
|
|
143
|
-
<body>
|
|
144
|
-
<h1>#{escape_html(@project_root.basename.to_s)} · 书库</h1>
|
|
145
|
-
<p>共 #{books.size} 本书</p>
|
|
146
|
-
<ul>#{rows}</ul>
|
|
147
|
-
</body>
|
|
148
|
-
</html>
|
|
149
|
-
HTML
|
|
150
|
-
end
|
|
151
|
-
|
|
152
114
|
def serve_cover(client, id, method)
|
|
153
115
|
book_dir = Project.books_dir(@project_root) + id
|
|
154
116
|
return not_found(client, method) unless book_dir.directory?
|
|
@@ -230,9 +192,5 @@ module Bindery
|
|
|
230
192
|
client.write(headers)
|
|
231
193
|
client.write(body) unless method == "HEAD"
|
|
232
194
|
end
|
|
233
|
-
|
|
234
|
-
def escape_html(str)
|
|
235
|
-
str.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
236
|
-
end
|
|
237
195
|
end
|
|
238
196
|
end
|
data/lib/bindery/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bindery-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Billow Wang
|
|
@@ -24,6 +24,7 @@ files:
|
|
|
24
24
|
- README.md
|
|
25
25
|
- exe/bindery
|
|
26
26
|
- lib/bindery.rb
|
|
27
|
+
- lib/bindery/assets/cover.html
|
|
27
28
|
- lib/bindery/assets/index.html
|
|
28
29
|
- lib/bindery/book.rb
|
|
29
30
|
- lib/bindery/builder.rb
|