konjac 0.2.9.5 → 0.3

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.
data/konjac.gemspec CHANGED
@@ -24,6 +24,7 @@ Gem::Specification.new do |s|
24
24
  s.add_runtime_dependency "highline"
25
25
  s.add_runtime_dependency "i18n"
26
26
  s.add_runtime_dependency "nokogiri"
27
+ s.add_runtime_dependency "rb-appscript"
27
28
  s.add_runtime_dependency "sdoc"
28
29
  s.add_runtime_dependency "term-ansicolor"
29
30
  s.add_runtime_dependency "trollop"
data/lib/konjac.rb CHANGED
@@ -18,10 +18,7 @@ module Konjac
18
18
  autoload :Installer, "konjac/installer"
19
19
  autoload :Language, "konjac/language"
20
20
  autoload :Office, "konjac/office"
21
- autoload :OS, "konjac/os"
22
21
  autoload :Suggestor, "konjac/suggestor"
23
- autoload :Tag, "konjac/tag"
24
- autoload :TagManager, "konjac/tag_manager"
25
22
  autoload :Translator, "konjac/translator"
26
23
  autoload :Utils, "konjac/utils"
27
24
 
data/lib/konjac/cli.rb CHANGED
@@ -80,7 +80,7 @@ eos
80
80
  :default => Config.dictionary, :multi => true
81
81
  opt :help, I18n.t(:help, :scope => :opts)
82
82
  end
83
- Office.export_tags ARGV, opts
83
+ Office.new(ARGV[0]).export
84
84
  when "exportxml"
85
85
  opts = Trollop::options do
86
86
  banner sc_banner % I18n.t(:filenames_arg)
@@ -100,7 +100,7 @@ eos
100
100
  opt :output, I18n.t(:output, :scope => :opts), :type => :string
101
101
  opt :help, I18n.t(:help, :scope => :opts)
102
102
  end
103
- Office.import_tags ARGV, opts
103
+ Office.new(ARGV[0]).import
104
104
  when "importxml"
105
105
  opts = Trollop::options do
106
106
  banner sc_banner % I18n.t(:filenames_arg)
@@ -5,4 +5,7 @@ module Konjac
5
5
  # The user has supplied an invalid language and Konjac can't continue to
6
6
  # process the request
7
7
  class InvalidLanguageError < StandardError; end
8
+
9
+ # The user has requested an unimplemented virtual method
10
+ class VirtualMethodError < StandardError; end
8
11
  end
data/lib/konjac/office.rb CHANGED
@@ -1,283 +1,90 @@
1
1
  module Konjac
2
2
  # A singleton for dealing with extracting and importing tags from Microsoft
3
- # Office documents
3
+ # Office documents. It also has abstract some of the lengthier calls to the
4
+ # different applications of different environments by detecting the user's
5
+ # operating system and using ghost methods. For example, the following are
6
+ # equivalent for opening the document at <tt>~/text.doc</tt> on a Mac:
7
+ #
8
+ # doc = Konjac::Office::Mac::Word.new("~/text.doc")
9
+ # doc = Konjac::Office.word("~/text.doc")
10
+ #
11
+ # In addition, if the document is already open and is the active document, the
12
+ # following will work too:
13
+ #
14
+ # doc = Konjac::Office.word
4
15
  module Office
5
- class << self
6
- # Imports the text content of a tag file into a Microsoft Office document
7
- def import_tags(files, opts = {})
8
- sub_files = Utils.parse_files(files)
9
- return if sub_files.empty?
10
- sub_files.each do |sub_file|
11
- case File.extname(sub_file)
12
- when ".doc", ".docx"
13
- return if OS.not_a_mac
14
- system File.join(File.dirname(__FILE__), "..", "applescripts", "konjac_word_import"), sub_file
15
- when ".ppt", ".pptx"
16
- return if OS.not_a_mac
17
- system File.join(File.dirname(__FILE__), "..", "applescripts", "konjac_powerpoint_import"), sub_file
18
- when ".xls", ".xlsx"
19
- return if OS.not_a_mac
20
- system File.join(File.dirname(__FILE__), "..", "applescripts", "konjac_excel_import"), sub_file
21
- else
22
- puts I18n.t(:unknown) % sub_file
23
- end
24
- end
25
- end
26
-
27
- # Imports the text content of a tag file into a Word 2003+, utilizing a
28
- # cleaned-up version of the document's original XML structure
29
- def import_xml(files, opts = {})
30
- sub_files = Utils.parse_files(files)
31
- return if sub_files.empty?
32
- sub_files.each do |sub_file|
33
- case File.extname(sub_file)
34
- when ".docx"
35
- # Build the list of paths we need to work with
36
- dirname = File.dirname(sub_file)
37
- basename = File.basename(sub_file, ".*")
38
- orig_docx = "#{dirname}/#{basename}.docx"
39
- new_path = "#{dirname}/#{basename}_imported.docx"
40
- xml_path = "#{dirname}/#{basename}.xml"
41
- tags_path = "#{dirname}/#{basename}.docx.diff"
42
- out_path = "#{dirname}/word/document.xml"
43
-
44
- # Open the original XML file and the updated tags
45
- writer = Nokogiri::XML(File.read(xml_path))
46
- nodes = writer.xpath("//w:t")
47
- tags = TagManager.new(tags_path)
16
+ autoload :Generic, "konjac/office/generic"
17
+ autoload :Mac, "konjac/office/mac"
18
+ autoload :OS, "konjac/office/os"
19
+ autoload :Tag, "konjac/office/tag"
20
+ autoload :Windows, "konjac/office/windows"
21
+ autoload :XML, "konjac/office/windows"
48
22
 
