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.

Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/ChangeLog.md +619 -432
  4. data/Gemfile +9 -0
  5. data/README.md +101 -76
  6. data/lib/color.rb +62 -22
  7. data/lib/command.rb +21 -19
  8. data/lib/command/alias.rb +9 -9
  9. data/lib/command/backup.rb +11 -4
  10. data/lib/command/browser.rb +8 -7
  11. data/lib/command/convert.rb +25 -77
  12. data/lib/command/diff.rb +67 -36
  13. data/lib/command/download.rb +5 -4
  14. data/lib/command/flag.rb +32 -6
  15. data/lib/command/folder.rb +7 -5
  16. data/lib/command/freeze.rb +21 -11
  17. data/lib/command/help.rb +29 -17
  18. data/lib/command/init.rb +7 -7
  19. data/lib/command/inspect.rb +6 -5
  20. data/lib/command/list.rb +106 -25
  21. data/lib/command/mail.rb +6 -7
  22. data/lib/command/remove.rb +9 -7
  23. data/lib/command/send.rb +6 -5
  24. data/lib/command/setting.rb +51 -16
  25. data/lib/command/tag.rb +179 -0
  26. data/lib/command/update.rb +9 -8
  27. data/lib/command/version.rb +4 -4
  28. data/lib/commandbase.rb +79 -8
  29. data/lib/commandline.rb +14 -8
  30. data/lib/converterbase.rb +78 -28
  31. data/lib/database.rb +3 -3
  32. data/lib/device.rb +26 -13
  33. data/lib/device/ibooks.rb +116 -0
  34. data/lib/device/ibunko.rb +45 -0
  35. data/lib/device/kindle.rb +4 -0
  36. data/lib/device/kobo.rb +4 -0
  37. data/lib/device/library/windows.rb +12 -4
  38. data/lib/device/reader.rb +9 -0
  39. data/lib/diffviewer.rb +147 -0
  40. data/lib/downloader.rb +195 -80
  41. data/lib/extensions/jruby.rb +33 -0
  42. data/lib/extensions/windows.rb +8 -9
  43. data/lib/helper.rb +79 -37
  44. data/lib/illustration.rb +1 -1
  45. data/lib/inspector.rb +2 -2
  46. data/lib/inventory.rb +48 -0
  47. data/lib/logger.rb +28 -4
  48. data/lib/narou.rb +9 -7
  49. data/lib/narou/api.rb +7 -2
  50. data/lib/novelconverter.rb +46 -19
  51. data/lib/novelinfo.rb +13 -3
  52. data/lib/novelsetting.rb +17 -9
  53. data/lib/version.rb +1 -1
  54. data/narou.gemspec +106 -27
  55. data/narou.rb +2 -2
  56. data/spec/convert_spec.rb +102 -0
  57. data/spec/data/convert_test/auto_indent/correct_test_auto_indent.txt +18 -0
  58. data/spec/data/convert_test/auto_indent/test_auto_indent.txt +21 -0
  59. data/spec/data/convert_test/auto_join_bracket/correct_test_auto_join_bracket.txt +12 -0
  60. data/spec/data/convert_test/auto_join_bracket/test_auto_join_bracket.txt +16 -0
  61. data/spec/data/convert_test/auto_join_line/correct_test_auto_join_line.txt +36 -0
  62. data/spec/data/convert_test/auto_join_line/test_auto_join_line.txt +51 -0
  63. data/spec/data/convert_test/convert_page_break/correct_test_convert_page_break.txt +21 -0
  64. data/spec/data/convert_test/convert_page_break/setting.ini +5 -0
  65. data/spec/data/convert_test/convert_page_break/test_convert_page_break.txt +60 -0
  66. data/spec/data/convert_test/force_indent_special_chapter/correct_test_force_indent_special_chapter.txt +64 -0
  67. data/spec/data/convert_test/force_indent_special_chapter/test_force_indent_special_chapter.txt +61 -0
  68. data/spec/data/convert_test/horizontal_ellipsis/correct_test_horizontal_ellipsis.txt +52 -0
  69. data/spec/data/convert_test/horizontal_ellipsis/test_horizontal_ellipsis.txt +57 -0
  70. data/spec/data/convert_test/kanji_num/correct_test_kanji_num.txt +49 -0
  71. data/spec/data/convert_test/kanji_num/test_kanji_num.txt +56 -0
  72. data/spec/data/convert_test/nonokagi/correct_test_nonokagi.txt +29 -0
  73. data/spec/data/convert_test/nonokagi/test_nonokagi.txt +34 -0
  74. data/spec/data/convert_test/replace/correct_test_replace.txt +12 -0
  75. data/spec/data/convert_test/replace/replace.txt +7 -0
  76. data/spec/data/convert_test/replace/test_replace.txt +14 -0
  77. data/spec/data/convert_test/ruby/correct_test_ruby.txt +131 -0
  78. data/spec/data/convert_test/ruby/test_ruby.txt +170 -0
  79. data/spec/data/convert_test/ruby_youon/correct_test_ruby_youon.txt +12 -0
  80. data/spec/data/convert_test/ruby_youon/setting.ini +2 -0
  81. data/spec/data/convert_test/ruby_youon/test_ruby_youon.txt +14 -0
  82. data/spec/data/convert_test/sesame/correct_test_sesame.txt +40 -0
  83. data/spec/data/convert_test/sesame/test_sesame.txt +52 -0
  84. data/spec/data/convert_test/to_odd_leader/correct_test_to_odd_leader.txt +20 -0
  85. data/spec/data/convert_test/to_odd_leader/test_to_odd_leader.txt +21 -0
  86. data/spec/device_spec.rb +3 -3
  87. data/spec/generator/convert_spec_gen.rb +95 -0
  88. data/spec/html_spec.rb +9 -8
  89. data/spec/ini_spec.rb +31 -31
  90. data/spec/novelinfo_spec.rb +2 -2
  91. data/spec/num_to_kanji_spec.rb +2 -2
  92. data/template/diff.txt.erb +5 -2
  93. data/template/ibunko_novel.txt.erb +2 -1
  94. data/template/novel.txt.erb +16 -3
  95. data/template/setting.ini.erb +1 -1
  96. data/webnovel/ncode.syosetu.com.yaml +7 -7
  97. data/webnovel/novel18.syosetu.com.yaml +7 -7
  98. data/webnovel/syosetu.org.yaml +8 -11
  99. metadata +193 -23
  100. data/lib/globalsetting.rb +0 -64
  101. 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 "localsetting"
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 = LocalSetting.get[DATABASE_NAME]
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
- LocalSetting.get.save_settings(DATABASE_NAME)
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
- @device = DEVICES[device_name.downcase]
57
- @name = @device::NAME
58
- @display_name = @device::DISPLAY_NAME
59
- @ebook_file_ext = @device::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
- @device::DOCUMENTS_PATH_LIST.each do |documents_path|
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(@device::VOLUME_NAME)
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
- if $?.exitstatus > 0
107
+ unless $?.success?
107
108
  raise capture.force_encoding(Encoding::Windows_31J).rstrip
