konjac 0.2.9.5 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
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