narou 2.5.2 → 2.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/ChangeLog.md +54 -0
  4. data/LICENSE.txt +26 -0
  5. data/README.md +70 -84
  6. data/lib/command/alias.rb +7 -0
  7. data/lib/command/convert.rb +40 -61
  8. data/lib/command/download.rb +9 -1
  9. data/lib/command/mail.rb +24 -13
  10. data/lib/command/send.rb +18 -2
  11. data/lib/command/setting.rb +152 -34
  12. data/lib/command/tag.rb +5 -0
  13. data/lib/command/update.rb +137 -4
  14. data/lib/converterbase.rb +12 -6
  15. data/lib/database.rb +1 -1
  16. data/lib/device/ibooks.rb +6 -5
  17. data/lib/downloader.rb +85 -83
  18. data/lib/helper.rb +3 -2
  19. data/lib/html.rb +3 -0
  20. data/lib/logger.rb +20 -12
  21. data/lib/narou.rb +16 -0
  22. data/lib/novelconverter.rb +209 -75
  23. data/lib/novelinfo.rb +1 -1
  24. data/lib/novelsetting.rb +53 -29
  25. data/lib/template.rb +4 -3
  26. data/lib/version.rb +1 -1
  27. data/lib/web/appserver.rb +46 -32
  28. data/lib/web/public/resources/common.ui.js +2 -4
  29. data/lib/web/public/resources/default-style.css +37 -0
  30. data/lib/web/public/resources/narou.library.js +48 -57
  31. data/lib/web/public/resources/narou.ui.js +71 -39
  32. data/lib/web/public/theme/Cerulean/bootstrap.min.css +11 -0
  33. data/lib/web/public/theme/Cerulean/style.css +49 -0
  34. data/lib/web/public/theme/Darkly/bootstrap.min.css +11 -0
  35. data/lib/web/public/theme/Darkly/style.css +53 -0
  36. data/lib/web/public/theme/Readable/bootstrap.min.css +11 -0
  37. data/lib/web/public/theme/Readable/style.css +53 -0
  38. data/lib/web/public/theme/Slate/bootstrap.min.css +11 -0
  39. data/lib/web/public/theme/Slate/style.css +40 -0
  40. data/lib/web/public/theme/Superhero/bootstrap.min.css +11 -0
  41. data/lib/web/public/theme/Superhero/style.css +40 -0
  42. data/lib/web/public/theme/United/bootstrap.min.css +11 -0
  43. data/lib/web/public/theme/United/style.css +47 -0
  44. data/lib/web/public/theme/fonts/glyphicons-halflings-regular.eot +0 -0
  45. data/lib/web/public/theme/fonts/glyphicons-halflings-regular.svg +288 -0
  46. data/lib/web/public/theme/fonts/glyphicons-halflings-regular.ttf +0 -0
  47. data/lib/web/public/theme/fonts/glyphicons-halflings-regular.woff +0 -0
  48. data/lib/web/public/theme/fonts/glyphicons-halflings-regular.woff2 +0 -0
  49. data/lib/web/settingmessages.rb +5 -0
  50. data/lib/web/views/index.haml +15 -19
  51. data/lib/web/views/layout.haml +6 -1
  52. data/lib/web/views/novels/setting.haml +76 -75
  53. data/lib/web/views/settings.haml +111 -107
  54. data/lib/web/views/style.scss +6 -21
  55. data/narou.gemspec +49 -6
  56. data/narou.rb +3 -1
  57. data/preset/ncode.syosetu.com/n7975cr/setting.ini +7 -0
  58. data/spec/README.txt +4 -0
  59. data/spec/data/convert_test/ruby/correct_test_ruby.txt +4 -4
  60. data/template/hotentry.txt.erb +28 -0
  61. data/template/ibunko_novel.txt.erb +4 -2
  62. data/template/novel.txt.erb +6 -3
  63. data/template/setting.ini.erb +18 -2
  64. metadata +73 -8
data/lib/helper.rb CHANGED
@@ -109,7 +109,8 @@ module Helper
109
109
  # ダウンロードしてきたデータを使いやすいように処理
