diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..fa8d8b2 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,2 @@ +Metrics/LineLength: + Enabled: false \ No newline at end of file diff --git a/constants.rb b/constants.rb index f414879..6714b4c 100644 --- a/constants.rb +++ b/constants.rb @@ -16,16 +16,16 @@ # You should have received a copy of the GNU General Public License # along with ltx2any. If not, see . -# TODO move to a properties file? -NAME = 'ltx2any' -VERSION = '0.9a' -YEAR = '2016' -AUTHOR = 'Raphael Reitzig' -TMPSUFFIX = '_tmp' -HASHFILE = '.hashes' # relative to tmp directory -# TODO move this constant to HashManager? +# TODO: move to a properties file? +NAME = 'ltx2any'.freeze +VERSION = '0.9b'.freeze +YEAR = '2018'.freeze +AUTHOR = 'Raphael Reitzig'.freeze +TMPSUFFIX = '_tmp'.freeze +HASHFILE = '.hashes'.freeze # relative to tmp directory +# TODO: move this constant to HashManager? -LIBDIR = 'lib' -EXTDIR = 'extensions' -ENGDIR = 'engines' -LOGWDIR = 'logwriters' +LIBDIR = 'lib'.freeze +EXTDIR = 'extensions'.freeze +ENGDIR = 'engines'.freeze +LOGWDIR = 'logwriters'.freeze diff --git a/engines/lualatex.rb b/engines/lualatex.rb index 59efffe..e78aabc 100644 --- a/engines/lualatex.rb +++ b/engines/lualatex.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -18,6 +18,7 @@ Dependency.new('lualatex', :binary, [:engine, 'lualatex'], :essential) +# TODO: document class LuaLaTeX < Engine def initialize @@ -25,28 +26,28 @@ def initialize @binary = 'lualatex' @extension = 'pdf' @description = 'Uses LuaLaTeX to create a PDF' - + @target_file = "#{ParameterManager.instance[:jobname]}.#{extension}" @old_hash = hash_result end - + def do? !File.exist?(@target_file) || hash_result != @old_hash end - + def hash_result - HashManager.hash_file(@target_file, + HashManager.hash_file(@target_file, without: /\/CreationDate|\/ModDate|\/ID|\/Type\/XRef\/Index/) end def exec @old_hash = hash_result - + # Command for the main LaTeX compilation work params = ParameterManager.instance lualatex = '"lualatex -file-line-error -interaction=nonstopmode #{params[:enginepar]} \"#{params[:jobfile]}\""' - f = IO::popen(eval(lualatex)) + f = IO.popen(eval(lualatex)) log = f.readlines.map! { |s| Log.fix(s) } { success: File.exist?(@target_file), messages: TeXLogParser.parse(log), log: log.join('').strip! } diff --git a/engines/pdflatex.rb b/engines/pdflatex.rb index 85d7be2..8f02105 100644 --- a/engines/pdflatex.rb +++ b/engines/pdflatex.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -18,22 +18,23 @@ Dependency.new('pdflatex', :binary, [:engine, 'pdflatex'], :essential) -class PdfLaTeX < Engine +# TODO: document +class PdfLaTeX < Engine def initialize super @binary = 'pdflatex' @extension = 'pdf' @description = 'Uses PdfLaTeX to create a PDF' - + @target_file = "#{ParameterManager.instance[:jobname]}.#{extension}" @old_hash = hash_result end - + def do? !File.exist?(@target_file) || hash_result != @old_hash end - + def hash_result HashManager.hash_file(@target_file, without: /\/CreationDate|\/ModDate|\/ID/) end @@ -45,7 +46,7 @@ def exec params = ParameterManager.instance pdflatex = '"pdflatex -file-line-error -interaction=nonstopmode #{params[:enginepar]} \"#{params[:jobfile]}\""' - f = IO::popen(eval(pdflatex)) + f = IO.popen(eval(pdflatex)) log = f.readlines.map! { |s| Log.fix(s) } { success: File.exist?(@target_file), messages: TeXLogParser.parse(log), log: log.join('').strip! } diff --git a/engines/xelatex.rb b/engines/xelatex.rb index c03e0d6..06afb0c 100644 --- a/engines/xelatex.rb +++ b/engines/xelatex.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -18,34 +18,34 @@ Dependency.new('xelatex', :binary, [:engine, 'xelatex'], :essential) +# TODO: document class XeLaTeX < Engine - def initialize super @binary = 'xelatex' @extension = 'pdf' @description = 'Uses XeLaTeX to create a PDF' - + @target_file = "#{ParameterManager.instance[:jobname]}.#{extension}" @old_hash = hash_result end - + def do? !File.exist?(@target_file) || hash_result != @old_hash end - + def hash_result HashManager.hash_file(@target_file, drop_from: /CIDFontType0C|Type1C/) end def exec @old_hash = hash_result - + # Command for the main LaTeX compilation work params = ParameterManager.instance xelatex = '"xelatex -file-line-error -interaction=nonstopmode #{params[:enginepar]} \"#{params[:jobfile]}\""' - f = IO::popen(eval(xelatex)) + f = IO.popen(eval(xelatex)) log = f.readlines.map! { |s| Log.fix(s) } { success: File.exist?(@target_file), messages: TeXLogParser.parse(log), log: log.join('').strip! } diff --git a/extensions/10_biber.rb b/extensions/10_biber.rb index 1b91257..9e1a971 100644 --- a/extensions/10_biber.rb +++ b/extensions/10_biber.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -18,6 +18,7 @@ Dependency.new('biber', :binary, [:extension, 'Biber'], :essential) +# TODO: document class Biber < Extension def initialize super @@ -28,12 +29,12 @@ def initialize def do?(time) return false unless time == 1 - + params = ParameterManager.instance - + usesbib = File.exist?("#{params[:jobname]}.bcf") needrerun = false - + if usesbib # Collect sources (needed for log parsing) @sources = [] @@ -43,8 +44,8 @@ def do?(time) end } @sources.uniq! - - + + # Aside from the first run (no bbl), # there are two things that prompt us to rerun: # * changes to the bcf file (which includes all kinds of things, @@ -53,7 +54,7 @@ def do?(time) needrerun = !File.exist?("#{params[:jobname]}.bbl") | # Is this the first run? HashManager.instance.files_changed?("#{params[:jobname]}.bcf", *@sources) - # Note: non-strict OR so that hashes are computed for next run + # Note: non-strict OR so that hashes are computed for next run end usesbib && needrerun @@ -61,13 +62,13 @@ def do?(time) def exec(time, progress) params = ParameterManager.instance - + # Command to process bibtex bibliography if necessary. # Uses the following variables: # * jobname -- name of the main LaTeX file (without file ending) biber = '"biber \"#{params[:jobname]}\""' - f = IO::popen(eval(biber)) + f = IO.popen(eval(biber)) log = f.readlines # Dig trough output and find errors @@ -92,5 +93,5 @@ def exec(time, progress) { success: !errors, messages: msgs, log: log.join('').strip! } end end - + Extension.add Biber diff --git a/extensions/10_bibtex.rb b/extensions/10_bibtex.rb index ace4c11..cbb6f88 100644 --- a/extensions/10_bibtex.rb +++ b/extensions/10_bibtex.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -18,13 +18,14 @@ Dependency.new('bibtex', :binary, [:extension, 'BibTeX'], :essential) +# TODO: document class BibTeX < Extension def initialize - super + super @name = 'BibTeX' @description = 'Creates bibliographies (old)' - - # For checking whether bibtex has to rerun, we need to keep the + + # For checking whether bibtex has to rerun, we need to keep the # relevant parts of the _.aux file handy. # TODO use internal store? @grepfile = 'bibtex_aux_grep' @@ -32,9 +33,9 @@ def initialize def do?(time) return false unless time == 1 - + params = ParameterManager.instance - + # Collect used bibdata files and style file stylefile = [] bibdata = [] @@ -42,24 +43,24 @@ def do?(time) if File.exist?("#{params[:jobname]}.aux") File.open("#{params[:jobname]}.aux", 'r') { |file| while ( line = file.gets ) - if /^\\bibdata\{(.+?)\}$/ =~ line + if /^\\bibdata{(.+?)}$/ =~ line # If commas occur, add both a split version (multiple files) # and the hole string (filename with comma), to be safe. bibdata += $~[1].split(',').map { |s| "#{s}.bib" } + [$~[1]] - grepdata.push line.strip - elsif /^\\bibstyle\{(.+?)\}$/ =~ line + grepdata.push line.strip + elsif /^\\bibstyle{(.+?)}$/ =~ line stylefile.push "#{$~[1]}.bst" - grepdata.push line.strip + grepdata.push line.strip elsif /^\\(bibcite|citation)/ =~ line - grepdata.push line.strip - end + grepdata.push line.strip + end end } - end - + end + # Check whether bibtex is necessary at all - usesbib = bibdata.size > 0 - + usesbib = !bibdata.empty? + # Write relevant part of the _.aux file into a separate file for hashing if usesbib File.open(@grepfile, 'w') { |f| @@ -78,13 +79,13 @@ def do?(time) def exec(time, progress) params = ParameterManager.instance - + # Command to process bibtex bibliography if necessary. # Uses the following variables: # * jobname -- name of the main LaTeX file (without file ending) bibtex = '"bibtex \"#{params[:jobname]}\""' - f = IO::popen(eval(bibtex)) + f = IO.popen(eval(bibtex)) log = f.readlines # Dig trough output and find errors @@ -103,7 +104,7 @@ def exec(time, progress) msg = lastline logline = [linectr - 1, linectr] end - + msgs.push(LogMessage.new(:error, $~[3], [Integer($~[2])], logline, msg)) errors = true end @@ -114,5 +115,5 @@ def exec(time, progress) { success: !errors, messages: msgs, log: log.join('').strip! } end end - + Extension.add BibTeX diff --git a/extensions/20_makeindex.rb b/extensions/20_makeindex.rb index d7a12ed..ae52c53 100644 --- a/extensions/20_makeindex.rb +++ b/extensions/20_makeindex.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -18,55 +18,56 @@ Dependency.new('makeindex', :binary, [:extension, 'makeindex'], :essential) +# TODO: document class MakeIndex < Extension def initialize super - + @name = 'makeindex' @description = 'Creates an index' end def do?(time) return false unless time == 1 - + params = ParameterManager.instance - - File.exist?("#{params[:jobname]}.idx") \ - && ( !File.exist?("#{params[:jobname]}.ind") \ - | HashManager.instance.files_changed?("#{params[:jobname]}.idx") - # Note: non-strict OR so that hashes are computed for next run - ) + + File.exist?("#{params[:jobname]}.idx") && + (!File.exist?("#{params[:jobname]}.ind") | + HashManager.instance.files_changed?("#{params[:jobname]}.idx") + # Note: non-strict OR so that hashes are computed for next run + ) end def exec(time, progress) params = ParameterManager.instance - + # Command to create the index if necessary. Provide two versions, # one without and one with stylefile # Uses the following variables: # * jobname -- name of the main LaTeX file (without file ending) # * mistyle -- name of the makeindex style file (with file ending) - makeindex = {'default' => '"makeindex -q \"#{params[:jobname]}\" 2>&1"', - 'styled' => '"makeindex -q -s \"#{mistyle}\" \"#{params[:jobname]}\" 2>&1"'} - - version = 'default' + makeindex = { default: '"makeindex -q \"#{params[:jobname]}\" 2>&1"', + styled: '"makeindex -q -s \"#{mistyle}\" \"#{params[:jobname]}\" 2>&1"' } + + version = :default mistyle = nil - Dir['*.ist'].each { |f| - version = 'styled' + Dir['*.ist'].each do |f| + version = :styled mistyle = f - } + end - # Even in quiet mode, some critical errors (e.g. regarding -g) + # Even in quiet mode, some critical errors (e.g. regarding -g) # only end up in the error stream, but not in the file. Doh. log1 = [] - IO::popen(eval(makeindex[version])) { |f| - log1 = f.readlines - } + IO.popen(eval(makeindex[version])) do |f| + log1 = f.readlines + end log2 = [] - File.open("#{params[:jobname]}.ilg", 'r') { |f| + File.open("#{params[:jobname]}.ilg", 'r') do |f| log2 = f.readlines - } + end log = [log2[0]] + log1 + log2[1,log2.length] @@ -74,15 +75,15 @@ def exec(time, progress) current = [] linectr = 1 errors = false - log.each { |line| + log.each do |line| if /^!! (.*?) \(file = (.+?), line = (\d+)\):$/ =~ line current = [:error, $~[2], [Integer($~[3])], [linectr], "#{$~[1]}: "] errors = true - elsif /^\#\# (.*?) \(input = (.+?), line = (\d+); output = .+?, line = \d+\):$/ =~ line + elsif /^## (.*?) \(input = (.+?), line = (\d+); output = .+?, line = \d+\):$/ =~ line current = [:warning, $~[2], [Integer($~[3])], [linectr], "#{$~[1]}: "] elsif current != [] && /^\s+-- (.*)$/ =~ line current[3][1] = linectr - msgs.push(LogMessage.new(current[0], current[1], current[2], + msgs.push(LogMessage.new(current[0], current[1], current[2], current[3], current[4] + $~[1].strip)) current = [] elsif /Option -g invalid/ =~ line @@ -93,7 +94,7 @@ def exec(time, progress) errors = true end linectr += 1 - } + end { sucess: !errors, messages: msgs, log: log.join('').strip! } end diff --git a/extensions/30_metapost.rb b/extensions/30_metapost.rb index bc1c175..fa84ecf 100644 --- a/extensions/30_metapost.rb +++ b/extensions/30_metapost.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -18,12 +18,13 @@ Dependency.new('mpost', :binary, [:extension, 'MetaPost'], :essential) +# TODO: document class MetaPost < Extension def initialize super @name = 'MetaPost' @description = 'Compiles generated MetaPost files' - + @mp_files = [] end @@ -35,27 +36,27 @@ def job_size # Count the number of changed _.mp files # Store because a check for changed hashes in exec later would give false! # Append because job_size may be called multiple times before exec - @mp_files += Dir.entries('.').delete_if { |f| - (/\.mp$/ !~ f) || !HashManager.instance.files_changed?(f) - } + @mp_files += Dir.entries('.').delete_if do |f| + (/\.mp$/ !~ f) || !HashManager.instance.files_changed?(f) + end @mp_files.size - - # TODO check for (non-)existing result? incorporate ir parameter? + + # TODO: check for (non-)existing result? incorporate ir parameter? end def exec(time, progress) params = ParameterManager.instance - + # Command to process metapost files if necessary. mpost = '"mpost -tex=#{params[:engine]} -file-line-error -interaction=nonstopmode \"#{f}\" 2>&1"' - + # Run mpost for each job file log = [[], []] - if !@mp_files.empty? + unless @mp_files.empty? # Run (latex) engine for each figure - log = self.class.execute_parts(@mp_files, progress) { |f| - compile(mpost, f) - }.transpose + log = self.class.execute_parts(@mp_files, progress) do |f| + compile(mpost, f) + end.transpose end @mp_files = [] # reset for next round of checks @@ -63,83 +64,76 @@ def exec(time, progress) # w.r.t. its own contribution. Later steps will only add the offset of the # whole metapost block, not those inside. offset = 0 - (0..(log[0].size - 1)).each { |i| - if log[0][i].size > 0 + (0..(log[0].size - 1)).each do |i| + unless log[0][i].empty? internal_offset = 3 # Stuff we print per plot before log excerpt (see :compile) - log[0][i].map! { |m| - LogMessage.new(m.type, m.srcfile, m.srcline, - if m.logline != nil then - m.logline.map { |ll| ll + offset + internal_offset} - else - nil + log[0][i].map! do |m| + LogMessage.new(m.type, m.srcfile, m.srcline, + unless m.logline.nil? + m.logline.map { |ll| ll + offset + internal_offset} end, - m.msg, if m.formatted? then :fixed else :none end) - } + m.msg, m.formatted? ? :fixed : :none) + end end - offset += log[1][i].count(?\n) - } + offset += log[1][i].count("\n") + end log[0].flatten! errors = log[0].count { |m| m.type == :error } { success: errors <= 0, messages: log[0], log: log[1].join } end - - private - def compile(cmd, f) - params = ParameterManager.instance - - log = '' - msgs = [] - - # Run twice to get LaTeX bits right - IO::popen(eval(cmd)) { |io| - io.readlines - # Closes IO - } - lines = IO::popen(eval(cmd)) { |io| - io.readlines - # Closes IO - } - output = lines.join('').strip - - log << "# #\n# #{f}\n\n" - if output != '' - log << output - msgs += msgs = parse(lines, f) - else - log << 'No output from mpost, so apparently everything went fine!' - end - log << "\n\n" - [msgs, log] + private + + def compile(cmd, f) + params = ParameterManager.instance + + log = '' + msgs = [] + + # Run twice to get LaTeX bits right + IO.popen(eval(cmd), &:readlines) + lines = IO.popen(eval(cmd), &:readlines) + output = lines.join('').strip + + log << "# #\n# #{f}\n\n" + if output != '' + log << output + msgs += parse(lines, f) + else + log << 'No output from mpost, so apparently everything went fine!' end - - def parse(strings, file) - msgs = [] - - linectr = 1 - curmsg = nil - curline = -1 - strings.each { |line| - # Messages have the format - # ! message - # ... - # l.\d+ ... - if /^! (.*)$/ =~ line - curmsg = $~[1].strip - curline = linectr - elsif curmsg != nil && /^l\.(\d+)/ =~ line - msgs.push(LogMessage.new(:error, "#{ParameterManager.instance[:tmpdir]}/#{file}", - [Integer($~[1])], [curline, linectr], - curmsg, :none)) - curmsg = nil - curline = -1 - end - linectr += 1 - } + log << "\n\n" + + [msgs, log] + end + + def parse(strings, file) + msgs = [] - msgs + linectr = 1 + curmsg = nil + curline = -1 + strings.each do |line| + # Messages have the format + # ! message + # ... + # l.\d+ ... + if /^! (.*)$/ =~ line + curmsg = $LAST_MATCH_INFO[1].strip + curline = linectr + elsif !curmsg.nil? && /^l\.(\d+)/ =~ line + msgs.push(LogMessage.new(:error, "#{ParameterManager.instance[:tmpdir]}/#{file}", + [Integer($LAST_MATCH_INFO[1])], [curline, linectr], + curmsg, :none)) + curmsg = nil + curline = -1 + end + linectr += 1 end + + msgs + end end Extension.add MetaPost diff --git a/extensions/30_sagetex.rb b/extensions/30_sagetex.rb index 8863ded..328159a 100644 --- a/extensions/30_sagetex.rb +++ b/extensions/30_sagetex.rb @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with ltx2any. If not, see . +# TODO: document class SageTeX < Extension def initialize super @@ -26,8 +27,8 @@ def initialize def do?(time) params = ParameterManager.instance time == 1 && - File.exist?("#{params[:jobname]}.sagetex.sage") && - HashManager.instance.files_changed?("#{params[:jobname]}.sagetex.sage") + File.exist?("#{params[:jobname]}.sagetex.sage") && + HashManager.instance.files_changed?("#{params[:jobname]}.sagetex.sage") end def exec(time, progress) @@ -53,19 +54,20 @@ def parse(lines) msg = nil linectr = 1 lines.each { |line| - if !msg.nil? && line.strip.length == 0 + if !msg.nil? && line.strip.empty? msg.logline << linectr - 1 msg = nil - elsif msg.nil? && line =~ /File "(.+)", line (\d+)/ + elsif msg.nil? && line =~ /File "(.+)",\s+line (\d+)/ msg = LogMessage.new(:warning, $~[1], [$~[2].to_i], [linectr], '', :fixed) messages << msg elsif line =~ /SyntaxError: \w+/ msg.type = :error msg.logline << linectr + msg.msg += line msg = nil elsif line =~ /sagetex\.VersionError:/ msg.type = :error - # TODO what are other patterns? + # TODO: what are other patterns? elsif !msg.nil? msg.msg += line end diff --git a/extensions/30_tikzext.rb b/extensions/30_tikzext.rb index 56ae7aa..ad84a4f 100644 --- a/extensions/30_tikzext.rb +++ b/extensions/30_tikzext.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -19,6 +19,7 @@ ParameterManager.instance.addParameter(Parameter.new( :imagerebuild, 'ir', String, '', "Specify externalised TikZ images to rebuild, separated by ':'. Set to 'all' to rebuild all.")) +# TODO: document class TikZExt < Extension def initialize super @@ -29,14 +30,14 @@ def initialize def do?(time) time == 1 && job_size > 0 end - + def job_size collect_pending[0].size end def exec(time, progress) params = ParameterManager.instance - + # Command to process externalised TikZ images if necessary. # Uses the following variables: # * $params["engine"] -- Engine used by the main job. @@ -47,134 +48,129 @@ def exec(time, progress) figures,rebuildlog = collect_pending log = [[], []] - if !figures.empty? + unless figures.empty? # Run (latex) engine for each figure - log = self.class.execute_parts(figures, progress) { |fig| + log = self.class.execute_parts(figures, progress) do |fig| compile(pdflatex, fig) - }.transpose + end.transpose end # Log line numbers are wrong since every compile determines log line numbers # w.r.t. its own contribution. Later steps will only add the offset of the # whole tikzext block, not those inside. offset = 0 - (0..(log[0].size - 1)).each { |i| - if log[0][i].size > 0 + (0..(log[0].size - 1)).each do |i| + if !log[0][i].empty? internal_offset = 5 # Stuff we print per figure before log excerpt (see :compile) - log[0][i].map! { |m| - LogMessage.new(m.type, m.srcfile, m.srcline, - if m.logline != nil then + log[0][i].map! do |m| + LogMessage.new(m.type, m.srcfile, m.srcline, + if m.logline != nil m.logline.map { |ll| ll + offset + internal_offset - 1} # -1 because we drop first line! else nil end, - m.msg, if m.formatted? then :fixed else :none end) - } - - log[0][i] = [LogMessage.new(:info, nil, nil, nil, - "The following messages refer to figure\n #{figures[i]}.\n" + + m.msg, m.formatted? ? :fixed : :none) + end + + log[0][i] = [LogMessage.new(:info, nil, nil, nil, + "The following messages refer to figure\n #{figures[i]}.\n" \ "See\n #{params[:tmpdir]}/#{figures[i]}.log\nfor the full log.", :fixed) ] + log[0][i] else - log[0][i] += [LogMessage.new(:info, nil, nil, nil, - "No messages for figure\n #{figures[i]}.\nfound. " + + log[0][i] += [LogMessage.new(:info, nil, nil, nil, + "No messages for figure\n #{figures[i]}.\nfound. " \ "See\n #{params[:tmpdir]}/#{figures[i]}.log\nfor the full log.", :fixed) ] end - offset += log[1][i].count(?\n) - } - + offset += log[1][i].count(?\n) + end + log[0].flatten! errors = log[0].count { |m| m.type == :error } { success: errors <= 0, messages: rebuildlog[0] + log[0], log: rebuildlog[1] + log[1].join } end - + private - def collect_pending - params = ParameterManager.instance - - figures,rebuildlog = [], [[], ''] - if File.exists?("#{params[:jobname]}.figlist") - figures = IO.readlines("#{params[:jobname]}.figlist").map { |fig| - if fig.strip != '' - fig.strip - else - nil - end - }.compact - # Remove results of figures that we want to rebuild - rebuild = [] - if params[:imagerebuild] == 'all' - rebuild = figures + def collect_pending + params = ParameterManager.instance + + figures = [] + rebuildlog = [[], ''] + if File.exist?("#{params[:jobname]}.figlist") + figures = IO.readlines("#{params[:jobname]}.figlist").map do |fig| + if fig.strip != '' + fig.strip else - params[:imagerebuild].split(':').map { |s| s.strip }.each { |fig| - if figures.include?(fig) - rebuild.push(fig) - else - msg = "User requested rebuild of figure `#{fig}` which does not exist." - rebuildlog[0].push(LogMessage.new(:warning, nil, nil, nil, msg)) - rebuildlog[1] += "#{msg}\n\n" - end - } + nil end - - - figures.select! { |fig| - !File.exist?("#{fig}.pdf") || rebuild.include?(fig) - } - end + end.compact - [figures, rebuildlog] - end - - def compile(cmd, fig) - params = ParameterManager.instance - - msgs = [] - log = "# #\n# Figure: #{fig}\n# See #{ParameterManager.instance[:tmpdir]}/#{fig}.log for full log.\n\n" - - # Run twice to clean up log? - # IO::popen(eval(cmd)).readlines - IO::popen(eval(cmd)) { |io| - io.readlines - # Closes IO - } - # Shell output does not contain error messages -> read log - output = File.open("#{fig}.log", 'r') { |f| - f.readlines.map { |s| Log.fix(s) } - } - - # These seems to describe reliable boundaries of that part in the log - # which deals with the processed TikZ figure. - startregexp = /^\\openout5 = `#{fig}\.dpth'\.\s*$/ - endregexp = /^\[\d+\s*$/ - - # Cut out relevant part for raw log (heuristic) - string = output.drop_while { |line| - startregexp !~ line - }.take_while { |line| - endregexp !~ line - }.drop(1).join('').strip - - if string != '' - log << "\n\n#{string}\n\n" + # Remove results of figures that we want to rebuild + rebuild = [] + if params[:imagerebuild] == 'all' + rebuild = figures else - log << 'No errors detected.' + params[:imagerebuild].split(':').map(&:strip).each do |fig| + if figures.include?(fig) + rebuild.push(fig) + else + msg = "User requested rebuild of figure `#{fig}` which does not exist." + rebuildlog[0].push(LogMessage.new(:warning, nil, nil, nil, msg)) + rebuildlog[1] += "#{msg}\n\n" + end + end end - - # Parse whole log for messages (needed for filenames) but restrict - # to messages from interesting part - msgs = TeXLogParser.parse(output, startregexp, endregexp) - - # Still necessary? Should get *some* error from the recursive call. - # if ( !File.exist?("#{fig}.pdf") ) - # log << "Fatal error on #{fig}. See #{$params["tmpdir"]}/#{fig}.log for details.\n" - # end - log << "\n\n" - - [msgs, log] + + + figures.select! do |fig| + !File.exist?("#{fig}.pdf") || rebuild.include?(fig) + end + end + + [figures, rebuildlog] + end + + def compile(cmd, fig) + params = ParameterManager.instance + + msgs = [] + log = "# #\n# Figure: #{fig}\n# See #{ParameterManager.instance[:tmpdir]}/#{fig}.log for full log.\n\n" + + # Run twice to clean up log? + # IO::popen(eval(cmd)).readlines + IO.popen(eval(cmd), &:readlines) + # Shell output does not contain error messages -> read log + output = File.open("#{fig}.log", 'r') do |f| + f.readlines.map { |s| Log.fix(s) } end + + # These seems to describe reliable boundaries of that part in the log + # which deals with the processed TikZ figure. + startregexp = /^\\openout5 = `#{fig}\.dpth'\.\s*$/ + endregexp = /^\[\d+\s*$/ + + # Cut out relevant part for raw log (heuristic) + string = output.drop_while do |line| + startregexp !~ line + end.take_while do |line| + endregexp !~ line + end.drop(1).join('').strip + + log << (string != '' ? "\n\n#{string}\n\n" : 'No errors detected.') + + # Parse whole log for messages (needed for filenames) but restrict + # to messages from interesting part + msgs = TeXLogParser.parse(output, startregexp, endregexp) + + # Still necessary? Should get *some* error from the recursive call. + # if ( !File.exist?("#{fig}.pdf") ) + # log << "Fatal error on #{fig}. See #{$params["tmpdir"]}/#{fig}.log for details.\n" + # end + log << "\n\n" + + [msgs, log] + end end Extension.add TikZExt diff --git a/extensions/50_gnuplot.rb b/extensions/50_gnuplot.rb index dcb40c1..1c5784a 100644 --- a/extensions/50_gnuplot.rb +++ b/extensions/50_gnuplot.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -18,12 +18,13 @@ Dependency.new('gnuplot', :binary, [:extension, 'Gnuplot'], :essential) +# TODO: document class Gnuplot < Extension def initialize super @name = 'Gnuplot' @description = 'Executes generated gnuplot files' - + @gnuplot_files = [] end @@ -49,7 +50,7 @@ def exec(time, progress) # Run gnuplot for each remaining file log = [[], []] - if !@gnuplot_files.empty? + unless @gnuplot_files.empty? log = self.class.execute_parts(@gnuplot_files, progress) { |f| compile(gnuplot, f) }.transpose @@ -60,84 +61,81 @@ def exec(time, progress) # whole gnuplot block, not those inside. offset = 0 (0..(log[0].size - 1)).each { |i| - if log[0][i].size > 0 + unless log[0][i].empty? internal_offset = 2 # Stuff we print per plot before log excerpt (see :compile) log[0][i].map! { |m| - LogMessage.new(m.type, m.srcfile, m.srcline, - if m.logline != nil then - m.logline.map { |ll| ll + offset + internal_offset} + LogMessage.new(m.type, m.srcfile, m.srcline, + if !m.logline.nil? + m.logline.map { |ll| ll + offset + internal_offset} else nil end, - m.msg, if m.formatted? then :fixed else :none end) + m.msg, m.formatted? ? :fixed : :none) } end - offset += log[1][i].count(?\n) + offset += log[1][i].count(?\n) } log[0].flatten! errors = log[0].count { |m| m.type == :error } { success: errors <= 0, messages: log[0], log: log[1].join } end - - private - def compile(cmd, f) - params = ParameterManager.instance - - log = '' - msgs = [] - - lines = IO::popen(eval(cmd)) { |io| - io.readlines - # Closes IO - } - output = lines.join('').strip - - log << "# #\n# #{f}\n\n" - if output != '' - log << output - msgs += parse(lines) + + private + + def compile(cmd, f) + params = ParameterManager.instance + + log = '' + msgs = [] + + lines = IO.popen(eval(cmd), &:readlines) + output = lines.join('').strip + + log << "# #\n# #{f}\n\n" + if output != '' + log << output + msgs += parse(lines) + else + log << 'No output from gnuplot, so apparently everything went fine!' + end + log << "\n\n" + + [msgs, log] + end + + def parse(strings) + msgs = [] + + context = '' + contextline = 1 + linectr = 1 + strings.each { |line| + # Messages have the format + # * context (at least one line) + # * ^ marking the point of issue in its own line + # * one line of error statement + # I have never seen more than one error (seems to abort). + # So I'm going to assume that multiple error messages + # are separated by empty lines. + if /^"(.+?)", line (\d+): (.*)$/ =~ line + msgs.push(LogMessage.new(:error, "#{ParameterManager.instance[:tmpdir]}/#{$~[1]}", + [Integer($~[2])], [[contextline, linectr].min, linectr], + "#{context}#{$~[3].strip}", :fixed)) + elsif line.strip == '' + context = '' + contextline = strings.size + 1 # Larger than every line number else - log << 'No output from gnuplot, so apparently everything went fine!' + contextline = [contextline, linectr].min + context += line end - log << "\n\n" + linectr += 1 + # TODO: break/strip long lines? Should be able to figure out relevant parts by position of circumflex + # TODO: drop context here and instead give log line numbers? + } - [msgs, log] - end - - def parse(strings) - msgs = [] - - context = '' - contextline = 1 - linectr = 1 - strings.each { |line| - # Messages have the format - # * context (at least one line) - # * ^ marking the point of issue in its own line - # * one line of error statement - # I have never seen more than one error (seems to abort). - # So I'm going to assume that multiple error messages - # are separated by empty lines. - if /^"(.+?)", line (\d+): (.*)$/ =~ line - msgs.push(LogMessage.new(:error, "#{ParameterManager.instance[:tmpdir]}/#{$~[1]}", - [Integer($~[2])], [[contextline, linectr].min, linectr], - "#{context}#{$~[3].strip}", :fixed)) - elsif line.strip == '' - context = '' - contextline = strings.size + 1 # Larger than every line number - else - contextline = [contextline, linectr].min - context += line - end - linectr += 1 - # TODO break/strip long lines? Should be able to figure out relevant parts - # by position of circumflex - # TODO drop context here and instead give log line numbers? - } - - msgs - end + msgs + end end Extension.add Gnuplot diff --git a/extensions/60_synctex.rb b/extensions/60_synctex.rb index a9b2a6d..8a59a2f 100644 --- a/extensions/60_synctex.rb +++ b/extensions/60_synctex.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -22,20 +22,20 @@ :synctex, 'synctex', Boolean, false, 'Set to make engines create SyncTeX files.')) # Add hook that adapts the :enginepar parameter whenever :synctex changes (including startup) -ParameterManager.instance.addHook(:synctex) { |key, val| +ParameterManager.instance.addHook(:synctex) do |key, val| params = ParameterManager.instance # Set engine parameter - # TODO make nicer with array parameters + # TODO: make nicer with array parameters parameter = '--synctex=-1' - if val && params[:enginepar][parameter] == nil # TODO what is the second access? + if val && params[:enginepar][parameter] == nil # TODO: what is the second access? params.add(:enginepar, parameter) elsif !val params[:enginepar] = params[:enginepar].gsub(parameter, '') end # Add synctex file to those that should be ignored - # TODO make nicer with array parameters + # TODO: make nicer with array parameters synctexfile = "#{params[:jobname]}.synctex.gz" if val && params[:ignore] == nil params.add(:ignore, synctexfile) @@ -46,11 +46,11 @@ elsif !val params[:ignore] = params[:ignore].gsub(/:?#{synctexfile}/, '') end -} +end class SyncTeX < Extension def initialize - super + super @name = 'SyncTeX' @description = 'Provides support for SyncTeX' end @@ -62,26 +62,26 @@ def do?(time) def exec(time, progress) params = ParameterManager.instance - if !File.exist?("#{params[:jobname]}.synctex") + unless File.exist?("#{params[:jobname]}.synctex") return { success: false, messages: [LogMessage.new(:error, nil, nil, nil, 'SyncTeX file not found.')], log: 'SyncTeX file not found.' } end # Fix paths in synctex file, gzip it and put result in main directory - Zlib::GzipWriter.open("#{params[:jobpath]}/#{params[:jobname]}.synctex.gz") { |gz| - File.open("#{params[:jobname]}.synctex", 'r') { |f| - f.readlines.each { |line| + Zlib::GzipWriter.open("#{params[:jobpath]}/#{params[:jobname]}.synctex.gz") do |gz| + File.open("#{params[:jobname]}.synctex", 'r') do |f| + f.readlines.each do |line| # Replace tmp path with job path. # Catch absolute tmp paths first, then try to match paths relative to job path. gz.write line.sub("#{params[:jobpath]}/#{params[:tmpdir]}", params[:jobpath])\ .sub(params[:tmpdir], params[:jobpath]) - } - } - } + end + end + end { success: true, messages: [], log: '' } end end - + Extension.add SyncTeX diff --git a/lib/CliHelp.rb b/lib/CliHelp.rb index 14c5f29..9886af9 100644 --- a/lib/CliHelp.rb +++ b/lib/CliHelp.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. diff --git a/lib/DependencyManager.rb b/lib/DependencyManager.rb index 487ad32..6ff2952 100644 --- a/lib/DependencyManager.rb +++ b/lib/DependencyManager.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. diff --git a/lib/Engine.rb b/lib/Engine.rb index 0acefd4..5b57d76 100644 --- a/lib/Engine.rb +++ b/lib/Engine.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. diff --git a/lib/Extension.rb b/lib/Extension.rb index f39c29e..23aa72c 100644 --- a/lib/Extension.rb +++ b/lib/Extension.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -27,15 +27,15 @@ def self.add(e) end def self.list - return @@list.values + @@list.values end def self.[](key) - return @@list[key] + @@list[key] end def self.to_sym - self.new.to_sym + new.to_sym end def initialize @@ -45,24 +45,24 @@ def initialize # Hacky hack? Need to refactor this def self.name - self.new.name + new.name end def self.description - self.new.description + new.description end - if !@@dependencies.all? { |d| d.available? } + unless @@dependencies.all?(&:available?) # Define skeleton class for graceful sequential fallback module Parallel class << self - # TODO implement map + # TODO: implement map # TODO test this! - def each(hash, options={}, &block) - hash.each { |k,v| + def each(hash, options = {}, &block) + hash.each do |k, v| block.call(k, v) options[:finish].call(nil, nil, nil) - } + end array end end @@ -71,22 +71,22 @@ def each(hash, options={}, &block) # Wrap execution of many items def self.execute_parts(jobs, when_done, &block) - if @@dependencies.all? { |d| d.available? } + if @@dependencies.all?(&:available?) require 'system' require 'parallel' end - Parallel.map(jobs, :finish => lambda { |a,b,c| when_done.call }) { |job| + Parallel.map(jobs, finish: ->(_, _, _) { when_done.call }) do |job| begin block.call(job) rescue Interrupt - raise Interrupt if !parallel # Sequential fallback needs exception! + raise Interrupt unless parallel # Sequential fallback needs exception! rescue => e Output.instance.msg("\tAn error occurred: #{e.to_s}") - # TODO Should we break? Let's see what kinds of errors we get... + # TODO: Should we break? Let's see what kinds of errors we get... end - } - # TODO do we have to care about Parallel::DeadWorker? + end + # TODO: do we have to care about Parallel::DeadWorker? end # Parameters @@ -94,55 +94,54 @@ def self.execute_parts(jobs, when_done, &block) # - output: an instance of Output # - log: an instance of Log def self.run_all(time, output, log) - list.each { |e| + list.each do |e| e = e.new - if e.do?(time) - # TODO make dep check more efficient - dependencies = DependencyManager.list(source: [:extension, e.name], relevance: :essential) - if dependencies.all? { |d| d.available? } - progress, stop = output.start("#{e.name} running", e.job_size) - r = e.exec(time, progress) - stop.call(if r[:success] then :success else :error end) - log.add_messages(e.name, :extension, r[:messages], r[:log]) - else - # TODO log message? - output.separate.error('Missing dependencies:', *dependencies.select { |d| !d.available? }.map { |d| d.to_s }) - end + next unless e.do?(time) + + # TODO: make dep check more efficient + dependencies = DependencyManager.list(source: [:extension, e.name], relevance: :essential) + if dependencies.all?(&:available?) + progress, stop = output.start("#{e.name} running", e.job_size) + r = e.exec(time, progress) + stop.call(r[:success] ? :success : :error) + log.add_messages(e.name, :extension, r[:messages], r[:log]) + else + # TODO: log message? + output.separate.error('Missing dependencies:', *dependencies.reject(&:available?).map(&:to_s)) end - } + end end public - def do?(time) - false - end - def job_size - 1 - end + def do?(time) + false + end - def exec(time, progress) - { success: true, messages: ['No execution code, need to overwrite!'], log: 'No execution code, need to overwrite!' } - end + def job_size + 1 + end - def to_s - @name - end + def exec(time, progress) + { success: true, messages: ['No execution code, need to overwrite!'], log: 'No execution code, need to overwrite!' } + end - def to_sym - self.class.name.downcase.to_sym - end + def to_s + @name + end + + def to_sym + self.class.name.downcase.to_sym + end - attr_accessor :name, :description + attr_reader :name, :description protected - attr_reader :params - attr_writer :name, :description + + attr_writer :name, :description end # Load all extensions -Dir["#{BASEDIR}/#{EXTDIR}/*.rb"].sort.each { |f| - if /^\d\d/ =~ File.basename(f) - load(f) - end -} +Dir["#{BASEDIR}/#{EXTDIR}/*.rb"].sort.each do |f| + load(f) if /^\d\d/ =~ File.basename(f) +end diff --git a/lib/FileListener.rb b/lib/FileListener.rb index d759624..e854dd2 100644 --- a/lib/FileListener.rb +++ b/lib/FileListener.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -39,9 +39,9 @@ def ignoreFileName(jobname = '') def initialize @ignore = [] - #ParameterManager.instance.addHook(:listeninterval) { |_,v| - # TODO implement hook that catches changes to listen interval - #} + # ParameterManager.instance.addHook(:listeninterval) { |_,v| + # TODO implement hook that catches changes to listen interval + # } @jobfilelistener = nil @ignfilelistener = nil @dependencies = DependencyManager.list(source: [:core, self.class.to_s]) @@ -55,25 +55,25 @@ def ignored # adds the contained files to the ignore list. def readIgnoreFile(ignoreFile) if File.exist?(ignoreFile) - IO.foreach(ignoreFile) { |line| + IO.foreach(ignoreFile) do |line| @ignore.push(line.strip) - } + end end end def start(jobname, ignores = []) # Make sure that the listen gem is available - @dependencies.each { |d| - if !d.available? + @dependencies.each do |d| + unless d.available? raise MissingDependencyError.new(d.to_s) end - } - - if @jobfilelistener != nil + end + + unless @jobfilelistener.nil? # Should never happen unless I programmed crap raise StandardError.new('Listener already running, what are you doing?!') end - + params = ParameterManager.instance # Add the files to ignore from this process @@ -82,17 +82,17 @@ def start(jobname, ignores = []) @ignore.push(@ignorefile) # Write ignore list for other processes - File.open("#{params[:jobpath]}/#{@ignorefile}", 'w') { |file| + File.open("#{params[:jobpath]}/#{@ignorefile}", 'w') do |file| file.write(@ignore.join("\n")) - # TODO make sure this file gets deleted! - } + # TODO: make sure this file gets deleted! + end # Collect all existing ignore files Dir.entries('.') \ - .select { |f| /(\.\/)?#{Regexp.escape(ignoreFileName(''))}[^\/]+/ =~ f } \ - .each { |f| + .select { |f| /(\.\/)?#{Regexp.escape(ignoreFileName(''))}[^\/]+/ =~ f } \ + .each do |f| readIgnoreFile(f) - } + end # Setup daemon mode @@ -107,53 +107,53 @@ def start(jobname, ignores = []) /\A(\.\/)?(#{@ignore.map { |s| Regexp.escape(s) }.join('|')})/ ], ) \ do |modified, added, removed| - # TODO cruel hack; can we do better? - removed.each { |r| + # TODO: cruel hack; can we do better? + removed.each do |r| @vanishedfiles.push File.path(r.to_s).sub(params[:jobpath], params[:tmpdir]) - } + end @changetime = Time.now end - params.addHook(:listeninterval) { |key,val| - # jobfilelistener.latency = val - # TODO tell change to listener; in worst case, restart? - } - # TODO need hook on -i parameter? - - # Secondary listener: this one checks for (new) ignore files, i.e. other - # jobs in the same directory. It then updates the main - # listener so that it does not react to changes in files - # generated by the other process. - @ignfilelistener = - Listen.to('.', - only: /\A(\.\/)?#{Regexp.escape(ignoreFileName())}[^\/]+/, - latency: 0.1 - ) \ - do |modified, added, removed| - @jobfilelistener.pause - - added.each { |ignf| - files = ignoremore(ignf) - @jobfilelistener.ignore(/\A(\.\/)?(#{files.map { |s| Regexp.escape(s) }.join('|')})/) - } - - # TODO If another daemon terminates we keep its ignorefiles. Potential leak! - # If this turns out to be a problem, update list & listener (from scratch) - - @jobfilelistener.unpause + params.addHook(:listeninterval) do |key,val| + # jobfilelistener.latency = val + # TODO tell change to listener; in worst case, restart? + end + # TODO: need hook on -i parameter? + + # Secondary listener: this one checks for (new) ignore files, i.e. other + # jobs in the same directory. It then updates the main + # listener so that it does not react to changes in files + # generated by the other process. + @ignfilelistener = + Listen.to('.', + only: /\A(\.\/)?#{Regexp.escape(ignoreFileName())}[^\/]+/, + latency: 0.1 + ) \ + do |modified, added, removed| + @jobfilelistener.pause + + added.each do |ignf| + files = ignoremore(ignf) + @jobfilelistener.ignore(/\A(\.\/)?(#{files.map { |s| Regexp.escape(s) }.join('|')})/) end - @ignfilelistener.start - @changetime = Time.now - @lastraise = @changetime - @jobfilelistener.start + # TODO: If another daemon terminates we keep its ignorefiles. Potential leak! + # If this turns out to be a problem, update list & listener (from scratch) + + @jobfilelistener.unpause + end + + @ignfilelistener.start + @changetime = Time.now + @lastraise = @changetime + @jobfilelistener.start end def waitForChanges(output) output.start('Waiting for file changes (press ENTER to pause)') @jobfilelistener.start if @jobfilelistener.paused? params = ParameterManager.instance - + files = Thread.new do while @changetime <= @lastraise || Time.now - @changetime < params[:listeninterval] sleep(params[:listeninterval] * 0.5) @@ -163,11 +163,11 @@ def waitForChanges(output) Thread.current[:raisetarget].raise(FilesChanged.new('Files have changed')) end files[:raisetarget] = Thread.current - + begin files.run STDIN.noecho(&:gets) - # User wants to enter prompt, so stop listening + # User wants to enter prompt, so stop listening files.kill @jobfilelistener.pause output.stop(:cancel) @@ -180,7 +180,7 @@ def waitForChanges(output) # Rerun! output.stop(:success) @jobfilelistener.pause - rescue Interrupt => e + rescue Interrupt => e # User hit CTRL+C while waiting raise e rescue SystemExit => e @@ -190,7 +190,7 @@ def waitForChanges(output) # Remove files reported missing since last run from tmp (so we don't hide errors) # Be extra careful, we don't want to delete non-tmp files! - @vanishedfiles.each { |f| FileUtils.rm_rf(f) if f.start_with?(params[:tmpdir]) && File.exists?(f) } + @vanishedfiles.each { |f| FileUtils.rm_rf(f) if f.start_with?(params[:tmpdir]) && File.exist?(f) } @vanishedfiles = [] end @@ -223,8 +223,8 @@ def runs? # Removes temporary files outside of the tmp folder, # closes file handlers, etc. def cleanup - # TODO this really needs to be done via CLEAN - FileUtils::rm("#{ParameterManager.instance[:jobpath]}/#{@ignorefile}") + # TODO: this really needs to be done via CLEAN + FileUtils.rm("#{ParameterManager.instance[:jobpath]}/#{@ignorefile}") end diff --git a/lib/HashManager.rb b/lib/HashManager.rb index 609843b..6e8667a 100644 --- a/lib/HashManager.rb +++ b/lib/HashManager.rb @@ -19,21 +19,22 @@ require 'digest' require 'singleton' +# TODO: Document class HashManager include Singleton - + def initialize @hashes = {} - end - + end + public - + # Hashes the given string def self.hash(string) Digest::MD5.hexdigest(string) - # TODO SHA-256? + # TODO: SHA-256? end - + # Computes a hash of the given file. # Parameters drop_from and without are optional; if specified, # they have to be regexps. @@ -50,81 +51,81 @@ def self.hash_file(filename, drop_from: nil, without: nil) elsif drop_from == nil && without == nil Digest::MD5.file(filename).to_s else - string = File.open(filename, 'r') { |f| f.read } - + string = File.open(filename, 'r', &:read) + # Fix string encoding; regexp matching below may fail otherwise # TODO check if this is necessary with Ruby versions beyond 2.0.0 - if !string.valid_encoding? + unless string.valid_encoding? string = string.encode('UTF-16be', - :invalid => :replace, - :replace => '?').encode('UTF-8') + invalid: :replace, + replace: '?').encode('UTF-8') end - + # Drop undesired prefix if necessary - if drop_from != nil && drop_from.is_a?(Regexp) + if !drop_from.nil? && drop_from.is_a?(Regexp) string = string.split(drop_from).first end # Drop undesired lines if necessary - if without != nil && without.is_a?(Regexp) + if !without.nil? && without.is_a?(Regexp) lines = string.split("\n") lines.reject! { |l| l =~ without } string = lines.join("\n") end - + hash(string.strip) end end - + # Returns true if (and only if) any of the specified files has been - # created, changed or deleted between this and the last call of + # created, changed or deleted between this and the last call of # the method of which it was a parameter. - def files_changed?(*files) + def files_changed?(*files) result = false - - files.each { |f| - # TODO allow arrays with parameters for hash_file? - if !File.exist?(f) - if @hashes.has_key?(f) + + files.each do |f| + # TODO: allow arrays with parameters for hash_file? + if !File.exist?(f) + if @hashes.key?(f) @hashes.remove(f) result = true end else hash = self.class.hash_file(f) - result = result || !@hashes.has_key?(f) || hash != @hashes[f] + result = result || !@hashes.key?(f) || hash != @hashes[f] # puts "new or changed file #{f}" if !@@hashes.has_key?(f) || hash != @@hashes[f] - @hashes[f] = hash - end - } + @hashes[f] = hash + end + end result end - + # Reads hashes from a file in format # filename,hash # Overwrites any hashes that are already known. def from_file(filename) if File.exist?(filename) - File.open(filename, 'r') { |f| - f.readlines.each { |l| + File.open(filename, 'r') do |f| + f.readlines.each do |l| parts = l.split(',') # Comma may appear as filename, so we have to make sure to only # use the last component as hash @hashes[parts.take(parts.size - 1).join(',').strip] = parts.last.strip - } - } + end + end end end - - # Writes hashes to a file in format + + # Writes hashes to a file in format # filename,hash # Overwrites existing file. def to_file(filename) - File.open(filename, 'w') { |f| - @hashes.each_pair { |k,v| + File.open(filename, 'w') do |f| + @hashes.each_pair do |k,v| f.write("#{k},#{v}\n") - } - } + end + end end def empty? diff --git a/lib/Log.rb b/lib/Log.rb index 1ecfbf4..81fcf35 100644 --- a/lib/Log.rb +++ b/lib/Log.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -18,116 +18,119 @@ require "#{File.dirname(__FILE__)}/LogMessage.rb" -class Log +# TODO: Document +class Log def initialize @messages = {} - @counts = { :error => {:total => 0}, - :warning => {:total => 0}, - :info => {:total => 0} - } - #@level = :warning # or :error, :info + @counts = { error: { total: 0 }, + warning: { total: 0 }, + info: { total: 0 } } + # @level = :warning # or :error, :info @rawoffsets = nil @mode = :structured # or :flat @dependencies = DependencyManager.list(source: [:core, self.class.to_s]) end - + def only_level(level) # Write messages from engine first # (Since @messages contains only one entry per run engine/extension, this is fast.) - keys = @messages.keys.select { |k| @messages[k][0] == :engine } + + keys = @messages.keys.select { |k| @messages[k][0] == :engine } + @messages.keys.select { |k| @messages[k][0] == :extension } - # TODO rewrite for efficiency: this should give an iterator without + # TODO: rewrite for efficiency: this should give an iterator without # actually doing anything. - keys.map { |k| - msgs = @messages[k][1].select { |m| + keys.map do |k| + msgs = @messages[k][1].select do |m| m.type == :error || # always show errors - level == :info || # show everything at info level - level == m.type # remaining case (warnings in :warning level) - } + level == :info || # show everything at info level + level == m.type # remaining case (warnings in :warning level) + end - {k => [@messages[k][0], msgs, @messages[k][2]] } - }.reduce({}) { |res, e| res.merge!(e) } + { k => [@messages[k][0], msgs, @messages[k][2]] } + end.reduce({}) { |res, e| res.merge!(e) } end - + public - attr_accessor :level # TODO implement flat mode? - attr_reader :rawoffsets # TODO implement differently? - - # Parameters - # 1. name of the source component (extension or engine) - # 2. :engine or :extension - # 3. List of LogMessage objects - # 4. Raw log/output - def add_messages(source, sourcetype, msgs, raw) - if !@messages.has_key?(source) - @messages[source] = [sourcetype, [], ''] - @counts[:error][source] = 0 - @counts[:warning][source] = 0 - @counts[:info][source] = 0 - end - - @messages[source][1] += msgs - @messages[source][2] += "#{raw}" - [:error, :warning, :info].each { |type| - cnt = msgs.count { |e| e.type == type } - @counts[type][source] += cnt - @counts[type][:total] += cnt - } - - @rawoffsets = nil - end - - def has_messages?(source) - @messages.has_key?(source) - end - - def messages(source) - @messages[source].clone - end - - def empty? - @messages.empty? + + attr_accessor :level # TODO: implement flat mode? + attr_reader :rawoffsets # TODO implement differently? + + # Parameters + # 1. name of the source component (extension or engine) + # 2. :engine or :extension + # 3. List of LogMessage objects + # 4. Raw log/output + def add_messages(source, sourcetype, msgs, raw) + unless @messages.key?(source) + @messages[source] = [sourcetype, [], ''] + @counts[:error][source] = 0 + @counts[:warning][source] = 0 + @counts[:info][source] = 0 end - - def count(type, part = :total) - @counts[type][part] + + @messages[source][1] += msgs + @messages[source][2] += "#{raw}" + [:error, :warning, :info].each do |type| + cnt = msgs.count { |e| e.type == type } + @counts[type][source] += cnt + @counts[type][:total] += cnt end - # Creates a string with the raw log messages. - def to_s - # TODO it should be possible to determine offsets without building the log - result = '' - messages = only_level(:info) - - offset = 0 - @rawoffsets = {} - messages.keys.each { |source| - result << "# # # # #\n" - result << "# Start #{source}" - result << "\n# # # # #\n\n" - - @rawoffsets[source] = offset + 4 - result << messages[source][2] - - result << "\n\n# # # # #\n" - result << "# Finished #{source}" - result << "\n# # # # #\n\n" - - offset += 10 + messages[source][2].count(?\n) - } - - result + @rawoffsets = nil + end + + def has_messages?(source) + @messages.key?(source) + end + + def messages(source) + @messages[source].clone + end + + def empty? + @messages.empty? + end + + def count(type, part = :total) + @counts[type][part] + end + + # Creates a string with the raw log messages. + def to_s + # TODO: it should be possible to determine offsets without building the log + result = '' + messages = only_level(:info) + + offset = 0 + @rawoffsets = {} + messages.each_key do |source| + result << "# # # # #\n" + result << "# Start #{source}" + result << "\n# # # # #\n\n" + + @rawoffsets[source] = offset + 4 + result << messages[source][2] + + result << "\n\n# # # # #\n" + result << "# Finished #{source}" + result << "\n# # # # #\n\n" + + offset += 10 + messages[source][2].count(?\n) end - - def self.fix(s) - # Prevents errors when engines write illegal symbols to log. - # Since the API changed between Ruby 1.8.x and 1.9, be - # careful. - RUBY_VERSION.to_f < 1.9 ? - Iconv.iconv('UTF-8//IGNORE', 'UTF-8', s) : - s.encode!(Encoding::UTF_16LE, :invalid => :replace, - :undef => :replace, - :replace => '?').encode!(Encoding::UTF_8) + + result + end + + def self.fix(s) + # Prevents errors when engines write illegal symbols to log. + # Since the API changed between Ruby 1.8.x and 1.9, be + # careful. + if RUBY_VERSION.to_f < 1.9 + Iconv.iconv('UTF-8//IGNORE', 'UTF-8', s) + else + s.encode!(Encoding::UTF_16LE, invalid: :replace, + undef: :replace, + replace: '?').encode!(Encoding::UTF_8) end + end end diff --git a/lib/LogMessage.rb b/lib/LogMessage.rb index 30a5399..3318bd4 100644 --- a/lib/LogMessage.rb +++ b/lib/LogMessage.rb @@ -17,12 +17,11 @@ # along with ltx2any. If not, see . class LogMessage - # Parameter type: one of :error, :warning, :info # Parameter srcfile: name of the source file the message originated at # nil if not available # Parameter srcline: lines in the given file the message originated at - # as array of integers [line] or [from,to]. + # as array of integers [line] or [from,to]. # nil if not available # Parameter logline: line(s) in the log the message was found at as array # of integers [line] or [from,to]. @@ -38,36 +37,35 @@ def initialize(type, srcfile, srcline, logline, msg, format = :none) @msg = msg @format = format end - + public - attr_accessor :type, :srcfile, :srcline, :logline, :msg - - def to_s - result = if @type == :warning - 'Warning' - elsif @type == :error - 'Error' - else - 'Info' - end - - if @srcfile != nil - result += " #{@srcfile}" - if @srcline != nil - result += ":#{@srcline.join('-')}" - end - end - - result += "\n" + @msg - - if @logline != nil - result +="\n\t(For details see original output from line #{@logline[0].to_s}.)" - end - result + attr_accessor :type, :srcfile, :srcline, :logline, :msg + + def to_s + result = if @type == :warning + 'Warning' + elsif @type == :error + 'Error' + else + 'Info' + end + + unless @srcfile.nil? + result += " #{@srcfile}" + result += ":#{@srcline.join('-')}" unless @srcline.nil? end - - def formatted? - @format == :fixed + + result += "\n" + @msg + + unless @logline.nil? + result += "\n\t(For details see original output from line #{@logline[0]}.)" end + + result + end + + def formatted? + @format == :fixed + end end diff --git a/lib/LogWriter.rb b/lib/LogWriter.rb index b356da7..d46a413 100644 --- a/lib/LogWriter.rb +++ b/lib/LogWriter.rb @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with ltx2any. If not, see . +# TODO: document class LogWriter @@list = {} @@dependencies = DependencyManager.list(source: [:core, 'LogWriter']) @@ -25,11 +26,11 @@ def self.add(lw) end def self.list - return @@list.values + @@list.values end def self.[](key) - return @@list[key] + @@list[key] end def self.name @@ -45,7 +46,7 @@ def self.to_sym end def self.to_s - self.name + name end # Returns the name of the written file, or raises an exception @@ -72,7 +73,7 @@ def self.break_at_spaces(s, length, indent) res = '' line = ' ' * [0, indent - 1].max - words.each { |w| + words.each do |w| newline = line + ' ' + w if newline.length > length res += line + "\n" @@ -80,23 +81,23 @@ def self.break_at_spaces(s, length, indent) else line = newline end - } + end res + line end end # Load all extensions -Dir["#{BASEDIR}/#{LOGWDIR}/*.rb"].sort.each { |f| +Dir["#{BASEDIR}/#{LOGWDIR}/*.rb"].sort.each do |f| load(f) -} +end # Add log-writer-related parameters [ - Parameter.new(:log, 'l', String, '"#{self[:user_jobname]}.log"', - '(Base-)Name of log file'), - Parameter.new(:logformat, 'lf', LogWriter.list.map { |lw| lw.to_sym }, :md, - 'The log format. Call with --logformats for a list.'), - Parameter.new(:loglevel, 'll', [:error, :warning, :info], :warning, - "Set to 'error' to see only errors, to 'warning' to also see warnings, or to 'info' for everything.") + Parameter.new(:log, 'l', String, '"#{self[:user_jobname]}.log"', + '(Base-)Name of log file'), + Parameter.new(:logformat, 'lf', LogWriter.list.map(&:to_sym), :md, + 'The log format. Call with --logformats for a list.'), + Parameter.new(:loglevel, 'll', [:error, :warning, :info], :warning, + "Set to 'error' to see only errors, to 'warning' to also see warnings, or to 'info' for everything.") ].each { |p| ParameterManager.instance.addParameter(p) } \ No newline at end of file diff --git a/lib/Output.rb b/lib/Output.rb index 5d296bb..1e95718 100644 --- a/lib/Output.rb +++ b/lib/Output.rb @@ -20,6 +20,7 @@ Dependency.new('ruby-progressbar', :gem, [:core, 'Output'], :recommended, 'nice progress indicators', '>=1.7.5') +# TODO: document class Output include Singleton @@ -35,81 +36,81 @@ def initialize end private - def puts_indented(msgs) - msgs.each { |m| - puts "#{' ' * @shortcode.length} #{m}" - } - end + + def puts_indented(msgs) + msgs.each { |m| + puts "#{' ' * @shortcode.length} #{m}" + } + end public - def msg(*msg) - if !msg.empty? && !@running - puts "#{@shortcode} #{msg[0]}" - puts_indented(msg.drop(1)) if msg.size > 1 - STDOUT.flush - elsif !msg.empty? - # Store message to be printed after the current - # process finished - @pending << msg - end + + def msg(*msg) + if !msg.empty? && !@running + puts "#{@shortcode} #{msg[0]}" + puts_indented(msg.drop(1)) if msg.size > 1 + STDOUT.flush + elsif !msg.empty? + # Store message to be printed after the current + # process finished + @pending << msg end + end - def warn(*msg) - if msg.size > 0 - msg[0] = "#{@warning}: #{msg[0]}" - msg(*msg) - end + def warn(*msg) + unless msg.empty? + msg[0] = "#{@warning}: #{msg[0]}" + msg(*msg) end - - def error(*msg) - if msg.size > 0 - msg[0] = "#{@error}: #{msg[0]}" - msg(*msg) - end + end + + def error(*msg) + unless msg.empty? + msg[0] = "#{@error}: #{msg[0]}" + msg(*msg) end + end - def start(msg, count=1) - @running = true - if count > 1 && @dependencies.all? { |d| d.available? } - # Set up progress bar if needed - require 'ruby-progressbar' - progress = ProgressBar.create(:title => "#{@shortcode} #{msg} ...", - :total => count, - :format => '%t [%c/%C]', - :autofinish => false) - return [lambda { progress.increment }, - lambda { |state, *msgs| - progress.format("#{@shortcode} #{msg} ... #{instance_variable_get(("@#{state}").intern).to_s}" + (' ' * 5)) # Add some spaces to hide all for up to 999 steps - # TODO We *know* that we need 2*ceil(log_2(count)) - 1 spaces... - progress.stop - puts_indented(*msgs) if msgs.size > 0 - STDOUT.flush - }] - # TODO notify user of missing dependency? - end - - # Fallback if progress bar not needed, or gem not available - print "#{@shortcode} #{msg} ... " - STDOUT.flush - [lambda {}, lambda { |state, *msgs| stop(state, *msgs) }] + def start(msg, count = 1) + @running = true + if count > 1 && @dependencies.all?(&:available?) + # Set up progress bar if needed + require 'ruby-progressbar' + progress = ProgressBar.create(:title => "#{@shortcode} #{msg} ...", + :total => count, + :format => '%t [%c/%C]', + :autofinish => false) + return [-> { progress.increment }, + lambda { |state, *msgs| + progress.format("#{@shortcode} #{msg} ... #{instance_variable_get("@#{state}".intern)}" + (' ' * 5)) # Add some spaces to hide all for up to 999 steps + # TODO We *know* that we need 2*ceil(log_2(count)) - 1 spaces... + progress.stop + puts_indented(*msgs) unless msgs.empty? + STDOUT.flush + }] + # TODO: notify user of missing dependency? end - def stop(state, *msg) - puts instance_variable_get(("@#{state}").intern).to_s - puts_indented(msg) if msg.size > 0 - STDOUT.flush + # Fallback if progress bar not needed, or gem not available + print "#{@shortcode} #{msg} ... " + STDOUT.flush + [-> {}, ->(state, *msgs) { stop(state, *msgs) }] + end - # Print messages that were held back during the process - # that just finished. - @running = false - @pending.each { |msgs| - msg(*msgs) - } - @pending.clear - end + def stop(state, *msg) + puts instance_variable_get("@#{state}".intern).to_s + puts_indented(msg) unless msg.empty? + STDOUT.flush - def separate - puts '' - self - end + # Print messages that were held back during the process + # that just finished. + @running = false + @pending.each { |msgs| msg(*msgs) } + @pending.clear + end + + def separate + puts '' + self + end end diff --git a/lib/ParameterManager.rb b/lib/ParameterManager.rb index 9d0abb0..7dc94c7 100644 --- a/lib/ParameterManager.rb +++ b/lib/ParameterManager.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # # This file is part of ltx2any. @@ -21,257 +21,254 @@ class ParameterManager include Singleton - # TODO Make it so that keys are (also) "long" codes as fas as users are concerned. Interesting for DaemonPrompt! - # TODO add Array type (for -i -ir -ep ...) + # TODO: Make it so that keys are (also) "long" codes as fas as users are concerned. Interesting for DaemonPrompt! + # TODO: add Array type (for -i -ir -ep ...) def initialize @values = {} @hooks = {} @code2key = {} @processed = false - #@frozen_copy = nil - #@copy_dirty = false - #frozenCopy() + # @frozen_copy = nil + # @copy_dirty = false + # frozenCopy() end public - def addParameter(p) - if !@processed - if p.is_a?(Parameter) - if @values.has_key?(p.key) - raise ParameterException.new("Parameter #{p.key} already exists.") - else - @values[p.key] = p - @code2key[p.code] = p.key - @hooks[p.key] = [] unless @hooks.has_key?(p.key) - end - else - raise ParameterException.new("Can not add object of type #{p.class} as parameter.") - end - else - raise ParameterException.new('Can not add parameters after CLI input has been processed.') - end + + def addParameter(p) + if @processed + raise ParameterException.new('Can not add parameters after CLI input has been processed.') end - def processArgs(args) - # Check for input file first - # Try to find an existing file by attaching common endings - original = ARGV.last - endings = ['tex', 'ltx', 'latex', '.tex', '.ltx', '.latex'] - jobfile = original - while !File.exist?(jobfile) || File.directory?(jobfile) - if endings.length == 0 - raise ParameterException.new("No input file fitting #{original} exists.") - end + unless p.is_a?(Parameter) + raise ParameterException.new("Can not add object of type #{p.class} as parameter.") + end - jobfile = "#{original}#{endings.pop}" - end - # TODO do basic checks as to whether we really have a LaTeX file? + if @values.key?(p.key) + raise ParameterException.new("Parameter #{p.key} already exists.") + end - addParameter(Parameter.new(:jobpath, nil, String, File.dirname(File.expand_path(jobfile)), - 'Absolute path of source directory')) - addHook(:tmpdir) { |key,val| - if self[:jobpath].start_with?(File.expand_path(val)) - raise ParameterException.new('Temporary directory may not contain job directory.') - end - } - addParameter(Parameter.new(:jobfile, nil, String, File.basename(jobfile), 'Name of the main input file')) - addParameter(Parameter.new(:jobname, nil, String, /\A(.+?)\.\w+\z/.match(self[:jobfile])[1], - 'Internal job name, in particular name of the main file and logs.')) - set(:user_jobname, self[:jobname]) if self[:user_jobname] == nil - - # Read in parameters - # TODO use/build proper CLI and parameter handler? - i = 0 - while i < ARGV.length - 1 - p = /\A-(\w+)\z/.match(ARGV[i]) - if p != nil - code = p[1] - key = @code2key[code] - - if @values.has_key?(key) - if @values[key].type == Boolean - set(key, :true) - i += 1 - else - val = ARGV[i+1] - if i + 1 < ARGV.length - 1 - set(key, val) # Does all the checking and converting - i += 2 - else - raise ParameterException.new("No value for parameter -#{code}.") - end - end - else - raise ParameterException.new("Parameter -#{code} does not exist.") - end - else - raise ParameterException.new("Don't know what to do with parameter #{ARGV[i]}.") - end - end - - # Evaluate remaining defaults that need to/can be evaluated - # TODO Parameter values now contain user input. Security risk? - keys.each { |key| - val = @values[key].value - if val != nil && val.is_a?(String) && val.length > 0 - begin - @values[key].value = eval(val) - rescue Exception => e - # Leave value unchanged - # puts "From eval on #{key}: #{e.message}" - end - end - } + @values[p.key] = p + @code2key[p.code] = p.key + @hooks[p.key] = [] unless @hooks.key?(p.key) + end - if jobfile == nil - raise ParameterException.new('Please provide an input file. Call with --help for details.') + def processArgs(args) + # Check for input file first + # Try to find an existing file by attaching common endings + original = ARGV.last + endings = ['tex', 'ltx', 'latex', '.tex', '.ltx', '.latex'] + jobfile = original + while !File.exist?(jobfile) || File.directory?(jobfile) + if endings.empty? + raise ParameterException.new("No input file fitting #{original} exists.") end - @processed = true + jobfile = "#{original}#{endings.pop}" end + # TODO: do basic checks as to whether we really have a LaTeX file? - def [](key) - if @values.has_key?(key) - @values[key].value - else - nil + addParameter(Parameter.new(:jobpath, nil, String, File.dirname(File.expand_path(jobfile)), + 'Absolute path of source directory')) + addHook(:tmpdir) do |key, val| + if self[:jobpath].start_with?(File.expand_path(val)) + raise ParameterException.new('Temporary directory may not contain job directory.') end end + addParameter(Parameter.new(:jobfile, nil, String, File.basename(jobfile), 'Name of the main input file')) + addParameter(Parameter.new(:jobname, nil, String, /\A(.+?)\.\w+\z/.match(self[:jobfile])[1], + 'Internal job name, in particular name of the main file and logs.')) + set(:user_jobname, self[:jobname]) if self[:user_jobname].nil? + + # Read in parameters + # TODO use/build proper CLI and parameter handler? + i = 0 + while i < ARGV.length - 1 + p = /\A-(\w+)\z/.match(ARGV[i]) + if p.nil? + raise ParameterException.new("Don't know what to do with parameter #{ARGV[i]}.") + end - def []=(key,val) - set(key, val, false) - end + code = p[1] + key = @code2key[code] - def set(key, val, once=false) # TODO implement "once" behaviour - # TODO allow for proper validation functions? - # TODO fall back to defaults instead of killing? - if !@values.has_key?(key) - raise ParameterException.new("Parameter #{key} does not exist.") + unless @values.key?(key) + raise ParameterException.new("Parameter -#{code} does not exist.") end - code = @values[key].code - - if @values[key].type == String - @values[key].value = val.strip - elsif @values[key].type == Integer - if val.is_a?(Integer) - @values[key].value = val - elsif /\d+/ =~ val - @values[key].value = val.to_i - else - raise ParameterException.new("Parameter -#{code} requires an integer ('#{val}' given).") - end - elsif @values[key].type == Float - if val.is_a?(Float) - @values[key].value = val - elsif /\d+(\.\d+)?/ =~ val - @values[key].value = val.to_f - else - raise ParameterException.new("Parameter -#{code} requires a number ('#{val}' given).") - end - elsif @values[key].type == Boolean - if val.is_a?(Boolean) - @values[key].value = val - elsif val.to_s.to_sym == :true || val.to_s.to_sym == :false - @values[key].value = ( val.to_s.to_sym == :true ) - else - raise ParameterException.new("Parameter -#{code} requires a boolean ('#{val}' given).") - end - elsif @values[key].type.is_a? Array - if @values[key].type.include?(val.to_sym) - @values[key].value = val.to_sym - else - raise ParameterException.new("Invalid value '#{val}' for parameter -#{code}\nChoose one of [#{@values[key].type.map { |e| e.to_s }.join(', ')}].") - end + + if @values[key].type == Boolean + set(key, :true) + i += 1 else - # This should never happen - raise RuntimeError.new("Parameter -#{code} has unknown type #{@values[key].type}.") + val = ARGV[i+1] + unless i + 1 < ARGV.length - 1 + raise ParameterException.new("No value for parameter -#{code}.") + end + + set(key, val) # Does all the checking and converting + i += 2 end + end - @hooks[key].each { |b| - b.call(key, @values[key].value) - } + # Evaluate remaining defaults that need to/can be evaluated + # TODO Parameter values now contain user input. Security risk? + keys.each do |key| + val = @values[key].value + next unless !val.nil? && val.is_a?(String) && !val.empty? + + begin + @values[key].value = eval(val) + rescue Exception => e + # Leave value unchanged + # puts "From eval on #{key}: #{e.message}" + end + end - #@copy_dirty = true + if jobfile.nil? + raise ParameterException.new('Please provide an input file. Call with --help for details.') end - def add(key, val, once=false) # TODO implement "once" behaviour - if @values.has_key?(key) - if @values[key].type == String - @values[key].value += val.to_s # TODO should we add separating `:`? + @processed = true + end + + def [](key) + if @values.key?(key) + @values[key].value + else + nil + end + end - @hooks[key].each { |b| - b.call(key, @values[key].value) - } + def []=(key, val) + set(key, val, false) + end - #@copy_dirty = true - else - raise ParameterException.new("Parameter #{key} does not support extension.") - end + def set(key, val, once = false) # TODO: implement "once" behaviour + # TODO allow for proper validation functions? + # TODO fall back to defaults instead of killing? + unless @values.key?(key) + raise ParameterException.new("Parameter #{key} does not exist.") + end + code = @values[key].code + + if @values[key].type == String + @values[key].value = val.strip + elsif @values[key].type == Integer + if val.is_a?(Integer) + @values[key].value = val + elsif /\d+/ =~ val + @values[key].value = val.to_i else - raise ParameterException.new("Parameter #{key} does not exist.") + raise ParameterException.new("Parameter -#{code} requires an integer ('#{val}' given).") end - end - - def addHook(key, &block) - #if ( @values.has_key?(key) ) - if !@hooks.has_key?(key) - @hooks[key] = [] + elsif @values[key].type == Float + if val.is_a?(Float) + @values[key].value = val + elsif /\d+(\.\d+)?/ =~ val + @values[key].value = val.to_f + else + raise ParameterException.new("Parameter -#{code} requires a number ('#{val}' given).") end - - if block.arity == 2 - @hooks[key].push(block) + elsif @values[key].type == Boolean + if val.is_a?(Boolean) + @values[key].value = val + elsif val.to_s.to_sym == :true || val.to_s.to_sym == :false + @values[key].value = ( val.to_s.to_sym == :true ) + else + raise ParameterException.new("Parameter -#{code} requires a boolean ('#{val}' given).") + end + elsif @values[key].type.is_a? Array + if @values[key].type.include?(val.to_sym) + @values[key].value = val.to_sym else - raise ParameterException.new('Parameter hooks need to take two parameters (key, new value).') + raise ParameterException.new("Invalid value '#{val}' for parameter -#{code}\nChoose one of [#{@values[key].type.map { |e| e.to_s }.join(', ')}].") end - #else - # raise ParameterException.new("Parameter #{key} does not exist.") - #end + else + # This should never happen + raise RuntimeError.new("Parameter -#{code} has unknown type #{@values[key].type}.") + end + + @hooks[key].each do |b| + b.call(key, @values[key].value) + end + + # @copy_dirty = true + end + + def add(key, val, once = false) # TODO: implement "once" behaviour + unless @values.key?(key) + raise ParameterException.new("Parameter #{key} does not exist.") + end + + unless @values[key].type == String + raise ParameterException.new("Parameter #{key} does not support extension.") end - def keys - @values.keys + @values[key].value += val.to_s # TODO: should we add separating `:`? + + @hooks[key].each do |b| + b.call(key, @values[key].value) end - def reset - # TODO clear "once" settings - # @copy_dirty = false + # @copy_dirty = true + end + + def addHook(key, &block) + # if ( @values.has_key?(key) ) + @hooks[key] = [] unless @hooks.key?(key) + + unless block.arity == 2 + raise ParameterException.new('Parameter hooks need to take two parameters (key, new value).') end - #def frozenCopy - # if ( @frozen_copy == nil || @copy_dirty ) - # # TODO create a deep copy - # copy = self.clone - # copy.values = @values.clone # not deep enough! - # copy.freeze - # @frozen_copy = copy - # @copy_dirty = false - # end - # - # return @frozen_copy - #end + @hooks[key].push(block) + end + + def keys + @values.keys + end + + def reset + # TODO: clear "once" settings + # @copy_dirty = false + end + + # def frozenCopy + # if ( @frozen_copy == nil || @copy_dirty ) + # # TODO create a deep copy + # copy = self.clone + # copy.values = @values.clone # not deep enough! + # copy.freeze + # @frozen_copy = copy + # @copy_dirty = false + # end + # + # return @frozen_copy + # end def user_info - @values.keys.select { |key| - @values[key].code != nil - }.sort { |a,b| - @values[a].code <=> @values[b].code - }. map { |key| - { :code => @values[key].code, :type => @values[key].type, :help => @values[key].help } - } + @values.keys.reject do |key| + @values[key].code.nil? + end.sort do |a, b| + @values[a].code <=> @values[b].code + end.map do |key| + { code: @values[key].code, type: @values[key].type, help: @values[key].help } + end end def to_s - @values.keys.map { |key| + @values.keys.map do |key| "#{key}\t\t#{self[key]}" - }.join("\n") + end.join("\n") end protected - attr_reader :values - attr_writer :values + + attr_accessor :values end +# TODO: document class Parameter # Pass code = nil for an internal parameter that is not shown to users. def initialize(key, code, type, default, help) @@ -283,18 +280,20 @@ def initialize(key, code, type, default, help) end public - def value=(val) - if (@type.is_a?(Array) && val.is_a?(@type[0].class)) || val.is_a?(@type) - @value = val - else - raise ParameterException.new("Value if type #{val.class} is not compatible with parameter #{@key}.") - end + + def value=(val) + unless (@type.is_a?(Array) && val.is_a?(@type[0].class)) || val.is_a?(@type) + raise ParameterException.new("Value if type #{val.class} is not compatible with parameter #{@key}.") end + @value = val + end + attr_reader :key, :code, :type, :value, :help end -class ParameterException < Exception +# TODO: document +class ParameterException < RuntimeError def initialize(msg) super(msg) end diff --git a/lib/TeXLogParser.rb b/lib/TeXLogParser.rb index 74d955b..d346bd7 100644 --- a/lib/TeXLogParser.rb +++ b/lib/TeXLogParser.rb @@ -18,9 +18,10 @@ require "#{File.dirname(__FILE__)}/LogMessage.rb" +# TODO: document class TeXLogParser - - # Input: + + # Input: # * log -- string array (one entry per line) # * startregexp -- start collecting messages after first match # Default: matches any line, thus collects from the start @@ -31,18 +32,18 @@ def self.parse(log, startregexp = /.*/, endregexp = /(?=a)b/) # Contains a stack of currently "open" files. # filestack.last is the current one. filestack = [] - + # Result collection messages = [] - + # The stack of files the log is currently "in" filestack = [] - + collecting = false linectr = 1 # Declared for the increment at the end of the loop current = Finalizer.new ongoing = nil - log.each { |line| + log.each do |line| if !collecting && startregexp =~ line collecting = true linectr = 1 @@ -55,37 +56,33 @@ def self.parse(log, startregexp = /.*/, endregexp = /(?=a)b/) # Even when not collecting, we need to keep track of which file # we are in. if collecting && line.strip == '' - # Empty line ends messages + # Empty line ends messages messages += [current.get_msg].compact elsif /^l\.(\d+) (.*)$/ =~ line # Line starting with a line number ends messages - if current.type != nil - if current.srcline == nil - current.srcline = [Integer($~[1])] - end + unless current.type.nil? + current.srcline = [Integer($~[1])] if current.srcline.nil? current.message += $~[2].strip current.logline[1] = linectr - messages += [current.get_msg].compact + messages += [current.get_msg].compact end elsif /^<\*> (.*)$/ =~ line # Some messages end with a line of the form '<*> file' - if current.type != nil + unless current.type.nil? current.srcfile = $~[1].strip current.logline[1] = linectr messages += [current.get_msg].compact end elsif /^(\([^()]*\)|[^()])*\)/ =~ line # End of messages regarding current file - if collecting - messages += [current.get_msg].compact - end + messages += [current.get_msg].compact if collecting filestack.pop - + # Multiple files may close; cut away matching part and start over. line = line.gsub($~.regexp, '') redo - elsif current.type == nil && # When we have an active message, it has + elsif current.type.nil? && # When we have an active message, it has # to complete before a new file can open. # Probably. (Without, error messages with # opening but no closing parenthesis would @@ -98,14 +95,14 @@ def self.parse(log, startregexp = /.*/, endregexp = /(?=a)b/) # # A new file has started. Match only those that don't close immediately. candidate = $~[2] - + while !File.exist?(candidate) && candidate != '' do # TODO can be long; use heuristics? candidate = candidate[0,candidate.length - 1] end if File.exist?(candidate) filestack.push(candidate) - else - # Lest we break everything by false negatives (due to linebreaks), + else + # Lest we break everything by false negatives (due to linebreaks), # add a dummy and hope it closes. filestack.push('dummy') end @@ -117,10 +114,10 @@ def self.parse(log, startregexp = /.*/, endregexp = /(?=a)b/) line = line.gsub($~.regexp, replace) redo elsif collecting # Do all the checks only when collecting - if /^(\S*?):(\d+): (.*)/ =~ line && ongoing == nil # such lines appear in fontspec-style messages, see below + if /^(\S*?):(\d+): (.*)/ =~ line && ongoing.nil? # such lines appear in fontspec-style messages, see below messages += [current.get_msg].compact # messages.push(LogMessage.new(:error, $~[1], [Integer($~[2])], [linectr], $~[3].strip)) - + current.type = :error current.srcfile = $~[1] current.srcline = [Integer($~[2])] @@ -131,12 +128,12 @@ def self.parse(log, startregexp = /.*/, endregexp = /(?=a)b/) elsif /(Package|Class)\s+([\w]+)\s+(Warning|Error|Info)/ =~ line # Message from some package or class, may be multi-line messages += [current.get_msg].compact - + current.type = if $~[3] == 'Warning' then :warning elsif $~[3] == 'Info' then :info - else :error + else :error end current.srcfile = filestack.last current.srcline = nil @@ -150,7 +147,7 @@ def self.parse(log, startregexp = /.*/, endregexp = /(?=a)b/) current.type = if $~[1] == 'Warning' then :warning elsif $~[1] == 'Info' - then :info + then :info else :error end current.srcfile = filestack.last @@ -161,10 +158,10 @@ def self.parse(log, startregexp = /.*/, endregexp = /(?=a)b/) elsif /^(LaTeX Font Warning: .*?)(?: #{space_sep('on input line')} (\d+).)?$/ =~ line # Some issue with fonts messages += [current.get_msg].compact - + current.type = :warning current.srcfile = filestack.last - current.srcline = if $~[2] then [Integer($~[2])] else nil end + current.srcline = $~[2] ? [Integer($~[2])] : nil current.logline = [linectr] current.message = $~[1].strip current.slicer = /^\(Font\)\s*/ @@ -183,7 +180,7 @@ def self.parse(log, startregexp = /.*/, endregexp = /(?=a)b/) # TODO What for chains? srcLine = [toLine] end - + messages.push(LogMessage.new(:warning, filestack.last, srcLine, [linectr], $~[1].strip)) elsif /^((Under|Over)full .*?)[\d\[\]]*$/ =~ line messages += [current.get_msg].compact @@ -199,7 +196,7 @@ def self.parse(log, startregexp = /.*/, endregexp = /(?=a)b/) elsif /^!!+/ =~ line # Messages in the style of fontspec messages += [current.get_msg].compact - + ongoing = :fontspec current.type = :error current.srcfile = filestack.last # may be overwritten later @@ -221,84 +218,77 @@ def self.parse(log, startregexp = /.*/, endregexp = /(?=a)b/) # Drop useless note elsif /^!(.*)/ =~ line # A new line - if $~[1].strip.size > 0 - current.message += $~[1].strip + "\n" - end - end + current.message += $~[1].strip + "\n" unless $~[1].strip.empty? + end elsif /^! (.*?)(after line (\d+).)?$/ =~ line messages += [current.get_msg].compact current.type = :error current.srcfile = filestack.last - current.srcline = if $~[3] then [Integer($~[3])] else nil end + current.srcline = $~[3] ? [Integer($~[3])] : nil current.logline = [linectr] - current.message = $~[1] + (if $~[2] then $~[2] else - '' - end) - elsif current.type != nil - if current.slicer != nil - line = line.gsub(current.slicer, '') - end - if current.format != :fixed - line = ' ' + line.strip! - end + current.message = $~[1] + ($~[2] ? $~[2] : '') + elsif !current.type.nil? + line = line.gsub(current.slicer, '') unless current.slicer.nil? + line = ' ' + line.strip! if current.format != :fixed current.message += line current.logline[1] = linectr end end - + linectr += 1 - } - + end + return messages end - + private - - # TeX logs may have spaces in weird places. If we don't want our regexp - # matching to stumble over that, longer strings have to be matched - # allowing for whitespace everywhere. - # Use the result of this method for this purpose. - def self.space_sep(s) - s.chars.join('\s*') + + # TeX logs may have spaces in weird places. If we don't want our regexp + # matching to stumble over that, longer strings have to be matched + # allowing for whitespace everywhere. + # Use the result of this method for this purpose. + def self.space_sep(s) + s.chars.join('\s*') + end + + # Some messages may run over multiple lines. Use an instance + # of this class to collect it completely. + class Finalizer + def initialize + reset + end + + def reset + @type = nil + @srcfile = nil + @srcline = nil + @logline = nil + @message = nil + @format = :none + + @slicer = nil end - - # Some messages may run over multiple lines. Use an instance - # of this class to collect it completely. - class Finalizer - def initialize + + public + + attr_accessor :type, :srcfile, :srcline, :logline, :message, :format, :slicer + + # (initially: @currentmessage = [nil, nil, nil, nil, nil, nil, :none] ) + def get_msg() + if !@type.nil? + if @srcline.nil? && @message =~ /(.+?) #{TeXLogParser::space_sep('on input line')} (\d+)\.?$/ + # The first line did not contain the line of warning, but + # the last did! + @message = $~[1].strip + @srcline = [Integer($~[2])] + end + res = LogMessage.new(@type, @srcfile, @srcline, @logline, @message, @format) + reset + res + else reset + nil end - - def reset - @type = nil - @srcfile = nil - @srcline = nil - @logline = nil - @message = nil - @format = :none - - @slicer = nil - end - - public - attr_accessor :type, :srcfile, :srcline, :logline, :message, :format, :slicer - - # (initially: @currentmessage = [nil, nil, nil, nil, nil, nil, :none] ) - def get_msg() - if @type != nil - if @srcline == nil && @message =~ /(.+?) #{TeXLogParser::space_sep('on input line')} (\d+)\.?$/ - # The first line did not contain the line of warning, but - # the last did! - @message = $~[1].strip - @srcline = [Integer($~[2])] - end - res = LogMessage.new(@type, @srcfile, @srcline, @logline, @message, @format) - reset - res - else - reset - nil - end - end end + end end diff --git a/logwriters/LaTeX.rb b/logwriters/LaTeX.rb index d62f562..cdc6758 100644 --- a/logwriters/LaTeX.rb +++ b/logwriters/LaTeX.rb @@ -31,7 +31,7 @@ def self.to_sym # Returns the name of the written file, or raises an exception def self.write(log, level = :warning) - # TODO compute offsets in a smarter way + # TODO: compute offsets in a smarter way log.to_s if log.rawoffsets == nil # Determines offsets in raw log params = ParameterManager.instance @@ -41,24 +41,24 @@ def self.write(log, level = :warning) File.open("#{File.dirname(__FILE__)}/logtemplate.tex", 'r') { |template| f.write(template.read) } - f.write("\\def\\author{ltx2any}\n\\def\\title{Log for #{params[:user_jobname]}}\n") - f.write("\\def\\fulllog{#{File.join(params[:tmpdir], "#{params[:log]}.full")}}\n") - f.write("\n\n\\begin{document}") + f.write("\\def\\author{ltx2any}\n\\def\\title{Log for #{params[:user_jobname]}}\n" \ + "\\def\\fulllog{#{File.join(params[:tmpdir], "#{params[:log]}.full")}}\n" \ + "\n\n\\begin{document}") f.write("\\section{Log for \\texttt{\\detokenize{#{params[:user_jobname]}}}}\n\n") messages = log.only_level(level) - f.write("\\textbf{Disclaimer:} This is but a digest of the original log file. " + - "For full detail, check out \\loglink. " + - 'In case we failed to pick up an error or warning, please ' + - "\\href{https://github.com/akerbos/ltx2any/issues/new}{report it to us}.\n\n") + f.write("\\textbf{Disclaimer:} This is but a digest of the original log file. " \ + "For full detail, check out \\loglink. " \ + 'In case we failed to pick up an error or warning, please ' \ + "\\href{https://github.com/akerbos/ltx2any/issues/new}{report it to us}.\n\n") - f.write("We found \\errlink{\\textbf{#{log.count(:error)}~error#{pls(log.count(:error))}}}, " + - "\\textsl{#{log.count(:warning)}~warning#{pls(log.count(:warning))}} " + - "and #{log.count(:info)}~other message#{pls(log.count(:info))} in total.\n\n") + f.write("We found \\errlink{\\textbf{#{log.count(:error)}~error#{pls(log.count(:error))}}}, " \ + "\\textsl{#{log.count(:warning)}~warning#{pls(log.count(:warning))}} " \ + "and #{log.count(:info)}~other message#{pls(log.count(:info))} in total.\n\n") # Write everything - messages.keys.each { |name| + messages.each_key { |name| # We get one block per tool that ran msgs = messages[name][1] @@ -93,9 +93,9 @@ def self.write(log, level = :warning) f.write("\n\\end{verbatim}") # Write the raw log reference - if m.logline != nil + unless m.logline.nil? # We have line offsets in the raw log! - logline = m.logline.map { |i| i += log.rawoffsets[name] } + logline = m.logline.map { |i| i + log.rawoffsets[name] } logline.push('') if logline.length < 2 f.write("\n\n\\logref{#{logline[0]}}{#{logline[1]}}") end @@ -117,8 +117,8 @@ def self.write(log, level = :warning) def self.makeFileref(file, linefrom, lineto) fileref = '' - if file != nil - #file = file.gsub(/_/, '\_') + unless file.nil? + # file = file.gsub(/_/, '\_') fileref = "\\fileref{#{file}}{#{linefrom}}{#{lineto}}" end fileref diff --git a/logwriters/Markdown.rb b/logwriters/Markdown.rb index 011241c..b431d40 100644 --- a/logwriters/Markdown.rb +++ b/logwriters/Markdown.rb @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with ltx2any. If not, see . +# TODO: Document class Markdown < LogWriter def self.name 'Markdown' @@ -31,25 +32,25 @@ def self.to_sym # Returns the name of the written file, or raises an exception def self.write(log, level = :warning) - # TODO compute rawoffsets in Log - log.to_s if log.rawoffsets == nil # Determines offsets in raw log + # TODO: compute rawoffsets in Log + log.to_s if log.rawoffsets.nil? # Determines offsets in raw log params = ParameterManager.instance result = "# Log for `#{params[:user_jobname]}`\n\n" messages = log.only_level(level) - result << "**Disclaimer:** \nThis is but a digest of the original log file.\n" + - "For full detail, check out `#{params[:tmpdir]}/#{params[:log]}.full`.\n" + - 'In case we failed to pick up an error or warning, please ' + - "[report it to us](https://github.com/akerbos/ltx2any/issues/new).\n\n" + result << "**Disclaimer:** \nThis is but a digest of the original log file.\n" \ + "For full detail, check out `#{params[:tmpdir]}/#{params[:log]}.full`.\n" \ + 'In case we failed to pick up an error or warning, please ' \ + "[report it to us](https://github.com/akerbos/ltx2any/issues/new).\n\n" - result << "We found **#{log.count(:error)} error#{pls(log.count(:error))}**, " + - "*#{log.count(:warning)} warning#{pls(log.count(:warning))}* " + - "and #{log.count(:info)} other message#{pls(log.count(:info))} in total.\n\n" + result << "We found **#{log.count(:error)} error#{pls(log.count(:error))}**, " \ + "*#{log.count(:warning)} warning#{pls(log.count(:warning))}* " \ + "and #{log.count(:info)} other message#{pls(log.count(:info))} in total.\n\n" # Write everything - messages.keys.each { |name| + messages.each_key do |name| msgs = messages[name][1] result << "## `#{name}`\n\n" @@ -58,26 +59,24 @@ def self.write(log, level = :warning) result << "Lucky you, `#{name}` had nothing to complain about!\n\n" if (level == :warning && log.count(:info, name) > 0) || - (level == :error && log.count(:info, name) + log.count(:warning, name) > 0) + (level == :error && log.count(:info, name) + log.count(:warning, name) > 0) if level != :info result << 'Note, though, that this log only lists errors' - if level == :warning - result << ' and warnings' - end + result << ' and warnings' if level == :warning result << '. There were ' if level == :error result << "#{log.count(:warning, name)} warning#{pls(log.count(:warning, name))} and " end - result << "#{log.count(:info, name)} information message#{pls(log.count(:info, name))} " + - "which you find in the full log.\n\n" + result << "#{log.count(:info, name)} information message#{pls(log.count(:info, name))} " \ + "which you find in the full log.\n\n" end end else - result << "**#{log.count(:error, name)} error#{pls(log.count(:error, name))}**, " + - "*#{log.count(:warning, name)} warning#{pls(log.count(:warning, name))}* " + - "and #{log.count(:info, name)} other message#{pls(log.count(:info, name))}\n\n" + result << "**#{log.count(:error, name)} error#{pls(log.count(:error, name))}**, " \ + "*#{log.count(:warning, name)} warning#{pls(log.count(:warning, name))}* " \ + "and #{log.count(:info, name)} other message#{pls(log.count(:info, name))}\n\n" - msgs.each { |m| + msgs.each do |m| # Lay out for 80 characters width # * 4 colums for list stuff # * 11 columns for type + space @@ -85,40 +84,34 @@ def self.write(log, level = :warning) # * The message, indented to the type stands out # * Log line, flushed right result << ' * ' + - { :error => '**Error**', - :warning => '*Warning*', - :info => 'Info ' - }[m.type] - if m.srcfile != nil + { error: '**Error**', + warning: '*Warning*', + info: 'Info ' }[m.type] + unless m.srcfile.nil? srcline = nil - if m.srcline != nil - srcline = m.srcline.join('--') - end - srcfilelength = 76 - 9 - (if srcline != nil then srcline.length + 1 else 0 end) - 2 + srcline = m.srcline.join('--') unless m.srcline.nil? + srcfilelength = 76 - 9 - (!srcline.nil? ? srcline.length + 1 : 0) - 2 result << if m.srcfile.length > srcfilelength " `...#{m.srcfile[m.srcfile.length - srcfilelength + 5, m.srcfile.length]}" else (' ' * (srcfilelength - m.srcfile.length)) + "`#{m.srcfile}" end - if srcline != nil - result << ":#{srcline}" - end + result << ":#{srcline}" unless srcline.nil? result << '`' end result << "\n\n" - if m.formatted? - result << indent(m.msg.strip, 8) + "\n\n" - else - result << break_at_spaces(m.msg.strip, 68, 8) + "\n\n" - end - if m.logline != nil - # We have line offset in the raw log! - logline = m.logline.map { |i| i += log.rawoffsets[name] }.join('--') - result << (' ' * (80 - (6 + logline.length))) + '`log:' + logline + "`\n\n\n" - end - } + result << if m.formatted? + indent(m.msg.strip, 8) + "\n\n" + else + break_at_spaces(m.msg.strip, 68, 8) + "\n\n" + end + next if m.logline.nil? + # We have line offset in the raw log! + logline = m.logline.map { |i| i + log.rawoffsets[name] }.join('--') + result << (' ' * (80 - (6 + logline.length))) + '`log:' + logline + "`\n\n\n" + end end - } + end target_file = "#{params[:log]}.md" File.open(target_file, 'w') { |f| f.write(result) } diff --git a/logwriters/PDF.rb b/logwriters/PDF.rb index 2c4c91e..cbaecf3 100644 --- a/logwriters/PDF.rb +++ b/logwriters/PDF.rb @@ -18,18 +18,19 @@ Dependency.new('xelatex', :binary, [:logwriter, 'pdf'], :essential, 'Compilation of PDF logs') -ParameterManager.instance.addHook(:logformat) { |_, newValue| - if newValue == :pdf +ParameterManager.instance.addHook(:logformat) { |_, new_value| + if new_value == :pdf DependencyManager.list(type: :all, source: [:logwriter, 'pdf'], relevance: :essential).each { |dep| - unless dep.available? - Output.instance.warn("#{dep.name} is not available to build PDF logs.", 'Falling back to Markdown log.') - ParameterManager.instance[:logformat] = :md - break - end + next if dep.available? + + Output.instance.warn("#{dep.name} is not available to build PDF logs.", 'Falling back to Markdown log.') + ParameterManager.instance[:logformat] = :md + break } end } +# TODO: Document class PDF < LogWriter def self.name 'PDF' @@ -49,11 +50,11 @@ def self.write(log, level = :warning) target_file = "#{params[:log]}.pdf" latex_log = LogWriter[:latex].write(log, level) - # TODO which engine to use? + # TODO: which engine to use? xelatex = '"xelatex -file-line-error -interaction=nonstopmode \"#{latex_log}\""' - IO::popen(eval(xelatex)) { |x| x.readlines } - IO::popen(eval(xelatex)) { |x| x.readlines } - # TODO parse log and rewrite a readable version? + IO.popen(eval(xelatex), &:readlines) + IO.popen(eval(xelatex), &:readlines) + # TODO: parse log and rewrite a readable version? # This is just the default of XeLaTeX xelatex_target = latex_log.sub(/\.tex$/, '.pdf') @@ -69,7 +70,7 @@ def self.write(log, level = :warning) Output.instance.error(*msg) target_file = LogWriter[:md].write(log, level) elsif xelatex_target != target_file - FileUtils::cp(xelatex_target, target_file) + FileUtils.cp(xelatex_target, target_file) end target_file diff --git a/logwriters/Raw.rb b/logwriters/Raw.rb index b0949ed..45ae08b 100644 --- a/logwriters/Raw.rb +++ b/logwriters/Raw.rb @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with ltx2any. If not, see . +# TODO: document class Raw < LogWriter def self.name 'Original Log' @@ -30,9 +31,10 @@ def self.to_sym end # Returns the name of the written file, or raises an exception - def self.write(log, level: :warning) + # @override + def self.write(log, level = :warning) target_file = "#{ParameterManager.instance[:log]}.full" - File.open("#{target_file}", 'w') { |f| f.write(log.to_s) } + File.open(target_file, 'w') { |f| f.write(log.to_s) } target_file end end diff --git a/ltx2any.rb b/ltx2any.rb index 0663e09..c362c2d 100644 --- a/ltx2any.rb +++ b/ltx2any.rb @@ -1,4 +1,4 @@ -# Copyright 2010-2016, Raphael Reitzig +# Copyright 2010-2018, Raphael Reitzig # # Version 0.9 alpha # @@ -39,9 +39,7 @@ # Initialize CLI output wrapper OUTPUT = Output.instance -if CliHelp.instance.provideHelp(ARGV) - Process.exit -end +Process.exit if CliHelp.instance.provideHelp(ARGV) CLEAN = [] CLEANALL = [] @@ -59,32 +57,30 @@ # Make sure all essential dependencies of core and engine are satisfied begin missing = [] - - (DependencyManager.list(source: :core, relevance: :essential) + - DependencyManager.list(source: [:engine, PARAMS[:engine].to_s], relevance: :essential)).each { |d| - missing.push(d) if !d.available? - } - - if !missing.empty? # TODO enter into log? + + (DependencyManager.list(source: :core, relevance: :essential) + + DependencyManager.list(source: [:engine, PARAMS[:engine].to_s], relevance: :essential)).each do |d| + missing.push(d) unless d.available? + end + + unless missing.empty? # TODO: enter into log? OUTPUT.separate.error('Missing dependencies', *missing) Process.exit end end - + # Check soft dependencies of core and engine; notify user if necessary begin missing = [] - - (DependencyManager.list(source: :core, relevance: :recommended) + - DependencyManager.list(source: [:engine, PARAMS[:engine].to_s], relevance: :recommended)).each { |d| - missing.push(d) if !d.available? - } - - if !missing.empty? # TODO enter into log? - OUTPUT.separate.warn('Missing dependencies', *missing) + + (DependencyManager.list(source: :core, relevance: :recommended) + + DependencyManager.list(source: [:engine, PARAMS[:engine].to_s], relevance: :recommended)).each do |d| + missing.push(d) unless d.available? end + + OUTPUT.separate.warn('Missing dependencies', *missing) unless missing.empty? # TODO: enter into log? end - + # Switch working directory to jobfile residence Dir.chdir(PARAMS[:jobpath]) @@ -92,9 +88,9 @@ # Some files we don't want to listen to toignore = [ "#{PARAMS[:tmpdir]}", - "#{PARAMS[:user_jobname]}.#{Engine[PARAMS[:engine]].extension}", - "#{PARAMS[:log]}", - "#{PARAMS[:user_jobname]}.err" + "#{PARAMS[:user_jobname]}.#{Engine[PARAMS[:engine]].extension}", + "#{PARAMS[:log]}", + "#{PARAMS[:user_jobname]}.err" ] + PARAMS[:ignore].split(':') begin @@ -120,8 +116,8 @@ exceptions = ignore + ignore.map { |s| "./#{s}" } + Dir['.*'] + Dir['./.*'] # drop hidden files, in p. . and .. - define_singleton_method(:copy2tmp) { |files| - files.each { |f| + define_singleton_method(:copy2tmp) do |files| + files.each do |f| if File.symlink?(f) # Avoid trouble with symlink loops @@ -130,23 +126,23 @@ # remove the obsolete stuff. # If there already is a symlink, delete because it might have been # relinked. - if File.exists?("#{PARAMS[:tmpdir]}/#{f}") - FileUtils::rm("#{PARAMS[:tmpdir]}/#{f}") + if File.exist?("#{PARAMS[:tmpdir]}/#{f}") + FileUtils.rm("#{PARAMS[:tmpdir]}/#{f}") end # Create new symlink instead of copying File.symlink("#{PARAMS[:jobpath]}/#{f}", "#{PARAMS[:tmpdir]}/#{f}") elsif File.directory?(f) - FileUtils::mkdir_p("#{PARAMS[:tmpdir]}/#{f}") + FileUtils.mkdir_p("#{PARAMS[:tmpdir]}/#{f}") copy2tmp(Dir.entries(f)\ - .delete_if { |s| ['.', '..', ]\ - .include?(s) }.map { |s| "#{f}/#{s}" }) - # TODO Is this necessary? Why not just copy? (For now, safer and more adaptable.) + .delete_if do |s| ['.', '..', ]\ + .include?(s) end.map { |s| "#{f}/#{s}" }) + # TODO: Is this necessary? Why not just copy? (For now, safer and more adaptable.) else - FileUtils::cp(f,"#{PARAMS[:tmpdir]}/#{f}") + FileUtils.cp(f,"#{PARAMS[:tmpdir]}/#{f}") end - } - } + end + end # tmp dir may have been removed (either by DaemonPrompt or the outside) if !File.exist?(PARAMS[:tmpdir]) @@ -165,7 +161,7 @@ # Delete former results in order not to pretend success if File.exist?("#{PARAMS[:jobname]}.#{engine.extension}") - FileUtils::rm("#{PARAMS[:jobname]}.#{engine.extension}") + FileUtils.rm("#{PARAMS[:jobname]}.#{engine.extension}") end # Read hashes @@ -181,7 +177,7 @@ # Run engine OUTPUT.start("#{engine.name}(#{run}) running") result = engine.exec - OUTPUT.stop(if result[:success] then :success else :error end) + OUTPUT.stop(result[:success] ? :success : :error) break unless File.exist?("#{PARAMS[:jobname]}.#{engine.extension}") @@ -200,53 +196,46 @@ Extension.run_all(:after, OUTPUT, log) # Give error/warning counts to user - errorS = if log.count(:error) != 1 then - 's' - else - '' - end - warningS = if log.count(:warning) != 1 then - 's' - else - '' - end + errorS = log.count(:error) != 1 ? 's' : '' + warningS = log.count(:warning) != 1 ? 's' : '' OUTPUT.msg("There were #{log.count(:error)} error#{errorS} " + "and #{log.count(:warning)} warning#{warningS}.") # Pick up output if present if File.exist?("#{PARAMS[:jobname]}.#{engine.extension}") - FileUtils::cp("#{PARAMS[:jobname]}.#{engine.extension}", "#{PARAMS[:jobpath]}/#{PARAMS[:user_jobname]}.#{engine.extension}") + FileUtils.cp("#{PARAMS[:jobname]}.#{engine.extension}", + "#{PARAMS[:jobpath]}/#{PARAMS[:user_jobname]}.#{engine.extension}") OUTPUT.msg("Output generated at #{PARAMS[:user_jobname]}.#{engine.extension}") else OUTPUT.msg('No output generated, probably due to fatal errors.') end # Write log - if !log.empty? + unless log.empty? OUTPUT.start('Assembling log files') # Manage messages from extensions - Extension.list.each { |ext| + Extension.list.each do |ext| if !log.has_messages?(ext.name) \ - && File.exist?(".#{NAME}_extensionmsg_#{ext.name}") + && File.exist?(".#{NAME}_extensionmsg_#{ext.name}") # Extension did not run but has run before; load messages from then! - old = File.open(".#{NAME}_extensionmsg_#{ext.name}", 'r') { |f| + old = File.open(".#{NAME}_extensionmsg_#{ext.name}", 'r') do |f| f.readlines.join - } + end old = YAML.load(old) log.add_messages(ext.name, old[0], old[1], old[2]) elsif log.has_messages?(ext.name) # Write new messages - File.open(".#{NAME}_extensionmsg_#{ext.name}", 'w') { |f| + File.open(".#{NAME}_extensionmsg_#{ext.name}", 'w') do |f| f.write(YAML.dump(log.messages(ext.name))) - } + end end - } + end logfile = LogWriter[:raw].write(log) logfile = LogWriter[PARAMS[:logformat]].write(log, PARAMS[:loglevel]) - FileUtils::cp(logfile, "#{PARAMS[:jobpath]}/#{logfile}") + FileUtils.cp(logfile, "#{PARAMS[:jobpath]}/#{logfile}") OUTPUT.stop(:success) OUTPUT.msg("Log file generated at #{logfile}") CLEANALL.push("#{PARAMS[:jobpath]}/#{logfile}") @@ -255,7 +244,7 @@ runtime = Time.now - start_time # Don't show runtimes of less than 5s (arbitrary) if runtime / 60 >= 1 || runtime % 60 >= 5 - OUTPUT.msg('Took ' + sprintf('%d min ', runtime / 60) + ' ' + sprintf('%d sec', runtime % 60)) + OUTPUT.msg("Took #{format('%d min ', runtime / 60)} #{format('%d sec', runtime % 60)}") end end rescue Interrupt, SystemExit # User cancelled current run @@ -274,12 +263,12 @@ end while ( PARAMS[:daemon] ) rescue Interrupt, SystemExit OUTPUT.separate.msg('Shutdown') -rescue Exception => e - if PARAMS[:user_jobname] != nil +rescue StandardError => e + if !PARAMS[:user_jobname].nil? OUTPUT.separate.error(e.message, "See #{PARAMS[:user_jobname]}.err for details.") - File.open("#{PARAMS[:jobpath]}/#{PARAMS[:user_jobname]}.err", 'w') { |file| + File.open("#{PARAMS[:jobpath]}/#{PARAMS[:user_jobname]}.err", 'w') do |file| file.write("#{e.inspect}\n\n#{e.backtrace.join("\n")}") - } + end CLEANALL.push("#{PARAMS[:jobpath]}/#{PARAMS[:user_jobname]}.err") else # This is reached due to programming errors or if ltx2any quits early, @@ -300,5 +289,5 @@ # Stop file listeners FileListener.instance.stop if PARAMS[:daemon] && FileListener.instance.runs? # Remove temps if so desired. -CLEAN.each { |f| FileUtils::rm_rf(f) } if PARAMS[:clean] -CLEANALL.each { |f| FileUtils::rm_rf(f) } if PARAMS[:cleanall] +CLEAN.each { |f| FileUtils.rm_rf(f) } if PARAMS[:clean] +CLEANALL.each { |f| FileUtils.rm_rf(f) } if PARAMS[:cleanall] diff --git a/parameters.rb b/parameters.rb index f377c64..e602da2 100644 --- a/parameters.rb +++ b/parameters.rb @@ -31,6 +31,6 @@ ParameterManager.instance.addParameter(p) } -ParameterManager.instance.addHook(:cleanall) { |k,v| +ParameterManager.instance.addHook(:cleanall) { |_, v| ParameterManager.instance[:clean] = true if v }