bindery-cli 0.2.0 → 0.2.1

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.
data/lib/bindery/book.rb CHANGED
@@ -57,38 +57,15 @@ module Bindery
57
57
  dir + "chapters"
58
58
  end
59
59
 
60
- # 平铺的章节文件(直接在 chapters/ 下,卷式书里通常为空)
60
+ # 平铺的章节文件(直接在 chapters/ 下,按文件名排序)
61
61
  def chapters
62
62
  return [] unless chapters_dir.directory?
63
63
 
64
64
  chapters_dir.children.select { |f| f.file? && f.extname == ".md" }.sort
65
65
  end
66
66
 
67
- # 卷目录(chapters/ 下的子目录,按文件名排序)
68
- def volumes
69
- return [] unless chapters_dir.directory?
70
-
71
- chapters_dir.children.select(&:directory?).sort
72
- end
73
-
74
- def has_volumes?
75
- !volumes.empty?
76
- end
77
-
78
- # 某个卷目录下的章节文件
79
- def volume_chapters(vol_dir)
80
- return [] unless vol_dir.directory?
81
-
82
- vol_dir.children.select { |f| f.file? && f.extname == ".md" }.sort
83
- end
84
-
85
- # 所有章节(平铺 + 各卷内),供统计 / 校验使用
86
- def all_chapters
87
- (chapters + volumes.flat_map { |v| volume_chapters(v) }).sort_by(&:to_s)
88
- end
89
-
90
67
  def valid?
91
- metadata_file.file? && all_chapters.any?
68
+ metadata_file.file? && chapters.any?
92
69
  end
93
70
 
94
71
  def epub_output(project_root = Project.root!)
@@ -17,12 +17,8 @@ module Bindery
17
17
 
18
18
  # 返回 true/false 表示是否成功;详细错误信息可通过 #last_error 获取
19
19
  def build_epub
20
- if @book.has_volumes?
21
- if @book.all_chapters.empty?
22
- @last_error = "#{@book.id} 的卷目录下没有任何 .md 文件"
23
- return false
24
- end
25
- elsif @book.chapters.empty?
20
+ chapters = @book.chapters
21
+ if chapters.empty?
26
22
  @last_error = "#{@book.id} 的 chapters/ 目录下没有任何 .md 文件"
27
23
  return false
28
24
  end
@@ -31,23 +27,17 @@ module Bindery
31
27
  ensure_pandoc!
32
28
 
33
29
  Dir.mktmpdir("bindery-#{@book.id}-") do |tmp|
34
- if @book.has_volumes?
35
- merged = merge_volumes(tmp)
36
- split_level = 2
37
- else
38
- merged = merge_chapters(@book.chapters, tmp)
39
- split_level = nil
40
- end
41
-
30
+ merged = merge_chapters(chapters, tmp)
42
31
  meta_file = write_pandoc_metadata(tmp, meta)
43
32
 
44
33
  out_dir = Project.output_dir(@project_root) + "epub"
45
34
  FileUtils.mkdir_p(out_dir)
46
35
  out_path = out_dir + "#{@book.id}.epub"
47
36
 
37
+ # 按内容中最顶层的标题级别拆分:h1=书名 / h2=卷 / h3=章
38
+ split_level = top_heading_level(merged)
48
39
  cmd = ["pandoc", merged.to_s, "-o", out_path.to_s,
49
- "--metadata-file", meta_file.to_s, "--toc"]
50
- cmd += ["--split-level", split_level.to_s] if split_level
40
+ "--metadata-file", meta_file.to_s, "--toc", "--split-level", split_level.to_s]
51
41
 
52
42
  css = Project.templates_dir(@project_root) + "style.css"
53
43
  cmd += ["--css", css.to_s] if css.file?
@@ -96,39 +86,16 @@ module Bindery
96
86
  path
97
87
  end
98
88
 
