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;
    }
}