110
110
  #
111
111
  def pretreatment_source(src, encoding = Encoding::UTF_8)
112
- restor_entity(src.force_encoding(encoding)).gsub("\r", "")
112
+ src.force_encoding(encoding).gsub("\r", "")
113
+ .encode("UTF-16BE", encoding, :invalid => :replace, :undef => :replace, :replace => "?").encode("UTF-8")
113
114
  end
114
115
 
115
116
  ENTITIES = { quot: '"', amp: "&", nbsp: " ", lt: "<", gt: ">", copy: "(c)", "#39" => "'" }
@@ -117,7 +118,7 @@ module Helper
117
118
  # エンティティ復号
118
119
  #
119
120
  def restor_entity(str)
120
- result = str.encode("UTF-16BE", "UTF-8", :invalid => :replace, :undef => :replace, :replace => "?").encode("UTF-8")
121
+ result = str.dup
121
122
  ENTITIES.each do |key, value|
122
123
  result.gsub!("&#{key};", value)
123
124
  end
data/lib/html.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  # Copyright 2013 whiteleaf. All rights reserved.
4
4
  #
5
5
 
6
+ require_relative "helper"
7
+
6
8
  class HTML
7
9
  attr_accessor :string
8
10
 
@@ -36,6 +38,7 @@ class HTML
36
38
  @string = s_to_aozora
37
39
  @string = img_to_aozora
38
40
  @string = delete_tag
41
+ @string = Helper.restor_entity(@string)
39
42
  @string
40
43
  end
41
44
 
data/lib/logger.rb CHANGED
@@ -107,18 +107,30 @@ module Narou::LoggerModule
107
107
  end
108
108
  end
109
109
  end
110
+
111
+ def write_base(str, stream)
112
+ str = str.to_s
113
+ if str.encoding == Encoding::ASCII_8BIT
114
+ str.force_encoding(Encoding::UTF_8)
115
+ end
116
+ write_console(str, stream)
117
+ end
118
+
119
+ def warn(str)
120
+ self.puts str
121
+ end
122
+
123
+ def error(str)
124
+ self.puts "<bold><red>[ERROR]</red></bold> ".termcolor + str
125
+ end
110
126
  end
111
127
 
112
128
  class Narou::Logger < StringIO
113
129
  include Narou::LoggerModule
114
130
 
115
131
  def write(str)
116
- str = str.to_s
117
- if str.encoding == Encoding::ASCII_8BIT
118
- str.force_encoding(Encoding::UTF_8)
119
- end
132
+ write_base(str, STDOUT)
120
133
  super(str)
121
- write_console(str, STDOUT)
122
134
  end
123
135
 
124
136
  def tty?
@@ -130,12 +142,8 @@ class Narou::LoggerError < StringIO
130
142
  include Narou::LoggerModule
131
143
 
132
144
  def write(str)
133
- str = str.to_s
134
- if str.encoding == Encoding::ASCII_8BIT
135
- str.force_encoding(Encoding::UTF_8)
136
- end
145
+ write_base(str, STDERR)
137
146
  super(str)
138
- write_console(str, STDERR)
139
147
  end
140
148
 
141
149
  def tty?
@@ -144,11 +152,11 @@ class Narou::LoggerError < StringIO
144
152
  end
145
153
 
146
154
  def warn(str)
147
- puts str
155
+ $stdout.warn str
148
156
  end
149
157
 
150
158
  def error(str)
151
- puts "<bold><red>[ERROR]</red></bold> ".termcolor + str
159
+ $stdout.error str
152
160
  end
153
161
 
154
162
  $stdout = Narou::Logger.new
data/lib/narou.rb CHANGED
@@ -221,5 +221,21 @@ module Narou
221
221
  "#{key.rjust(width)} : #{summary}"
222
222
  }.join("\n")
223
223
  end
