narou 2.4.2 → 2.5.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of narou might be problematic. Click here for more details.

Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/ChangeLog.md +56 -0
  4. data/README.md +56 -29
  5. data/lib/command/convert.rb +39 -7
  6. data/lib/command/diff.rb +42 -11
  7. data/lib/command/inspect.rb +1 -1
  8. data/lib/command/setting.rb +150 -45
  9. data/lib/command/tag.rb +8 -8
  10. data/lib/command/update.rb +57 -1
  11. data/lib/commandbase.rb +3 -0
  12. data/lib/converterbase.rb +17 -9
  13. data/lib/database.rb +4 -0
  14. data/lib/device/epub.rb +1 -1
  15. data/lib/device/ibooks.rb +1 -1
  16. data/lib/device/ibunko.rb +13 -6
  17. data/lib/device/kindle.rb +1 -1
  18. data/lib/device/kobo.rb +1 -1
  19. data/lib/device/reader.rb +1 -1
  20. data/lib/downloader.rb +10 -5
  21. data/lib/helper.rb +114 -3
  22. data/lib/ini.rb +3 -1
  23. data/lib/inventory.rb +3 -1
  24. data/lib/loadconverter.rb +1 -11
  25. data/lib/mailer.rb +1 -0
  26. data/lib/narou.rb +56 -5
  27. data/lib/novelconverter.rb +7 -5
  28. data/lib/novelsetting.rb +116 -63
  29. data/lib/template.rb +4 -4
  30. data/lib/version.rb +1 -1
  31. data/lib/web/appserver.rb +40 -9
  32. data/lib/web/public/resources/narou.library.js +35 -3
  33. data/lib/web/public/resources/narou.ui.js +16 -1
  34. data/lib/web/pushserver.rb +1 -0
  35. data/lib/web/settingmessages.rb +6 -3
  36. data/lib/web/views/diff_list.haml +11 -0
  37. data/lib/web/views/edit_replace_txt.haml +59 -0
  38. data/lib/web/views/help.haml +2 -2
  39. data/lib/web/views/index.haml +6 -4
  40. data/lib/web/views/layout.haml +11 -2
  41. data/lib/web/views/novels/setting.haml +51 -66
  42. data/lib/web/views/settings.haml +52 -15
  43. data/lib/web/views/style.scss +44 -2
  44. data/lib/web/views/widget.haml +6 -8
  45. data/narou.gemspec +45 -6
  46. data/spec/convert_spec.rb +1 -1
  47. data/spec/converterbase_spec.rb +25 -1
  48. data/spec/generator/convert_spec_gen.rb +1 -1
  49. data/spec/helper_spec.rb +8 -0
  50. data/spec/novelsetting_spec.rb +1 -1
  51. data/template/novel.txt.erb +1 -1
  52. data/template/setting.ini.erb +15 -3
  53. metadata +49 -8
@@ -171,21 +171,21 @@ module Command
171
171
  end
172
172
 
173
173
  def self.get_color(tagname)
174
- @@tag_colors ||= Inventory.load("tag_colors", :local)
175
- color = @@tag_colors[tagname]
174
+ tag_colors = Inventory.load("tag_colors", :local)
175
+ color = tag_colors[tagname]
176
176
  return color if color
177
- last_color = @@tag_colors.values.last || COLORS.last
177
+ last_color = tag_colors.values.last || COLORS.last
178
178
  index = (COLORS.index(last_color) + 1) % COLORS.size
179
179
  color = COLORS[index]
180
- @@tag_colors[tagname] = color
181
- @@tag_colors.save
180
+ tag_colors[tagname] = color
181
+ tag_colors.save
182
182
  color
183
183
  end
184
184
 
185
185
  def set_color(tagname, color)
186
- @@tag_colors ||= Inventory.load("tag_colors", :local)
187
- @@tag_colors[tagname] = color
188
- @@tag_colors.save
186
+ tag_colors = Inventory.load("tag_colors", :local)
187
+ tag_colors[tagname] = color
188
+ tag_colors.save
189
189
  end
190
190
  end
191
191
  end
@@ -3,11 +3,14 @@
3
3
  # Copyright 2013 whiteleaf. All rights reserved.
4
4
  #
5
5
 
6
+ require "memoist"
6
7
  require_relative "../database"
7
8
  require_relative "../downloader"
8
9
 
9
10
  module Command
10
11
  class Update < CommandBase
12
+ extend Memoist
13
+
11
14
  LOG_DIR_NAME = "log"
12
15
  LOG_NUM_LIMIT = 30 # ログの保存する上限数
