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