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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/ChangeLog.md +56 -0
- data/README.md +56 -29
- data/lib/command/convert.rb +39 -7
- data/lib/command/diff.rb +42 -11
- data/lib/command/inspect.rb +1 -1
- data/lib/command/setting.rb +150 -45
- data/lib/command/tag.rb +8 -8
- data/lib/command/update.rb +57 -1
- data/lib/commandbase.rb +3 -0
- data/lib/converterbase.rb +17 -9
- data/lib/database.rb +4 -0
- data/lib/device/epub.rb +1 -1
- data/lib/device/ibooks.rb +1 -1
- data/lib/device/ibunko.rb +13 -6
- data/lib/device/kindle.rb +1 -1
- data/lib/device/kobo.rb +1 -1
- data/lib/device/reader.rb +1 -1
- data/lib/downloader.rb +10 -5
- data/lib/helper.rb +114 -3
- data/lib/ini.rb +3 -1
- data/lib/inventory.rb +3 -1
- data/lib/loadconverter.rb +1 -11
- data/lib/mailer.rb +1 -0
- data/lib/narou.rb +56 -5
- data/lib/novelconverter.rb +7 -5
- data/lib/novelsetting.rb +116 -63
- data/lib/template.rb +4 -4
- data/lib/version.rb +1 -1
- data/lib/web/appserver.rb +40 -9
- data/lib/web/public/resources/narou.library.js +35 -3
- data/lib/web/public/resources/narou.ui.js +16 -1
- data/lib/web/pushserver.rb +1 -0
- data/lib/web/settingmessages.rb +6 -3
- data/lib/web/views/diff_list.haml +11 -0
- data/lib/web/views/edit_replace_txt.haml +59 -0
- data/lib/web/views/help.haml +2 -2
- data/lib/web/views/index.haml +6 -4
- data/lib/web/views/layout.haml +11 -2
- data/lib/web/views/novels/setting.haml +51 -66
- data/lib/web/views/settings.haml +52 -15
- data/lib/web/views/style.scss +44 -2
- data/lib/web/views/widget.haml +6 -8
- data/narou.gemspec +45 -6
- data/spec/convert_spec.rb +1 -1
- data/spec/converterbase_spec.rb +25 -1
- data/spec/generator/convert_spec_gen.rb +1 -1
- data/spec/helper_spec.rb +8 -0
- data/spec/novelsetting_spec.rb +1 -1
- data/template/novel.txt.erb +1 -1
- data/template/setting.ini.erb +15 -3
- metadata +49 -8
data/lib/command/tag.rb
CHANGED
@@ -171,21 +171,21 @@ module Command
|
|
171
171
|
end
|
172
172
|
|
173
173
|
def self.get_color(tagname)
|
174
|
-
|
175
|
-
color =
|
174
|
+
tag_colors = Inventory.load("tag_colors", :local)
|
175
|
+
color = tag_colors[tagname]
|
176
176
|
return color if color
|
177
|
-
last_color =
|
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
|
-
|
181
|
-
|
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
|
-
|
187
|
-
|
188
|
-
|
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
|
data/lib/command/update.rb
CHANGED
@@ -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
|
data/lib/commandbase.rb
CHANGED
@@ -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
|
data/lib/converterbase.rb
CHANGED
@@ -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=\"#{
|
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 << "
|
1176
|
-
|
1176
|
+
buffer << "#{ss.matched}"
|
1177
|
+
else
|
1178
|
+
before_symbol = false
|
1177
1179
|
end
|
1178
|
-
|
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 << "
|
1230
|
-
|
1232
|
+
buffer << "#{ss.matched}"
|
1233
|
+
else
|
1234
|
+
before_symbol = false
|
1231
1235
|
end
|
1232
|
-
|
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
|
data/lib/database.rb
CHANGED
data/lib/device/epub.rb
CHANGED
data/lib/device/ibooks.rb
CHANGED
@@ -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
|
-
"
|
17
|
+
"default.enable_half_indent_bracket" => false,
|
18
18
|
}
|
19
19
|
|
20
20
|
def hook_change_settings(&original_func)
|
data/lib/device/ibunko.rb
CHANGED
@@ -12,8 +12,8 @@ module Device::Ibunko
|
|
12
12
|
DISPLAY_NAME = "i文庫"
|
13
13
|
|
14
14
|
RELATED_VARIABLES = {
|
15
|
-
"
|
16
|
-
"
|
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
|
35
|
-
|
36
|
-
|
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
|
# 表紙画像
|
data/lib/device/kindle.rb
CHANGED
data/lib/device/kobo.rb
CHANGED
data/lib/device/reader.rb
CHANGED
data/lib/downloader.rb
CHANGED
@@ -729,14 +729,14 @@ class Downloader
|
|
729
729
|
info = NovelInfo.load(@setting)
|
730
730
|
end
|
731
731
|
if info
|
732
|
-
raise
|
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
|
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
|
-
|
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
|
-
|
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
|
data/lib/helper.rb
CHANGED
@@ -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("
|
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
|
|