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