108
109
  end
109
- rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
110
- # Windows-31J に変換できない文字をファイル名に含むものはRubyでコピーする
110
+ rescue StandardError
111
+ # コマンドで送信出来ないものはRubyで直接コピーする
111
112
  FileUtils.cp(src_file, dst_path)
112
113
  end
113
114
  else
114
- capture = `cp "#{src_file}" "#{dst_path}"`
115
- raise capture.rstrip if $?.exitstatus > 0
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
- @device::PHYSICAL_SUPPORT
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
@@ -10,4 +10,8 @@ module Device::Kindle
10
10
  EBOOK_FILE_EXT = ".mobi"
11
11
  NAME = "Kindle"
12
12
  DISPLAY_NAME = "Kindle"
13
+
14
+ RELATED_VARIABLES = {
15
+ "force.enable_half_indent_bracket" => true,
16
+ }
13
17
  end
data/lib/device/kobo.rb CHANGED
@@ -10,4 +10,8 @@ module Device::Kobo
10
10
  EBOOK_FILE_EXT = ".kepub.epub"
11
11
  NAME = "Kobo"
12
12
  DISPLAY_NAME = "Kobo"
13
+
14
+ RELATED_VARIABLES = {
15
+ "force.enable_half_indent_bracket" => false,
16
+ }
13
17
  end
@@ -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
- drive_strings = " " * 1000
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