13
16
  LOG_FILENAME_FORMAT = "update_log_%s.txt"
@@ -53,6 +56,47 @@ module Command
53
56
  update_general_lastup
54
57
  exit 0
55
58
  }
59
+ @opt.on("-s", "--sort-by KEY", "アップデートする順番を変更する\n#{Narou.update_sort_key_summaries}") { |key|
60
+ @options["sort-by"] = key
61
+ }
62
+ end
63
+
64
+ def get_data_value(target, key)
65
+ data = Downloader.get_data_by_target(target) or return nil
66
+ value = data[key]
67
+ value ? value : Time.new(0)
68
+ end
69
+ memoize :get_data_value
70
+
71
+ #
72
+ # 項目名でアップデート対象をソートする
73
+ #
74
+ # key に偽を渡した場合はソートしない
75
+ #
76
+ def sort_by_key(key, list)
77
+ return list unless key
78
+ list.sort { |a, b|
79
+ value_a, value_b = [a, b].map { |target|
80
+ get_data_value(target, key)
81
+ }
82
+ if value_a.nil? && !value_b.nil?
83
+ next 1
84
+ elsif !value_a.nil? && value_b.nil?
85
+ next -1
86
+ elsif value_a.nil? && value_b.nil?
87
+ next 0
88
+ end
89
+ # 日付系は降順にする
90
+ if value_a.class == Time
91
+ value_b <=> value_a
92
+ else
93
+ value_a <=> value_b
94
+ end
95
+ }
96
+ end
97
+
98
+ def valid_sort_key?(key)
99
+ Narou::UPDATE_SORT_KEYS.keys.include?(key)
56
100
  end
57
101
 
58
102
  def execute(argv)
@@ -67,8 +111,20 @@ module Command
67
111
  no_open = true
68
112
  end
69
113
  tagname_to_ids(update_target_list)
114
+
115
+ sort_key = @options["sort-by"]
116
+ if sort_key
117
+ sort_key.downcase!
118
+ unless valid_sort_key?(sort_key)
119
+ error "#{sort_key} は正しいキーではありません。次の中から選択して下さい\n " \
120
+ "#{Narou.update_sort_key_summaries(17)}"
121
+ exit Narou::EXIT_ERROR_CODE
122
+ end
123
+ end
124
+ flush_cache # memoist のキャッシュ削除
125
+
70
126
  update_log = $stdout.capture(quiet: false) do
71
- update_target_list.each_with_index do |target, i|
127
+ sort_by_key(sort_key, update_target_list).each_with_index do |target, i|
72
128
  display_message = nil
73
129
  data = Downloader.get_data_by_target(target)
74
130
  if !data
@@ -48,6 +48,9 @@ module Command
48
48
  rescue OptionParser::MissingArgument => e
49
49
  error "オプションの引数が指定されていないか正しくありません(#{e})"
50
50
  exit Narou::EXIT_ERROR_CODE
51
+ rescue OptionParser::AmbiguousOption => e
52
+ error "曖昧な省略オプションです(#{e})"
53
+ exit Narou::EXIT_ERROR_CODE
51
54
  end
52
55
 
53
56
  def load_local_settings
@@ -952,7 +952,7 @@ class ConverterBase
952
952
  def rebuild_url(data)
953
953
  @url_list.each_with_index do |url, id|
954
954
  data.sub!("[#URL=#{convert_numbers(id.to_s)}]",
955
- "<a href=\"#{Helper.ampersand_to_entity(url)}\">#{url}</a>")
955
+ "<a href=\"#{url}\">#{url}</a>")
956
956
  end
957
957
  end
958
958
 
@@ -1171,11 +1171,13 @@ class ConverterBase
1171
1171
  when "|"
1172
1172
  ss.scan(/.+?》/)
1173
1173
  when "["
