InsParser.java
/*
* Copyright © 2017-2025 The CTAN Team and individual authors
*
* This file is distributed under the 3-clause BSD license.
* See file LICENSE for details.
*/
package minitex;
import java.io.EOFException;
import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class contains a parser to analyse ins files.
*
* @author <a href="gene@ctan.org">Gerd Neugebauer</a>
*/
public class InsParser {
/**
* This interface contains the description of a function with one run
* method.
*/
private interface Code {
/**
* The method <code>run</code> contains a description of a method to
* execute code.
*
* @param r the reader for further tokens
* @param files the list of files
* @param dir the current directory
* @throws IOException in case of an I/O error
*/
void run(PushbackReader r, List<String> files, String dir)
throws IOException;
}
/**
* The field <code>DUMMY_CODE</code> contains the empty code which does
* nothing.
*/
private static final Code DUMMY_CODE = (r, files, dir) -> {
};
/**
* The field <code>macros</code> contains the map of all known macros.
*/
private Map<String, Code> macros = new HashMap<String, Code>();
/**
* This is the constructor for <code>InsParser</code>.
*/
public InsParser() {
macros.put("file", (r, files, dir) -> {
StringBuilder buffer = new StringBuilder();
buffer.append(dir);
int c = r.read();
if (c == '{') {
for (c = r.read(); c != '}'; c = r.read()) {
if (c < 0) {
throw new IOException("missing } for \\file");
}
buffer.append((char) c);
}
files.add(buffer.toString());
} else {
// this should not happen
}
});
macros.put("generateFile", (r, files, dir) -> {
StringBuilder buffer = new StringBuilder();
buffer.append(dir);
int c = r.read();
if (c == '{') {
for (c = r.read(); c != '}'; c = r.read()) {
if (c < 0) {
throw new IOException("missing } for \\generateFile");
}
buffer.append((char) c);
}
files.add(buffer.toString());
} else {
// this should not happen
}
});
}
/**
* This method parses the ins file.
*
* @param dir the directory
* @param reader the reader for more characters
*
* @return the list of files
*
* @throws IOException in case of an I/O error
*/
public List<String> parse(String dir, Reader reader) throws IOException {
List<String> files = new ArrayList<String>();
PushbackReader r = new PushbackReader(reader);
try {
for (Code t = scan(r); t != null; t = scan(r)) {
t.run(r, files, dir);
}
} finally {
reader.close();
}
return files;
}
/**
* This method scans the input for more macros.
*
* @param reader the reader for more characters
*
* @return the code read
*
* @throws IOException in case of an I/O error
*/
private Code scan(PushbackReader reader) throws IOException {
for (int c = reader.read(); c >= 0; c = reader.read()) {
switch (c) {
case '%':
for (c = reader.read(); c >= 0 && c != '\n'; c =
reader.read()) {
}
break;
case '\\':
return scanMacro(reader);
case '{':
case '}':
default:
return DUMMY_CODE;
}
}
return null;
}
/**
* This method scans the name of a macro after the initial \ has been
* encountered.
*
* @param reader the reader for more characters
*
* @return the code read
* @throws IOException in case of an I/O error
*/
private Code scanMacro(PushbackReader reader) throws IOException {
int c = reader.read();
if (c < 0) {
throw new EOFException("found \\ at EOF");
}
StringBuilder buffer = new StringBuilder();
buffer.append((char) c);
if (Character.isAlphabetic(c)) {
for (c = reader.read(); c >= 0 && Character.isAlphabetic(c); c =
reader.read()) {
buffer.append((char) c);
}
} else {
c = reader.read();
}
while (c >= 0 && Character.isWhitespace(c)) {
c = reader.read();
}
if (c >= 0) {
reader.unread(c);
}
Code m = macros.get(buffer.toString());
return m != null ? m : DUMMY_CODE;
}
}