/*
* $Id$
*
* Copyright (c) 2004-2005 by the TeXlapse Team.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package net.sourceforge.texlipse.builder;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.texlipse.TexlipsePlugin;
import net.sourceforge.texlipse.filefinder.ProjectFileFinder;
import net.sourceforge.texlipse.properties.TexlipseProperties;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
/**
* Run the external latex program.
*
* @author Kimmo Karlsson
* @author Oskar Ojala
* @author Boris von Loesch
*/
public class LatexRunner extends AbstractProgramRunner {
private static final int MAX_LINE_LENGTH = 79;
protected Stack<String> parsingStack;
private boolean alreadyShowError;
/**
* Create a new ProgramRunner.
*/
public LatexRunner() {
super();
this.parsingStack = new Stack<String>();
}
protected String getWindowsProgramName() {
return "latex.exe";
}
protected String getUnixProgramName() {
return "latex";
}
public String getDescription() {
return "Latex program";
}
public String getDefaultArguments() {
/**
* --c-style-errors: latex should issue warnings and errors starting
* with filename and followed by line number. This
* makes it possible for the parser to correctly
* determine the source file and the location
* --synctex: Generate SyncTeX data for previewers
* --interaction: non-stop mode since the console does not allow
* input to the running program such as pdftex
* --src-specials: Embed source file information (source specials) in
* the DVI file
*
* As seen for pdftex with MikTex 2.9 all options need --
*/
return "--synctex=1 --c-style-errors --interaction=nonstopmode --src-specials %input";
}
public String getInputFormat() {
return TexlipseProperties.INPUT_FORMAT_TEX;
}
/**
* Used by the DviBuilder to figure out what the latex program produces.
*
* @return output file format (dvi)
*/
public String getOutputFormat() {
return TexlipseProperties.OUTPUT_FORMAT_DVI;
}
protected String[] getQueryString() {
return new String[] { "\nPlease type another input file name:" , "\nEnter file name:" };
}
/**
* Adds a problem marker
*
* @param error The error or warning string
* @param causingSourceFile name of the sourcefile
* @param linenr where the error occurs
* @param severity
* @param resource
* @param layout true, if this is a layout warning
*/
private void addProblemMarker(String error, String causingSourceFile,
int linenr, int severity, IResource resource, boolean layout) {
IProject project = resource.getProject();
IContainer sourceDir = TexlipseProperties.getProjectSourceDir(project);
IResource extResource = null;
if (causingSourceFile != null) {
IPath p = new Path(causingSourceFile);
if (p.isAbsolute()) {
//Make absolute path relative to source directory
//or to the directory of the resource
if (sourceDir.getLocation().isPrefixOf(p)) {
p = p.makeRelativeTo(sourceDir.getLocation());
}
else if (resource.getParent().getLocation().isPrefixOf(p)) {
p = p.makeRelativeTo(resource.getParent().getLocation());
}
}
extResource = sourceDir.findMember(p);
if (extResource == null) {
extResource = resource.getParent().findMember(p);
/**
* Try to find the file according to its absolute path.
* ProjectFileFinder will also check linked resources
*/
if (extResource == null)
{
extResource = ProjectFileFinder.findFile(project, causingSourceFile);
}
}
}
if (extResource == null)
createMarker(resource, null, error + (causingSourceFile != null ? " (Occurance: "
+ causingSourceFile + ")" : ""), severity);
else {
if (linenr >= 0) {
if (layout)
createLayoutMarker(extResource, new Integer(linenr), error);
else
createMarker(extResource, new Integer(linenr), error, severity);
} else
createMarker(extResource, null, error, severity);
}
}
/**
* Parse the output of the LaTeX program.
*
* @param resource the input file that was processed
* @param output the output of the external program
* @return true, if error messages were found in the output, false otherwise
*/
protected boolean parseErrors(IResource resource, String output) {
TexlipseProperties.setSessionProperty(resource.getProject(), TexlipseProperties.SESSION_LATEX_RERUN, null);
TexlipseProperties.setSessionProperty(resource.getProject(), TexlipseProperties.SESSION_BIBTEX_RERUN, null);
parsingStack.clear();
boolean errorsFound = false;
boolean citeNotfound = false;
alreadyShowError = false;
StringTokenizer st = new StringTokenizer(output, "\r\n");
final Pattern LATEXERROR = Pattern.compile("^! LaTeX Error: (.*)$");
final Pattern LATEXCERROR = Pattern.compile("^(.+?\\.\\w{3}):(\\d+): (.+)$");
final Pattern TEXERROR = Pattern.compile("^!\\s+(.*)$");
final Pattern FULLBOX = Pattern.compile("^(?:Over|Under)full \\\\[hv]box .* at lines? (\\d+)-?-?(\\d+)?");
final Pattern WARNING = Pattern.compile("^.+[Ww]arning.*: (.*)$");
final Pattern ATLINE = Pattern.compile("^l\\.(\\d+)(.*)$");
final Pattern ATLINE2 = Pattern.compile(".* line (\\d+).*");
final Pattern NOBIBFILE = Pattern.compile("^No file .+\\.bbl\\.$");
final Pattern NOTOCFILE = Pattern.compile("^No file .+\\.toc\\.$");
String line;
boolean hasProblem = false;
String error = null;
int severity = IMarker.SEVERITY_WARNING;
int linenr = -1;
String occurance = null;
while (st.hasMoreTokens()) {
line = st.nextToken();
//Add more lines if line length is a multiple of 79 and
//it does not end with ...
while (!line.endsWith("...") && st.hasMoreTokens()
&& line.length() % MAX_LINE_LENGTH == 0) {
line = line + st.nextToken();
}
line = line.replaceAll(" {2,}", " ").trim();
Matcher m = LATEXCERROR.matcher(line);
if (m.matches()) {
//C-Style LaTeX error
addProblemMarker(m.group(3), m.group(1), Integer.parseInt(m.group(2)), IMarker.SEVERITY_ERROR, resource, false);
//Maybe parsingStack is empty...
if (parsingStack.isEmpty()) {
//Add the file to the stack
parsingStack.push("(" + m.group(1));
}
continue;
}
m = TEXERROR.matcher(line);
if (m.matches() && line.toLowerCase().indexOf("warning") == -1) {
if (hasProblem) {
// We have a not reported problem
addProblemMarker(error, occurance, linenr, severity, resource, false);
linenr = -1;
}
hasProblem = true;
errorsFound = true;
severity = IMarker.SEVERITY_ERROR;
occurance = determineSourceFile();
Matcher m2 = LATEXERROR.matcher(line);
if (m2.matches()) {
// LaTex error
error = m2.group(1);
String part2 = st.nextToken().trim();
if (Character.isLowerCase(part2.charAt(0))) {
error += ' ' + part2;
}
updateParsedFile(part2);
continue;
}
if (line.startsWith("! Undefined control sequence.")){
// Undefined Control Sequence
error = "Undefined control sequence: ";
continue;
}
m2 = WARNING.matcher(line);
if (m2.matches())
severity = IMarker.SEVERITY_WARNING;
error = m.group(1);
continue;
}
m = WARNING.matcher(line);
if (m.matches()){
if (hasProblem){
// We have a not reported problem
addProblemMarker(error, occurance, linenr, severity, resource, false);
linenr = -1;
hasProblem = false;
}
if (line.indexOf("Label(s) may have changed.") > -1) {
// prepare to re-run latex
TexlipseProperties.setSessionProperty(resource.getProject(),
TexlipseProperties.SESSION_LATEX_RERUN, "true");
continue;
}
else if (line.indexOf("There were undefined") > -1) {
if (citeNotfound) {
// prepare to run bibtex
TexlipseProperties.setSessionProperty(resource.getProject(),
TexlipseProperties.SESSION_BIBTEX_RERUN, "true");
}
continue;
}
// Ignore undefined references because they are
// found by the parser
if (line.indexOf("Warning: Reference ") > -1)
continue;
if (line.indexOf("Warning: Citation ") > -1) {
citeNotfound = true;
continue;
}
severity = IMarker.SEVERITY_WARNING;
occurance = determineSourceFile();
hasProblem = true;
if (line.startsWith("LaTeX Warning: ") || line.indexOf("pdfTeX warning") != -1) {
error = m.group(1);
//Try to get the line number
Matcher pM = ATLINE2.matcher(line);
if (pM.matches()) {
linenr = Integer.parseInt(pM.group(1));
}
String nextLine = st.nextToken().replaceAll(" {2,}", " ");
pM = ATLINE2.matcher(nextLine);
if (pM.matches()) {
linenr = Integer.parseInt(pM.group(1));
}
updateParsedFile(nextLine);
error += nextLine;
if (linenr != -1) {
addProblemMarker(line, occurance, linenr, severity,
resource, false);
hasProblem = false;
linenr = -1;
}
continue;
} else {
error = m.group(1);
//Try to get the line number
Matcher pM = ATLINE2.matcher(line);
if (pM.matches()) {
linenr = Integer.parseInt(pM.group(1));
}
continue;
}
}
m = FULLBOX.matcher(line);
if (m.matches()) {
if (hasProblem) {
// We have a not reported problem
addProblemMarker(error, occurance, linenr, severity,
resource, false);
linenr = -1;
hasProblem = false;
}
severity = IMarker.SEVERITY_WARNING;
occurance = determineSourceFile();
error = line;
linenr = Integer.parseInt(m.group(1));
addProblemMarker(line, occurance, linenr, severity, resource,
true);
hasProblem = false;
linenr = -1;
continue;
}
m = NOBIBFILE.matcher(line);
if (m.matches()){
// prepare to run bibtex
TexlipseProperties.setSessionProperty(resource.getProject(),
TexlipseProperties.SESSION_BIBTEX_RERUN, "true");
continue;
}
m = NOTOCFILE.matcher(line);
if (m.matches()){
// prepare to re-run latex
TexlipseProperties.setSessionProperty(resource.getProject(),
TexlipseProperties.SESSION_LATEX_RERUN, "true");
continue;
}
m = ATLINE.matcher(line);
if (hasProblem && m.matches()) {
linenr = Integer.parseInt(m.group(1));
String part2 = st.nextToken();
int index = line.indexOf(' ');
if (index > -1) {
error += " " + line.substring(index).trim() + " (followed by: "
+ part2.trim() + ")";
addProblemMarker(error, occurance, linenr, severity, resource,
false);
linenr = -1;
hasProblem = false;
continue;
}
}
m = ATLINE2.matcher(line);
if (hasProblem && m.matches()) {
linenr = Integer.parseInt(m.group(1));
addProblemMarker(error, occurance, linenr, severity, resource,
false);
linenr = -1;
hasProblem = false;
continue;
}
updateParsedFile(line);
}
if (hasProblem) {
// We have a not reported problem
addProblemMarker(error, occurance, linenr, severity, resource, false);
}
return errorsFound;
}
/**
* Updates the stack that determines which file we are currently
* parsing, so that errors can be annotated in the correct file.
*
* @param logLine A line from latex' output containing which file we are in
*/
private void updateParsedFile(String logLine) {
if (logLine.indexOf('(') == -1 && logLine.indexOf(')') == -1)
return;
for (int i = 0; i < logLine.length(); i++) {
if (logLine.charAt(i) == '(') {
int j;
for (j = i + 1; j < logLine.length()
&& isAllowedinName(logLine.charAt(j)); j++)
;
parsingStack.push(logLine.substring(i, j).trim());
i = j - 1;
} else if (logLine.charAt(i) == ')' && !parsingStack.isEmpty()) {
parsingStack.pop();
} else if (logLine.charAt(i) == ')' && !alreadyShowError) {
alreadyShowError = true;
// There was a parsing error, this is very rare
TexlipsePlugin.log("Error while parsing the LaTeX output. " +
"Please consult the console output", null);
}
}
}
/**
* Check if the character is allowed in a filename
* @param c the character
* @return true if the character is legal
*/
private boolean isAllowedinName(char c) {
if (c == '(' || c == ')' || c == '[')
return false;
else
return true;
}
private static boolean isValidName(String name) {
//File must have a file ending
int p = name.lastIndexOf('.');
if (p < 0) return false;
//File ending must be shorter than 9 characters
if (name.length()-p > 10) return false;
return true;
}
/**
* Determines the source file we are currently parsing.
*
* @return The filename or null if no file could be determined
*/
protected String determineSourceFile() {
int i = parsingStack.size()-1;
while (i >= 0) {
String fileName = parsingStack.get(i).substring(1);
//Remove "
if (fileName.startsWith("\"") && fileName.endsWith("\"")) {
fileName = fileName.substring(1, fileName.length() - 1);
}
if (isValidName(fileName))
{
return fileName;
}
i--;
}
return null;
}
}