224
+
225
+ def get_theme
226
+ Inventory.load("local_setting", :local)["theme"]
227
+ end
228
+
229
+ def get_theme_dir(name = nil)
230
+ File.join([get_script_dir, "lib/web/public/theme", name].compact)
231
+ end
232
+
233
+ def get_theme_names
234
+ Dir.glob(get_theme_dir("*")).map do |path|
235
+ name = File.basename(path)
236
+ name == "fonts" ? nil : name
237
+ end.compact
238
+ end
239
+ memoize :get_theme_names
224
240
  end
225
241
  end
@@ -15,8 +15,11 @@ require_relative "progressbar"
15
15
  require_relative "helper"
16
16
  require_relative "inventory"
17
17
  require_relative "html"
18
+ require_relative "eventable"
18
19
 
19
20
  class NovelConverter
21
+ include Narou::Eventable
22
+
20
23
  NOVEL_TEXT_TEMPLATE_NAME = "novel.txt"
21
24
  NOVEL_TEXT_TEMPLATE_NAME_FOR_IBUNKO = "ibunko_novel.txt"
22
25
 
@@ -208,7 +211,7 @@ class NovelConverter
208
211
  # EPUBファイルをkindlegenでMOBIへ
209
212
  # AozoraEpub3.jar と同じ場所に kindlegen が無ければ何もしない
210
213
  #
211
- # 返り値:正常終了 :success、エラー終了 :error、中断終了 :abort、kindlegenがなかった nil
214
+ # 返り値:正常終了 :success、エラー終了 :error、中断終了 :abort
212
215
  #
213
216
  def self.epub_to_mobi(epub_path, verbose = false)
214
217
  kindlegen_path = File.join(File.dirname(Narou.get_aozoraepub3_path), "kindlegen")
@@ -256,6 +259,60 @@ class NovelConverter
256
259
  :success
257
260
  end
258
261
 
262
+ #
263
+ # 変換された整形済みテキストファイルをデバイスに対応した書籍データに変換する
264
+ #
265
+ def self.convert_txt_to_ebook_file(txt_path, options)
266
+ options = {
267
+ use_dakuten_font: false,
268
+ dst_dir: nil,
269
+ device: nil,
270
+ verbose: false,
271
+ no_epub: false,
272
+ no_mobi: false,
273
+ no_strip: false
274
+ }.merge(options)
275
+
276
+ device = options[:device]
277
+
278
+ return false if options[:no_epub]
279
+ # epub
280
+ status = NovelConverter.txt_to_epub(txt_path, options[:use_dakuten_font],
281
+ options[:dst_dir], device, options[:verbose])
282
+ return nil if status != :success
283
+ if device && device.kobo?
284
+ epub_ext = device.ebook_file_ext
285
+ else
286
+ epub_ext = ".epub"
287
+ end
288
+ epub_path = txt_path.sub(/.txt$/, epub_ext)
289
+
290
+ if !device || !device.kindle? || options[:no_mobi]
291
+ puts File.basename(epub_path) + " を出力しました"
292
+ puts "<bold><green>EPUBファイルを出力しました</green></bold>".termcolor
293
+ return epub_path
294
+ end
295
+
296
+ # mobi
297
+ status = NovelConverter.epub_to_mobi(epub_path, options[:verbose])
298
+ return nil if status != :success
299
+ mobi_path = epub_path.sub(/\.epub$/, device.ebook_file_ext)
300
+
301
+ # strip
302
+ unless options[:no_strip]
303
+ puts "kindlestrip実行中"
304
+ begin
305
+ SectionStripper.strip(mobi_path, nil, false)
306
+ rescue StripException => e
307
+ error "#{e.message}"
308
+ end
309
+ end
310
+ puts File.basename(mobi_path).encode(Encoding::UTF_8) + " を出力しました"
311
+ puts "<bold><green>MOBIファイルを出力しました</green></bold>".termcolor
312
+
313
+ return mobi_path
314
+ end
315
+
259
316
  def initialize(setting, output_filename = nil, display_inspector = false)
260
317
  @setting = setting
261
318
  @novel_id = setting.id
