• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1require 'tmpdir'
2require 'fileutils'
3
4class TeXRunner
5  attr_reader :dir, :path, :file, :engines, :output, :latex_class, :switches
6  attr_accessor :verbose, :mode
7  attr_accessor :do_not_cleanup # TODO Spec for that?
8
9  @@formats = { 'xetex' => 'xelatex', 'luatex' => 'lualatex' }
10  @@basename = 'feature'
11  @@fonts = { 'hebrew' => 'SBL Hebrew', 'cyrillic' => 'PT Serif' }
12  @@babel_encodings = { 'hebrew' => 'LHE' }
13
14  def initialize(latex_class = "minimal")
15    @latex_class = latex_class || "minimal"
16    @dir = Dir.mktmpdir
17    @path = File.join(@dir, "#{@@basename}.tex")
18    @file = File.open(@path, "w")
19    @file.puts "\\documentclass{#{@latex_class}}"
20    @engines = @@formats.keys.clone
21    @mode = 'nonstop'
22    @switches = []
23  end
24
25  def self.fonts
26    @@fonts
27  end
28
29  def self.babel_encodings
30    @@babel_encodings
31  end
32
33  # TODO Redirect standard error as well
34  def verbose?
35    verbose
36  end
37
38  def has_engine(engine)
39    return true if engine == nil
40    engines.include?(engine.downcase)
41  end
42
43  def set_engine(engine)
44    @engines = [engine.downcase]
45  end
46
47  def set_engines(engines)
48    @engines = engines.map(&:downcase)
49  end
50
51  def exclude_engine(engine)
52    @engines.delete(engine.downcase)
53  end
54
55  def add_switch(switch)
56    @switches << switch
57  end
58
59  def output
60    Hash.new.tap do |o|
61      @engines.each do |engine|
62        o[engine] = File.join(@dir, engine, "#{@@basename}.pdf")
63      end
64    end
65  end
66
67  def append(content)
68    @file.puts content
69  end
70
71  def starttyping
72    @file.puts "\\begin{document}"
73    @file.flush
74    @inside_body = true
75  end
76
77  def flush_preamble(lines = [])
78    raise if @inside_body
79    lines.each do |line|
80      append(line)
81    end
82    ensure_inside_body
83  end
84
85  def ensure_inside_body
86    unless @inside_body
87      starttyping
88    end
89  end
90
91  def stoptyping
92    ensure_inside_body
93    @file.puts "\\end{document}"
94    @file.close
95  end
96
97  def source
98    stoptyping unless @file.closed?
99    File.read(@file)
100  end
101
102  def has_already_run
103    File.file?(output.values.first.gsub(/\.pdf$/, '.log'))
104  end
105
106  def run(count = 1)
107  # def run(count = 1, params = { })
108    stoptyping unless @file.closed?
109    begin
110      @origdir = FileUtils.getwd
111    rescue Errno::ENOENT # Should not happen, but it does # TODO Audit for directories disappearing
112      @origdir = ENV['HOME']
113    end
114    FileUtils.chdir(@dir)
115
116    ENV['max_print_line'] = "2048"
117    Hash.new.tap do |result|
118      @engines.each do |engine|
119        FileUtils.mkdir(engine) unless File.directory?(engine)
120        FileUtils.chdir(engine)
121        count.times do
122          call = "#{@@formats[engine]} #{@switches.join(' ')} -interaction=#{@mode}mode #{@path}"
123          # TODO?  Or return this as status if failure
124          # call = "#{@@formats[engine]} -interaction=#{@mode}mode #{@path} 2>/dev/null"
125          if @mode == 'errorstop' || verbose? # TODO Spec this out
126            system(call)
127            # out = params[:out]
128            # if out
129            #   spawn(call, :out => out)
130            # else
131            #   spawn(call)
132            # end
133          else
134            `#{call}`
135            # TODO Use threads
136          end
137        end
138        FileUtils.chdir('..')
139        result[engine] = ($?.exitstatus == 0) if $? # TODO and over all runs
140      end
141    end
142
143    # TODO: Collect status in a variable, so that we can retrieve it
144    # later on without running
145  end
146
147  def warnings(regexp = nil)
148    errors_or_warnings(/Package .* Warning/, regexp)
149  end
150
151  def package_errors(regexp = nil)
152    errors_or_warnings(/Package .* Error/, regexp)
153  end
154
155  def errors
156    errors_or_warnings(/^!/)
157  end
158
159  def errors_or_warnings(regexp, message_regexp = nil)
160    run unless has_already_run
161
162    errors = { }
163
164    output.each do |engine, pdf|
165      append = false
166      errors[engine] = []
167
168      log = pdf.gsub(/\.pdf$/, '.log')
169      next unless File.file?(log)
170      logfile = File.open(log, 'r')
171      logfile.each_line do |line|
172        if append
173          err = errors[engine].pop
174          errors[engine] << err + "\n" + line
175          append = false
176        end
177
178        if line =~ regexp # TODO Catch invalid UTF-8 errors
179          if message_regexp
180            errors[engine] << line.strip if line =~ /#{regexp}.*#{message_regexp}/
181          else
182            errors[engine] << line.strip
183          end
184          append = true
185        end
186      end
187      logfile.close
188    end
189
190    errors = errors.values.first if errors.values.uniq.count == 1
191
192    errors
193  end
194
195  def value(control_sequence)
196  # FIXME remove that
197    tweak_end_document
198    append "\\makeatletter\\show\\#{control_sequence}"
199    show_things
200  end
201
202  def lua_value(luacode)
203    tweak_end_document
204    append "\\directlua{"
205    append "f = io.open('#{File.join(@dir, 'luaout.txt')}', 'w')"
206    append "local value = #{luacode}"
207    append "f:write(tostring(value))"
208    append "f:close()"
209    append "}"
210    run
211    File.read(File.join(@dir, 'luaout.txt'))
212  end
213
214  # TODO This is obviously awful.  Refactor when I can.
215  def tweak_end_document
216    if @file.closed?
217      tex_source = File.read(path)
218      tex_source.gsub!(/\\end{document}/m, '')
219      @engines.each do |engine|
220        FileUtils.rm_f(File.join(@dir, engine, @@basename + '.log'))
221      end
222      @file = File.open(path, 'w')
223      @file.write(tex_source)
224   end
225
226  end
227
228  def showthe(variable)
229    append "\\showthe\\#{variable}"
230    show_things
231  end
232
233  def show_things
234    run unless has_already_run
235
236    values = { }
237
238    append = false
239    output.each do |engine, pdf|
240      log = pdf.gsub(/\.pdf$/, '.log')
241      next unless File.file?(log)
242      logfile = File.open(log, 'r')
243      logfile.each_line do |line|
244        if line =~ /^l\.\d/
245          append = false
246        end
247
248        values[engine] += "\n#{line}" if append
249
250        if line =~ /^> (.*)$/
251          values[engine] = $1.strip
252          append = true
253        end
254      end
255      logfile.close
256    end
257
258    if values.values.uniq.count == 1
259      values = values.values.first
260    end
261
262    values
263  end
264
265  def aux
266    byproduct('aux')
267  end
268
269  def log
270    byproduct('log')
271  end
272
273  def grep(regexp, ext = 'log')
274    lineno = 0
275    matching_lines = []
276    byproduct(ext).each_line do |line|
277      lineno += 1 # TODO Better formatting
278      matching_lines << "#{lineno}:#{line}" if line =~ regexp
279    end
280
281    matching_lines
282  end
283
284  def byproduct(ext)
285    run unless has_already_run
286    companion_file = output.values.first.gsub(/pdf$/, ext)
287    File.read(companion_file)
288  end
289
290  def pdftotext
291    run unless has_already_run
292    output.each_value do |pdf|
293      `pdftotext -layout -enc UTF-8 #{pdf}`
294    end
295  end
296
297  def text(engine = nil)
298    set_engine(engine) if engine
299    pdftotext
300    texts = { }
301    output.keys.each do |engine|
302      ff = File.join(@dir, engine, "#{@@basename}.txt")
303      texts[engine] = if File.file?(ff)
304        File.read(ff).strip
305      else
306        ""
307      end
308    end
309
310    texts
311  end
312
313  def cleanup
314    if @origdir
315      FileUtils.chdir(@origdir)
316    else
317      FileUtils.chdir(ENV['HOME'])
318    end
319
320    FileUtils.rmtree(@dir)
321  end
322end
323