MacroProcessor.java
package edu.odu.cs.cowem.macroproc;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Transforms a text file via application of macros. Supported commands are
*
* #ifdef macroName or #if macroName or %ifnot macroname
* #else
* #endif
*
* #include (filename)
*
* The above are all analogous to the familiar C/C++ pre-processor
*
* #define (macroName) (argslist) (macrobody)
*
* defines a macro with the given name, a possibly empty, comma-separated
* arguments list, and a body. In this command (and in the #include, above),
* the ( ) may be any matching pair of: (), [], {}, or <>.
* The macro body may span multiple lines and the line feeds are considered
* part of the body.
*
* Examples of macro definitions:
* #define (authorName) () (Steven Zeil)
* #define (slide) (title) {
* \begin{slide}{title}
* }
* #define (picture)(file,sizepct) {<img src="file"
* style="max-width: sizepct%"/>}
*
* @author zeil
*
*/
public class MacroProcessor {
/**
* Character used to introduce a basic macro command.
*/
private String commandPrefix = "#";
/**
* The macros defined in this processor.
*/
private List<Macro> macros;
/**
* Names of all defined macros.
*/
private Set<String> macroNames;
/**
* Partially processed text.
*/
@SuppressWarnings("PMD.AvoidStringBufferField")
private StringBuffer accumulated;
/**
* Stack of states during document processing.
*/
private List<InputState> stack;
/**
* For logging error messages.
*/
private static Logger logger
= LoggerFactory.getLogger(MacroProcessor.class);
/**
* Describes a stackable state during macro processing.
*
* @author zeil
*/
private class InputState {
// CHECKSTYLE IGNORE VisibilityModifierCheck FOR NEXT 20 LINES
/**
* Indicates a character that opened a macro parameter list
* or macro body that needs to be matched to close that construct.
*/
public char matching;
/**
* Is copying of text to output suppresed due to a failed #if test?
*/
public boolean suppressed;
/**
* Holds a macro that we are currnetly paring.
*/
public Macro incompleteMacro;
/**
* Create a state.
* @param matching0 matching character
* @param suppr true to suppress copying of text to output
*/
InputState (final char matching0, final boolean suppr) {
matching = matching0;
suppressed = suppr;
incompleteMacro = null;
}
}
/**
* Defines a new macro processor with no currently defined macros,
* using # as the command prefix.
*/
public MacroProcessor () {
macros = new ArrayList<Macro>();
macroNames = new HashSet<String>();
stack = new ArrayList<>();
stack.add(new InputState(' ', false));
accumulated = new StringBuffer();
}
/**
* Defines a new macro processor with no currently defined macros.
* @param commandPrefix0 character to use as the command prefix
*/
public MacroProcessor (final String commandPrefix0) {
macros = new ArrayList<Macro>();
macroNames = new HashSet<String>();
this.commandPrefix = commandPrefix0;
stack = new ArrayList<>();
stack.add(new InputState(' ', false));
accumulated = new StringBuffer();
}
/**
* Add a new macro to the processor state.
* @param macro macro
*/
public final void defineMacro (final Macro macro) {
//System.err.println ("Defining " + macro);
macros.add(macro);
macroNames.add(macro.getName());
}
/**
* Process a single line of input, including commands and macro expansions.
*
* @param line input line
* @return processed input or null if line generates no input
*/
private String processLine (final String line) {
InputState topState = stack.get(stack.size() - 1);
if (topState.matching != ' ') {
int pos = line.indexOf(topState.matching);
if (pos < 0) {
topState.incompleteMacro.setBody(
topState.incompleteMacro.getBody() + "\n" + line);
return null;
} else {
topState.incompleteMacro.setBody(
topState.incompleteMacro.getBody()
+ "\n" + line.substring(0, pos));
stack.remove(stack.size() - 1);
return null;
}
}
int pos = 0;
while (pos < line.length() && line.charAt(pos) == ' ') {
++pos;
}
String trimmedString = line.substring(pos);
if (trimmedString.startsWith(commandPrefix)) {
String accumulation = accumulated.toString();
String result = null;
if (accumulation.length() > 0) {
result = processMacros(accumulation);
}
accumulated = new StringBuffer();
if (trimmedString.startsWith(commandPrefix + "include")) {
if (result == null) {
result = "";
}
return result + processInclude(trimmedString);
} else if (trimmedString.startsWith(commandPrefix + "ifdef")) {
processIfDef (trimmedString, "ifdef");
return result;
} else if (trimmedString.startsWith(commandPrefix + "ifnot")) {
processIfNot (trimmedString, "ifnot");
return result;
} else if (trimmedString.startsWith(commandPrefix + "if")) {
processIfDef (trimmedString, "if");
return result;
} else if (trimmedString.startsWith(commandPrefix + "else")) {
processElse ();
return result;
} else if (trimmedString.startsWith(commandPrefix + "endif")) {
processEndif ();
return result;
} else if (trimmedString.startsWith(commandPrefix + "define")) {
processDefine (trimmedString);
return result;
} else {
accumulated.append(accumulation);
accumulated.append(line);
accumulated.append(System.lineSeparator());
}
} else if (!topState.suppressed) {
accumulated.append(line);
accumulated.append(System.lineSeparator());
}
return null;
/*
if (!topState.suppressed){
return processMacros (line);
} else {
return null;
}
*/
}
/**
* Flush accumulated output to the output, clearing the accumulated buffer.
* @return String to be flushed
*/
private String flush() {
String accumulation = accumulated.toString();
accumulated = new StringBuffer();
if (accumulation.length() > 0) {
return processMacros(accumulation);
} else {
return "";
}
}
/**
* Process any macros found in a line of text.
* @param line Input text
* @return output from macro processing.
*/
private String processMacros(final String line) {
String result = line;
for (Macro m: macros) {
result = m.apply(result);
}
return result;
}
/**
* Parse and process a #define command.
* @param defineCommandStart the opening phrase of the command
*/
private void processDefine(final String defineCommandStart) {
InputState topState = stack.get(stack.size() - 1);
if (!topState.suppressed) {
int start = commandPrefix.length() + "define".length();
ParseResult pr = parseEnclosure(defineCommandStart, start);
if (pr == null) {
return;
}
String name = pr.getSelectedString();
pr = parseEnclosure(defineCommandStart,
pr.getStoppingPosition() + 1);
if (pr == null) {
return;
}
// CHECKSTYLE IGNORE AvoidInlineConditionals FOR NEXT 2 LINES
String[] args = (pr.getSelectedString().length() > 0)
? pr.getSelectedString().split(",") : new String[0];
start = pr.getStoppingPosition() + 1;
pr = parseEnclosure(defineCommandStart, start);
if (pr != null) {
Macro m = new Macro(name, Arrays.asList(args),
pr.getSelectedString());
defineMacro(m);
} else {
InputState state = new InputState(' ', true);
char opener = ' ';
while (start < defineCommandStart.length() && opener == ' ') {
char c = defineCommandStart.charAt(start);
if (c == '(' || c == '[' || c == '{' || c == '<') {
opener = c;
}
++start;
}
if (start > defineCommandStart.length()) {
return;
}
char closer = ' ';
switch (opener) {
case '(': closer = ')'; break;
case '[': closer = ']'; break;
case '{': closer = '}'; break;
case '<': closer = '>'; break;
default:
}
Macro m = new Macro (name, Arrays.asList(args),
defineCommandStart.substring(start));
defineMacro(m);
state.matching = closer;
state.incompleteMacro = m;
stack.add(state);
}
}
}
/**
* Parse and process an #endif command.
*/
private void processEndif() {
if (stack.size() > 1) {
stack.remove(stack.size() - 1);
}
}
/**
* Parse and process an #else command.
*/
private void processElse() {
if (stack.size() > 1) {
InputState topState = stack.get(stack.size() - 1);
InputState priorState = stack.get(stack.size() - 2);
stack.remove(stack.size() - 1);
stack.add (new InputState(' ',
priorState.suppressed || !topState.suppressed));
}
}
/**
* Parse and process an #ifdef command.
* @param ifDefCommand Lexeme of the command.
* @param ifdefLexeme Lexeme of the opening word of the command/
*/
private void processIfDef(final String ifDefCommand,
final String ifdefLexeme) {
InputState topState = stack.get(stack.size() - 1);
if (topState.suppressed) {
// Doesn't matter if the condition is true or not
stack.add (new InputState(' ', true));
} else {
int start = commandPrefix.length() + ifdefLexeme.length();
while (start < ifDefCommand.length()
&& ifDefCommand.charAt(start) == ' ') {
++start;
}
int stop = start;
while (stop < ifDefCommand.length()
&& ifDefCommand.charAt(stop) != ' ') {
++stop;
}
if (start >= ifDefCommand.length() || stop == start) {
stack.add (new InputState(' ', true));
} else {
String macroName = ifDefCommand.substring(start, stop);
stack.add (new InputState(' ',
!macroNames.contains(macroName)));
}
}
}
/**
* Parse and process an #ifnot command.
* @param ifNotCommand Lexeme of the command.
* @param ifnotLexeme Lexeme of the opening word of the command/
*/
private void processIfNot(final String ifNotCommand,
final String ifnotLexeme) {
InputState topState = stack.get(stack.size() - 1);
if (topState.suppressed) {
// Doesn't matter if the condition is true or not
stack.add (new InputState(' ', true));
} else {
int start = commandPrefix.length() + ifnotLexeme.length();
while (start < ifNotCommand.length()
&& ifNotCommand.charAt(start) == ' ') {
++start;
}
int stop = start;
while (stop < ifNotCommand.length()
&& ifNotCommand.charAt(stop) != ' ') {
++stop;
}
if (start >= ifNotCommand.length() || stop == start) {
stack.add (new InputState(' ', true));
} else {
String macroName = ifNotCommand.substring(start, stop);
stack.add (new InputState(' ',
macroNames.contains(macroName)));
}
}
}
/**
* Parse and process an #include command.
* @param includeCommand Lexeme of the command.
* @return String to be inserted in place of the command
*/
private String processInclude(final String includeCommand) {
InputState topState = stack.get(stack.size() - 1);
if (!topState.suppressed) {
List<InputState> savedStack = stack;
stack = new ArrayList<>();
stack.add(new InputState(' ', false));
String fileName;
try {
fileName = parseEnclosure(includeCommand,
commandPrefix.length()
+ "include".length()).getSelectedString();
} catch (Exception e) {
return "**Error " + includeCommand
+ "\n: " + commandPrefix + "\n**";
}
File input = new File(fileName);
if (input.exists()) {
String result = process(input);
stack = savedStack;
return result;
} else {
return "\n\n** Missing file: " + fileName + " **\n\n";
}
} else {
return null;
}
}
/**
* A description of an attempted command parse.
*
* @author zeil
*
*/
public class ParseResult {
/**
* A parsed construct.
*/
private String selectedString;
/**
* Position at which the recognized construct ended.
*/
private int stoppingPosition;
/**
* Create a parse result.
* @param sel the selected string
* @param stop the stopping position
*/
public ParseResult (final String sel, final int stop) {
selectedString = sel;
stoppingPosition = stop;
}
/**
* @return the selectedString
*/
final String getSelectedString() {
return selectedString;
}
/**
* @param selectedString0 the selectedString to set
*/
final void setSelectedString(final String selectedString0) {
this.selectedString = selectedString0;
}
/**
* @return the stoppingPosition
*/
final int getStoppingPosition() {
return stoppingPosition;
}
/**
* @param stoppingPosition0 the stoppingPosition to set
*/
final void setStoppingPosition(final int stoppingPosition0) {
this.stoppingPosition = stoppingPosition0;
}
}
/**
* Scan forward to next non-black character. Should be one of: ([{<.
* Extract the string between that and the appropriate closing character.
*
* @param commandString command that is being parsed
* @param startingAt position within string to start parsing
* @return enclosed string or null if one cannot be found
*/
private ParseResult parseEnclosure(final String commandString,
final int startingAt) {
int start = startingAt;
char opener = ' ';
while (start < commandString.length()
&& commandString.charAt(start) == ' ') {
++start;
}
if (start < commandString.length()) {
opener = commandString.charAt(start);
}
char closer = ' ';
switch (opener) {
case '(': closer = ')'; break;
case '{': closer = '}'; break;
case '[': closer = ']'; break;
case '<': closer = '>'; break;
default:
}
if (closer == ' ') {
return null;
}
int stop = start;
while (stop < commandString.length()
&& commandString.charAt(stop) != closer) {
++stop;
}
if (stop < commandString.length()) {
return new ParseResult(commandString.substring(start + 1, stop),
stop);
} else {
return null;
}
}
/**
* Process a block of text obtained from a reader. Processing can both
* alter the text (macro substitution) and affect the state of the
* processor by defining new macros.
* @param input source of text to be processed
* @return processed text
* @throws IOException on failure of reader
*/
public final String process (final BufferedReader input)
throws IOException {
StringBuffer results = new StringBuffer();
String line = input.readLine();
while (line != null) {
String processed = processLine(line);
if (processed != null) {
results.append(processed);
results.append(System.lineSeparator());
}
line = input.readLine();
}
results.append(flush());
return results.toString();
}
/**
* Process a block of text. Processing can both
* alter the text (macro substitution) and affect the state of the
* processor by defining new macros.
*
* @param inputString text to process
* @return processed text
*/
public final String process (final String inputString) {
String results = "";
BufferedReader input = null;
try {
input = new BufferedReader(new StringReader (inputString));
results = process (input);
} catch (IOException e) {
logger.error("**Unexpected I/O error in " + inputString, e);
} finally {
try {
if (input != null) {
input.close();
}
} catch (IOException e) {
logger.error("**Unexpected I/O error", e);
}
}
return results.replace("\r", "");
}
/**
* Process a block of text obtained from a file. Processing can both
* alter the text (macro substitution) and affect the state of the
* processor by defining new macros.
*
* @param inputFile text to process
* @return processed text
*/
public final String process (final File inputFile) {
String results = "";
BufferedReader input = null;
try {
input = new BufferedReader(new FileReader (inputFile));
results = process (input);
} catch (FileNotFoundException e) {
logger.error ("**Could not open " + inputFile.getAbsolutePath(), e);
} catch (IOException e) {
logger.error ("**Unexpected I/O error in "
+ inputFile.getAbsolutePath(), e);
} finally {
try {
if (input != null) {
input.close();
}
} catch (IOException e) {
logger.error ("**Unexpected I/O error in "
+ inputFile.getAbsolutePath(), e);
}
}
return results.replace("\r", "");
}
/**
* Driver for macro processor. Accepts args:
* -c? changes the macro prefix string (default is '#')
* -Dmacro defines a macro name
* -iFile processes File, ignoring output but keeping any
* macro definitions
* -oFile directs output to the indicated file
* fileName input file name
* @param args Command line parameters as described above
* @throws IOException on input failure
*/
public static void main(final String[] args) throws IOException {
String inputFileName = null;
String outputFileName = null;
MacroProcessor processor = new MacroProcessor();
for (int i = 0; i < args.length; ++i) {
if (args[i].startsWith("-c")) {
String prefix = args[i].substring(2);
processor = new MacroProcessor(prefix);
} else if (args[i].startsWith("-D")) {
String macroName = args[i].substring(2);
processor.defineMacro(new Macro(macroName, ""));
} else if (args[i].startsWith("-o")) {
String fileName = args[i].substring(2);
outputFileName = fileName;
} else if (args[i].startsWith("-i")) {
String fileName = args[i].substring(2);
File input = new File(fileName);
processor.process(input);
} else {
inputFileName = args[i];
}
}
BufferedReader mainInput;
if (inputFileName != null) {
mainInput = new BufferedReader(new FileReader(inputFileName));
} else {
mainInput = new BufferedReader(new InputStreamReader(System.in));
}
BufferedWriter mainOutput;
if (outputFileName != null) {
mainOutput = new BufferedWriter(new FileWriter(outputFileName));
} else {
mainOutput = new BufferedWriter(
new OutputStreamWriter(System.out));
}
transformText (mainInput, mainOutput, processor);
if (outputFileName != null) {
mainOutput.close();
}
if (inputFileName != null) {
mainInput.close();
}
}
/**
* Transforms text using a macro processor.
* @param input input text source
* @param output output text destination
* @param processor the macro processor to use
* @throws IOException on input failure
*/
private static void transformText(final BufferedReader input,
final BufferedWriter output, final MacroProcessor processor)
throws IOException {
String line = input.readLine();
while (line != null) {
String processed = processor.processLine(line);
if (processed != null) {
output.write(processed);
output.write(System.lineSeparator());
}
line = input.readLine();
}
output.write(processor.flush());
output.write(System.lineSeparator());
}
}