@@ -266,24 +323,73 @@ class NovelConverter
266
323
  @illustration = Illustration.new(@setting, @inspector)
267
324
  @display_inspector = display_inspector
268
325
  @use_dakuten_font = false
326
+ @converter = create_converter
269
327
  end
270
328
 
271
- def load_novel_section(subtitle_info)
329
+ #
330
+ # 変換処理メインループ
331
+ #
332
+ def convert_main(text = nil)
333
+ display_header
334
+ initialize_event
335
+
336
+ if text
337
+ converted_text = convert_main_for_text(text)
338
+ else
339
+ converted_text = convert_main_for_novel
340
+ update_latest_convert_novel
341
+ end
342
+
343
+ inspect_novel(converted_text)
344
+
345
+ output_path = create_output_path(text, converted_text)
346
+ File.write(output_path, converted_text)
347
+
348
+ display_footer
349
+
350
+ output_path
351
+ end
352
+
353
+ def initialize_event
354
+ progressbar = nil
355
+
356
+ one(:"convert_main.init") do |subtitles|
357
+ progressbar = ProgressBar.new(subtitles.size)
358
+ end
359
+ on(:"convert_main.loop") do |i|
360
+ progressbar.output(i)
361
+ end
362
+ one(:"convert_main.finish") do
363
+ progressbar.clear
364
+ end
365
+ end
366
+
367
+ def display_header
368
+ print "ID:#{@novel_id} " if @novel_id
369
+ puts "#{@novel_title} の変換を開始"
370
+ end
371
+
372
+ def display_footer
373
+ puts "縦書用の変換が終了しました"
374
+ end
375
+
376
+ def load_novel_section(subtitle_info, section_save_dir)
272
377
  file_subtitle = subtitle_info["file_subtitle"] || subtitle_info["subtitle"] # 互換性維持のため
273
- path = File.join(@section_save_dir, "#{subtitle_info["index"]} #{file_subtitle}.yaml")
378
+ path = File.join(section_save_dir, "#{subtitle_info["index"]} #{file_subtitle}.yaml")
274
379
  YAML.load_file(path)
275
380
  end
276
381
 
277
- def create_novel_text_by_template(sections)
278
- toc = @toc
279
- cover_chuki = @cover_chuki
382
+ # is_hotentry を有効にすると、テンプレートで作成するテキストファイルに
383
+ # あらすじ、作品タイトル、本の読み終わり表示が付与されなくなる
384
+ def create_novel_text_by_template(sections, toc, is_hotentry = false)
385
+ cover_chuki = create_cover_chuki
280
386
  device = Narou.get_device
281
387
  setting = @setting
282
388
  processed_title = toc["title"]
283
389
  data = Database.instance.get_data("id", @novel_id)
284
390
  # タイトルに新着更新日を付加する
285
391
  if @setting.enable_add_date_to_title
286
- new_arrivals_date = data["new_arrivals_date"] || data["last_update"]
392
+ new_arrivals_date = data[@setting.title_date_target] || Time.now
287
393
  date_str = new_arrivals_date.strftime(@setting.title_date_format)
288
394
  if @setting.title_date_align == "left"
289
395
  processed_title = date_str + processed_title
@@ -300,7 +406,7 @@ class NovelConverter
300
406
  processed_title = processed_title.gsub("《", "※[#始め二重山括弧]")
301
407
  .gsub("》", "※[#終わり二重山括弧]")
302
408
  tempalte_name = (device && device.ibunko? ? NOVEL_TEXT_TEMPLATE_NAME_FOR_IBUNKO : NOVEL_TEXT_TEMPLATE_NAME)
303
- Template.get(tempalte_name, binding, 1.0)
409
+ Template.get(tempalte_name, binding, 1.1)
304
410
  end
305
411
 
306
412
  #
@@ -329,84 +435,111 @@ class NovelConverter
329
435
  end
330
436
  end
331
437
 