49
- # Overwrite each <w:t> tag's content with the new tag
50
- tags.all.each do |tag|
51
- if tag.translated?
52
- nodes[tag.index].content = tag.translated
53
- end
54
- end
55
-
56
- # Create a directory for word/document.xml if necessary
57
- unless File.directory?("#{dirname}/word")
58
- FileUtils.mkdir "#{dirname}/word"
59
- end
60
-
61
- # Write the modified XML to a file
62
- File.open(out_path, "w") do |file|
63
- file.write writer.to_xml.gsub(/\n\s*/, "").sub(/\?></, "?>\n<")
64
- end
65
-
66
- # Copy the original file
67
- FileUtils.cp orig_docx, new_path
68
-
69
- # Add the new document XML to the copied file
70
- system "cd #{dirname} && zip -q #{new_path} word/document.xml"
71
- else
72
- puts I18n.t(:unknown) % sub_file
73
- end
74
- end
75
- end
76
-
77
- # Exports the text content of Microsoft Office document
78
- def export_tags(files, opts = {})
79
- # Determine whether to attempt translating
80
- if opts[:from_given] && opts[:to_given]
81
- from_lang = Language.find(opts[:from])
82
- to_lang = Language.find(opts[:to])
83
- unless from_lang.nil? || to_lang.nil?
84
- Translator.load_dictionary from_lang, to_lang, opts
85
- attempting_to_translate = true
86
- end
87
- end
88
-
89
- sub_files = Utils.parse_files(files)
90
- return if sub_files.empty?
91
- sub_files.each do |sub_file|
92
- case File.extname(sub_file)
93
- when ".doc", ".docx"
94
- return if OS.not_a_mac
95
- break unless Utils.user_allows_overwrite?(sub_file + ".diff")
96
-
97
- system File.join(File.dirname(__FILE__), "..", "applescripts", "konjac_word_export"), sub_file
98
- when ".ppt", ".pptx"
99
- return if OS.not_a_mac
100
- break unless Utils.user_allows_overwrite?(sub_file + ".diff")
101
-
102
- system File.join(File.dirname(__FILE__), "..", "applescripts", "konjac_powerpoint_export"), sub_file
103
- when ".xls", ".xlsx"
104
- return if OS.not_a_mac
105
- break unless Utils.user_allows_overwrite?(sub_file + ".diff")
106
-
107
- system File.join(File.dirname(__FILE__), "..", "applescripts", "konjac_excel_export"), sub_file
108
- else
109
- puts I18n.t(:unknown) % sub_file
110
- end
23
+ class << self
24
+ def new(path)
25
+ env = environment
26
+ return nil if env.nil?
27
+
28
+ case File.basename(path)
29
+ when /\.docx?/
30
+ env::Word.new(path)
31
+ when /\.pptx?/
32
+ env::PowerPoint.new(path)
33
+ when /\.xlsx?/
34
+ env::Excel.new(path)
111
35
  end
112
36
  end
113
37
 