1174
+ buffer << char
1174
1175
  if ss.scan(/^#.+?]/)
1175
- buffer << "[#{ss.matched}"
1176
- next
1176
+ buffer << "#{ss.matched}"
1177
+ else
1178
+ before_symbol = false
1177
1179
  end
1178
- symbol = true
1180
+ next
1179
1181
  when "<"
1180
1182
  if ss.scan(/.+?>/)
1181
1183
  buffer << "<#{ss.matched}"
@@ -1189,7 +1191,7 @@ class ConverterBase
1189
1191
  when /[ァ-ヶ]/
1190
1192
  ss.scan(/[ァ-ヶー・]+/)
1191
1193
  when /[A-Za-zA-Za-z]/
1192
- ss.scan(/[A-Za-zA-Za-z]+/)
1194
+ ss.scan(/[A-Za-zA-Za-z ]+/)
1193
1195
  when /[一-龥朗-鶴]/
1194
1196
  ss.scan(/[一-龥朗-鶴]+/)
1195
1197
  when /[〔「『\((【〈《≪〝]/
@@ -1225,17 +1227,23 @@ class ConverterBase
1225
1227
  when "|"
1226
1228
  ss.scan(/.+?》/)
1227
1229
  when "["
1230
+ buffer << char
1228
1231
  if ss.scan(/^#.+?]/)
1229
- buffer << "[#{ss.matched}"
1230
- next
1232
+ buffer << "#{ss.matched}"
1233
+ else
1234
+ before_symbol = false
1231
1235
  end
1232
- symbol = true
1236
+ next
1233
1237
  when "<"
1234
1238
  if ss.scan(/.+?>/)
1235
1239
  buffer << "<#{ss.matched}"
1236
1240
  next
1237
1241
  end
1238
1242
  symbol = true
1243
+ when /[〔「『\((【〈《≪〝]/
1244
+ buffer << char
1245
+ before_symbol = false
1246
+ next
1239
1247
  when /[―…!?!?※]/
1240
1248
  symbol = true
1241
1249
  end
@@ -1362,7 +1370,7 @@ class ConverterBase
1362
1370
  #
1363
1371
  def replace_by_replace_txt(text)
1364
1372
  result = text.dup
1365
- @setting.replace_pattern.each do |pattern|
1373
+ (@setting.replace_pattern + Narou.global_replace_pattern).each do |pattern|
1366
1374
  src, dst = pattern
1367
1375
  result.gsub!(src, dst)
1368
1376
  end
@@ -48,6 +48,10 @@ class Database
48
48
  end
49
49
 
50
50
  def initialize
51
+ refresh
52
+ end
53
+
54
+ def refresh
51
55
  @database = Inventory.load(DATABASE_NAME, :local)
52
56
  end
53
57
 
@@ -15,6 +15,6 @@ module Device::Epub
15
15
  DISPLAY_NAME = "EPUB"
16
16
 
17
17
  RELATED_VARIABLES = {
18
- "force.enable_half_indent_bracket" => false,
18
+ "default.enable_half_indent_bracket" => false,
19
19
  }
20
20
  end
@@ -14,7 +14,7 @@ module Device::Ibooks
14
14
  IBOOKS_CONTAINER_DIR = "~/Library/Containers/com.apple.BKAgentService/Data/Documents/iBooks/Books"
15
15
 
16
16
  RELATED_VARIABLES = {
17
- "force.enable_half_indent_bracket" => false,
17
+ "default.enable_half_indent_bracket" => false,
18
18
  }
19
19
 
20
20
  def hook_change_settings(&original_func)
@@ -12,8 +12,8 @@ module Device::Ibunko
12
12
  DISPLAY_NAME = "i文庫"
13
13
 
14
14
  RELATED_VARIABLES = {
15
- "force.enable_half_indent_bracket" => false,
16
- "force.enable_dakuten_font" => false
15
+ "default.enable_half_indent_bracket" => false,
16
+ "default.enable_dakuten_font" => false
17
17
  }
18
18
 
19
19
  #
@@ -23,17 +23,24 @@ module Device::Ibunko
23
23
  return false if @options["no-zip"]
24
24
  require "zip"
25
25
  Zip.unicode_names = true
26
+ # TODO: テキストファイル変換時もsettingを取れるようにする
27
+ setting = {}
28
+ if @novel_data
29
+ setting = NovelSetting.load(@novel_data["id"], @options["ignore-force"], @options["ignore-default"])
30
+ end
26
31
  dirpath = File.dirname(@converted_txt_path)
27
32
  translate_illust_chuki_to_img_tag
28
33
  zipfile_path = @converted_txt_path.sub(/.txt$/, @device.ebook_file_ext)
29
34
  File.delete(zipfile_path) if File.exist?(zipfile_path)
30
35
  Zip::File.open(zipfile_path, Zip::File::CREATE) do |zip|
31
36
  zip.add(File.basename(@converted_txt_path), @converted_txt_path)
32
- illust_dirpath = File.join(dirpath, Illustration::ILLUST_DIR)
33
37
  # 挿絵
34
- if File.exist?(illust_dirpath)
35
- Dir.glob(File.join(illust_dirpath, "*")) do |img_path|
36
- zip.add(File.join(Illustration::ILLUST_DIR, File.basename(img_path)), img_path)
38
+ if setting["enable_illust"]
39
+ illust_dirpath = File.join(dirpath, Illustration::ILLUST_DIR)
40
+ if File.exist?(illust_dirpath)
41
+ Dir.glob(File.join(illust_dirpath, "*")) do |img_path|
42
+ zip.add(File.join(Illustration::ILLUST_DIR, File.basename(img_path)), img_path)
43
+ end
37
44
  end
38
45
  end
39
46
  # 表紙画像
@@ -14,7 +14,7 @@ module Device::Kindle
14
14
  DISPLAY_NAME = "Kindle"
15
15
 
16
16
  RELATED_VARIABLES = {
17
- "force.enable_half_indent_bracket" => true,
17
+ "default.enable_half_indent_bracket" => true,
18
18
  }
19
19
 
20
20
  include Device::BackupBookmarkUtility
@@ -12,6 +12,6 @@ module Device::Kobo
12
12
  DISPLAY_NAME = "Kobo"
13
13
 
14
14
  RELATED_VARIABLES = {
15
- "force.enable_half_indent_bracket" => false,
15
+ "default.enable_half_indent_bracket" => false,
16
16
  }
17
17
  end
@@ -12,6 +12,6 @@ module Device::Reader
12
12
  DISPLAY_NAME = "SonyReader"
13
13
 
14
14
  RELATED_VARIABLES = {
15
- "force.enable_half_indent_bracket" => false,
15
+ "default.enable_half_indent_bracket" => false,
16
16
  }
17
17
  end
@@ -729,14 +729,14 @@ class Downloader
729
729
  info = NovelInfo.load(@setting)
730
730
  end
731
731
  if info
732
- raise DownloaderHTTP404Error unless info["title"]
732
+ raise DownloaderNotFoundError unless info["title"]
733
733
  @setting["title"] = info["title"]
734
734
  @setting["author"] = info["writer"]
735
735
  @setting["story"] = info["story"]
736
736
  else
737
737
  # 小説情報ページがないサイトの場合は目次ページから取得する
738
738
  @setting.multi_match(toc_source, "title", "author", "story")
739
- raise DownloaderHTTP404Error unless @setting.matched?("title")
739
+ raise DownloaderNotFoundError unless @setting.matched?("title")
740
740
  @setting["story"] = HTML.new(@setting["story"]).to_aozora
741
741
  end
742
742
  @setting["info"] = info
@@ -1193,10 +1193,15 @@ class Downloader
1193
1193
  novel_dir_path = get_novel_data_dir
1194
1194
  file_title = File.basename(novel_dir_path)
1195
1195
  FileUtils.mkdir_p(novel_dir_path) unless File.exist?(novel_dir_path)
1196
- default_settings = NovelSetting::DEFAULT_SETTINGS
1196
+ original_settings = NovelSetting::ORIGINAL_SETTINGS
1197
1197
  special_preset_dir = File.join(Narou.get_preset_dir, @setting["domain"], @setting["ncode"])
1198
1198
  exists_special_preset_dir = File.exist?(special_preset_dir)
1199
- [NovelSetting::INI_NAME, "converter.rb", NovelSetting::REPLACE_NAME].each do |filename|
1199
+ templates = [
1200
+ [NovelSetting::INI_NAME, 1.1],
1201
+ ["converter.rb", 1.0],
1202
+ [NovelSetting::REPLACE_NAME, 1.0]
1203
+ ]
1204
+ templates.each do |(filename, binary_version)|
1200
1205
  if exists_special_preset_dir
1201
1206
  preset_file_path = File.join(special_preset_dir, filename)
1202
1207
  if File.exist?(preset_file_path)
@@ -1206,7 +1211,7 @@ class Downloader
1206
1211
  next
1207
1212
  end
1208
1213
  end
1209
- Template.write(filename, novel_dir_path, binding)
1214
+ Template.write(filename, novel_dir_path, binding, binary_version)
1210
1215
  end
1211
1216
  end
1212
1217
  end
@@ -4,6 +4,7 @@
4
4
  #
5
5
 
6
6
  require "open3"
7
+ require "time"
7
8
 
8
9
  #
9
10
  # 雑多なお助けメソッド群
@@ -174,7 +175,7 @@ module Helper
174
175
  "整数 "
175
176
  when :float
176
177
  "小数点数 "
177
- when :string
178
+ when :string, :select, :multiple
178
179
  "文字列 "
179
180
  when :directory
180
181
  "フォルダパス"
@@ -218,7 +219,7 @@ module Helper
218
219
  else
219
220
  raise InvalidVariableType, type
220
221
  end
221
- when :string
222
+ when :string, :select, :multiple
222
223
  result = value
223
224
  else
224
225
  raise UnknownVariableType, type
@@ -261,7 +262,26 @@ module Helper
261
262
  # 日付形式の文字列をTime型に変換する
262
263
  #
263
264
  def date_string_to_time(date)
264
- date ? Time.parse(date.sub(/[\((].+?[\))]/, "").tr("年月日時分秒", "///:::")) : nil
265
+ date ? Time.parse(date.sub(/[\((].+?[\))]/, "").tr("年月日時分秒@;", "///::: :")) : nil
266
+ end
267
+
268
+ #
269
+ # 指定のファイルが前回のチェック時より新しいかどうか
270
+ #
271
+ # 初回チェック時は無条件で新しいと判定
272
+ #
273
+ def file_latest?(path)
274
+ @@file_mtime_list ||= {}
275
+ fullpath = File.expand_path(path)
276
+ last_mtime = @@file_mtime_list[fullpath]
277
+ mtime = File.mtime(fullpath)
278
+ if mtime == last_mtime
279
+ result = false
280
+ else
281
+ result = true
282
+ @@file_mtime_list[fullpath] = mtime
283
+ end
284
+ result
265
285
  end
266
286
 
267
287
  #
@@ -302,5 +322,96 @@ module Helper
302
322
  }
303
323
  end
304
324
  end
325
+
326
+ #
327
+ # 更新時刻を考慮したファイルのローダー
328
+ #
329
+ module CacheLoader
330
+ module_function
331
+
332
+ @@mutex = Mutex.new
333
+ @@caches = {}
334
+ @@result_caches = {}
335
+
336
+ DEFAULT_OPTIONS = { mode: "r:BOM|UTF-8" }
337
+
338
+ #
339
+ # ファイルの更新時刻を考慮してファイルのデータを取得する。
340
+ # 前回取得した時からファイルが変更されていない場合は、キャッシュを返す
341
+ #
342
+ # options にはファイルを読み込む時に File.read に渡すオプションを指定できる
343
+ #
344
+ def load(path, options = DEFAULT_OPTIONS)
345
+ @@mutex.synchronize do
346
+ fullpath = File.expand_path(path)
347
+ cache_data = @@caches[fullpath]
348
+ if Helper.file_latest?(fullpath) || !cache_data
349
+ body = File.read(fullpath, options)
350
+ @@caches[fullpath] = body
351
+ return body
352
+ else
353
+ return cache_data
354
+ end
355
+ end
356
+ end
357
+
358
+ #
359
+ # ファイルを処理するブロックの結果をキャッシュ化する
360
+ #
361
+ # CacheLoader.load がファイルの中身だけをキャッシュ化するのに対して
362
+ # これはブロックの結果をキャッシュする。ファイルが更新されない限り、
363
+ # ブロックの結果は変わらない
364
+ #
365
+ # ex.)
366
+ # Helper::CacheLoader.memo("filepath") do |data|
367
+ # # data に関する処理
368
+ # result # ここで nil を返すと次回も再度読み込まれる
369
+ # end
370
+ #
371
+ def memo(path, options = DEFAULT_OPTIONS, &block)
372
+ @@mutex.synchronize do
373
+ fail ArgumentError, "need a block" unless block
374
+ fullpath = File.expand_path(path)
375
+ key = generate_key(fullpath, block)
376
+ cache = @@result_caches[key]
377
+ if Helper.file_latest?(fullpath) || !cache
378
+ data = File.read(fullpath, options)
379
+ @@result_caches[key] = result = block.call(data)
380
+ return result
381
+ else
382
+ return cache
383
+ end
384
+ end
385
+ end
386
+
387
+ #
388
+ # キャッシュを格納する際に必要なキーを生成する
389
+ #
390
+ # ブロックはその場所が実行されるたびに違うprocオブジェクトが生成されるため、
391
+ # 同一性判定のために「どのソース」の「何行目」かで判定を行う
392
+ #
393
+ def generate_key(fullpath, block)
394
+ src, line = block.source_location
395
+ "#{fullpath}:#{src}:#{line}"
396
+ end
397
+
398
+ #
399
+ # 指定したファイルのキャッシュを削除する
400
+ #
401
+ # path を指定しなかった場合、全てのキャッシュを削除する
402
+ #
403
+ def clear(path = nil)
404
+ @@mutex.synchronize do
405
+ if path
406
+ fullpath = File.expand_path(path)
407
+ @@cache.delete(fullpath)
408
+ @@result_caches.delete(fullpath)
409
+ else
410
+ @@cache.clear
411
+ @@result_caches.clear
412
+ end
413
+ end
414
+ end
415
+ end
305
416
  end
306
417