332
- def find_site_setting
333
- @@site_settings.find { |s| s.multi_match(@toc["toc_url"], "url") }
438
+ #
439
+ # 目次情報からサイト設定を取得
440
+ #
441
+ def find_site_setting(toc_url)
442
+ @@site_settings.find { |s| s.multi_match(toc_url, "url") }
334
443
  end
335
444
 
336
445
  #
337
- # 変換処理メイン
446
+ # 各小説用の converter.rb 変換オブジェクトを生成
338
447
  #
339
- def convert_main(text = nil)
340
- print "ID:#{@novel_id} " if @novel_id
341
- puts "#{@novel_title} の変換を開始"
342
- sections = []
343
- @cover_chuki = create_cover_chuki
344
-
345
- conv = load_converter(@novel_title, @setting.archive_path).new(@setting, @inspector, @illustration)
346
- if text
347
- result = conv.convert(text, "textfile")
348
- unless @setting.enable_enchant_midashi
349
- @inspector.info "テキストファイルの処理を実行しましたが、改行直後の見出し付与は有効になっていません。" +
350
- "setting.ini の enable_enchant_midashi を true にすることをお薦めします。"
351
- end
352
- splited = result.split("\n", 3)
353
- result = [splited[0], splited[1], @cover_chuki, splited[2]].join("\n") # 表紙の挿絵注記を3行目に挟み込む
354
- else
355
- @section_save_dir = Downloader.get_novel_section_save_dir(@setting.archive_path)
356
- @toc = Downloader.get_toc_data(@setting.archive_path)
357
- @toc["story"] = conv.convert(@toc["story"], "story")
358
- html = HTML.new
359
- site_setting = find_site_setting
360
- html.set_illust_setting({current_url: site_setting["illust_current_url"],
361
- grep_pattern: site_setting["illust_grep_pattern"]})
362
- progressbar = ProgressBar.new(@toc["subtitles"].size)
363
- @toc["subtitles"].each_with_index do |subinfo, i|
364
- progressbar.output(i)
365
- section = load_novel_section(subinfo)
366
- if section["chapter"].length > 0
367
- section["chapter"] = conv.convert(section["chapter"], "chapter")
368
- end
369
- @inspector.subtitle = section["subtitle"]
370
- element = section["element"]
371
- data_type = element.delete("data_type") || "text"
372
- element.each do |text_type, elm_text|
373
- if data_type == "html"
374
- html.string = elm_text
375
- elm_text = html.to_aozora
376
- end
377
- element[text_type] = conv.convert(elm_text, text_type)
378
- end
379
- section["subtitle"] = conv.convert(section["subtitle"], "subtitle")
380
- sections << section
381
- end
382
- progressbar.clear
383
- result = create_novel_text_by_template(sections)
384
- end
385
-
386
- @use_dakuten_font = conv.use_dakuten_font
387
-
388
- inspect_novel(result)
448
+ def create_converter
449
+ load_converter(@novel_title, @setting.archive_path).new(@setting, @inspector, @illustration)
450
+ end
389
451
 
452
+ #
453
+ # 最終的に出力するパスを生成
454
+ #
455
+ def create_output_path(is_text_file_mode, converted_text)
456
+ output_path = ""
390
457
  if @output_filename
391
- save_path = File.join(@setting.archive_path, File.basename(@output_filename))
458
+ output_path = File.join(@setting.archive_path, File.basename(@output_filename))
392
459
  else
393
- if text
394
- info = get_title_and_author_by_text(result)
460
+ if is_text_file_mode
461
+ info = get_title_and_author_by_text(converted_text)
395
462
  else
396
463
  info = { "author" => @novel_author, "title" => @novel_title }
397
464
  end
398
- save_filename = Narou.create_novel_filename(info)
399
- save_path = File.join(@setting.archive_path, save_filename)
400
- if save_path !~ /\.\w+$/
401
- save_path += ".txt"
465
+ filename = Narou.create_novel_filename(info)
466
+ output_path = File.join(@setting.archive_path, filename)
467
+ if output_path !~ /\.\w+$/
468
+ output_path += ".txt"
402
469
  end
403
470
  end