114
- # Exports the Word document in XML then extracts the tags and condenses
115
- # like paragraphs
116
- #
117
- # I might deprecate this, but it exports XML. It's much faster, but
118
- # supporting two methods might not be a great idea.
119
- def export_xml(files, opts = {})
120
- # Determine whether to attempt translating
121
- if opts[:from_given] && opts[:to_given]
122
- from_lang = Language.find(opts[:from])
123
- to_lang = Language.find(opts[:to])
124
- unless from_lang.nil? || to_lang.nil?
125
- Translator.load_dictionary from_lang, to_lang, opts
126
- attempting_to_translate = true
127
- end
128
- end
129
-
130
- sub_files = Utils.parse_files(files)
131
- return if sub_files.empty?
132
- sub_files.each do |sub_file|
133
- case File.extname(sub_file)
134
- when ".docx"
135
- # Build a list of all the paths we're working with
136
- dirname = File.dirname(sub_file)
137
- basename = File.basename(sub_file, ".*")
138
- orig_docx = "#{dirname}/#{basename}.docx"
139
- xml_path = "#{dirname}/#{basename}_orig.xml"
140
- clean_path = "#{dirname}/#{basename}.xml"
141
- tags_path = "#{dirname}/#{basename}.docx.diff"
142
-
143
- break unless Utils.user_allows_overwrite?(tags_path)
144
-
145
- # Unzip the DOCX's word/document.xml file and pipe the output into
146
- # an XML with the same base name as the DOCX
147
- system "unzip -p #{orig_docx} word/document.xml > #{xml_path}"
148
-
149
- # Read in the XML file and extract the content from each <w:t> tag
150
- cleaner = Nokogiri::XML(File.read(xml_path))
151
- File.open(tags_path, "w") do |tags_file|
152
- # Remove all grammar and spellcheck tags
153
- cleaner.xpath("//w:proofErr").remove
154
-
155
- nodes = cleaner.xpath("//w:r")
156
- prev = nil
157
- nodes.each do |node|
158
- unless prev.nil?
159
- if (prev.next_sibling == node) && compare_nodes(prev, node)
160
- begin
161
- node.at_xpath("w:t").content = prev.at_xpath("w:t").content +
162
- node.at_xpath("w:t").content
163
- prev.remove
164
- rescue
165
- end
166
- end
167
- end
168
-
169
- prev = node
170
- end
171
-
172
- # Write the tags file
173
- tags_file.puts "---" + orig_docx
174
- tags_file.puts "+++" + orig_docx
175
- cleaner.xpath("//w:t").each_with_index do |node, index|
176
- tags_file.puts "@@ %i @@" % [index, additional_info(node)]
177
- tags_file.puts "-" + node.content
178
- if attempting_to_translate
179
- tags_file.puts "+" + Translator.translate_content(node.content)
180
- else
181
- tags_file.puts "+" + node.content
182
- end
183
- end
184
- end
185
-
186
- # Write the cleaned-up XML to a file for inspection
187
- File.open(clean_path, "w") do |xml|
188
- xml.puts cleaner.to_xml
189
- end
190
- else
191
- puts I18n.t(:unknown) % sub_file
192
- end
38
+ def environment
39
+ if OS.mac?
40
+ Mac
41
+ elsif OS.windows?
42
+ Windows
43
+ else
44
+ nil
193
45
  end
194
46
  end
195
47
 
196
48
  private
197
49
 
198
- # Performs a comparison between two nodes and accepts them as equivalent
199
- # if the differences are very minor
200
- def compare_nodes(a, b) # :doc:
201
- c = clean_hash(xml_node_to_hash(a))
202
- d = clean_hash(xml_node_to_hash(b))
203
- c == d
204
- end
205
-
206
- # Converts an XML node into a Ruby hash
207
- def xml_node_to_hash(node) # :doc:
208
- # If we are at the root of the document, start the hash
209
- if node.element?
210
- result_hash = {}
211
- if node.attributes != {}
212
- result_hash[:attributes] = {}
213
- node.attributes.keys.each do |key|
214
- result_hash[:attributes][node.attributes[key].name.to_sym] = prepare(node.attributes[key].value)
215
- end
216
- end
217
- if node.children.size > 0
218
- node.children.each do |child|
219
- result = xml_node_to_hash(child)
220
-
221
- if child.name == "text"
222
- unless child.next_sibling || child.previous_sibling
223
- return prepare(result)
224
- end
225
- elsif result_hash[child.name.to_sym]
226
- if result_hash[child.name.to_sym].is_a?(Array)
227
- result_hash[child.name.to_sym] << prepare(result)
228
- else
229
- result_hash[child.name.to_sym] = [result_hash[child.name.to_sym]] << prepare(result)
230
- end
231
- else
232
- result_hash[child.name.to_sym] = prepare(result)
233
- end
234
- end
235
-
236
- return result_hash
237
- else
238
- return result_hash
239
- end
50
+ def method_missing(name, *args, &block)
51
+ env = environment
52
+ return super if env.nil?
53
+ index = env.constants.index(camelize(name).to_sym)
54
+ if index.nil?
55
+ super
240
56
  else
