TexAnalyser.java

/*
 * Copyright © 2026 The CTAN Team and individual authors
 *
 * This file is distributed under the 3-clause BSD license.
 * See file LICENSE for details.
 */

package org.ctan.analysers;

import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.ctan.analysers.Analysis.DefType;

/**
 * The class <code>TexAnalyser</code> contains an analyser for TeX code.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
public class TexAnalyser {

    /**
     * The field <code>OF</code> contains the classification of macros to types.
     */
    public static final Map<String, DefType> OF = Map.ofEntries(
        Map.entry("def", DefType.MACRO),
        Map.entry("edef", DefType.MACRO),
        Map.entry("gdef", DefType.MACRO),
        Map.entry("xdef", DefType.MACRO),
        Map.entry("let", DefType.MACRO),
        Map.entry("font", DefType.FONT),
        Map.entry("newfam", DefType.FONT),
        Map.entry("mathchardef", DefType.CHAR),
        Map.entry("chardef", DefType.CHAR),
        Map.entry("newbox", DefType.BOX),
        Map.entry("newcount", DefType.COUNT),
        Map.entry("countdef", DefType.COUNT),
        Map.entry("newdimen", DefType.DIMEN),
        Map.entry("dimendef", DefType.DIMEN),
        Map.entry("newif", DefType.IF),
        Map.entry("newskip", DefType.SKIP),
        Map.entry("newmuskip", DefType.SKIP),
        Map.entry("skipdef", DefType.SKIP),
        Map.entry("newinsert", DefType.INSERT),
        Map.entry("newtoks", DefType.TOKS),
        Map.entry("toksdef", DefType.TOKS),
        Map.entry("newcommand", DefType.LATEX_MACRO),
        Map.entry("renewcommand", DefType.LATEX_MACRO),
        Map.entry("providecommand", DefType.LATEX_MACRO),
        Map.entry("newenvironment", DefType.LATEX_ENVIRONMENT),
        Map.entry("renewenvironment", DefType.LATEX_ENVIRONMENT),
        Map.entry("ProvidesPackage", DefType.LATEX_PACKAGE),
        Map.entry("ProvidesClass", DefType.LATEX_CLASS),
        Map.entry("define", DefType.CONTEXT_DEF),
        Map.entry("defineexpandable", DefType.CONTEXT_DEF),
        Map.entry("starttexdefinition", DefType.CONTEXT_DEF));

    /**
     * The field <code>TEX_PATTERN</code> contains the pattern for the
     * definition of TeX macros.
     */
    private static final Pattern TEX_PATTERN =
        Pattern.compile(
            "\\\\(def"
                + "|chardef"
                + "|countdef"
                + "|dimendef"
                + "|edef"
                + "|font"
                + "|gdef"
                + "|let"
                + "|mathchardef"
                + "|newdimen"
                + "|newfam"
                + "|newif"
                + "|newinsert"
                + "|newcount"
                + "|newtoks"
                + "|newbox"
                + "|newmuskip"
                + "|newskip"
                + "|skipdef"
                + "|toksdef"
                + "|xdef"
                + ")\\s*(\\\\([a-zA-Z@]+|^^[A-Z]|[^a-zA-Z@]))");

    /**
     * The field <code>LATEX_PATTERN</code> contains the pattern for the
     * definition of LaTeX macros.
     */
    private static final Pattern LATEX_PATTERN =
        Pattern
            .compile(
                "\\\\(renewcommand|newcommand)"
                    + "\\s*[{]?(\\\\([a-zA-Z@]+|[^a-zA-Z@]))[}]?");

    /**
     * The field <code>LATEX_PATTERN_WITH_NAME</code> contains the pattern for
     * the definition of LaTeX macros with braces.
     */
    private static final Pattern LATEX_PATTERN_WITH_NAME =
        Pattern
            .compile(
                "\\\\(newenvironment"
                    + "|renewenvironment"
                    + "|ProvidesClass"
                    + "|ProvidesPackage"
                    + "|title)"
                    + "\\s*\\{([^}#]+)\\}");

    /**
     * The field <code>CONTEXT_PATTERN</code> contains the pattern for the
     * definition of macros in ConTeXt.
     */
    private static final Pattern CONTEXT_PATTERN =
        Pattern
            .compile(
                "\\\\(define|defineexpandable)"
                    + "\\s*(\\[[0-9]\\])?"
                    + "\\s*\\{?(\\\\[a-zA-Z@]+|[^a-zA-Z@])\\}?");

    /**
     * The field <code>CONTEXT_PATTERN_2</code> contains another pattern for the
     * definition of macros in ConTeXt.
     */
    private static final Pattern CONTEXT_PATTERN_2 =
        Pattern
            .compile(
                "\\\\(starttexdefinition)"
                    + "\\s*([a-zA-Z@]+|[^a-zA-Z@])");

    /**
     * The method <code>analyse</code> provides means to analyse a single file.
     *
     * @param in the reader to get lines from
     * @param source the source file name
     * @return the analysis result
     * @throws IOException in case of an I/O error
     */
    public Analysis analyse(Reader in, String source)
        throws IOException {

        Analysis analysis = Analysis.builder()
            .source(source)
            .build();
        LineNumberReader reader = new LineNumberReader(in);
        for (String line = reader.readLine(); line != null; line =
            reader.readLine()) {
            if (!line.startsWith("%")) {
                line = line.replaceAll("([^\\\\])%.*", "\\1");
                if (line.length() > 1) {
                    findPattern(line, reader.getLineNumber(), analysis);
                }
            }
        }
        return analysis;
    }

    /**
     * The method <code>findPattern</code> provides means to analyse a line with
     * the help of some regular expressions.
     *
     * @param line the input line
     * @param lineNo the input line number
     * @param analysis the analysis result container
     */
    protected void findPattern(String line, int lineNo, Analysis analysis) {

        Matcher matcher = TEX_PATTERN.matcher(line);
        while (matcher.find()) {
            analysis.set(matcher.group(2),
                OF.get(matcher.group(1)),
                lineNo);
        }
        matcher = LATEX_PATTERN.matcher(line);
        while (matcher.find()) {
            analysis.set(matcher.group(2),
                OF.get(matcher.group(1)),
                lineNo);
        }
        matcher = LATEX_PATTERN_WITH_NAME.matcher(line);
        while (matcher.find()) {
            String group1 = matcher.group(1);
            String group2 = matcher.group(2);
            if (group1.equals("title")) {
                analysis.setTitle(group2);
            } else {
                analysis.set(group2,
                    OF.get(group1),
                    lineNo);
            }
        }
        matcher = CONTEXT_PATTERN.matcher(line);
        while (matcher.find()) {
            analysis.set(matcher.group(3),
                OF.get(matcher.group(1)),
                lineNo);
        }
        matcher = CONTEXT_PATTERN_2.matcher(line);
        while (matcher.find()) {
            analysis.set("\\" + matcher.group(2),
                OF.get(matcher.group(1)),
                lineNo);
        }
    }
}