narou 1.5.11 → 1.6.0
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 +4 -0
- data/ChangeLog.md +619 -432
- data/Gemfile +9 -0
- data/README.md +101 -76
- data/lib/color.rb +62 -22
- data/lib/command.rb +21 -19
- data/lib/command/alias.rb +9 -9
- data/lib/command/backup.rb +11 -4
- data/lib/command/browser.rb +8 -7
- data/lib/command/convert.rb +25 -77
- data/lib/command/diff.rb +67 -36
- data/lib/command/download.rb +5 -4
- data/lib/command/flag.rb +32 -6
- data/lib/command/folder.rb +7 -5
- data/lib/command/freeze.rb +21 -11
- data/lib/command/help.rb +29 -17
- data/lib/command/init.rb +7 -7
- data/lib/command/inspect.rb +6 -5
- data/lib/command/list.rb +106 -25
- data/lib/command/mail.rb +6 -7
- data/lib/command/remove.rb +9 -7
- data/lib/command/send.rb +6 -5
- data/lib/command/setting.rb +51 -16
- data/lib/command/tag.rb +179 -0
- data/lib/command/update.rb +9 -8
- data/lib/command/version.rb +4 -4
- data/lib/commandbase.rb +79 -8
- data/lib/commandline.rb +14 -8
- data/lib/converterbase.rb +78 -28
- data/lib/database.rb +3 -3
- data/lib/device.rb +26 -13
- data/lib/device/ibooks.rb +116 -0
- data/lib/device/ibunko.rb +45 -0
- data/lib/device/kindle.rb +4 -0
- data/lib/device/kobo.rb +4 -0
- data/lib/device/library/windows.rb +12 -4
- data/lib/device/reader.rb +9 -0
- data/lib/diffviewer.rb +147 -0
- data/lib/downloader.rb +195 -80
- data/lib/extensions/jruby.rb +33 -0
- data/lib/extensions/windows.rb +8 -9
- data/lib/helper.rb +79 -37
- data/lib/illustration.rb +1 -1
- data/lib/inspector.rb +2 -2
- data/lib/inventory.rb +48 -0
- data/lib/logger.rb +28 -4
- data/lib/narou.rb +9 -7
- data/lib/narou/api.rb +7 -2
- data/lib/novelconverter.rb +46 -19
- data/lib/novelinfo.rb +13 -3
- data/lib/novelsetting.rb +17 -9
- data/lib/version.rb +1 -1
- data/narou.gemspec +106 -27
- data/narou.rb +2 -2
- data/spec/convert_spec.rb +102 -0
- data/spec/data/convert_test/auto_indent/correct_test_auto_indent.txt +18 -0
- data/spec/data/convert_test/auto_indent/test_auto_indent.txt +21 -0
- data/spec/data/convert_test/auto_join_bracket/correct_test_auto_join_bracket.txt +12 -0
- data/spec/data/convert_test/auto_join_bracket/test_auto_join_bracket.txt +16 -0
- data/spec/data/convert_test/auto_join_line/correct_test_auto_join_line.txt +36 -0
- data/spec/data/convert_test/auto_join_line/test_auto_join_line.txt +51 -0
- data/spec/data/convert_test/convert_page_break/correct_test_convert_page_break.txt +21 -0
- data/spec/data/convert_test/convert_page_break/setting.ini +5 -0
- data/spec/data/convert_test/convert_page_break/test_convert_page_break.txt +60 -0
- data/spec/data/convert_test/force_indent_special_chapter/correct_test_force_indent_special_chapter.txt +64 -0
- data/spec/data/convert_test/force_indent_special_chapter/test_force_indent_special_chapter.txt +61 -0
- data/spec/data/convert_test/horizontal_ellipsis/correct_test_horizontal_ellipsis.txt +52 -0
- data/spec/data/convert_test/horizontal_ellipsis/test_horizontal_ellipsis.txt +57 -0
- data/spec/data/convert_test/kanji_num/correct_test_kanji_num.txt +49 -0
- data/spec/data/convert_test/kanji_num/test_kanji_num.txt +56 -0
- data/spec/data/convert_test/nonokagi/correct_test_nonokagi.txt +29 -0
- data/spec/data/convert_test/nonokagi/test_nonokagi.txt +34 -0
- data/spec/data/convert_test/replace/correct_test_replace.txt +12 -0
- data/spec/data/convert_test/replace/replace.txt +7 -0
- data/spec/data/convert_test/replace/test_replace.txt +14 -0
- data/spec/data/convert_test/ruby/correct_test_ruby.txt +131 -0
- data/spec/data/convert_test/ruby/test_ruby.txt +170 -0
- data/spec/data/convert_test/ruby_youon/correct_test_ruby_youon.txt +12 -0
- data/spec/data/convert_test/ruby_youon/setting.ini +2 -0
- data/spec/data/convert_test/ruby_youon/test_ruby_youon.txt +14 -0
- data/spec/data/convert_test/sesame/correct_test_sesame.txt +40 -0
- data/spec/data/convert_test/sesame/test_sesame.txt +52 -0
- data/spec/data/convert_test/to_odd_leader/correct_test_to_odd_leader.txt +20 -0
- data/spec/data/convert_test/to_odd_leader/test_to_odd_leader.txt +21 -0
- data/spec/device_spec.rb +3 -3
- data/spec/generator/convert_spec_gen.rb +95 -0
- data/spec/html_spec.rb +9 -8
- data/spec/ini_spec.rb +31 -31
- data/spec/novelinfo_spec.rb +2 -2
- data/spec/num_to_kanji_spec.rb +2 -2
- data/template/diff.txt.erb +5 -2
- data/template/ibunko_novel.txt.erb +2 -1
- data/template/novel.txt.erb +16 -3
- data/template/setting.ini.erb +1 -1
- data/webnovel/ncode.syosetu.com.yaml +7 -7
- data/webnovel/novel18.syosetu.com.yaml +7 -7
- data/webnovel/syosetu.org.yaml +8 -11
- metadata +193 -23
- data/lib/globalsetting.rb +0 -64
- data/lib/localsetting.rb +0 -63
data/lib/database.rb
CHANGED
@@ -7,7 +7,7 @@ require "fileutils"
|
|
7
7
|
require "singleton"
|
8
8
|
require "yaml"
|
9
9
|
require_relative "narou"
|
10
|
-
require_relative "
|
10
|
+
require_relative "inventory"
|
11
11
|
|
12
12
|
class Database
|
13
13
|
include Singleton
|
@@ -48,7 +48,7 @@ class Database
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def initialize
|
51
|
-
@database =
|
51
|
+
@database = Inventory.load(DATABASE_NAME, :local)
|
52
52
|
end
|
53
53
|
|
54
54
|
#
|
@@ -69,7 +69,7 @@ class Database
|
|
69
69
|
end
|
70
70
|
|
71
71
|
def save_database
|
72
|
-
|
72
|
+
@database.save
|
73
73
|
end
|
74
74
|
|
75
75
|
def get_object
|
data/lib/device.rb
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
#
|
5
5
|
|
6
6
|
require "fileutils"
|
7
|
+
require_relative "narou"
|
7
8
|
require_relative "helper"
|
8
9
|
|
9
10
|
class Device
|
@@ -51,12 +52,12 @@ class Device
|
|
51
52
|
|
52
53
|
def initialize(device_name)
|
53
54
|
unless Device.exists?(device_name)
|
54
|
-
raise UnknownDevice, "#{device_name}
|
55
|
+
raise UnknownDevice, "#{device_name} という端末は存在しません"
|
55
56
|
end
|
56
|
-
@
|
57
|
-
@name = @
|
58
|
-
@display_name = @
|
59
|
-
@ebook_file_ext = @
|
57
|
+
@device_module = DEVICES[device_name.downcase]
|
58
|
+
@name = @device_module::NAME
|
59
|
+
@display_name = @device_module::DISPLAY_NAME
|
60
|
+
@ebook_file_ext = @device_module::EBOOK_FILE_EXT
|
60
61
|
create_device_check_methods
|
61
62
|
end
|
62
63
|
|
@@ -65,7 +66,7 @@ class Device
|
|
65
66
|
end
|
66
67
|
|
67
68
|
def find_documents_directory(device_root_dir)
|
68
|
-
@
|
69
|
+
@device_module::DOCUMENTS_PATH_LIST.each do |documents_path|
|
69
70
|
documents_directory = File.join(device_root_dir, documents_path)
|
70
71
|
return documents_directory if File.directory?(documents_directory)
|
71
72
|
end
|
@@ -74,7 +75,7 @@ class Device
|
|
74
75
|
|
75
76
|
def get_documents_path
|
76
77
|
if Device.respond_to?(:get_device_root_dir)
|
77
|
-
dir = Device.get_device_root_dir(@
|
78
|
+
dir = Device.get_device_root_dir(@device_module::VOLUME_NAME)
|
78
79
|
if dir
|
79
80
|
return find_documents_directory(dir)
|
80
81
|
end
|
@@ -103,16 +104,16 @@ class Device
|
|
103
104
|
# Rubyでコピーするのは遅いのでOSのコマンドを叩く
|
104
105
|
cmd = "copy /B " + %!"#{src_file}" "#{dst_path}"!.gsub("/", "\\").encode(Encoding::Windows_31J)
|
105
106
|
capture = `#{cmd}`
|
106
|
-
|
107
|
+
unless $?.success?
|
107
108
|
raise capture.force_encoding(Encoding::Windows_31J).rstrip
|
108
109
|
end
|
109
|
-
rescue
|
110
|
-
#
|
110
|
+
rescue StandardError
|
111
|
+
# コマンドで送信出来ないものはRubyで直接コピーする
|
111
112
|
FileUtils.cp(src_file, dst_path)
|
112
113
|
end
|
113
114
|
else
|
114
|
-
|
115
|
-
raise
|
115
|
+
status = system(%!cp "#{src_file}" "#{dst_path}"!)
|
116
|
+
raise "コピーできませんでした" unless status
|
116
117
|
end
|
117
118
|
dst_path
|
118
119
|
else
|
@@ -125,7 +126,19 @@ class Device
|
|
125
126
|
end
|
126
127
|
|
127
128
|
def physical_support?
|
128
|
-
@
|
129
|
+
@device_module::PHYSICAL_SUPPORT
|
130
|
+
end
|
131
|
+
|
132
|
+
def get_hook_module
|
133
|
+
@device_module
|
134
|
+
end
|
135
|
+
|
136
|
+
def get_relative_variables
|
137
|
+
if @device_module.const_defined?(:RELATED_VARIABLES)
|
138
|
+
@device_module::RELATED_VARIABLES
|
139
|
+
else
|
140
|
+
{}
|
141
|
+
end
|
129
142
|
end
|
130
143
|
|
131
144
|
private
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright 2013 whiteleaf. All rights reserved.
|
4
|
+
#
|
5
|
+
|
6
|
+
module Device::Ibooks
|
7
|
+
PHYSICAL_SUPPORT = false
|
8
|
+
VOLUME_NAME = nil
|
9
|
+
DOCUMENTS_PATH_LIST = nil
|
10
|
+
EBOOK_FILE_EXT = ".epub"
|
11
|
+
NAME = "iBooks"
|
12
|
+
DISPLAY_NAME = "iBooks"
|
13
|
+
|
14
|
+
IBOOKS_CONTAINER_DIR = "~/Library/Containers/com.apple.BKAgentService/Data/Documents/iBooks/Books"
|
15
|
+
|
16
|
+
RELATED_VARIABLES = {
|
17
|
+
"force.enable_half_indent_bracket" => false,
|
18
|
+
"force.enable_add_date_to_title" => false, # タイトルを変えてもiBooksに反映されないため
|
19
|
+
}
|
20
|
+
|
21
|
+
def hook_change_settings(&original_func)
|
22
|
+
@@__already_exec_change_settings ||= false
|
23
|
+
return if @@__already_exec_change_settings
|
24
|
+
force_change_settings_function({
|
25
|
+
"force.enable_half_indent_bracket" => false,
|
26
|
+
})
|
27
|
+
@@__ibooks_container_dir = File.expand_path(IBOOKS_CONTAINER_DIR)
|
28
|
+
unless File.exists?(@@__ibooks_container_dir)
|
29
|
+
error "iBooksの管理フォルダが見つかりませんでした。" \
|
30
|
+
"MacOSX Mavericks以降のiBooksのみ管理に対応しています"
|
31
|
+
@@__ibooks_container_dir = nil
|
32
|
+
end
|
33
|
+
@@__already_exec_change_settings = true
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# EPUBへ変換したあとiBooksが管理しているディレクトリに展開する
|
38
|
+
#
|
39
|
+
def hook_convert_txt_to_ebook_file(&original_func)
|
40
|
+
ebook_file_path = original_func.call
|
41
|
+
return ebook_file_path unless ebook_file_path
|
42
|
+
return ebook_file_path unless @@__ibooks_container_dir
|
43
|
+
if @argument_target_type == :file
|
44
|
+
warn "テキストファイル変換時はiBooksへの登録は行われません"
|
45
|
+
return ebook_file_path
|
46
|
+
end
|
47
|
+
@toc_url = @novel_data["toc_url"]
|
48
|
+
epubdir_path = get_epubdir_path_in_ibooks_container
|
49
|
+
if epubdir_path && File.exists?(epubdir_path)
|
50
|
+
extract_epub(ebook_file_path, epubdir_path)
|
51
|
+
puts "iBooksに登録してあるEPUBを更新しました"
|
52
|
+
else
|
53
|
+
epubdir_path = watch_ibooks_container(ebook_file_path)
|
54
|
+
if epubdir_path
|
55
|
+
regist_epubdir_path_to_setting(epubdir_path)
|
56
|
+
puts "iBooksへの登録を確認しました"
|
57
|
+
else
|
58
|
+
error "EPUBの展開後のフォルダが見つかりませんでした。" \
|
59
|
+
"iBooksがインストールされているか確認して下さい"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
ebook_file_path
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_epubdir_path_in_ibooks_container
|
66
|
+
list = Inventory.load("ibooks_epubdir_path_list", :local)
|
67
|
+
if list[@toc_url]
|
68
|
+
list[@toc_url]
|
69
|
+
else
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def extract_epub(ebook_file_path, epubdir_path)
|
75
|
+
require "zip"
|
76
|
+
Zip.on_exists_proc = true
|
77
|
+
Zip::File.open(ebook_file_path) do |zip_file|
|
78
|
+
zip_file.each do |entry|
|
79
|
+
extract_path = File.join(epubdir_path, entry.name)
|
80
|
+
FileUtils.mkdir_p(File.dirname(extract_path))
|
81
|
+
entry.extract(extract_path)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def watch_ibooks_container(ebook_file_path)
|
87
|
+
just_before_list = get_ibooks_containing_epub_list
|
88
|
+
unless system(%!open -a iBooks "#{ebook_file_path}"!)
|
89
|
+
error "iBooksが開けませんでした"
|
90
|
+
return nil
|
91
|
+
end
|
92
|
+
limit = 15
|
93
|
+
found_path = nil
|
94
|
+
while limit > 0
|
95
|
+
sleep(1)
|
96
|
+
just_after_list = get_ibooks_containing_epub_list
|
97
|
+
differ = just_after_list - just_before_list
|
98
|
+
if differ.length == 1
|
99
|
+
found_path = differ[0]
|
100
|
+
break
|
101
|
+
end
|
102
|
+
limit -= 1
|
103
|
+
end
|
104
|
+
found_path
|
105
|
+
end
|
106
|
+
|
107
|
+
def get_ibooks_containing_epub_list
|
108
|
+
Dir.glob("#{@@__ibooks_container_dir}/*.epub/")
|
109
|
+
end
|
110
|
+
|
111
|
+
def regist_epubdir_path_to_setting(path)
|
112
|
+
list = Inventory.load("ibooks_epubdir_path_list", :local)
|
113
|
+
list[@toc_url] = path
|
114
|
+
list.save
|
115
|
+
end
|
116
|
+
end
|
data/lib/device/ibunko.rb
CHANGED
@@ -10,4 +10,49 @@ module Device::Ibunko
|
|
10
10
|
EBOOK_FILE_EXT = ".zip"
|
11
11
|
NAME = "iBunko"
|
12
12
|
DISPLAY_NAME = "i文庫"
|
13
|
+
|
14
|
+
RELATED_VARIABLES = {
|
15
|
+
"force.enable_half_indent_bracket" => false,
|
16
|
+
"force.enable_dakuten_font" => false
|
17
|
+
}
|
18
|
+
|
19
|
+
#
|
20
|
+
# i文庫用にテキストと挿絵ファイルをzipアーカイブ化する
|
21
|
+
#
|
22
|
+
def hook_convert_txt_to_ebook_file(&original_func)
|
23
|
+
return false if @options["no-zip"]
|
24
|
+
require "zip"
|
25
|
+
Zip.unicode_names = true
|
26
|
+
dirpath = File.dirname(@converted_txt_path)
|
27
|
+
translate_illust_chuki_to_img_tag
|
28
|
+
zipfile_path = @converted_txt_path.sub(/.txt$/, @device.ebook_file_ext)
|
29
|
+
File.delete(zipfile_path) if File.exists?(zipfile_path)
|
30
|
+
Zip::File.open(zipfile_path, Zip::File::CREATE) do |zip|
|
31
|
+
zip.add(File.basename(@converted_txt_path), @converted_txt_path)
|
32
|
+
illust_dirpath = File.join(dirpath, Illustration::ILLUST_DIR)
|
33
|
+
# 挿絵
|
34
|
+
if File.exists?(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)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
# 表紙画像
|
40
|
+
cover_name = NovelConverter.get_cover_filename(dirpath)
|
41
|
+
if cover_name
|
42
|
+
zip.add(cover_name, File.join(dirpath, cover_name))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
puts File.basename(zipfile_path) + " を出力しました"
|
46
|
+
puts "<bold><green>#{@device.display_name}用ファイルを出力しました</green></bold>".termcolor
|
47
|
+
zipfile_path
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# 挿絵注記をimgタグに変換する
|
52
|
+
#
|
53
|
+
def translate_illust_chuki_to_img_tag
|
54
|
+
data = File.read(@converted_txt_path, encoding: Encoding::UTF_8)
|
55
|
+
data.gsub!(/[#挿絵((.+?))入る]/, "<img src=\"\\1\">")
|
56
|
+
File.write(@converted_txt_path, data)
|
57
|
+
end
|
13
58
|
end
|
data/lib/device/kindle.rb
CHANGED
data/lib/device/kobo.rb
CHANGED
@@ -10,10 +10,7 @@ module Device::Library
|
|
10
10
|
module Windows
|
11
11
|
def get_device_root_dir(volume_name)
|
12
12
|
@@FileSystemObject ||= WIN32OLE.new("Scripting.FileSystemObject")
|
13
|
-
|
14
|
-
result_len = WinAPI.GetLogicalDriveStrings(1000, drive_strings)
|
15
|
-
drives = drive_strings[0, result_len].split("\0")
|
16
|
-
drives.each do |drive_letter|
|
13
|
+
get_drives.each do |drive_letter|
|
17
14
|
drive_info = @@FileSystemObject.GetDrive(drive_letter)
|
18
15
|
vol = drive_info.VolumeName rescue ""
|
19
16
|
if vol.downcase == volume_name.downcase
|
@@ -22,5 +19,16 @@ module Device::Library
|
|
22
19
|
end
|
23
20
|
nil
|
24
21
|
end
|
22
|
+
|
23
|
+
def get_drives
|
24
|
+
result = []
|
25
|
+
bits = WinAPI.GetLogicalDrives
|
26
|
+
26.times do |i|
|
27
|
+
if bits & (1 << i) != 0
|
28
|
+
result << "#{(65 + i).chr}:\\"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
result
|
32
|
+
end
|
25
33
|
end
|
26
34
|
end
|
data/lib/device/reader.rb
CHANGED
@@ -10,4 +10,13 @@ module Device::Reader
|
|
10
10
|
EBOOK_FILE_EXT = ".epub"
|
11
11
|
NAME = "Reader"
|
12
12
|
DISPLAY_NAME = "SonyReader"
|
13
|
+
|
14
|
+
def hook_change_settings(&original_func)
|
15
|
+
@@__already_exec_change_settings ||= false
|
16
|
+
return if @@__already_exec_change_settings
|
17
|
+
force_change_settings_function({
|
18
|
+
"force.enable_half_indent_bracket" => false,
|
19
|
+
})
|
20
|
+
@@__already_exec_change_settings = true
|
21
|
+
end
|
13
22
|
end
|
data/lib/diffviewer.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright 2013 whiteleaf. All rights reserved.
|
4
|
+
#
|
5
|
+
|
6
|
+
require "diff/lcs"
|
7
|
+
require "termcolor"
|
8
|
+
|
9
|
+
#
|
10
|
+
# 指定されたソースの差分を作成する
|
11
|
+
#
|
12
|
+
# 書式はなんちゃってunified contextなので、パッチ作成には使えません
|
13
|
+
#
|
14
|
+
class DiffViewer
|
15
|
+
class InvalidSourceType < StandardError; end
|
16
|
+
|
17
|
+
def initialize(old, new, source = :file)
|
18
|
+
case source
|
19
|
+
when :file
|
20
|
+
old_strings = File.read(old, :encoding => Encoding::UTF_8)
|
21
|
+
new_strings = File.read(new, :encoding => Encoding::UTF_8)
|
22
|
+
when :string
|
23
|
+
old_strings = old
|
24
|
+
new_strings = new
|
25
|
+
else
|
26
|
+
raise InvalidSourceType, "source supported options are ':file' and ':string'"
|
27
|
+
end
|
28
|
+
old_strings.gsub!("\r", "")
|
29
|
+
new_strings.gsub!("\r", "")
|
30
|
+
@builded_buffer = ""
|
31
|
+
@events = Diff::LCS.sdiff(old_strings.split("\n"), new_strings.split("\n"))
|
32
|
+
build
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
@builded_buffer
|
37
|
+
end
|
38
|
+
|
39
|
+
def view
|
40
|
+
puts @builded_buffer
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def build
|
46
|
+
@buffer = {}
|
47
|
+
pos = 0
|
48
|
+
events_size = @events.size
|
49
|
+
while pos < events_size
|
50
|
+
pos = output_differing_line_and_before_and_behind(pos)
|
51
|
+
end
|
52
|
+
before_index = -1000
|
53
|
+
@builded_buffer = @buffer.sort_by { |index|
|
54
|
+
# buffer の格納順はバラバラなのでソートしておく
|
55
|
+
index
|
56
|
+
}.map { |(index, (event, str))|
|
57
|
+
# index が途切れたら、ポジション情報を付与する
|
58
|
+
result = ""
|
59
|
+
if index - before_index >= 2
|
60
|
+
result += "<bold><cyan>@@ -#{event.old_position+1}, " \
|
61
|
+
"+#{event.new_position+1} @@</cyan></bold>\n".termcolor
|
62
|
+
end
|
63
|
+
result += str
|
64
|
+
before_index = index
|
65
|
+
result
|
66
|
+
}.join("\n")
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# 指定ラインが編集・追加・削除されていた行だったら前後3行をバッファに出力
|
71
|
+
#
|
72
|
+
def output_differing_line_and_before_and_behind(index, force = false)
|
73
|
+
last = index + 1
|
74
|
+
return last if @buffer[index] || index < 0
|
75
|
+
event = @events[index] or return last
|
76
|
+
if %w(! + -).include?(event.action)
|
77
|
+
@buffer[index] = [event, decorate_event(event)]
|
78
|
+
((index - 3)..(index + 3)).each do |i|
|
79
|
+
output_differing_line_and_before_and_behind(i, true)
|
80
|
+
end
|
81
|
+
last = index + 3 + 1
|
82
|
+
elsif force
|
83
|
+
@buffer[index] = [event, " #{event.old_element}"]
|
84
|
+
end
|
85
|
+
last
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# レーベンシュタイン距離を計算する
|
90
|
+
#
|
91
|
+
# differ には Diff::LCS.sdiff で処理したものをそのまま渡す
|
92
|
+
#
|
93
|
+
def calc_levenshtein_distance(differ)
|
94
|
+
differ.reject { |e| e.unchanged? }.count
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# event.action を見てラインを装飾
|
99
|
+
#
|
100
|
+
def decorate_event(event)
|
101
|
+
result = ""
|
102
|
+
old_element = event.old_element
|
103
|
+
new_element = event.new_element
|
104
|
+
case event.action
|
105
|
+
when "!"
|
106
|
+
old_str = ""
|
107
|
+
new_str = ""
|
108
|
+
line_events = Diff::LCS.sdiff(old_element, new_element)
|
109
|
+
distance = calc_levenshtein_distance(line_events)
|
110
|
+
# レーベンシュタイン距離を正規化する
|
111
|
+
size = [old_element.length, new_element.length].max
|
112
|
+
normalized_distance = distance / size.to_f
|
113
|
+
if normalized_distance > 0.7
|
114
|
+
# 双方の文字列があまりにも似ていない場合、編集部分をカラー化すると
|
115
|
+
# 非常に見づらい表示になってしまうので、単純に削除・追加のみ装飾する
|
116
|
+
old_str = TermColor.escape(old_element)
|
117
|
+
new_str = TermColor.escape(new_element)
|
118
|
+
else
|
119
|
+
line_events.each do |e|
|
120
|
+
os = TermColor.escape(e.old_element) rescue ""
|
121
|
+
ns = TermColor.escape(e.new_element) rescue ""
|
122
|
+
case e.action
|
123
|
+
when "="
|
124
|
+
old_str += os
|
125
|
+
new_str += ns
|
126
|
+
when "!"
|
127
|
+
os = "<underline>#{os}</underline>" if os == " " || os == " "
|
128
|
+
ns = "<underline>#{ns}</underline>" if ns == " " || os == " "
|
129
|
+
old_str += "<reverse><on_black><yellow>#{os}</yellow></on_black></reverse>"
|
130
|
+
new_str += "<reverse><on_black><yellow>#{ns}</yellow></on_black></reverse>"
|
131
|
+
when "-"
|
132
|
+
old_str += "<reverse>#{os}</reverse>"
|
133
|
+
when "+"
|
134
|
+
new_str += "<reverse>#{ns}</reverse>"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
result = "<bold><red>-#{old_str}</red>\n" \
|
139
|
+
"<green>+#{new_str}</green></bold>"
|
140
|
+
when "-"
|
141
|
+
result = "<bold><red>-#{TermColor.escape(old_element)}</red></bold>"
|
142
|
+
when "+"
|
143
|
+
result = "<bold><green>+#{TermColor.escape(new_element)}</green></bold>"
|
144
|
+
end
|
145
|
+
result.termcolor
|
146
|
+
end
|
147
|
+
end
|