404
- File.write(save_path, result)
405
- puts "縦書用の変換が終了しました"
471
+ output_path
472
+ end
473
+
474
+ #
475
+ # テキストファイル変換時の実質的なメイン処理
476
+ #
477
+ def convert_main_for_text(text)
478
+ result = @converter.convert(text, "textfile")
479
+ unless @setting.enable_enchant_midashi
480
+ @inspector.info "テキストファイルの処理を実行しましたが、改行直後の見出し付与は有効になっていません。" +
481
+ "setting.ini の enable_enchant_midashi を true にすることをお薦めします。"
482
+ end
483
+ splited = result.split("\n", 3)
484
+ # 表紙の挿絵注記を3行目に挟み込む
485
+ converted_text = [splited[0], splited[1], create_cover_chuki, splited[2]].join("\n")
406
486
 
407
- update_latest_convert_novel
487
+ @use_dakuten_font = @converter.use_dakuten_font
408
488
 
409
- save_path
489
+ converted_text
490
+ end
491
+
492
+ #
493
+ # 管理小説変換時の実質的なメイン処理
494
+ #
495
+ # 引数 subtitles にデータを渡した場合はそれを直接使う
496
+ # is_hotentry を有効にすると出力されるテキストファイルにあらすじや作品タイトル等が含まれなくなる
497
+ #
498
+ def convert_main_for_novel(subtitles = nil, is_hotentry = false)
499
+ toc = Downloader.get_toc_data(@setting.archive_path)
500
+ toc["story"] = @converter.convert(toc["story"], "story")
501
+ html = HTML.new
502
+ site_setting = find_site_setting(toc["toc_url"])
503
+ html.set_illust_setting({current_url: site_setting["illust_current_url"],
504
+ grep_pattern: site_setting["illust_grep_pattern"]})
505
+
506
+ subtitles ||= toc["subtitles"]
507
+ sections = subtitles_to_sections(subtitles, html)
508
+ converted_text = create_novel_text_by_template(sections, toc, is_hotentry)
509
+
510
+ converted_text
511
+ end
512
+
513
+ #
514
+ # subtitle info から変換処理をする
515
+ #
516
+ def subtitles_to_sections(subtitles, html)
517
+ sections = []
518
+ section_save_dir = Downloader.get_novel_section_save_dir(@setting.archive_path)
519
+
520
+ trigger(:"convert_main.init", subtitles)
521
+ subtitles.each_with_index do |subinfo, i|
522
+ trigger(:"convert_main.loop", i)
523
+ section = load_novel_section(subinfo, section_save_dir)
524
+ if section["chapter"].length > 0
525
+ section["chapter"] = @converter.convert(section["chapter"], "chapter")
526
+ end
527
+ @inspector.subtitle = section["subtitle"]
528
+ element = section["element"]
529
+ data_type = element.delete("data_type") || "text"
530
+ element.each do |text_type, elm_text|
531
+ if data_type == "html"
532
+ html.string = elm_text
533
+ elm_text = html.to_aozora
534
+ end
535
+ element[text_type] = @converter.convert(elm_text, text_type)
536
+ end
537
+ section["subtitle"] = @converter.convert(section["subtitle"], "subtitle")
538
+ sections << section
539
+ end
540
+ trigger(:"convert_main.finish")
541
+ @use_dakuten_font = @converter.use_dakuten_font
542
+ sections
410
543
  end
411
544
 
412
545
  #
@@ -418,9 +551,10 @@ class NovelConverter
418
551
  end
419
552
 
420
553
  def inspect_novel(text)
421
- # 行末読点の現在状況を調査する
422
- @inspector.inspect_end_touten_conditions(text)
423
- @inspector.countup_return_in_brackets(text)
554
+ if @setting.enable_inspect
555
+ @inspector.inspect_end_touten_conditions(text) # 行末読点の現在状況を調査する
556
+ @inspector.countup_return_in_brackets(text) # カギ括弧内の改行状況を調査する
557
+ end
424
558
 
425
559
  if !@display_inspector
426
560
  unless @inspector.empty?