trex 1.0.1
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/bin/trex +7 -0
- data/lib/trex.rb +949 -0
- metadata +51 -0
data/bin/trex
ADDED
data/lib/trex.rb
ADDED
@@ -0,0 +1,949 @@
|
|
1
|
+
# == Synopsis
|
2
|
+
# trex is script simplifying the compilation of latex files by creating
|
3
|
+
# proper human-readable error output with repeating patterns. Unlike the
|
4
|
+
# original latex output which is oververbosified.
|
5
|
+
#
|
6
|
+
# == Examples
|
7
|
+
# ./trex view # compiles and opens the pdf, equivalent to just ./trex
|
8
|
+
# ./trex clean # removes all generated files
|
9
|
+
# ./trex tex # compiles the latex sources
|
10
|
+
# ./trex count # gives different word approximations of the latex file
|
11
|
+
#
|
12
|
+
# == Usage
|
13
|
+
# trex [options] [view|compile|tex|clean]
|
14
|
+
# For help use: trex -h
|
15
|
+
#
|
16
|
+
# == Options
|
17
|
+
# -h, --help Displays help message
|
18
|
+
# -v, --version Display the version, then exit
|
19
|
+
# -q, --quiet Output as little as possible, overrides verbose
|
20
|
+
# -V, --verbose Output lists all errors, overrides quiet
|
21
|
+
# --figures Set the figures dir, default is './figures'
|
22
|
+
# --hack Open this script for direct changes in the sources
|
23
|
+
#
|
24
|
+
# == Authors
|
25
|
+
# Camillo Bruni, Toon Verwaest, Damien Pollet
|
26
|
+
#
|
27
|
+
|
28
|
+
# ============================================================================
|
29
|
+
# CHANGE THESE PROPERTIES FOR YOUR PROJECT
|
30
|
+
SOURCE = nil
|
31
|
+
FIGURES = 'figures' # not yet used
|
32
|
+
GARBAGE = 'log aux out blg pdfsync synctex.gz'
|
33
|
+
|
34
|
+
# ============================================================================
|
35
|
+
|
36
|
+
require 'optparse'
|
37
|
+
require 'ostruct'
|
38
|
+
require 'date'
|
39
|
+
require 'logger'
|
40
|
+
require 'open3'
|
41
|
+
|
42
|
+
#require 'ruby-debug19'
|
43
|
+
#Debugger.start(:post_mortem => true)
|
44
|
+
# Debugger.start()
|
45
|
+
|
46
|
+
# ============================================================================
|
47
|
+
#TODO track changed files and only recompile then...
|
48
|
+
class TReX
|
49
|
+
VERSION = '1.0.1'
|
50
|
+
|
51
|
+
attr_reader :options
|
52
|
+
|
53
|
+
# mapping of input command shortcuts and method names
|
54
|
+
VALID_ACTIONS = {
|
55
|
+
"view" => :view,
|
56
|
+
"compile" => :compile,
|
57
|
+
"tex" => :compile,
|
58
|
+
"count" => :count,
|
59
|
+
"clean" => :clean,
|
60
|
+
"bibtex" => :bibtex,
|
61
|
+
"bib" => :bibtex,
|
62
|
+
"v" => :view,
|
63
|
+
}
|
64
|
+
|
65
|
+
# ------------------------------------------------------------------------
|
66
|
+
def initialize(arguments, stdin)
|
67
|
+
@arguments = arguments
|
68
|
+
@stdin = stdin
|
69
|
+
@action = :compile
|
70
|
+
@opts = nil
|
71
|
+
# Set defaults
|
72
|
+
@options = OpenStruct.new
|
73
|
+
@options.verbose = false
|
74
|
+
@options.quiet = false
|
75
|
+
@options.interaction = 'nonstopmode'
|
76
|
+
@options.garbage = GARBAGE
|
77
|
+
@options.figures = FIGURES
|
78
|
+
end
|
79
|
+
|
80
|
+
# ------------------------------------------------------------------------
|
81
|
+
def source!(sourceFile)
|
82
|
+
@options.source = sourceFile
|
83
|
+
@options.sourceBase = sourceFile.sub(/\.\w+$/,'')
|
84
|
+
@options.output = @options.sourceBase + '.pdf'
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
# ------------------------------------------------------------------------
|
89
|
+
def check_source
|
90
|
+
return if File.exist?(@options.source)
|
91
|
+
if not @options.source.end_with? "tex"
|
92
|
+
@options.source += '.' if not @options.source.end_with? '.'
|
93
|
+
@options.source += 'tex'
|
94
|
+
self.source! @options.source
|
95
|
+
end
|
96
|
+
return if File.exist?(@options.source)
|
97
|
+
print "File doesn't exist: ".redb, @options.source, "\n"
|
98
|
+
exit 1
|
99
|
+
end
|
100
|
+
|
101
|
+
# ------------------------------------------------------------------------
|
102
|
+
def run
|
103
|
+
begin
|
104
|
+
if self.parsed_options? && self.arguments_valid?
|
105
|
+
self.process_arguments
|
106
|
+
self.process_command
|
107
|
+
else
|
108
|
+
self.output_usage
|
109
|
+
end
|
110
|
+
rescue Interrupt
|
111
|
+
puts ''
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# ------------------------------------------------------------------------
|
116
|
+
def hack
|
117
|
+
editor = ENV['EDITOR'] || "vim"
|
118
|
+
cmd = "sudo #{editor} #{__FILE__}"
|
119
|
+
print cmd
|
120
|
+
exec(cmd)
|
121
|
+
end
|
122
|
+
|
123
|
+
# ------------------------------------------------------------------------
|
124
|
+
def clean
|
125
|
+
@options.garbage.split.each { |f|
|
126
|
+
`rm -f #{@options.sourceBase}.#{f}`
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
# ------------------------------------------------------------------------
|
131
|
+
def compile
|
132
|
+
self.check_commands
|
133
|
+
self.check_source
|
134
|
+
baseCommand = self.create_compile_baseCommand
|
135
|
+
# 1/4 first run interactive... ignoring cite/ref errors
|
136
|
+
formatter = TexOutputFormatter.new(`#{baseCommand}`,
|
137
|
+
@options.verbose, @options.quiet,
|
138
|
+
@options.source)
|
139
|
+
if not formatter.hasCitationWarnings? \
|
140
|
+
and not formatter.hasReferenceWarnings?
|
141
|
+
formatter.print unless @options.quiet
|
142
|
+
return true
|
143
|
+
end
|
144
|
+
# 2/4 bibtex on citation warnings
|
145
|
+
if formatter.hasCitationWarnings? and not self.bibtex \
|
146
|
+
and not @options.quiet
|
147
|
+
# obviously something went wrong in either compiling bibtex or the
|
148
|
+
# latex file itself. Print everything except for reference and
|
149
|
+
# citation warnings
|
150
|
+
errorGroups = formatter.errorGroups
|
151
|
+
errorGroups.delete formatter.referenceWarnings
|
152
|
+
errorGroups.delete formatter.citationWarnings
|
153
|
+
formatter.print
|
154
|
+
end
|
155
|
+
# 3/4 second run
|
156
|
+
if not system("#{baseCommand} > /dev/null 2>&1")
|
157
|
+
formatter.print
|
158
|
+
"\n! Could not create PDF !".error
|
159
|
+
return false
|
160
|
+
end
|
161
|
+
# 4/4 looking for ref/cite errors
|
162
|
+
formatter = TexOutputFormatter.new(`#{baseCommand}`,
|
163
|
+
@options.verbose, @options.quiet,
|
164
|
+
@options.source)
|
165
|
+
formatter.print if not @options.quiet
|
166
|
+
return true
|
167
|
+
end
|
168
|
+
|
169
|
+
# ------------------------------------------------------------------------
|
170
|
+
def bibtex
|
171
|
+
if not File.exist?(@options.sourceBase + '.aux')
|
172
|
+
return self.compile
|
173
|
+
end
|
174
|
+
warning = BibtexWarning.new()
|
175
|
+
warning.handle(`bibtex #{@options.sourceBase + '.aux'}`)
|
176
|
+
puts warning.to_s unless warning.empty?
|
177
|
+
return warning.empty?
|
178
|
+
end
|
179
|
+
|
180
|
+
# ------------------------------------------------------------------------
|
181
|
+
def view
|
182
|
+
self.compile
|
183
|
+
open = nil
|
184
|
+
if RUBY_PLATFORM.include? "linux"
|
185
|
+
'gnome-open evince kpdf Xpdf'.split.each { |c|
|
186
|
+
if self.has_command? c
|
187
|
+
open = c
|
188
|
+
break
|
189
|
+
end
|
190
|
+
}
|
191
|
+
elsif RUBY_PLATFORM.include? "darwin"
|
192
|
+
open = 'open'
|
193
|
+
end
|
194
|
+
if open.nil?
|
195
|
+
"\n no command found to open #{ @options.output} !\n".error 1
|
196
|
+
end
|
197
|
+
system("#{open} #{@options.output}")
|
198
|
+
end
|
199
|
+
|
200
|
+
# ------------------------------------------------------------------------
|
201
|
+
def count
|
202
|
+
@options.quiet = true
|
203
|
+
self.compile
|
204
|
+
puts 'Sources'.red + ":"
|
205
|
+
self.print_wc_results `wc #{@options.source}`
|
206
|
+
puts 'PDFtoText'.red + ":"
|
207
|
+
#TODO create a tmp file instead of saving locally
|
208
|
+
self.print_wc_results `pdftotext -enc UTF-8 -nopgbrk #{@options.output} - | wc`
|
209
|
+
`rm -f #{@options.sourceBase}.txt`
|
210
|
+
if self.has_command? 'texcount'
|
211
|
+
puts 'texcount'.red + ":"
|
212
|
+
puts `texcount -relaxed -sum #{@options.source}`
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def print_wc_results(result)
|
217
|
+
result = result.split()[0..2]
|
218
|
+
max = result.map{|l| l.size}.max
|
219
|
+
puts " #{result[0].rjust(max).yellow} lines"
|
220
|
+
puts " #{result[1].rjust(max).yellow} words"
|
221
|
+
puts " #{result[2].rjust(max).yellow} characters"
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
# ------------------------------------------------------------------------
|
226
|
+
protected
|
227
|
+
|
228
|
+
# Specify options
|
229
|
+
def option_parser
|
230
|
+
return @opts unless @opts.nil?
|
231
|
+
|
232
|
+
@opts = OptionParser.new do |opts|
|
233
|
+
opts.banner = "#{executable_name} [options] [view|compile|tex|clean]"
|
234
|
+
|
235
|
+
opts.separator ''
|
236
|
+
opts.separator 'Automate compilation of LaTeX documents, making the output human-readable.'
|
237
|
+
opts.separator ''
|
238
|
+
|
239
|
+
opts.on '--figures PATH', "Set the figures directory (default #{FIGURES})" do |path|
|
240
|
+
@options.figures = path
|
241
|
+
end
|
242
|
+
opts.on '-V', '--verbose', 'List all errors (overrides --quiet)' do
|
243
|
+
@options.verbose = true
|
244
|
+
end
|
245
|
+
opts.on '-q', '--quiet', 'Output as little as possible (overrides --verbose)' do
|
246
|
+
@options.quiet = true
|
247
|
+
end
|
248
|
+
|
249
|
+
opts.separator ''
|
250
|
+
opts.on '-h', '--help', 'Display this help message' do
|
251
|
+
self.output_help
|
252
|
+
exit 0
|
253
|
+
end
|
254
|
+
opts.on '-v', '--version', 'Show version' do
|
255
|
+
self.output_version
|
256
|
+
exit 0
|
257
|
+
end
|
258
|
+
opts.on '--hack', 'Open this script for direct changes in the sources' do
|
259
|
+
self.hack
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def parsed_options?
|
265
|
+
option_parser.parse!(@arguments) rescue return false
|
266
|
+
self.process_options
|
267
|
+
true
|
268
|
+
end
|
269
|
+
|
270
|
+
# Performs post-parse processing on options
|
271
|
+
def process_options
|
272
|
+
@options.verbose = false if @options.quiet
|
273
|
+
end
|
274
|
+
|
275
|
+
# True if required arguments were provided
|
276
|
+
def arguments_valid?
|
277
|
+
return false if not @action and not VALID_ACTIONS.keys.include? @arguments[0]
|
278
|
+
true
|
279
|
+
end
|
280
|
+
|
281
|
+
# Setup the arguments
|
282
|
+
def process_arguments
|
283
|
+
if not @action or VALID_ACTIONS.keys.include? @arguments[0]
|
284
|
+
@action = VALID_ACTIONS[@arguments.shift]
|
285
|
+
end
|
286
|
+
if not @arguments.empty?
|
287
|
+
self.source! @arguments.shift
|
288
|
+
elsif not SOURCE.nil?
|
289
|
+
self.source! SOURCE
|
290
|
+
end
|
291
|
+
if @options.source.nil?
|
292
|
+
self.extract_source_from_current_dir
|
293
|
+
end
|
294
|
+
'Source file not set!'.error 1 if @options.source.nil?
|
295
|
+
end
|
296
|
+
|
297
|
+
# ---------------------------------------------------------------------
|
298
|
+
|
299
|
+
def executable_name
|
300
|
+
File.basename(__FILE__)
|
301
|
+
end
|
302
|
+
|
303
|
+
def output_help
|
304
|
+
self.output_version
|
305
|
+
output_usage
|
306
|
+
end
|
307
|
+
|
308
|
+
def output_usage
|
309
|
+
puts @opts.help
|
310
|
+
end
|
311
|
+
|
312
|
+
def output_version
|
313
|
+
puts "#{executable_name} version #{VERSION}"
|
314
|
+
end
|
315
|
+
|
316
|
+
# ---------------------------------------------------------------------
|
317
|
+
def extract_source_from_current_dir
|
318
|
+
texFiles = Dir['*.tex']
|
319
|
+
texFiles = texFiles.select { |file|
|
320
|
+
File.read(file).match(/\\documentclass.*?\{.*?\}/)
|
321
|
+
}
|
322
|
+
return if texFiles.empty?
|
323
|
+
return self.source! texFiles.first if texFiles.length == 1
|
324
|
+
self.source! self.promptChoice texFiles
|
325
|
+
end
|
326
|
+
|
327
|
+
def promptChoice(args)
|
328
|
+
args.sort!
|
329
|
+
for i in 1..args.length
|
330
|
+
puts "[#{i}] #{args[i-1]}"
|
331
|
+
end
|
332
|
+
choice = nil
|
333
|
+
while choice.nil?
|
334
|
+
print "Which is the source file?[number]: "
|
335
|
+
choice = args[gets.chomp.to_i-1]
|
336
|
+
end
|
337
|
+
return choice
|
338
|
+
end
|
339
|
+
|
340
|
+
def check_commands
|
341
|
+
missing = []
|
342
|
+
'pdflatex bibtex'.split.each { |c|
|
343
|
+
missing.push(c) unless self.has_command? c
|
344
|
+
}
|
345
|
+
if not missing.empty?
|
346
|
+
print 'Missing commands for running the script: '.redb
|
347
|
+
puts missing.join(', ')
|
348
|
+
if RUBY_PLATFORM.include? "linux"
|
349
|
+
puts 'install latex with your favorite package manager'
|
350
|
+
elsif RUBY_PLATFORM.include? "darwin"
|
351
|
+
puts 'install the latest latex from http://www.tug.org/mactex'
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
def process_command
|
357
|
+
self.send @action
|
358
|
+
end
|
359
|
+
|
360
|
+
# ---------------------------------------------------------------------
|
361
|
+
def has_command?(command)
|
362
|
+
# disable warnings during the script checking, so we get only
|
363
|
+
# errors in stderr. Warnings can cause problems during the
|
364
|
+
# detection, for example ( warning: Insecure world writable dir
|
365
|
+
# /some/path in PATH, mode 040777 )
|
366
|
+
@warn = $-v
|
367
|
+
$-v = nil
|
368
|
+
Open3.popen3(command+' --help') { |stdin, stdout, stderr|
|
369
|
+
return stderr.read.length == 0
|
370
|
+
}
|
371
|
+
$-v = @warn
|
372
|
+
return true
|
373
|
+
end
|
374
|
+
|
375
|
+
# ---------------------------------------------------------------------
|
376
|
+
def create_compile_baseCommand
|
377
|
+
"pdflatex -synctex=1 --interaction #{@options.interaction} '#{@options.source}'"
|
378
|
+
end
|
379
|
+
|
380
|
+
end
|
381
|
+
|
382
|
+
# ============================================================================
|
383
|
+
|
384
|
+
class TexOutputFormatter
|
385
|
+
attr_reader :errorGroups, :citationWarnings, :referenceWarnings
|
386
|
+
attr_writer :errorGroups
|
387
|
+
|
388
|
+
def initialize(texOutput, verbose=false, quiet=false, source=nil,
|
389
|
+
errorGroups=nil)
|
390
|
+
#TODO add arg check
|
391
|
+
@texOutput = texOutput
|
392
|
+
@source = source
|
393
|
+
@totalWarnings = 0
|
394
|
+
@parsed = false
|
395
|
+
@verbose = verbose
|
396
|
+
@quiet = quiet
|
397
|
+
@errorGroups = errorGroups
|
398
|
+
self.initialize_error_groups
|
399
|
+
end
|
400
|
+
|
401
|
+
def initialize_error_groups
|
402
|
+
interestingLimits = 50
|
403
|
+
interestingLimits = 0 if @quiet
|
404
|
+
uninterestingLimits = 0
|
405
|
+
uninterestingLimits = 50 if @verbose
|
406
|
+
return unless @errorGroups.nil?
|
407
|
+
|
408
|
+
@filestate = FilenameParser.new
|
409
|
+
@citationWarnings = CitationWarning.new(interestingLimits)
|
410
|
+
@referenceWarnings = ReferenceWarning.new(interestingLimits)
|
411
|
+
@errorGroups = [
|
412
|
+
PDFVersionMismatchWarning.new(uninterestingLimits),
|
413
|
+
|
414
|
+
TexWarning.new('Underfull lines',
|
415
|
+
/Underfull.*/,
|
416
|
+
/\(badness [0-9]+\) in \w+/,
|
417
|
+
uninterestingLimits),
|
418
|
+
|
419
|
+
TexWarning.new('Overfull lines',
|
420
|
+
/Overfull.*/,
|
421
|
+
/\w+ \(.*?\) in \w+/,
|
422
|
+
uninterestingLimits),
|
423
|
+
|
424
|
+
TexWarning.new('Float changes',
|
425
|
+
/Warning:.*?float specifier changed to/,
|
426
|
+
/float specifier .*/,
|
427
|
+
uninterestingLimits),
|
428
|
+
|
429
|
+
TexWarning.new('Package Warning',
|
430
|
+
/Package .* Warning/,
|
431
|
+
/.*/,
|
432
|
+
uninterestingLimits),
|
433
|
+
|
434
|
+
|
435
|
+
FontWarning.new(uninterestingLimits),
|
436
|
+
|
437
|
+
@citationWarnings, @referenceWarnings,
|
438
|
+
|
439
|
+
TooManyWarning.new(interestingLimits),
|
440
|
+
|
441
|
+
RepeatedPageNumberWarning.new(uninterestingLimits),
|
442
|
+
|
443
|
+
TexError.new('Undefined Control Sequence',
|
444
|
+
/! Undefined control /,
|
445
|
+
/\\.*/, interestingLimits),
|
446
|
+
|
447
|
+
TexError.new('LaTeX error', /! LaTeX Error/, /.*/,
|
448
|
+
interestingLimits),
|
449
|
+
|
450
|
+
MissingParenthesisWarning.new(@source, interestingLimits),
|
451
|
+
|
452
|
+
PharagraphEndedWarning.new(@source, interestingLimits),
|
453
|
+
|
454
|
+
TexWarning.new('File not Found',
|
455
|
+
/Warning: File/,
|
456
|
+
/[^ ]+ not found/,
|
457
|
+
interestingLimits),
|
458
|
+
|
459
|
+
MultipleLabelWarning.new(@source, interestingLimits),
|
460
|
+
|
461
|
+
TexError.new('Other Errors', /^! /, /.*/, interestingLimits),
|
462
|
+
|
463
|
+
@filestate,
|
464
|
+
|
465
|
+
OtherWarning.new,
|
466
|
+
]
|
467
|
+
@errorGroups.each { |eg| eg.filestate = @filestate }
|
468
|
+
end
|
469
|
+
|
470
|
+
# ------------------------------------------------------------------------
|
471
|
+
def hasCitationWarnings?
|
472
|
+
self.parse unless @parsed
|
473
|
+
not @citationWarnings.empty?
|
474
|
+
end
|
475
|
+
|
476
|
+
def hasReferenceWarnings?
|
477
|
+
self.parse unless @parsed
|
478
|
+
not @referenceWarnings.empty?
|
479
|
+
end
|
480
|
+
|
481
|
+
# ------------------------------------------------------------------------
|
482
|
+
|
483
|
+
def add(handler)
|
484
|
+
handler.filestate = @filestate
|
485
|
+
@errorGroups.push handler
|
486
|
+
end
|
487
|
+
|
488
|
+
def print
|
489
|
+
self.parse unless @parsed
|
490
|
+
@errorGroups.each { |group|
|
491
|
+
if not group.empty?
|
492
|
+
Kernel.print group.to_s
|
493
|
+
end
|
494
|
+
}
|
495
|
+
end
|
496
|
+
|
497
|
+
# ------------------------------------------------------------------------
|
498
|
+
protected
|
499
|
+
def parse
|
500
|
+
lineNumberPattern = /lines ([0-9\-]+)/
|
501
|
+
@parsed = true
|
502
|
+
mergedLine = ""
|
503
|
+
lines = @texOutput.split "\n"
|
504
|
+
lines.each_with_index { |line,index|
|
505
|
+
#magic to unwrap the autowrapped lines from latex.. silly
|
506
|
+
mergedLine += line
|
507
|
+
if line.size == 79
|
508
|
+
next
|
509
|
+
elsif line.chomp.start_with? "! Undefined"
|
510
|
+
next
|
511
|
+
end
|
512
|
+
# start of the actual error parsing
|
513
|
+
line = mergedLine
|
514
|
+
handled = false
|
515
|
+
@errorGroups.each { |group|
|
516
|
+
break if group.handle(line, index, lines)
|
517
|
+
}
|
518
|
+
#Kernel.print line, "\n"
|
519
|
+
#Kernel.print line, "\n" if not handled
|
520
|
+
mergedLine = ""
|
521
|
+
}
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
# ============================================================================
|
526
|
+
class TexWarning
|
527
|
+
attr_reader :limit
|
528
|
+
attr_accessor :filestate
|
529
|
+
|
530
|
+
def initialize(name, pattern, printPattern=/.*/, limit=10,
|
531
|
+
additionaInputLines=0)
|
532
|
+
@name = name
|
533
|
+
@pattern = pattern
|
534
|
+
@printPattern = printPattern
|
535
|
+
@errors = []
|
536
|
+
@limit = limit
|
537
|
+
@maxLinesWidth = 0
|
538
|
+
@additionaInputLines = additionaInputLines
|
539
|
+
end
|
540
|
+
|
541
|
+
def handle(string, index=nil, lines=nil)
|
542
|
+
if not string.match(@pattern) or string.empty?
|
543
|
+
return false
|
544
|
+
end
|
545
|
+
string = self.expand_input(string, index, lines)
|
546
|
+
line = self.extract_line(string, index, lines)
|
547
|
+
self.add_error(line, string)
|
548
|
+
true
|
549
|
+
end
|
550
|
+
|
551
|
+
def expand_input(string, index, lines)
|
552
|
+
return string if @additionaInputLines == 0
|
553
|
+
to = [index + @additionaInputLines, lines.size].min
|
554
|
+
return lines[index..to].join("\n") + "\n"
|
555
|
+
end
|
556
|
+
|
557
|
+
def extract_line(string, index, lines)
|
558
|
+
# force 'line ..' first
|
559
|
+
if /line(?:s)? (?<line>[0-9\-]+)/ =~ string
|
560
|
+
return line
|
561
|
+
end
|
562
|
+
if /(?:line(?:s)? |l[^0-9])(?<line>[0-9\-]+)/ =~ string
|
563
|
+
return line
|
564
|
+
end
|
565
|
+
return "-"
|
566
|
+
end
|
567
|
+
|
568
|
+
def render_line(line)
|
569
|
+
(@filestate and @filestate.state or "") + line.to_s
|
570
|
+
end
|
571
|
+
|
572
|
+
def add_error(line, string)
|
573
|
+
return if self.has_error?(line, string) # avoid duplicate errors
|
574
|
+
line = self.render_line(line) unless line == "-"
|
575
|
+
@errors.push([line, string.to_s])
|
576
|
+
@maxLinesWidth = [@maxLinesWidth, line.size].max
|
577
|
+
end
|
578
|
+
|
579
|
+
def has_error?(line, string)
|
580
|
+
@errors.each { |e|
|
581
|
+
return true if e[0] == line and e[1] == string
|
582
|
+
}
|
583
|
+
false
|
584
|
+
end
|
585
|
+
|
586
|
+
def to_s
|
587
|
+
if self.empty?
|
588
|
+
return ""
|
589
|
+
end
|
590
|
+
self.sort
|
591
|
+
limit = [@limit, @errors.size].min
|
592
|
+
str = @name.redb + " [#{@errors.size}]: "
|
593
|
+
str += "\n" #if self.size > 1 and limit > 0
|
594
|
+
limit.times { |i|
|
595
|
+
str += " #{self.format_error(@errors[i][0], @errors[i][1])}\n"
|
596
|
+
}
|
597
|
+
if @limit < @errors.size and @limit != 0
|
598
|
+
str += " #{"...".ljust(@maxLinesWidth)} ...\n"
|
599
|
+
end
|
600
|
+
#str += "\n"
|
601
|
+
str
|
602
|
+
end
|
603
|
+
|
604
|
+
def format_error(line, string)
|
605
|
+
message = self.format_warning_message(line, string)
|
606
|
+
"#{line.ljust(@maxLinesWidth).yellow} #{message.to_s.chomp}"
|
607
|
+
end
|
608
|
+
|
609
|
+
def format_warning_message(line, string)
|
610
|
+
if @printPattern.instance_of? Proc
|
611
|
+
puts self.class, @name
|
612
|
+
return @printPattern.call(line, string)
|
613
|
+
end
|
614
|
+
matched = @printPattern.match string
|
615
|
+
return string if not matched
|
616
|
+
return matched
|
617
|
+
end
|
618
|
+
|
619
|
+
def sort
|
620
|
+
# magic from
|
621
|
+
# http://www.bofh.org.uk/2007/12/16/comprehensible-sorting-in-ruby
|
622
|
+
# for number sensible sorting
|
623
|
+
@errors.sort_by {|k|
|
624
|
+
k.to_s.split(/((?:(?:^|\s)[-+])?(?:\.\d+|\d+(?:\.\d+?(?:[eE]\d+)?(?:$|(?![eE\.])))?))/ms).
|
625
|
+
map {|v| Float(v) rescue v.downcase}
|
626
|
+
}
|
627
|
+
end
|
628
|
+
|
629
|
+
def size
|
630
|
+
@errors.size
|
631
|
+
end
|
632
|
+
|
633
|
+
def empty?
|
634
|
+
self.size == 0
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
class FilenameParser
|
639
|
+
|
640
|
+
def initialize
|
641
|
+
@nesting = []
|
642
|
+
end
|
643
|
+
|
644
|
+
def filestate=(state)
|
645
|
+
end
|
646
|
+
|
647
|
+
def handle(string, index, lines)
|
648
|
+
unless string =~ /^\([\.\/]/ || string =~ /^\[[^\]]/ || string =~ /^\)/
|
649
|
+
return false
|
650
|
+
end
|
651
|
+
self.parse(string)
|
652
|
+
return true
|
653
|
+
end
|
654
|
+
|
655
|
+
def parse(string)
|
656
|
+
while string and string != ""
|
657
|
+
|
658
|
+
if string =~ /^\s+(.*)/
|
659
|
+
string = $1
|
660
|
+
end
|
661
|
+
|
662
|
+
break unless string
|
663
|
+
|
664
|
+
if string =~ /^\[[^\]]+\](.*)/
|
665
|
+
string = $1
|
666
|
+
next
|
667
|
+
end
|
668
|
+
|
669
|
+
if string =~ /^(\)+)(.*)/
|
670
|
+
$1.size.times { @nesting.pop }
|
671
|
+
break unless $2
|
672
|
+
string = $2[1,$2.size]
|
673
|
+
next
|
674
|
+
end
|
675
|
+
|
676
|
+
if string =~ /^\(([^\(\)\[]+)(.*)/
|
677
|
+
@nesting.push($1)
|
678
|
+
string = $2
|
679
|
+
next
|
680
|
+
end
|
681
|
+
|
682
|
+
break
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
def empty?
|
687
|
+
return true
|
688
|
+
end
|
689
|
+
|
690
|
+
def state
|
691
|
+
if @nesting.size <= 1
|
692
|
+
return ""
|
693
|
+
end
|
694
|
+
return @nesting[1,@nesting.size].join("|") + ": "
|
695
|
+
end
|
696
|
+
end
|
697
|
+
|
698
|
+
class OtherWarning < TexWarning
|
699
|
+
def initialize(limits=10)
|
700
|
+
super('Other Warnings', /LaTeX Warning:/,
|
701
|
+
/[^(LaTeX Warning)].*/, limits)
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
class PDFVersionMismatchWarning < TexWarning
|
706
|
+
def initialize(limits=10)
|
707
|
+
super('PDF Version mismatches', /PDF version/, /found PDF .*/, limits)
|
708
|
+
end
|
709
|
+
|
710
|
+
def format_warning_message(line, string)
|
711
|
+
result = string.match(/found PDF version <(.*?)>.*?<(.*?)>/)
|
712
|
+
"found #{result[1]} instead of #{result[2]}"
|
713
|
+
end
|
714
|
+
|
715
|
+
def extract_line(string, index, lines)
|
716
|
+
/file (.*?)\):/.match(string)[1]
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
class TooManyWarning < TexWarning
|
721
|
+
def initialize(limits=10)
|
722
|
+
super('Too Many XYZ Warning', /Too many /, /^l\.[0-9]+ (.*)/, limits)
|
723
|
+
@additionaInputLines = 10
|
724
|
+
end
|
725
|
+
|
726
|
+
def format_warning_message(line, string)
|
727
|
+
@printPattern.match(string)[1]
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
|
732
|
+
class RepeatedPageNumberWarning < TexWarning
|
733
|
+
def initialize(limits=1)
|
734
|
+
super('PDF Repeated Page Number',
|
735
|
+
/destination with the same identifier \(name\{page\./, limits)
|
736
|
+
@additionaInputLines = 4
|
737
|
+
end
|
738
|
+
|
739
|
+
def format_warning_message(line, string)
|
740
|
+
match = /^l\.[0-9]+ (.*\n.*)/.match(string)[1]
|
741
|
+
match.gsub!(/\s*\n\s*/, '')
|
742
|
+
name = /^(.*)\)/.match(string)
|
743
|
+
if not name.nil?
|
744
|
+
name = name[1]
|
745
|
+
else
|
746
|
+
#fix this
|
747
|
+
end
|
748
|
+
if line.to_i == 0
|
749
|
+
prefix = "\n"
|
750
|
+
separator = ""
|
751
|
+
else
|
752
|
+
prefix = ""
|
753
|
+
separator = " "*Math.log(line.to_i).floor
|
754
|
+
end
|
755
|
+
"#{prefix} near: #{match}.\n#{separator} try using plainpages=false or pdfpagelabels in hyperref
|
756
|
+
see: http://en.wikibooks.org/wiki/LaTeX/Hyperlinks#Problems_with_Links_and_Pages"
|
757
|
+
end
|
758
|
+
end
|
759
|
+
|
760
|
+
|
761
|
+
class FontWarning < TexWarning
|
762
|
+
def initialize(limits=10)
|
763
|
+
super('Font Shape Warnings', /Font Warning: Font shape /, /.*/, limits, 1)
|
764
|
+
end
|
765
|
+
def format_warning_message(line, message)
|
766
|
+
message.sub(/\(Font\)\s+/, '').
|
767
|
+
sub("\n", ', ').
|
768
|
+
sub(", Font shape", ',').
|
769
|
+
match(/Font shape (.*?) on input/)[1]
|
770
|
+
end
|
771
|
+
end
|
772
|
+
|
773
|
+
class BibtexWarning < TexWarning
|
774
|
+
def initialize(limits=10)
|
775
|
+
super('Bibtex Warnings',
|
776
|
+
/I found no/,
|
777
|
+
/I f.*? command/, limits)
|
778
|
+
end
|
779
|
+
end
|
780
|
+
|
781
|
+
class CitationWarning < TexWarning
|
782
|
+
def initialize(limit=10)
|
783
|
+
super('Citation Undefined',
|
784
|
+
pattern=/Warning: Citation/,
|
785
|
+
printPattern=/Citation (?<citation>[^ ]+).*on page (?<page>[0-9]+) undefined/, limit=limit)
|
786
|
+
end
|
787
|
+
|
788
|
+
def format_warning_message(line, string)
|
789
|
+
matched = @printPattern.match string
|
790
|
+
"#{matched[:citation][1..-2]} (output page #{matched[:page]})"
|
791
|
+
end
|
792
|
+
end
|
793
|
+
|
794
|
+
class ReferenceWarning < TexWarning
|
795
|
+
def initialize(limits=1)
|
796
|
+
super('Reference Warnings',
|
797
|
+
/Warning: Reference/,
|
798
|
+
/[^ ]+ on page [0-9]+ undefined/, limits)
|
799
|
+
end
|
800
|
+
end
|
801
|
+
|
802
|
+
class MissingParenthesisWarning < TexWarning
|
803
|
+
def initialize(source, limits=10)
|
804
|
+
super('Missing Parenthesis',
|
805
|
+
/File ended /, /[^! ].*/,
|
806
|
+
limits)
|
807
|
+
@source = source
|
808
|
+
@sourceContents = File.read(source)
|
809
|
+
end
|
810
|
+
|
811
|
+
def extract_line(string, index, lines)
|
812
|
+
label = self.format_warning_message(0, string)
|
813
|
+
errorSource = Regexp.escape(lines[index - 1].strip()[0..-6])
|
814
|
+
errorSource.gsub!('\\ ', '\s*') # make sure we can match newlines
|
815
|
+
lineNumbers = []
|
816
|
+
matches = @sourceContents.match(errorSource)
|
817
|
+
(0..matches.size-1).each { |i|
|
818
|
+
to = matches.begin(i)
|
819
|
+
linenumber = @sourceContents[0..to].scan("\n").size + 1
|
820
|
+
lineNumbers.push(linenumber)
|
821
|
+
}
|
822
|
+
lineNumbers.join(',')
|
823
|
+
end
|
824
|
+
end
|
825
|
+
|
826
|
+
class PharagraphEndedWarning < TexWarning
|
827
|
+
def initialize(source, limits=10)
|
828
|
+
super('Runaway Argument',
|
829
|
+
/! Pharagraph ended /, /[^! ].*/,
|
830
|
+
limits)
|
831
|
+
@source = source
|
832
|
+
#@sourceContents = File.read(source)
|
833
|
+
end
|
834
|
+
|
835
|
+
def extract_line__(string, index, lines)
|
836
|
+
label = self.format_warning_message(0, string)
|
837
|
+
errorSource = Regexp.escape(lines[index - 1].strip()[0..-6])
|
838
|
+
errorSource.gsub!('\\ ', '\s*') # make sure we can match newlines
|
839
|
+
lineNumbers = []
|
840
|
+
matches = @sourceContents.match(errorSource)
|
841
|
+
(0..matches.size-1).each { |i|
|
842
|
+
to = matches.begin(i)
|
843
|
+
linenumber = @sourceContents[0..to].scan("\n").size + 1
|
844
|
+
lineNumbers.push(linenumber)
|
845
|
+
}
|
846
|
+
lineNumbers.join(',')
|
847
|
+
end
|
848
|
+
end
|
849
|
+
|
850
|
+
class MultipleLabelWarning < TexWarning
|
851
|
+
def initialize(source, limits=40)
|
852
|
+
super('Multiply defined Labels', /LaTeX Warning: Label.*? multiply/,
|
853
|
+
/.*/, limits)
|
854
|
+
@source = source
|
855
|
+
end
|
856
|
+
|
857
|
+
def format_warning_message(line, string)
|
858
|
+
string.match(/Label `(.*?)' /)[1]
|
859
|
+
end
|
860
|
+
|
861
|
+
def extract_line(string, index, lines)
|
862
|
+
label = self.format_warning_message(0, string)
|
863
|
+
`sed -ne /\label{#{label}}/= #{@source}`.split.join(',')
|
864
|
+
end
|
865
|
+
|
866
|
+
end
|
867
|
+
|
868
|
+
class TexError < TexWarning
|
869
|
+
def initialize(name, pattern, printPattern=/[^!].*/, limit=10)
|
870
|
+
super(name, pattern, printPattern, limit)
|
871
|
+
end
|
872
|
+
|
873
|
+
def extract_line(string, index, lines)
|
874
|
+
match = /(?:line(?:s)? |l.)([0-9\-]+( \\.*)?)/
|
875
|
+
if string =~ match
|
876
|
+
return $1.to_s
|
877
|
+
end
|
878
|
+
(index+1).upto(lines.size-1) { |i|
|
879
|
+
return "-" if lines[i].start_with? "!"
|
880
|
+
if lines[i] =~ match
|
881
|
+
return $1.to_s
|
882
|
+
end
|
883
|
+
}
|
884
|
+
return "-"
|
885
|
+
end
|
886
|
+
end
|
887
|
+
# ===========================================================================
|
888
|
+
|
889
|
+
def color_terminal?
|
890
|
+
[/linux/, /xterm.*/, /rxvt.*/].each { |m|
|
891
|
+
return true if ENV['TERM'] =~ m
|
892
|
+
}
|
893
|
+
return false
|
894
|
+
end
|
895
|
+
|
896
|
+
def colorize(text, color_code)
|
897
|
+
return "\033#{color_code}#{text}\033[0m" if color_terminal?()
|
898
|
+
return text
|
899
|
+
end
|
900
|
+
|
901
|
+
class String
|
902
|
+
def error(exitValue=nil)
|
903
|
+
puts self.redb
|
904
|
+
exit exitValue unless exitValue.nil?
|
905
|
+
end
|
906
|
+
|
907
|
+
def red
|
908
|
+
colorize(self, "[31m")
|
909
|
+
end
|
910
|
+
|
911
|
+
def redb
|
912
|
+
colorize(self, "[31;1m")
|
913
|
+
end
|
914
|
+
|
915
|
+
def green
|
916
|
+
colorize(self, "[32m")
|
917
|
+
end
|
918
|
+
|
919
|
+
def greenb
|
920
|
+
colorize(self, "[32;1m")
|
921
|
+
end
|
922
|
+
|
923
|
+
def yellow
|
924
|
+
colorize(self, "[33m")
|
925
|
+
end
|
926
|
+
|
927
|
+
def yellowb
|
928
|
+
colorize(self, "[33;1m")
|
929
|
+
end
|
930
|
+
|
931
|
+
def pink
|
932
|
+
colorize(self, "[35m")
|
933
|
+
end
|
934
|
+
|
935
|
+
def pinkb
|
936
|
+
colorize(self, "[35;1m")
|
937
|
+
end
|
938
|
+
|
939
|
+
def cyan
|
940
|
+
colorize(self, "[36m")
|
941
|
+
end
|
942
|
+
|
943
|
+
def cyanb
|
944
|
+
colorize(self, "[36;1m")
|
945
|
+
end
|
946
|
+
|
947
|
+
end
|
948
|
+
|
949
|
+
# vim: set ts=4 sw=4 ts=4 :
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: trex
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Camillo Bruni
|
9
|
+
- Toon Verwaest
|
10
|
+
- Damien Pollet
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2012-01-20 00:00:00.000000000 Z
|
15
|
+
dependencies: []
|
16
|
+
description: trex is script simplifying the compilation of latex files by creating
|
17
|
+
proper human-readable error output with repeating patterns. Unlike the original
|
18
|
+
latex output which is oververbosified.
|
19
|
+
email: camillorbuni@cxg.ch
|
20
|
+
executables:
|
21
|
+
- trex
|
22
|
+
extensions: []
|
23
|
+
extra_rdoc_files: []
|
24
|
+
files:
|
25
|
+
- lib/trex.rb
|
26
|
+
- bin/trex
|
27
|
+
homepage: https://github.com/dh83/trex
|
28
|
+
licenses: []
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
requirements: []
|
46
|
+
rubyforge_project:
|
47
|
+
rubygems_version: 1.8.11
|
48
|
+
signing_key:
|
49
|
+
specification_version: 3
|
50
|
+
summary: Automate compilation of LaTeX documents, making the output human-readable.
|
51
|
+
test_files: []
|