241
- return prepare(node.content.to_s)
57
+ env.const_get(env.constants[index]).new *args
242
58
  end
243
59
  end
244
60
 
245
- # Prepares data according to whether it's string or integer content
246
- def prepare(data) # :doc:
247
- (data.class == String && data.to_i.to_s == data) ? data.to_i : data
248
- end
61
+ def valid_environments
62
+ return @environments unless @environments.nil?
249
63
 
250
- # Delete extraneous attributes for comparison
251
- def clean_hash(hash) # :doc:
252
- delete_attribute_or_child hash, :t
253
- delete_attribute_or_child hash, :rPr, :rFonts, :attributes, :hint
254
- end
64
+ @environments = []
65
+ downcased = constants.map { |c| underscore(c.to_s) }
66
+ Dir.glob(File.join(File.dirname(File.expand_path(__FILE__)), "office/*/")).each do |dir|
67
+ index = downcased.index(dir.split("/").last)
68
+ @environments << constants[index] unless index.nil?
69
+ end
255
70
 
256
- # Get additional information on the node for context in tags file
257
- def additional_info(node) # :doc:
258
- info = []
259
- info << "hyperlink" if node.parent.parent.name == "hyperlink"
71
+ @environments
72
+ end
260
73
 
261
- if info.empty?
262
- ""
74
+ def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
75
+ if first_letter_in_uppercase
76
+ lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
263
77
  else
264
- " #=> #{info.join(", ")}"
78
+ lower_case_and_underscored_word.to_s[0].chr.downcase + camelize(lower_case_and_underscored_word)[1..-1]
265
79
  end
266
80
  end
267
81
 
268
- # Deletes the specified attribute or child node for the supplied node,
269
- # following the hierarchy provided
270
- #
271
- # Delete <tt>hash[:rPr][:rFonts][:attributes][:hint]</tt>:
272
- # delete_attribute_or_child hash, :rPr, :rFonts, :attributes, :hint
273
- def delete_attribute_or_child(node, *hierarchy) # :doc:
274
- last_member = hierarchy.pop
275
-
276
- hierarchy.each do |member|
277
- node = node[member] unless node.nil?
278
- end
279
-
280
- node.delete last_member unless node.nil?
82
+ def underscore(camel_cased_word)
83
+ camel_cased_word.to_s.gsub(/::/, '/').
84
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
85
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
86
+ tr("-", "_").
87
+ downcase
281
88
  end
282
89
  end
283
90
  end
@@ -0,0 +1,76 @@
1
+ # coding: utf-8
2
+ module Konjac
3
+ module Office
4
+ # This is a basic class that contains all the methods that are universal
5
+ # across OSes, file formats, applications, etc.
6
+ class Generic
7
+ attr_reader :document, :index, :current
8
+
9
+ def initialize(location = nil)
10
+ @document = open(File.expand_path(location)) unless location.nil?
11
+ @document = active_document
12
+ @index = 0
13
+ @current = nil
14
+ end
15
+
16
+ # This only does the bare minimum, extracting arguments from
17
+ # <tt>*args</tt>, so that subclass methods have their parameters parsed
18
+ def write(text, *args)
19
+ parse_args *args
20
+ end
21
+
22
+ def read(*args)
23
+ opts = parse_args(*args)
24
+ clean find(opts), (opts.nil? ? nil : opts[:type])
25
+ end
26
+
27
+ def tags
28
+ Tag.load path
29
+ end
30
+
31
+ def export
32
+ Tag.dump data, path
33
+ end
34
+
35
+ def import
36
+ tags.each do |tag|
37
+ if tag.changed? && !tag.blank?
38
+ write tag.added.join(delimiter(tag.type)), *tag.indices,
39
+ :type => tag.type
40
+ end
41
+ end
42
+ end
43
+
44
+ def delimiter(type)
45
+ if type.nil?
46
+ "\v"
47
+ else
48
+ case type
49
+ when :shape
50
+ "\r"
51
+ else
52
+ "\n"
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def parse_args(*args)
60
+ return nil if args.empty? || args.nil?
61
+
62
+ # Extract final argument if it's a hash
63
+ parsed = args.last.is_a?(Hash) ? args.pop : {}
64
+
65
+ # Create hash using @parse_order as keys and args as values, then merge
66
+ # that with any pre-parsed hashes. Arguments specified via the hash have
67
+ # priority
68
+ parsed = Hash[@parse_order.zip(args)].merge(parsed)
69
+ end
70
+
71
+ def clean(text, type = nil)
72
+ text.gsub(@strippable, "").split delimiter(type)
73
+ end
74
+ end
75
+ end
76
+ end