99
- # 卷式书:每卷生成一个 ## 卷名(h2)页,卷内章节标题降级为 h3
100
- def merge_volumes(tmp_dir)
101
- path = Pathname.new(tmp_dir) + "merged.md"
102
- path.open("w") do |out|
103
- @book.volumes.each_with_index do |vol_dir, i|
104
- out.puts if i.positive?
105
- name = vol_dir.basename.to_s.sub(/\A\d+-/, "")
106
- out.puts "## #{name}"
107
- out.puts
108
- @book.volume_chapters(vol_dir).each do |chapter|
109
- out.puts demote_headings(chapter.read, 2)
110
- out.puts
111
- end
112
- end
113
- end
114
- path
115
- end
89
+ # 返回合并后内容中最顶层的标题级别(1..6),无标题时返回 1
90
+ def top_heading_level(merged)
91
+ level = nil
92
+ merged.each_line do |line|
93
+ next unless line =~ /^(\#{1,6})\s/
116
94
 
117
- # ATX 标题降级指定级数(跳过代码围栏内部)
118
- def demote_headings(text, levels)
119
- in_fence = false
120
- text.lines.map do |line|
121
- if line =~ /^\s*(`{3,}|~{3,})/
122
- in_fence = !in_fence
123
- line
124
- elsif in_fence
125
- line
126
- elsif line =~ /^(\#{1,6})(\s|$)/
127
- ("#" * levels) + line
128
- else
129
- line
130
- end
131
- end.join
95
+ l = Regexp.last_match(1).length
96
+ level = l if level.nil? || l < level
97
+ end
98
+ level || 1
132
99
  end
133
100
 
134
101
  def write_pandoc_metadata(tmp_dir, meta)
data/lib/bindery/cli.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "optparse"
2
2
  require "fileutils"
3
+ require "rbconfig"
3
4
 
4
5
  module Bindery
5
6
  # 命令行入口:解析子命令并分发
@@ -24,10 +25,14 @@ module Bindery
24
25
  cmd_validate(rest)
25
26
  when "cover"
26
27
  cmd_cover(rest)
28
+ when "rename"
29
+ cmd_rename(rest)
27
30
  when "watch"
28
31
  cmd_watch(rest)
29
32
  when "serve"
30
33
  cmd_serve(rest)
34
+ when "web"
35
+ cmd_web(rest)
31
36
  when "status"
32
37
  cmd_status(rest)
33
38
  when "version", "-v", "--version"
@@ -117,6 +122,19 @@ module Bindery
117
122
  puts "已生成封面: #{path}"
118
123
  end
119
124
 
125
+ # ---------- bindery rename chapters <book-id> ----------
126
+ def cmd_rename(args)
127
+ type, book_id = args
128
+ if type != "chapters" || book_id.nil?
129
+ warn "用法: bindery rename chapters <book-id>"
130
+ exit 1
131
+ end
132
+ project_root = Project.root!
133
+ book = Bindery::Book.find(book_id, project_root)
134
+ count = Bindery::Renamer.rename_chapters(book)
135
+ puts "重命名完成:更新 #{count} 个文件"
136
+ end
137
+
120
138
  def cmd_new_book(args)
121
139
  options = { author: "佚名", dynasty: "", category: "", translator: "", isbn: "", id: nil, cover: false }
122
140
  parser = OptionParser.new do |o|
@@ -193,7 +211,7 @@ module Bindery
193
211
 
194
212
  if options[:dry_run]
195
213
  books.each do |book|
196
- puts "[#{book.id}] 将构建 → #{book.epub_output(project_root)}(#{book.all_chapters.size} 个章节)"
214
+ puts "[#{book.id}] 将构建 → #{book.epub_output(project_root)}(#{book.chapters.size} 个章节)"
197
215
  end
198
216
  return
199
217
  end
@@ -345,6 +363,46 @@ module Bindery
345
363
  Bindery::Server.new(project_root, host: options[:host], port: options[:port]).start
346
364
  end
347
365
 
366
+ # ---------- bindery web [--port N] [--host H] ----------
367
+ def cmd_web(args)
368
+ options = { port: 8000, host: "127.0.0.1" }
369
+ parser = OptionParser.new do |o|
370
+ o.on("--port N", Integer) { |v| options[:port] = v }
371
+ o.on("--host H") { |v| options[:host] = v }
372
+ end
373
+ parser.parse(args)
374
+
375
+ project_root = Project.root!
376
+ Index.rebuild(project_root)
377
+
378
+ url = "http://#{options[:host]}:#{options[:port]}/cover"
379
+ server = Bindery::Server.new(project_root, host: options[:host], port: options[:port])
380
+ thread = Thread.new do
381
+ begin
382
+ server.start
383
+ rescue StandardError => e
384
+ warn "服务启动失败: #{e.message}"
385
+ end
386
+ end
387
+ sleep 0.5 # 等服务就绪
388
+ open_browser(url)
389
+ puts "封面生成器已打开: #{url}(Ctrl-C 退出)"
390
+ thread.join
391
+ rescue Interrupt
392
+ puts "\n已退出"
393
+ end
394
+
395
+ def open_browser(url)
396
+ case RbConfig::CONFIG["host_os"]
397
+ when /darwin/
398
+ system("open", url)
399
+ when /mswin|mingw|cygwin/
400
+ system("cmd", "/c", "start", "", url)
401
+ else
402
+ system("xdg-open", url)
403
+ end
404
+ end
405
+
348
406
  # ---------- bindery status ----------
349
407
  def cmd_status(_args)
350
408
  project_root = Project.root!
@@ -359,7 +417,7 @@ module Bindery
359
417
  books.each do |book|
360
418
  printf(
361
419
  "%-16s %-16s %-6d %-4s %-6s %-16s\n",
362
- book.id, book.title, book.all_chapters.size,
420
+ book.id, book.title, book.chapters.size,
363
421
  book.cover? ? "✅" : "—",
364
422
  book.built?(project_root) ? "✅" : "—",
365
423
  book.built_at(project_root)&.strftime("%m-%d %H:%M") || "—"
@@ -380,8 +438,9 @@ module Bindery
380
438
  选项: --id ID(可选) --author NAME --dynasty 朝代 --category 分类
381
439
  --translator 译者 --isbn ISBN --cover 自动生成封面
382
440
  bindery new chapter <book-id> <数量> 批量创建章节(编号自动接续)
383
- bindery new volume <book-id> <卷名> [--chapters N] 新建卷(每卷在 EPUB 中独立成页)
441
+ bindery new volume <book-id> <卷名> [--chapters N] 新建卷(平铺 md,h2=卷名/h3=章名)
384
442
  bindery cover <book-id> 自动生成装饰性封面
443
+ bindery rename chapters <book-id> 按标题自动更新章节文件名
385
444
  bindery build <id> 构建单本书的 epub
386
445
  bindery build --all 批量构建所有书
387
446
  bindery build <id> --check 构建前校验(不实际构建)
@@ -393,6 +452,7 @@ module Bindery
393
452
  bindery clean [--all] 清理构建产物(--all 连 books.json 一起删)
394
453
  bindery watch [--interval 秒] 监听 books/ 自动重建
395
454
  bindery serve [--port N] [--host H] 启动本地书库站点
455
+ bindery web [--port N] 打开 Web 封面生成器
396
456
  bindery status 查看所有书籍状态
397
457
  bindery version 查看版本号
398
458
  HELP
data/lib/bindery/cover.rb CHANGED
@@ -5,7 +5,7 @@ module Bindery
5
5
  # 纯 Ruby 生成装饰性封面 PNG:配色由书名稳定派生,不含文字(文字渲染需要字体光栅化,超出标准库能力)
6
6
  module Cover
7
7
  WIDTH = 600
8
- HEIGHT = 800
8
+ HEIGHT = 960
9
9
 
10
10
  module_function
11
11
 
@@ -38,7 +38,7 @@ module Bindery
38
38
  translator: translator, isbn: isbn,
39
39
  lang: lang, publisher: publisher, rights: rights,
40
40
  })
41
- template("book", "chapters/01-chapter.md", locals: { title: title })
41
+ template("book", "chapters/chapter-0001.md", locals: { title: title })
42
42
  end
43
43
  end
44
44
  end
@@ -4,7 +4,7 @@ require_relative "base"
4
4
  module Bindery
5
5
  module Generators
6
6
  # bindery new chapter <book-id> <数量>
7
- # 在指定书籍里批量创建章节文件,编号自动接续已有章节(01-chapter.md 之后是 02-chapter.md…)
7
+ # 在指定书籍里批量创建章节文件,编号自动接续已有章节(chapter-0001.md 之后是 chapter-0002.md…)
8
8
  class ChapterGenerator < Base
9
9
  def self.call(book_id, count, project_root: Project.root!)
10
10
  book = Bindery::Book.find(book_id, project_root)
@@ -18,8 +18,8 @@ module Bindery
18
18
  puts "创建章节: #{book.id}"
19
19
  count.times do |i|
20
20
  n = start + i
21
- file = chapters_dir + format("%02d-chapter.md", n)
22
- file.write("# 第#{n}章\n\n")
21
+ file = chapters_dir + format("chapter-%04d.md", n)
22
+ file.write("### 第#{n}章\n\n")
23
23
  rel = begin
24
24
  file.relative_path_from(Pathname.pwd)
25
25
  rescue StandardError
@@ -31,7 +31,7 @@ module Bindery
31
31
  puts "完成!已创建 #{count} 个章节(从第 #{start} 章开始)"
32
32
  end
33
33
 
34
- # 扫描已有章节文件名开头的数字,返回下一个可用编号
34
+ # 扫描已有章节文件名中的编号(chapter-0001),返回下一个可用编号
35
35
  def self.next_number(chapters_dir)
36
36
  max = 0
37
37
  return max + 1 unless chapters_dir.directory?
@@ -39,8 +39,8 @@ module Bindery
39
39
  chapters_dir.children.each do |f|
40
40
  next unless f.file? && f.extname == ".md"
41
41
 
42
- n = f.basename.to_s[/\A(\d+)/, 1].to_i
43
- max = n if n > max
42
+ n = f.basename.to_s[/\Achapter-(\d+)/, 1]
43
+ max = n.to_i if n && n.to_i > max
44
44
  end
45
45
  max + 1
46
46
  end
@@ -1,6 +1,7 @@
1
- # 第一章
1
+ ### 第一章
2
2
 
3
3
  《<%= title %>》正文从这里开始……
4
4
 
5
+ 标题层级约定:书名 = h1(由 metadata 提供),卷名 = h2,章名 = h3。
5
6
  在 chapters/ 目录下按顺序添加更多章节文件(如 02-xxx.md, 03-xxx.md),
6
7
  文件名的字典序决定了它们在成书中的先后顺序。
@@ -16,6 +16,9 @@ bindery new volume <书籍ID> "<卷名>" --chapters <数量>
16
16
  # 单独批量创建章节(编号自动接续已有章节)
17
17
  bindery new chapter <书籍ID> <数量>
18
18
 
19
+ # 按标题自动更新章节文件名(chapter-0001-标题.md)
20
+ bindery rename chapters <书籍ID>
21
+
19
22
  # 单独生成封面
20
23
  bindery cover <书籍ID>
21
24
 
@@ -41,6 +44,9 @@ bindery watch
41
44
  # 本地启动书库站点
42
45
  bindery serve
43
46
 
47
+ # 打开 Web 封面生成器(自动填充书籍信息)
48
+ bindery web
49
+
44
50
  # 清理构建产物
45
51
  bindery clean --all
46
52
  ```
@@ -4,37 +4,33 @@ require_relative "base"
4
4
  module Bindery
5
5
  module Generators
6
6
  # bindery new volume <book-id> <卷名> [--chapters N]
7
- # 在书籍下创建一个卷目录(编号自动接续),可选批量创建卷内章节
7
+ # 在书籍下创建一个平铺卷文件(卷01-学而.md),卷结构由标题层级定义:
8
+ # ## 卷名(h2)
9
+ # ### 第1章(h3)
8
10
  class VolumeGenerator < Base
9
11
  def self.call(book_id, volume_name, chapters: 0, project_root: Project.root!)
10
12
  book = Bindery::Book.find(book_id, project_root)
11
13
  volume_name = volume_name.to_s.strip
12
14
  raise Bindery::Error, "卷名不能为空" if volume_name.empty?
13
15
 
14
- if book.volumes.empty? && book.chapters.any?
15
- warn "注意:chapters/ 下已有平铺章节文件(#{book.chapters.size} 个),卷式书构建时会忽略它们"
16
- end
17
-
18
16
  chapters_dir = book.chapters_dir
19
17
  FileUtils.mkdir_p(chapters_dir) unless chapters_dir.directory?
20
18
 
21
19
  n = next_volume_number(chapters_dir)
22
- vol_dir = chapters_dir + format("%02d-%s", n, volume_name)
23
- raise Bindery::Error, "卷目录已存在: #{vol_dir}" if vol_dir.exist?
20
+ file = chapters_dir + format("卷%02d-%s.md", n, volume_name)
21
+ raise Bindery::Error, "卷文件已存在: #{file}" if file.exist?
24
22
 
25
23
  chapters = chapters.to_i
26
24
  raise Bindery::Error, "章节数必须是正整数" if chapters.negative?
27
25
 
28
- FileUtils.mkdir_p(vol_dir)
29
- puts "创建卷: #{volume_name}"
30
- say_rel(vol_dir, trailing: "/")
31
-
26
+ content = +"## #{volume_name}\n\n"
32
27
  chapters.times do |i|
33
- file = vol_dir + format("%02d.md", i + 1)
34
- file.write("# 第#{i + 1}章\n\n")
35
- say_rel(file)
28
+ content << "### 第#{i + 1}章\n\n"
36
29
  end
30
+ file.write(content)
37
31
 
32
+ puts "创建卷: #{volume_name}"
33
+ say_rel(file)
38
34
  puts ""
39
35
  summary = chapters.positive? ? ",含 #{chapters} 章" : ""
40
36
  puts "完成!已创建卷 #{format('%02d', n)}(#{volume_name})#{summary}"
@@ -42,13 +38,13 @@ module Bindery
42
38
 
43
39
  def self.next_volume_number(chapters_dir)
44
40
  max = 0
45
- if chapters_dir.directory?
46
- chapters_dir.children.each do |d|
47
- next unless d.directory?
41
+ return max + 1 unless chapters_dir.directory?
42
+
43
+ chapters_dir.children.each do |f|
44
+ next unless f.file? && f.extname == ".md"
48
45
 
49
- m = d.basename.to_s[/\A(\d+)/, 1]
50
- max = m.to_i if m && m.to_i > max
51
- end
46
+ m = f.basename.to_s[/\A(\d+)/, 1]
47
+ max = m.to_i if m && m.to_i > max
52
48
  end
53
49
  max + 1
54
50
  end
@@ -0,0 +1,57 @@
1
+ module Bindery
2
+ # 根据章节 md 里的首个标题,自动把文件名更新为 chapter-xxxx-标题.md
3
+ module Renamer
4
+ module_function
5
+
6
+ # 返回重命名的文件数
7
+ def rename_chapters(book)
8
+ dir = book.chapters_dir
9
+ return 0 unless dir.directory?
10
+
11
+ renamed = 0
12
+ dir.children.select { |f| f.file? && f.extname == ".md" }.sort.each do |f|
13
+ base = f.basename.to_s
14
+ m = base.match(/\A(chapter-\d+)(?:-.*)?\.md\z/)
15
+ next unless m
16
+
17
+ title = extract_title(f)
18
+ next if title.nil? || title.empty?
19
+
20
+ safe = sanitize(title)
21
+ new_name = "#{m[1]}-#{safe}.md"
22
+ next if new_name == base
23
+
24
+ new_path = f.dirname + new_name
25
+ raise Bindery::Error, "目标文件已存在: #{new_path}" if new_path.exist?
26
+
27
+ File.rename(f, new_path)
28
+ renamed += 1
29
+ puts " rename #{base} -> #{new_name}"
30
+ end
31
+ renamed
32
+ end
33
+
34
+ # 取文件里的首个 ATX 标题文本
35
+ def extract_title(path)
36
+ path.each_line do |line|
37
+ next unless line =~ /^(\#{1,6})\s+(.+)$/
38
+
39
+ title = Regexp.last_match(2).sub(/\s*#+\s*$/, "").strip
40
+ return title unless title.empty?
41
+ end
42
+ nil
43
+ end
44
+
45
+ # 清理标题里的非法文件名字符,跨平台安全(Windows 也兼容)
46
+ def sanitize(text)
47
+ s = text
48
+ .gsub(/[\\\/:*?"<>|\r\n\t]/, "-")
49
+ .gsub(/\s+/, "-")
50
+ .gsub(/-+/, "-")
51
+ .gsub(/\A-+|-\z/, "")
52
+ .gsub(/\A\.+|\.+\z/, "")
53
+ s = "章节" if s.empty?
54
+ s[0, 40]
55
+ end
56
+ end
57
+ end
@@ -2,6 +2,8 @@ require "socket"
2
2
  require "uri"
3
3
  require "json"
4
4
  require "yaml"
5
+ require "base64"
6
+ require "pathname"
5
7
 
6
8
  module Bindery
7
9
  # 极简静态服务器:展示书库并提供 EPUB 下载。
@@ -18,6 +20,9 @@ module Bindery
18
20
  ".svg" => "image/svg+xml",
19
21
  }.freeze
20
22
 
23
+ # 封面编辑器页面(随 gem 一起打包)
24
+ ASSETS_DIR = Pathname.new(__dir__) + "assets"
25
+
21
26
  def initialize(project_root, host: "127.0.0.1", port: 8000)
22
27
  @project_root = project_root
23
28
  @host = host
@@ -58,22 +63,33 @@ module Bindery
58
63
  return unless request_line
59
64
 
60
65
  method, raw_path, = request_line.split(" ")
61
- # 丢弃其余请求头
66
+ # 读取请求头,POST 需要 Content-Length
67
+ content_length = 0
62
68
  while (line = client.gets)
63
69
  break if line.nil? || line == "\r\n" || line == "\n"
70
+ if line =~ /\AContent-Length:\s*(\d+)/i
71
+ content_length = Regexp.last_match(1).to_i
72
+ end
64
73
  end
65
- return unless method == "GET" || method == "HEAD"
66
74
 
67
75
  path = (URI::DEFAULT_PARSER.unescape(raw_path.split("?").first) rescue raw_path)
68
76
  path = "/" if path.nil? || path.empty?
69
77
  path = path.gsub("..", "") # 防目录穿越
70
78
 
79
+ if method == "POST" && path == "/cover/save"
80
+ save_cover(client, content_length)
81
+ return
82
+ end
83
+ return unless method == "GET" || method == "HEAD"
84
+
71
85
  case path
72
86
  when "/", "/index.html"
73
87
  body = index_html
74
88
  send_response(client, 200, MIME[".html"], body, method)
75
89
  when "/books.json"
76
90
  serve_file(client, Project.index_file(@project_root), MIME[".json"], method)
91
+ when "/cover"
92
+ serve_file(client, ASSETS_DIR + "index.html", MIME[".html"], method)
77
93
  when %r{\A/epub/([\w\-.]+\.epub)\z}
78
94
  serve_file(client, Project.output_dir(@project_root) + "epub" + Regexp.last_match(1), MIME[".epub"], method)
79
95
  when %r{\A/cover/([\w\-]+)\z}
@@ -117,7 +133,7 @@ module Bindery
117
133
  h1 { font-size: 1.6em; border-bottom: 1px solid #eee; padding-bottom: .4em; }
118
134
  ul { list-style: none; padding: 0; }
119
135
  .book { display: flex; gap: 1em; padding: 1em 0; border-bottom: 1px solid #f0f0f0; }
120
- .cover { width: 64px; height: 96px; object-fit: cover; background: #f5f5f5; border-radius: 4px; }
136
+ .cover { width: 64px; height: 102px; object-fit: cover; background: #f5f5f5; border-radius: 4px; }
121
137
  .info h2 { margin: 0; font-size: 1.1em; }
122
138
  .author { color: #666; margin: .3em 0 .5em; }
123
139
  a { color: #b45309; }
@@ -155,6 +171,43 @@ module Bindery
155
171
  serve_file(client, path, nil, method)
156
172
  end
157
173
 
174
+ # 保存封面编辑器生成的 PNG 到书籍目录,并更新 metadata 的 cover 字段
175
+ def save_cover(client, content_length)
176
+ body = client.read(content_length).to_s
177
+ return send_json(client, 400, { ok: false, error: "请求体为空" }) if body.empty?
178
+
179
+ data = JSON.parse(body)
180
+ book_id = data["book"].to_s
181
+ data_url = data["data"].to_s
182
+
183
+ book_dir = Project.books_dir(@project_root) + book_id
184
+ return send_json(client, 404, { ok: false, error: "书籍不存在: #{book_id}" }) unless book_dir.directory?
185
+
186
+ b64 = data_url.sub(/\Adata:image\/\w+;base64,/, "")
187
+ png = Base64.decode64(b64)
188
+ path = book_dir + "cover.png"
189
+ path.binwrite(png)
190
+
191
+ meta_file = book_dir + "metadata.yaml"
192
+ if meta_file.file?
193
+ text = meta_file.read
194
+ text = if text =~ /^cover:.*$/
195
+ text.sub(/^cover:.*$/, "cover: cover.png")
196
+ else
197
+ text.chomp + "\ncover: cover.png\n"
198
+ end
199
+ meta_file.write(text)
200
+ end
201
+
202
+ send_json(client, 200, { ok: true, path: "books/#{book_id}/cover.png" })
203
+ rescue JSON::ParserError => e
204
+ send_json(client, 400, { ok: false, error: "JSON 解析失败: #{e.message}" })
205
+ end
206
+
207
+ def send_json(client, status, hash)
208
+ send_response(client, status, MIME[".json"], JSON.generate(hash), "POST")
209
+ end
210
+
158
211
  def serve_file(client, path, content_type, method)
159
212
  return not_found(client, method) unless path.file?
160
213
 
@@ -20,7 +20,7 @@ module Bindery
20
20
  issues << "缺少 metadata.yaml"
21
21
  end
22
22
 
23
- issues << "chapters/ 目录下没有任何 .md 文件" if book.all_chapters.empty?
23
+ issues << "chapters/ 目录下没有任何 .md 文件" if book.chapters.empty?
24
24
  issues << "未安装 pandoc(构建前请先安装)" unless pandoc_installed?
25
25
 
26
26
  issues
@@ -37,8 +37,8 @@ module Bindery
37
37
  dims = Bindery::Image.dimensions(cover)
38
38
  if dims.nil?
39
39
  warns << "封面不是有效的 JPEG/PNG 图片"
40
- elsif dims[0] < 600 || dims[1] < 800
41
- warns << "封面尺寸偏小(#{dims[0]}×#{dims[1]},建议至少 600×800)"
40
+ elsif dims[0] < 600 || dims[1] < 960
41
+ warns << "封面尺寸偏小(#{dims[0]}×#{dims[1]},建议至少 600×960,比例 1:1.6)"
42
42
  end
43
43
  end
44
44
 
@@ -1,3 +1,3 @@
1
1
  module Bindery
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
data/lib/bindery.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "bindery/builder"
6
6
  require_relative "bindery/image"
7
7
  require_relative "bindery/cover"
8
8
  require_relative "bindery/validator"
9
+ require_relative "bindery/renamer"
9
10
  require_relative "bindery/index"
10
11
  require_relative "bindery/server"
11
12
  require_relative "bindery/generators/project_generator"
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.2.0
4
+ version: 0.2.1
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/index.html
27
28
  - lib/bindery/book.rb
28
29
  - lib/bindery/builder.rb
29
30
  - lib/bindery/cli.rb
@@ -33,7 +34,7 @@ files:
33
34
  - lib/bindery/generators/book_generator.rb
34
35
  - lib/bindery/generators/chapter_generator.rb
35
36
  - lib/bindery/generators/project_generator.rb
36
- - lib/bindery/generators/templates/book/chapters/01-chapter.md.erb
37
+ - lib/bindery/generators/templates/book/chapters/chapter-0001.md.erb
37
38
  - lib/bindery/generators/templates/book/metadata.yaml.erb
38
39
  - lib/bindery/generators/templates/project/README.md.erb
39
40
  - lib/bindery/generators/templates/project/config/bindery.yml.erb
@@ -44,6 +45,7 @@ files:
44
45
  - lib/bindery/image.rb
45
46
  - lib/bindery/index.rb
46
47
  - lib/bindery/project.rb
48
+ - lib/bindery/renamer.rb
47
49
  - lib/bindery/server.rb
48
50
  - lib/bindery/validator.rb
49
51
  - lib/bindery/version.rb