diff --git a/build.gradle b/build.gradle index 3c75afdba28..0fc50f10ed7 100644 --- a/build.gradle +++ b/build.gradle @@ -85,7 +85,6 @@ repositories { } configurations { - antlr3 antlr4 // TODO: Remove the following workaround for split error messages such as // error: module java.xml.bind reads package javax.annotation from both jsr305 and java.annotation @@ -139,9 +138,6 @@ dependencies { implementation 'io.github.java-diff-utils:java-diff-utils:4.12' implementation 'info.debatty:java-string-similarity:2.0.0' - antlr3 'org.antlr:antlr:3.5.3' - implementation 'org.antlr:antlr-runtime:3.5.3' - antlr4 'org.antlr:antlr4:4.9.3' implementation 'org.antlr:antlr4-runtime:4.9.3' @@ -273,14 +269,14 @@ task generateSource(dependsOn: ["generateBstGrammarSource", } tasks.register("generateBstGrammarSource", JavaExec) { - main = "org.antlr.Tool" - classpath = configurations.antlr3 + main = "org.antlr.v4.Tool" + classpath = configurations.antlr4 group = "JabRef" - description = 'Generates BstLexer.java and BstParser.java from the Bst.g grammar file using antlr3.' + description = 'Generates BstLexer.java and BstParser.java from the Bst.g grammar file using antlr4.' - inputs.dir('src/main/antlr3/org/jabref/bst/') + inputs.dir('src/main/antlr4/org/jabref/bst/') outputs.dir("src-gen/main/java/org/jabref/logic/bst/") - args = ["-o", "src-gen/main/java/org/jabref/logic/bst/" , "$projectDir/src/main/antlr3/org/jabref/bst/Bst.g" ] + args = ["-o", "src-gen/main/java/org/jabref/logic/bst/", "-visitor", "-no-listener", "-package", "org.jabref.logic.bst", "$projectDir/src/main/antlr4/org/jabref/bst/Bst.g4"] } tasks.register("generateSearchGrammarSource", JavaExec) { diff --git a/src/main/antlr3/org/jabref/bst/Bst.g b/src/main/antlr3/org/jabref/bst/Bst.g deleted file mode 100644 index 498621ad1a1..00000000000 --- a/src/main/antlr3/org/jabref/bst/Bst.g +++ /dev/null @@ -1,96 +0,0 @@ -grammar Bst; - -options { - output=AST; -} - -tokens { - IDLIST; - STACK; - ENTRY; - COMMANDS; -} - -// applies only to the parser: -@header {// Generated by ANTLR -package org.jabref.logic.bst;} - -// applies only to the lexer: -@lexer::header {// Generated by ANTLR -package org.jabref.logic.bst;} - -program : commands+ -> ^(COMMANDS commands+); - -commands - : STRINGS^ idList - | INTEGERS^ idList - | FUNCTION^ id stack - | MACRO^ id '{'! STRING '}'! - | READ^ - | EXECUTE^ '{'! function '}'! - | ITERATE^ '{'! function '}'! - | REVERSE^ '{'! function '}'! - | ENTRY^ idList0 idList0 idList0 - | SORT^; - -identifier - : IDENTIFIER; - -id - : '{'! identifier '}'!; - -idList - : '{' identifier+ '}' -> ^(IDLIST identifier+); - -idList0 - : '{' identifier* '}' -> ^(IDLIST identifier*); - -function - : '<' | '>' | '=' | '+' | '-' | ':=' | '*' | identifier; - -stack - : '{' stackitem+ '}' -> ^(STACK stackitem+); - -stackitem - : function - | STRING - | INTEGER - | QUOTED - | stack; - -STRINGS : 'STRINGS'; -INTEGERS : 'INTEGERS'; -FUNCTION : 'FUNCTION'; -EXECUTE : 'EXECUTE'; -SORT : 'SORT'; -ITERATE : 'ITERATE'; -REVERSE : 'REVERSE'; -ENTRY : 'ENTRY'; -READ : 'READ'; -MACRO : 'MACRO'; - -QUOTED - : '\'' IDENTIFIER; - -IDENTIFIER - : LETTER (LETTER|NUMERAL|'_')* ; - -fragment LETTER - : ('a'..'z'|'A'..'Z'|'.'|'$'); - -STRING - : '"' (~('"'))* '"'; - -INTEGER - : '#' ('+'|'-')? NUMERAL+ ; - -fragment NUMERAL - : ('0'..'9'); - -WS - : (' '|'\t'|'\n'|'\r')+ {_channel=99;} ; - -LINE_COMMENT - : '%' ~('\n'|'\r')* '\r'? '\n' {_channel=99;} - ; - diff --git a/src/main/antlr4/org/jabref/bst/Bst.g4 b/src/main/antlr4/org/jabref/bst/Bst.g4 new file mode 100644 index 00000000000..92b96ce18df --- /dev/null +++ b/src/main/antlr4/org/jabref/bst/Bst.g4 @@ -0,0 +1,85 @@ +grammar Bst; + +// Lexer + +STRINGS : 'STRINGS'; +INTEGERS : 'INTEGERS'; +FUNCTION : 'FUNCTION'; +EXECUTE : 'EXECUTE'; +SORT : 'SORT'; +ITERATE : 'ITERATE'; +REVERSE : 'REVERSE'; +ENTRY : 'ENTRY'; +READ : 'READ'; +MACRO : 'MACRO'; + +GT : '>'; +LT : '<'; +EQUAL : '='; +ASSIGN : ':='; +ADD : '+'; +SUB : '-'; +CONCAT : '*'; +LBRACE : '{'; +RBRACE : '}'; + +fragment LETTER : ('a'..'z'|'A'..'Z'|'.'|'$'); +fragment NUMERAL : ('0'..'9'); + +IDENTIFIER : LETTER (LETTER|NUMERAL|'_')*; +INTEGER : '#' ('+'|'-')? NUMERAL+; +QUOTED : '\'' IDENTIFIER; +STRING : '"' (~('"'))* '"'; + +WS: [ \r\n\t]+ -> skip; +LINE_COMMENT : '%' ~('\n'|'\r')* '\r'? '\n' -> skip; + +// Parser + +bstFile + : commands+ EOF + ; + +commands + : STRINGS ids=idListObl #stringsCommand + | INTEGERS ids=idListObl #integersCommand + | FUNCTION LBRACE id=identifier RBRACE function=stack #functionCommand + | MACRO LBRACE id=identifier RBRACE LBRACE repl=STRING RBRACE #macroCommand + | READ #readCommand + | EXECUTE LBRACE bstFunction RBRACE #executeCommand + | ITERATE LBRACE bstFunction RBRACE #iterateCommand + | REVERSE LBRACE bstFunction RBRACE #reverseCommand + | ENTRY idListOpt idListOpt idListOpt #entryCommand + | SORT #sortCommand + ; + +identifier + : IDENTIFIER + ; + +// Obligatory identifier list +idListObl + : LBRACE identifier+ RBRACE + ; + +// Optional identifier list +idListOpt + : LBRACE identifier* RBRACE + ; + +bstFunction + : LT | GT | EQUAL | ADD | SUB | ASSIGN | CONCAT + | identifier + ; + +stack + : LBRACE stackitem+ RBRACE + ; + +stackitem + : bstFunction + | STRING + | INTEGER + | QUOTED + | stack + ; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 751863df3e1..c05e10bda2b 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -85,7 +85,6 @@ requires org.mariadb.jdbc; uses org.mariadb.jdbc.credential.CredentialPlugin; requires org.apache.commons.lang3; - requires antlr.runtime; requires org.antlr.antlr4.runtime; requires org.fxmisc.flowless; requires org.apache.tika.core; diff --git a/src/main/java/org/jabref/logic/bst/BibtexWidth.java b/src/main/java/org/jabref/logic/bst/BibtexWidth.java deleted file mode 100644 index 61e0b099f20..00000000000 --- a/src/main/java/org/jabref/logic/bst/BibtexWidth.java +++ /dev/null @@ -1,241 +0,0 @@ -package org.jabref.logic.bst; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * - * The |built_in| function {\.{purify\$}} pops the top (string) literal, removes - * nonalphanumeric characters except for |white_space| and |sep_char| characters - * (these get converted to a |space|) and removes certain alphabetic characters - * contained in the control sequences associated with a special character, and - * pushes the resulting string. If the literal isn't a string, it complains and - * pushes the null string. - * - */ -public class BibtexWidth { - - private static final Logger LOGGER = LoggerFactory.getLogger(BibtexWidth.class); - - /* - * Quoted from Bibtex: - * - * Now we initialize the system-dependent |char_width| array, for which - * |space| is the only |white_space| character given a nonzero printing - * width. The widths here are taken from Stanford's June~'87 $cmr10$~font - * and represent hundredths of a point (rounded), but since they're used - * only for relative comparisons, the units have no meaning. - */ - - private static int[] widths; - - static { - if (BibtexWidth.widths == null) { - BibtexWidth.widths = new int[128]; - - for (int i = 0; i < 128; i++) { - BibtexWidth.widths[i] = 0; - } - BibtexWidth.widths[32] = 278; - BibtexWidth.widths[33] = 278; - BibtexWidth.widths[34] = 500; - BibtexWidth.widths[35] = 833; - BibtexWidth.widths[36] = 500; - BibtexWidth.widths[37] = 833; - BibtexWidth.widths[38] = 778; - BibtexWidth.widths[39] = 278; - BibtexWidth.widths[40] = 389; - BibtexWidth.widths[41] = 389; - BibtexWidth.widths[42] = 500; - BibtexWidth.widths[43] = 778; - BibtexWidth.widths[44] = 278; - BibtexWidth.widths[45] = 333; - BibtexWidth.widths[46] = 278; - BibtexWidth.widths[47] = 500; - BibtexWidth.widths[48] = 500; - BibtexWidth.widths[49] = 500; - BibtexWidth.widths[50] = 500; - BibtexWidth.widths[51] = 500; - BibtexWidth.widths[52] = 500; - BibtexWidth.widths[53] = 500; - BibtexWidth.widths[54] = 500; - BibtexWidth.widths[55] = 500; - BibtexWidth.widths[56] = 500; - BibtexWidth.widths[57] = 500; - BibtexWidth.widths[58] = 278; - BibtexWidth.widths[59] = 278; - BibtexWidth.widths[60] = 278; - BibtexWidth.widths[61] = 778; - BibtexWidth.widths[62] = 472; - BibtexWidth.widths[63] = 472; - BibtexWidth.widths[64] = 778; - BibtexWidth.widths[65] = 750; - BibtexWidth.widths[66] = 708; - BibtexWidth.widths[67] = 722; - BibtexWidth.widths[68] = 764; - BibtexWidth.widths[69] = 681; - BibtexWidth.widths[70] = 653; - BibtexWidth.widths[71] = 785; - BibtexWidth.widths[72] = 750; - BibtexWidth.widths[73] = 361; - BibtexWidth.widths[74] = 514; - BibtexWidth.widths[75] = 778; - BibtexWidth.widths[76] = 625; - BibtexWidth.widths[77] = 917; - BibtexWidth.widths[78] = 750; - BibtexWidth.widths[79] = 778; - BibtexWidth.widths[80] = 681; - BibtexWidth.widths[81] = 778; - BibtexWidth.widths[82] = 736; - BibtexWidth.widths[83] = 556; - BibtexWidth.widths[84] = 722; - BibtexWidth.widths[85] = 750; - BibtexWidth.widths[86] = 750; - BibtexWidth.widths[87] = 1028; - BibtexWidth.widths[88] = 750; - BibtexWidth.widths[89] = 750; - BibtexWidth.widths[90] = 611; - BibtexWidth.widths[91] = 278; - BibtexWidth.widths[92] = 500; - BibtexWidth.widths[93] = 278; - BibtexWidth.widths[94] = 500; - BibtexWidth.widths[95] = 278; - BibtexWidth.widths[96] = 278; - BibtexWidth.widths[97] = 500; - BibtexWidth.widths[98] = 556; - BibtexWidth.widths[99] = 444; - BibtexWidth.widths[100] = 556; - BibtexWidth.widths[101] = 444; - BibtexWidth.widths[102] = 306; - BibtexWidth.widths[103] = 500; - BibtexWidth.widths[104] = 556; - BibtexWidth.widths[105] = 278; - BibtexWidth.widths[106] = 306; - BibtexWidth.widths[107] = 528; - BibtexWidth.widths[108] = 278; - BibtexWidth.widths[109] = 833; - BibtexWidth.widths[110] = 556; - BibtexWidth.widths[111] = 500; - BibtexWidth.widths[112] = 556; - BibtexWidth.widths[113] = 528; - BibtexWidth.widths[114] = 392; - BibtexWidth.widths[115] = 394; - BibtexWidth.widths[116] = 389; - BibtexWidth.widths[117] = 556; - BibtexWidth.widths[118] = 528; - BibtexWidth.widths[119] = 722; - BibtexWidth.widths[120] = 528; - BibtexWidth.widths[121] = 528; - BibtexWidth.widths[122] = 444; - BibtexWidth.widths[123] = 500; - BibtexWidth.widths[124] = 1000; - BibtexWidth.widths[125] = 500; - BibtexWidth.widths[126] = 500; - } - } - - private BibtexWidth() { - } - - private static int getSpecialCharWidth(char[] c, int pos) { - if ((pos + 1) < c.length) { - if ((c[pos] == 'o') && (c[pos + 1] == 'e')) { - return 778; - } - if ((c[pos] == 'O') && (c[pos + 1] == 'E')) { - return 1014; - } - if ((c[pos] == 'a') && (c[pos + 1] == 'e')) { - return 722; - } - if ((c[pos] == 'A') && (c[pos + 1] == 'E')) { - return 903; - } - if ((c[pos] == 's') && (c[pos + 1] == 's')) { - return 500; - } - } - return BibtexWidth.getCharWidth(c[pos]); - } - - public static int getCharWidth(char c) { - if ((c >= 0) && (c < 128)) { - return BibtexWidth.widths[c]; - } else { - return 0; - } - } - - public static int width(String toMeasure) { - /* - * From Bibtex: We use the natural width for all but special characters, - * and we complain if the string isn't brace-balanced. - */ - - int i = 0; - int n = toMeasure.length(); - int braceLevel = 0; - char[] c = toMeasure.toCharArray(); - int result = 0; - - /* - * From Bibtex: - * - * We use the natural widths of all characters except that some - * characters have no width: braces, control sequences (except for the - * usual 13 accented and foreign characters, whose widths are given in - * the next module), and |white_space| following control sequences (even - * a null control sequence). - * - */ - while (i < n) { - if (c[i] == '{') { - braceLevel++; - if ((braceLevel == 1) && ((i + 1) < n) && (c[i + 1] == '\\')) { - i++; // skip brace - while ((i < n) && (braceLevel > 0)) { - i++; // skip backslash - - int afterBackslash = i; - while ((i < n) && Character.isLetter(c[i])) { - i++; - } - if ((i < n) && (i == afterBackslash)) { - i++; // Skip non-alpha control seq - } else { - if (BibtexCaseChanger.findSpecialChar(c, afterBackslash).isPresent()) { - result += BibtexWidth.getSpecialCharWidth(c, afterBackslash); - } - } - while ((i < n) && Character.isWhitespace(c[i])) { - i++; - } - while ((i < n) && (braceLevel > 0) && (c[i] != '\\')) { - if (c[i] == '}') { - braceLevel--; - } else if (c[i] == '{') { - braceLevel++; - } else { - result += BibtexWidth.getCharWidth(c[i]); - } - i++; - } - } - continue; - } - } else if (c[i] == '}') { - if (braceLevel > 0) { - braceLevel--; - } else { - LOGGER.warn("Too many closing braces in string: " + toMeasure); - } - } - result += BibtexWidth.getCharWidth(c[i]); - i++; - } - if (braceLevel > 0) { - LOGGER.warn("No enough closing braces in string: " + toMeasure); - } - return result; - } -} diff --git a/src/main/java/org/jabref/logic/bst/BstEntry.java b/src/main/java/org/jabref/logic/bst/BstEntry.java new file mode 100644 index 00000000000..89571de8046 --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/BstEntry.java @@ -0,0 +1,21 @@ +package org.jabref.logic.bst; + +import java.util.HashMap; +import java.util.Map; + +import org.jabref.model.entry.BibEntry; + +public class BstEntry { + + public final BibEntry entry; + + public final Map localStrings = new HashMap<>(); + + public final Map fields = new HashMap<>(); + + public final Map localIntegers = new HashMap<>(); + + public BstEntry(BibEntry e) { + this.entry = e; + } +} diff --git a/src/main/java/org/jabref/logic/bst/BstFunctions.java b/src/main/java/org/jabref/logic/bst/BstFunctions.java new file mode 100644 index 00000000000..6689f83b2a5 --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/BstFunctions.java @@ -0,0 +1,931 @@ +package org.jabref.logic.bst; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jabref.logic.bst.util.BstCaseChanger; +import org.jabref.logic.bst.util.BstNameFormatter; +import org.jabref.logic.bst.util.BstPurifier; +import org.jabref.logic.bst.util.BstTextPrefixer; +import org.jabref.logic.bst.util.BstWidthCalculator; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.Author; +import org.jabref.model.entry.AuthorList; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.ParseTree; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BstFunctions { + private static final Logger LOGGER = LoggerFactory.getLogger(BstFunctions.class); + private static final Pattern ADD_PERIOD_PATTERN = Pattern.compile("([^.?!}\\s])(}|\\s)*$"); + + private final Map strings; + private final Map integers; + private final Map functions; + private final String preamble; + + private final Stack stack; + private final StringBuilder bbl; + + private int bstWarning = 0; + + @FunctionalInterface + public interface BstFunction { + + void execute(BstVMVisitor visitor, ParserRuleContext ctx); + + default void execute(BstVMVisitor visitor, ParserRuleContext ctx, BstEntry bstEntryContext) { + this.execute(visitor, ctx); + } + } + + public BstFunctions(BstVMContext bstVMContext, + StringBuilder bbl) { + this.strings = bstVMContext.strings(); + this.integers = bstVMContext.integers(); + this.functions = bstVMContext.functions(); + this.preamble = Optional.ofNullable(bstVMContext.bibDatabase()).flatMap(BibDatabase::getPreamble).orElse(""); + this.stack = bstVMContext.stack(); + + this.bbl = bbl; + } + + protected Map getBuiltInFunctions() { + Map builtInFunctions = new HashMap<>(); + + builtInFunctions.put(">", this::bstIsGreaterThan); + builtInFunctions.put("<", this::bstIsLowerThan); + builtInFunctions.put("=", this::bstEquals); + builtInFunctions.put("+", this::bstAdd); + builtInFunctions.put("-", this::bstSubtract); + builtInFunctions.put("*", this::bstConcat); + builtInFunctions.put(":=", new BstAssignFunction()); + builtInFunctions.put("add.period$", this::bstAddPeriod); + builtInFunctions.put("call.type$", new BstCallTypeFunction()); + builtInFunctions.put("change.case$", this::bstChangeCase); + builtInFunctions.put("chr.to.int$", this::bstChrToInt); + builtInFunctions.put("cite$", new BstCiteFunction()); + builtInFunctions.put("duplicate$", this::bstDuplicate); + builtInFunctions.put("empty$", this::bstEmpty); + builtInFunctions.put("format.name$", this::bstFormatName); + builtInFunctions.put("if$", this::bstIf); + builtInFunctions.put("int.to.chr$", this::bstIntToChr); + builtInFunctions.put("int.to.str$", this::bstIntToStr); + builtInFunctions.put("missing$", this::bstMissing); + builtInFunctions.put("newline$", this::bstNewLine); + builtInFunctions.put("num.names$", this::bstNumNames); + builtInFunctions.put("pop$", this::bstPop); + builtInFunctions.put("preamble$", this::bstPreamble); + builtInFunctions.put("purify$", this::bstPurify); + builtInFunctions.put("quote$", this::bstQuote); + builtInFunctions.put("skip$", this::bstSkip); + builtInFunctions.put("stack$", this::bstStack); + builtInFunctions.put("substring$", this::bstSubstring); + builtInFunctions.put("swap$", this::bstSwap); + builtInFunctions.put("text.length$", this::bstTextLength); + builtInFunctions.put("text.prefix$", this::bstTextPrefix); + builtInFunctions.put("top$", this::bstTop); + builtInFunctions.put("type$", new BstTypeFunction()); + builtInFunctions.put("warning$", this::bstWarning); + builtInFunctions.put("while$", this::bstWhile); + builtInFunctions.put("width$", this::bstWidth); + builtInFunctions.put("write$", this::bstWrite); + + return builtInFunctions; + } + + /** + * Pops the top two (integer) literals, compares them, and pushes + * the integer 1 if the second is greater than the first, 0 + * otherwise. + */ + private void bstIsGreaterThan(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation > (line %d)".formatted(ctx.start.getLine())); + } + Object o2 = stack.pop(); + Object o1 = stack.pop(); + + if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { + throw new BstVMException("Can only compare two integers with >"); + } + + stack.push(((Integer) o1).compareTo((Integer) o2) > 0 ? BstVM.TRUE : BstVM.FALSE); + } + + /** + * Pops the top two (integer) literals, compares them, and pushes + * the integer 1 if the second is lower than the first, 0 + * otherwise. + */ + private void bstIsLowerThan(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation <"); + } + Object o2 = stack.pop(); + Object o1 = stack.pop(); + + if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { + throw new BstVMException("Can only compare two integers with < (line %d)".formatted(ctx.start.getLine())); + } + + stack.push(((Integer) o1).compareTo((Integer) o2) < 0 ? BstVM.TRUE : BstVM.FALSE); + } + + /** + * Pops the top two (both integer or both string) literals, compares + * them, and pushes the integer 1 if they're equal, 0 otherwise. + */ + private void bstEquals(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation = (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + Object o2 = stack.pop(); + + if ((o1 == null) ^ (o2 == null)) { + stack.push(BstVM.FALSE); + return; + } + + if ((o1 == null) && (o2 == null)) { + stack.push(BstVM.TRUE); + return; + } + + stack.push(o1.equals(o2) ? BstVM.TRUE : BstVM.FALSE); + } + + /** + * Pops the top two (integer) literals and pushes their sum. + */ + private void bstAdd(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation + (line %d)".formatted(ctx.start.getLine())); + } + Object o2 = stack.pop(); + Object o1 = stack.pop(); + + if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { + throw new BstVMException("Can only compare two integers with + (line %d)".formatted(ctx.start.getLine())); + } + + stack.push((Integer) o1 + (Integer) o2); + } + + /** + * Pops the top two (integer) literals and pushes their difference + * (the first subtracted from the second). + */ + private void bstSubtract(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation - (line %d)".formatted(ctx.start.getLine())); + } + Object o2 = stack.pop(); + Object o1 = stack.pop(); + + if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { + throw new BstVMException("Can only subtract two integers with - (line %d)".formatted(ctx.start.getLine())); + } + + stack.push((Integer) o1 - (Integer) o2); + } + + /** + * Pops the top two (string) literals, concatenates them (in reverse + * order, that is, the order in which pushed), and pushes the + * resulting string. + */ + private void bstConcat(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation * (line %d)".formatted(ctx.start.getLine())); + } + Object o2 = stack.pop(); + Object o1 = stack.pop(); + + if (o1 == null) { + o1 = ""; + } + if (o2 == null) { + o2 = ""; + } + + if (!((o1 instanceof String) && (o2 instanceof String))) { + LOGGER.error("o1: {} ({})", o1, o1.getClass()); + LOGGER.error("o2: {} ({})", o2, o2.getClass()); + throw new BstVMException("Can only concatenate two String with * (line %d)".formatted(ctx.start.getLine())); + } + + stack.push(o1.toString() + o2); + } + + /** + * Pops the top two literals and assigns to the first (which must be + * a global or entry variable) the value of the second. + */ + public class BstAssignFunction implements BstFunction { + + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx) { + this.execute(visitor, ctx, null); + } + + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx, BstEntry bstEntry) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation := (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + Object o2 = stack.pop(); + + if (!(o1 instanceof BstVMVisitor.Identifier identifier)) { + throw new BstVMException("Invalid parameters (line %d)".formatted(ctx.start.getLine())); + } + String name = identifier.name(); + + if (o2 instanceof String value) { + if ((bstEntry != null) && bstEntry.localStrings.containsKey(name)) { + bstEntry.localStrings.put(name, value); + return; + } + + if (strings.containsKey(name)) { + strings.put(name, value); + } + } else if (o2 instanceof Integer value) { + if ((bstEntry != null) && bstEntry.localIntegers.containsKey(name)) { + bstEntry.localIntegers.put(name, value); + return; + } + + if (integers.containsKey(name)) { + integers.put(name, value); + } + } else { + throw new BstVMException("Invalid parameters (line %d)".formatted(ctx.start.getLine())); + } + } + } + + /** + * Pops the top (string) literal, adds a `.' to it if the last non + * '}' character isn't a `.', `?', or `!', and pushes this resulting + * string. + */ + private void bstAddPeriod(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation add.period$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof String s)) { + throw new BstVMException("Can only add a period to a string for add.period$ (line %d)".formatted(ctx.start.getLine())); + } + + Matcher m = ADD_PERIOD_PATTERN.matcher(s); + + if (m.find()) { + StringBuilder sb = new StringBuilder(); + m.appendReplacement(sb, m.group(1)); + sb.append('.'); + String group2 = m.group(2); + if (group2 != null) { + sb.append(m.group(2)); + } + stack.push(sb.toString()); + } else { + stack.push(s); + } + } + + /** + * Executes the function whose name is the entry type of an entry. + * For example if an entry is of type book, this function executes + * the book function. When given as an argument to the ITERATE + * command, call.type$ actually produces the output for the entries. + * For an entry with an unknown type, it executes the function + * default.type. Thus you should define (before the READ command) + * one function for each standard entry type as well as a + * default.type function. + */ + public class BstCallTypeFunction implements BstFunction { + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx) { + throw new BstVMException("Call.type$ can only be called from within a context (ITERATE or REVERSE). (line %d)".formatted(ctx.start.getLine())); + } + + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx, BstEntry bstEntry) { + if (bstEntry == null) { + this.execute(visitor, ctx); // Throw error + } else { + functions.get(bstEntry.entry.getType().getName()).execute(visitor, ctx, bstEntry); + } + } + } + + /** + * Pops the top two (string) literals; it changes the case of the second + * according to the specifications of the first, as follows. (Note: The word + * `letters' in the next sentence refers only to those at brace-level 0, the + * top-most brace level; no other characters are changed, except perhaps for + * \special characters", described in Section 4.) If the first literal is the + * string `t', it converts to lower case all letters except the very first + * character in the string, which it leaves alone, and except the first + * character following any colon and then nonnull white space, which it also + * leaves alone; if it's the string `l', it converts all letters to lower case; + * and if it's the string `u', it converts all letters to upper case. It then + * pushes this resulting string. If either type is incorrect, it complains and + * pushes the null string; however, if both types are correct but the + * specification string (i.e., the first string) isn't one of the legal ones, it + * merely pushes the second back onto the stack, after complaining. (Another + * note: It ignores case differences in the specification string; for example, + * the strings t and T are equivalent for the purposes of this built-in + * function.) + */ + private void bstChangeCase(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation change.case$ (line %d)".formatted(ctx.start.getLine())); + } + + Object o1 = stack.pop(); + if (!((o1 instanceof String format) && (format.length() == 1))) { + throw new BstVMException("A format string of length 1 is needed for change.case$ (line %d)".formatted(ctx.start.getLine())); + } + + Object o2 = stack.pop(); + if (!(o2 instanceof String toChange)) { + throw new BstVMException("A string is needed as second parameter for change.case$ (line %d)".formatted(ctx.start.getLine())); + } + + stack.push(BstCaseChanger.changeCase(toChange, BstCaseChanger.FormatMode.of(format))); + } + + /** + * Pops the top (string) literal, makes sure it's a single + * character, converts it to the corresponding ASCII integer, and + * pushes this integer. + */ + private void bstChrToInt(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation chr.to.int$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!((o1 instanceof String s) && (((String) o1).length() == 1))) { + throw new BstVMException("Can only perform chr.to.int$ on string with length 1 (line %d)".formatted(ctx.start.getLine())); + } + + stack.push((int) s.charAt(0)); + } + + /** + * Pushes the string that was the \cite-command argument for this + * entry. + */ + public class BstCiteFunction implements BstFunction { + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx) { + throw new BstVMException("Must have an entry to cite$ (line %d)".formatted(ctx.start.getLine())); + } + + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx, BstEntry bstEntryContext) { + if (bstEntryContext == null) { + execute(visitor, ctx); + return; + } + + stack.push(bstEntryContext.entry.getCitationKey().orElse(null)); + } + } + + /** + * Pops the top literal from the stack and pushes two copies of it. + */ + private void bstDuplicate(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation duplicate$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + stack.push(o1); + stack.push(o1); + } + + /** + * Pops the top literal and pushes the integer 1 if it's a missing + * field or a string having no non-white-space characters, 0 + * otherwise. + */ + private void bstEmpty(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation empty$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (o1 == null) { + stack.push(BstVM.TRUE); + return; + } + + if (!(o1 instanceof String s)) { + throw new BstVMException("Operand does not match function empty$ (line %d)".formatted(ctx.start.getLine())); + } + + stack.push("".equals(s.trim()) ? BstVM.TRUE : BstVM.FALSE); + } + + /** + * The |built_in| function {\.{format.name\$}} pops the + * top three literals (they are a string, an integer, and a string + * literal, in that order). The last string literal represents a + * name list (each name corresponding to a person), the integer + * literal specifies which name to pick from this list, and the + * first string literal specifies how to format this name, as + * described in the \BibTeX\ documentation. Finally, this function + * pushes the formatted name. If any of the types is incorrect, it + * complains and pushes the null string. + */ + private void bstFormatName(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 3) { + throw new BstVMException("Not enough operands on stack for operation format.name$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + Object o2 = stack.pop(); + Object o3 = stack.pop(); + + if (!(o1 instanceof String) && !(o2 instanceof Integer) && !(o3 instanceof String)) { + // warning("A string is needed for change.case$"); + stack.push(""); + return; + } + + String format = (String) o1; + Integer name = (Integer) o2; + String names = (String) o3; + + if (names == null) { + stack.push(""); + } else { + AuthorList a = AuthorList.parse(names); + if (name > a.getNumberOfAuthors()) { + throw new BstVMException("Author Out of Bounds. Number %d invalid for %s (line %d)".formatted(name, names, ctx.start.getLine())); + } + Author author = a.getAuthor(name - 1); + + stack.push(BstNameFormatter.formatName(author, format)); + } + } + + /** + * Pops the top three literals (they are two function literals and + * an integer literal, in that order); if the integer is greater + * than 0, it executes the second literal, else it executes the + * first. + */ + private void bstIf(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 3) { + throw new BstVMException("Not enough operands on stack for if$ (line %d)".formatted(ctx.start.getLine())); + } + + Object f1 = stack.pop(); + Object f2 = stack.pop(); + Object i = stack.pop(); + + if (!((f1 instanceof BstVMVisitor.Identifier) || (f1 instanceof ParseTree)) + && ((f2 instanceof BstVMVisitor.Identifier) || (f2 instanceof ParseTree)) + && (i instanceof Integer)) { + throw new BstVMException("Expecting two functions and an integer for if$ (line %d)".formatted(ctx.start.getLine())); + } + + if (((Integer) i) > 0) { + callIdentifierOrTree(f2, visitor, ctx); + } else { + callIdentifierOrTree(f1, visitor, ctx); + } + } + + private void callIdentifierOrTree(Object f, BstVMVisitor visitor, ParserRuleContext ctx) { + if (f instanceof ParseTree tree) { + visitor.visit(tree); + } else if (f instanceof BstVMVisitor.Identifier identifier) { + visitor.resolveIdentifier(identifier.name(), ctx); + } else { + stack.push(f); + } + } + + /** + * Pops the top (integer) literal, interpreted as the ASCII integer + * value of a single character, converts it to the corresponding + * single-character string, and pushes this string. + */ + private void bstIntToChr(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation int.to.chr$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof Integer i)) { + throw new BstVMException("Can only perform operation int.to.chr$ on an Integer (line %d)".formatted(ctx.start.getLine())); + } + + stack.push(String.valueOf((char) i.intValue())); + } + + /** + * Pops the top (integer) literal, converts it to its (unique) + * string equivalent, and pushes this string. + */ + private void bstIntToStr(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation int.to.str$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof Integer)) { + throw new BstVMException("Can only transform an integer to an string using int.to.str$ (line %d)".formatted(ctx.start.getLine())); + } + + stack.push(o1.toString()); + } + + /** + * Pops the top literal and pushes the integer 1 if it's a missing + * field, 0 otherwise. + */ + private void bstMissing(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation missing$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (o1 == null) { + stack.push(BstVM.TRUE); + return; + } + + if (!(o1 instanceof String)) { + LOGGER.warn("Not a string or missing field in operation missing$ (line %d)".formatted(ctx.start.getLine())); + stack.push(BstVM.TRUE); + return; + } + + stack.push(BstVM.FALSE); + } + + /** + * Writes onto the bbl file what is accumulated in the output buffer. + * It writes a blank line if and only if the output buffer is empty. + * Since write$ does reasonable line breaking, you should use this + * function only when you want a blank line or an explicit line + * break. + */ + private void bstNewLine(BstVMVisitor visitor, ParserRuleContext ctx) { + this.bbl.append('\n'); + } + + /** + * Pops the top (string) literal and pushes the number of names the + * string represents one plus the number of occurrences of the + * substring "and" (ignoring case differences) surrounded by + * non-null white-space at the top brace level. + */ + private void bstNumNames(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation num.names$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof String s)) { + throw new BstVMException("Need a string at the top of the stack for num.names$ (line %d)".formatted(ctx.start.getLine())); + } + + stack.push(AuthorList.parse(s).getNumberOfAuthors()); + } + + /** + * Pops the top of the stack but doesn't print it; this gets rid of + * an unwanted stack literal. + */ + private void bstPop(BstVMVisitor visitor, ParserRuleContext ctx) { + stack.pop(); + } + + /** + * The |built_in| function {\.{preamble\$}} pushes onto the stack + * the concatenation of all the \.{preamble} strings read from the + * database files. (or the empty string if there were none) + * '@PREAMBLE' strings are read from the database files. + */ + private void bstPreamble(BstVMVisitor visitor, ParserRuleContext ctx) { + stack.push(preamble); + } + + /** + * Pops the top (string) literal, removes nonalphanumeric characters + * except for white-space characters and hyphens and ties (these all get + * converted to a space), removes certain alphabetic characters + * contained in the control sequences associated with a \special + * character", and pushes the resulting string. + */ + private void bstPurify(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation purify$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof String)) { + LOGGER.warn("A string is needed for purify$"); + stack.push(""); + return; + } + + stack.push(BstPurifier.purify((String) o1)); + } + + /** + * Pushes the string consisting of the double-quote character. + */ + private void bstQuote(BstVMVisitor visitor, ParserRuleContext ctx) { + stack.push("\""); + } + + /** + * Does nothing. + */ + private void bstSkip(BstVMVisitor visitor, ParserRuleContext ctx) { + // no-op + } + + /** + * Pops and prints the whole stack; it's meant to be used for style + * designers while debugging. + */ + private void bstStack(BstVMVisitor visitor, ParserRuleContext ctx) { + while (!stack.empty()) { + LOGGER.debug("Stack entry {}", stack.pop()); + } + } + + /** + * Pops the top three literals (they are the two integers literals + * len and start, and a string literal, in that order). It pushes + * the substring of the (at most) len consecutive characters + * starting at the startth character (assuming 1-based indexing) if + * start is positive, and ending at the start-th character + * (including) from the end if start is negative (where the first + * character from the end is the last character). + */ + private void bstSubstring(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 3) { + throw new BstVMException("Not enough operands on stack for operation substring$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + Object o2 = stack.pop(); + Object o3 = stack.pop(); + + if (!((o1 instanceof Integer len) && (o2 instanceof Integer start) && (o3 instanceof String s))) { + throw new BstVMException("Expecting two integers and a string for substring$ (line %d)".formatted(ctx.start.getLine())); + } + + int lenI = len; + int startI = start; + + if (lenI > (Integer.MAX_VALUE / 2)) { + lenI = Integer.MAX_VALUE / 2; + } + + if (startI > (Integer.MAX_VALUE / 2)) { + startI = Integer.MAX_VALUE / 2; + } + + if (startI < (Integer.MIN_VALUE / 2)) { + startI = -Integer.MIN_VALUE / 2; + } + + if (startI < 0) { + startI += s.length() + 1; + startI = Math.max(1, (startI + 1) - lenI); + } + stack.push(s.substring(startI - 1, Math.min((startI - 1) + lenI, s.length()))); + } + + /** + * Swaps the top two literals on the stack. text.length$ Pops the + * top (string) literal, and pushes the number of text characters + * it contains, where an accented character (more precisely, a + * \special character", defined in Section 4) counts as a single + * text character, even if it's missing its matching right brace, + * and where braces don't count as text characters. + */ + private void bstSwap(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation swap$ (line %d)".formatted(ctx.start.getLine())); + } + Object f1 = stack.pop(); + Object f2 = stack.pop(); + + stack.push(f1); + stack.push(f2); + } + + /** + * text.length$ Pops the top (string) literal, and pushes the number + * of text characters it contains, where an accented character (more + * precisely, a "special character", defined in Section 4) counts as + * a single text character, even if it's missing its matching right + * brace, and where braces don't count as text characters. + * + * From BibTeXing: For the purposes of counting letters in labels, + * BibTEX considers everything contained inside the braces as a + * single letter. + */ + private void bstTextLength(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation text.length$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof String s)) { + throw new BstVMException("Can only perform operation on a string text.length$ (line %d)".formatted(ctx.start.getLine())); + } + + char[] c = s.toCharArray(); + int result = 0; + int i = 0; + int n = s.length(); + int braceLevel = 0; + + while (i < n) { + i++; + if (c[i - 1] == '{') { + braceLevel++; + if ((braceLevel == 1) && (i < n)) { + if (c[i] == '\\') { + i++; // skip over backslash + while ((i < n) && (braceLevel > 0)) { + if (c[i] == '}') { + braceLevel--; + } else if (c[i] == '{') { + braceLevel++; + } + i++; + } + result++; + } + } + } else if (c[i - 1] == '}') { + if (braceLevel > 0) { + braceLevel--; + } + } else { + result++; + } + } + stack.push(result); + } + + /** + * Pops the top two literals (the integer literal len and a string + * literal, in that order). It pushes the substring of the (at most) len + * consecutive text characters starting from the beginning of the + * string. This function is similar to substring$, but this one + * considers a \special character", even if it's missing its matching + * right brace, to be a single text character (rather than however many + * ASCII characters it actually comprises), and this function doesn't + * consider braces to be text characters; furthermore, this function + * appends any needed matching right braces. + */ + private void bstTextPrefix(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation text.prefix$ (line %d)".formatted(ctx.start.getLine())); + } + + Object o1 = stack.pop(); + if (!(o1 instanceof Integer)) { + LOGGER.warn("An integer is needed as first parameter to text.prefix$ (line {})", ctx.start.getLine()); + stack.push(""); + return; + } + + Object o2 = stack.pop(); + if (!(o2 instanceof String)) { + LOGGER.warn("A string is needed as second parameter to text.prefix$ (line {})", ctx.start.getLine()); + stack.push(""); + return; + } + + stack.push(BstTextPrefixer.textPrefix((Integer) o1, (String) o2)); + } + + /** + * Pops and prints the top of the stack to the log file. It's useful for debugging. + */ + private void bstTop(BstVMVisitor visitor, ParserRuleContext ctx) { + LOGGER.debug("Stack entry {} (line {})", stack.pop(), ctx.start.getLine()); + } + + /** + * Pushes the current entry's type (book, article, etc.), but pushes + * the null string if the type is either unknown or undefined. + */ + public class BstTypeFunction implements BstFunction { + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx) { + throw new BstVMException("type$ need a context (line %d)".formatted(ctx.start.getLine())); + } + + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx, BstEntry bstEntryContext) { + if (bstEntryContext == null) { + this.execute(visitor, ctx); + return; + } + + stack.push(bstEntryContext.entry.getType().getName()); + } + } + + /** + * Pops the top (string) literal and prints it following a warning + * message. This also increments a count of the number of warning + * messages issued. + */ + private void bstWarning(BstVMVisitor visitor, ParserRuleContext ctx) { + LOGGER.warn("Warning (#{}): {}", bstWarning++, stack.pop()); + } + + /** + * Pops the top two (function) literals, and keeps executing the + * second as long as the (integer) literal left on the stack by + * executing the first is greater than 0. + */ + private void bstWhile(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation while$ (line %d)".formatted(ctx.start.getLine())); + } + Object f2 = stack.pop(); + Object f1 = stack.pop(); + + if (!((f1 instanceof BstVMVisitor.Identifier) || (f1 instanceof ParseTree)) + && ((f2 instanceof BstVMVisitor.Identifier) || (f2 instanceof ParseTree))) { + throw new BstVMException("Expecting two functions for while$ (line %d)".formatted(ctx.start.getLine())); + } + + do { + visitor.visit((ParseTree) f1); + + Object i = stack.pop(); + if (!(i instanceof Integer)) { + throw new BstVMException("First parameter to while has to return an integer but was %s (line %d)" + .formatted(i.toString(), ctx.start.getLine())); + } + if ((Integer) i <= 0) { + break; + } + visitor.visit((ParseTree) f2); + } while (true); + } + + /** + * The |built_in| function {\.{width\$}} pops the top (string) literal and + * pushes the integer that represents its width in units specified by the + * |char_width| array. This function takes the literal literally; that is, it + * assumes each character in the string is to be printed as is, regardless of + * whether the character has a special meaning to \TeX, except that special + * characters (even without their |right_brace|s) are handled specially. If the + * literal isn't a string, it complains and pushes~0. + */ + private void bstWidth(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation width$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof String)) { + LOGGER.warn("A string is needed for width$"); + stack.push(0); + return; + } + + stack.push(BstWidthCalculator.width((String) o1)); + } + + /** + * Pops the top (string) literal and writes it on the output buffer + * (which will result in stuff being written onto the bbl file when + * the buffer fills up). + */ + private void bstWrite(BstVMVisitor visitor, ParserRuleContext ctx) { + String s = (String) stack.pop(); + bbl.append(s); + } +} diff --git a/src/main/java/org/jabref/logic/bst/BstPreviewLayout.java b/src/main/java/org/jabref/logic/bst/BstPreviewLayout.java index d0c040842fd..a002d388e60 100644 --- a/src/main/java/org/jabref/logic/bst/BstPreviewLayout.java +++ b/src/main/java/org/jabref/logic/bst/BstPreviewLayout.java @@ -23,7 +23,7 @@ public class BstPreviewLayout implements PreviewLayout { private final String name; - private VM vm; + private BstVM bstVM; private String error; public BstPreviewLayout(Path path) { @@ -34,7 +34,7 @@ public BstPreviewLayout(Path path) { return; } try { - vm = new VM(path.toFile()); + bstVM = new BstVM(path); } catch (Exception e) { LOGGER.error("Could not read {}.", path.toAbsolutePath(), e); error = Localization.lang("Error opening file '%0'.", path.toString()); @@ -49,7 +49,7 @@ public String generatePreview(BibEntry originalEntry, BibDatabaseContext databas // ensure that the entry is of BibTeX format (and do not modify the original entry) BibEntry entry = (BibEntry) originalEntry.clone(); new ConvertToBibtexCleanup().cleanup(entry); - String result = vm.run(List.of(entry)); + String result = bstVM.render(List.of(entry)); // Remove all comments result = result.replaceAll("%.*", ""); // Remove all LaTeX comments diff --git a/src/main/java/org/jabref/logic/bst/BstVM.java b/src/main/java/org/jabref/logic/bst/BstVM.java new file mode 100644 index 00000000000..a2428d683d9 --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/BstVM.java @@ -0,0 +1,114 @@ +package org.jabref.logic.bst; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Stack; + +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; + +import org.antlr.v4.runtime.BailErrorStrategy; +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.antlr.v4.runtime.tree.ParseTree; + +public class BstVM { + + protected static final Integer FALSE = 0; + protected static final Integer TRUE = 1; + + protected final ParseTree tree; + protected BstVMContext latestContext; // for testing + + private Path path = null; + + public BstVM(Path path) throws RecognitionException, IOException { + this(CharStreams.fromPath(path)); + this.path = path; + } + + public BstVM(String s) throws RecognitionException { + this(CharStreams.fromString(s)); + } + + protected BstVM(CharStream bst) throws RecognitionException { + this(charStream2CommonTree(bst)); + } + + private BstVM(ParseTree tree) { + this.tree = tree; + } + + private static ParseTree charStream2CommonTree(CharStream query) { + BstLexer lexer = new BstLexer(query); + lexer.removeErrorListeners(); + lexer.addErrorListener(ThrowingErrorListener.INSTANCE); + BstParser parser = new BstParser(new CommonTokenStream(lexer)); + parser.removeErrorListeners(); + parser.addErrorListener(ThrowingErrorListener.INSTANCE); + parser.setErrorHandler(new BailErrorStrategy()); + return parser.bstFile(); + } + + /** + * Transforms the given list of BibEntries to a rendered list of references using the parsed bst program + * + * @param bibEntries list of entries to convert + * @param bibDatabase (may be null) the bibDatabase used for resolving strings / crossref + * @return list of references in plain text form + */ + public String render(Collection bibEntries, BibDatabase bibDatabase) { + Objects.requireNonNull(bibEntries); + + List entries = new ArrayList<>(bibEntries.size()); + for (BibEntry entry : bibEntries) { + entries.add(new BstEntry(entry)); + } + + StringBuilder resultBuffer = new StringBuilder(); + + BstVMContext bstVMContext = new BstVMContext(entries, bibDatabase, path); + bstVMContext.functions().putAll(new BstFunctions(bstVMContext, resultBuffer).getBuiltInFunctions()); + bstVMContext.integers().put("entry.max$", Integer.MAX_VALUE); + bstVMContext.integers().put("global.max$", Integer.MAX_VALUE); + + BstVMVisitor bstVMVisitor = new BstVMVisitor(bstVMContext, resultBuffer); + bstVMVisitor.visit(tree); + + latestContext = bstVMContext; + + return resultBuffer.toString(); + } + + public String render(Collection bibEntries) { + return render(bibEntries, null); + } + + protected Stack getStack() { + if (latestContext != null) { + return latestContext.stack(); + } else { + throw new BstVMException("BstVM must have rendered at least once to provide the latest stack"); + } + } + + private static class ThrowingErrorListener extends BaseErrorListener { + public static final ThrowingErrorListener INSTANCE = new ThrowingErrorListener(); + + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, + int line, int charPositionInLine, String msg, RecognitionException e) + throws ParseCancellationException { + throw new ParseCancellationException("line " + line + ":" + charPositionInLine + " " + msg); + } + } +} diff --git a/src/main/java/org/jabref/logic/bst/BstVMContext.java b/src/main/java/org/jabref/logic/bst/BstVMContext.java new file mode 100644 index 00000000000..8a98f605dd8 --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/BstVMContext.java @@ -0,0 +1,22 @@ +package org.jabref.logic.bst; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Stack; + +import org.jabref.model.database.BibDatabase; + +public record BstVMContext(List entries, + Map strings, + Map integers, + Map functions, + Stack stack, + BibDatabase bibDatabase, + Optional path) { + public BstVMContext(List entries, BibDatabase bibDatabase, Path path) { + this(entries, new HashMap<>(), new HashMap<>(), new HashMap<>(), new Stack<>(), bibDatabase, Optional.ofNullable(path)); + } +} diff --git a/src/main/java/org/jabref/logic/bst/BstVMException.java b/src/main/java/org/jabref/logic/bst/BstVMException.java new file mode 100644 index 00000000000..2851166d094 --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/BstVMException.java @@ -0,0 +1,7 @@ +package org.jabref.logic.bst; + +public class BstVMException extends RuntimeException { + public BstVMException(String string) { + super(string); + } +} diff --git a/src/main/java/org/jabref/logic/bst/BstVMVisitor.java b/src/main/java/org/jabref/logic/bst/BstVMVisitor.java new file mode 100644 index 00000000000..a815480e506 --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/BstVMVisitor.java @@ -0,0 +1,263 @@ +package org.jabref.logic.bst; + +import java.util.Comparator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import org.jabref.logic.bibtex.FieldContentFormatterPreferences; +import org.jabref.logic.bibtex.FieldWriter; +import org.jabref.logic.bibtex.FieldWriterPreferences; +import org.jabref.logic.bibtex.InvalidFieldValueException; +import org.jabref.model.entry.Month; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.StandardField; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class BstVMVisitor extends BstBaseVisitor { + private static final Logger LOGGER = LoggerFactory.getLogger(BstVMVisitor.class); + + private final BstVMContext bstVMContext; + private final StringBuilder bbl; + + private BstEntry selectedBstEntry = null; + + public record Identifier(String name) { + } + + public BstVMVisitor(BstVMContext bstVMContext, StringBuilder bbl) { + this.bstVMContext = bstVMContext; + this.bbl = bbl; + } + + @Override + public Integer visitStringsCommand(BstParser.StringsCommandContext ctx) { + if (ctx.ids.identifier().size() > 20) { + throw new BstVMException("Strings limit reached"); + } + + for (BstParser.IdentifierContext identifierContext : ctx.ids.identifier()) { + bstVMContext.strings().put(identifierContext.getText(), null); + } + return BstVM.TRUE; + } + + @Override + public Integer visitIntegersCommand(BstParser.IntegersCommandContext ctx) { + for (BstParser.IdentifierContext identifierContext : ctx.ids.identifier()) { + bstVMContext.integers().put(identifierContext.getText(), 0); + } + return BstVM.TRUE; + } + + @Override + public Integer visitFunctionCommand(BstParser.FunctionCommandContext ctx) { + bstVMContext.functions().put(ctx.id.getText(), + (visitor, functionContext) -> visitor.visit(ctx.function)); + return BstVM.TRUE; + } + + @Override + public Integer visitMacroCommand(BstParser.MacroCommandContext ctx) { + String replacement = ctx.repl.getText().substring(1, ctx.repl.getText().length() - 1); + bstVMContext.functions().put(ctx.id.getText(), + (visitor, functionContext) -> bstVMContext.stack().push(replacement)); + return BstVM.TRUE; + } + + @Override + public Integer visitReadCommand(BstParser.ReadCommandContext ctx) { + FieldWriter fieldWriter = new FieldWriter(new FieldWriterPreferences(true, List.of(StandardField.MONTH), new FieldContentFormatterPreferences())); + for (BstEntry e : bstVMContext.entries()) { + for (Map.Entry mEntry : e.fields.entrySet()) { + Field field = FieldFactory.parseField(mEntry.getKey()); + String fieldValue = e.entry.getResolvedFieldOrAlias(field, bstVMContext.bibDatabase()) + .map(content -> { + try { + String result = fieldWriter.write(field, content); + if (result.startsWith("{")) { + // Strip enclosing {} from the output + return result.substring(1, result.length() - 1); + } + if (field == StandardField.MONTH) { + // We don't have the internal BibTeX strings at hand. + // Thus, we look up the full month name in the generic table. + return Month.parse(result) + .map(Month::getFullName) + .orElse(result); + } + return result; + } catch ( + InvalidFieldValueException invalidFieldValueException) { + // in case there is something wrong with the content, just return the content itself + return content; + } + }) + .orElse(null); + mEntry.setValue(fieldValue); + } + } + + for (BstEntry e : bstVMContext.entries()) { + if (!e.fields.containsKey(StandardField.CROSSREF.getName())) { + e.fields.put(StandardField.CROSSREF.getName(), null); + } + } + + return BstVM.TRUE; + } + + @Override + public Integer visitExecuteCommand(BstParser.ExecuteCommandContext ctx) { + this.selectedBstEntry = null; + visit(ctx.bstFunction()); + + return BstVM.TRUE; + } + + @Override + public Integer visitIterateCommand(BstParser.IterateCommandContext ctx) { + for (BstEntry entry : bstVMContext.entries()) { + this.selectedBstEntry = entry; + visit(ctx.bstFunction()); + } + + return BstVM.TRUE; + } + + @Override + public Integer visitReverseCommand(BstParser.ReverseCommandContext ctx) { + ListIterator i = bstVMContext.entries().listIterator(bstVMContext.entries().size()); + while (i.hasPrevious()) { + this.selectedBstEntry = i.previous(); + visit(ctx.bstFunction()); + } + + return BstVM.TRUE; + } + + @Override + public Integer visitEntryCommand(BstParser.EntryCommandContext ctx) { + // ENTRY command contains 3 optionally filled identifier lists: + // Fields, Integers and Strings + + BstParser.IdListOptContext entryFields = ctx.idListOpt(0); + for (BstParser.IdentifierContext identifierContext : entryFields.identifier()) { + for (BstEntry entry : bstVMContext.entries()) { + entry.fields.put(identifierContext.getText(), null); + } + } + + BstParser.IdListOptContext entryIntegers = ctx.idListOpt(1); + for (BstParser.IdentifierContext identifierContext : entryIntegers.identifier()) { + for (BstEntry entry : bstVMContext.entries()) { + entry.localIntegers.put(identifierContext.getText(), 0); + } + } + + BstParser.IdListOptContext entryStrings = ctx.idListOpt(2); + for (BstParser.IdentifierContext identifierContext : entryStrings.identifier()) { + for (BstEntry entry : bstVMContext.entries()) { + entry.localStrings.put(identifierContext.getText(), null); + } + } + + for (BstEntry entry : bstVMContext.entries()) { + entry.localStrings.put("sort.key$", null); + } + + return BstVM.TRUE; + } + + @Override + public Integer visitSortCommand(BstParser.SortCommandContext ctx) { + bstVMContext.entries().sort(Comparator.comparing(o -> (o.localStrings.get("sort.key$")))); + return BstVM.TRUE; + } + + @Override + public Integer visitIdentifier(BstParser.IdentifierContext ctx) { + resolveIdentifier(ctx.IDENTIFIER().getText(), ctx); + return BstVM.TRUE; + } + + protected void resolveIdentifier(String name, ParserRuleContext ctx) { + if (selectedBstEntry != null) { + if (selectedBstEntry.fields.containsKey(name)) { + bstVMContext.stack().push(selectedBstEntry.fields.get(name)); + return; + } + if (selectedBstEntry.localStrings.containsKey(name)) { + bstVMContext.stack().push(selectedBstEntry.localStrings.get(name)); + return; + } + if (selectedBstEntry.localIntegers.containsKey(name)) { + bstVMContext.stack().push(selectedBstEntry.localIntegers.get(name)); + return; + } + } + + if (bstVMContext.strings().containsKey(name)) { + bstVMContext.stack().push(bstVMContext.strings().get(name)); + return; + } + if (bstVMContext.integers().containsKey(name)) { + bstVMContext.stack().push(bstVMContext.integers().get(name)); + return; + } + if (bstVMContext.functions().containsKey(name)) { + bstVMContext.functions().get(name).execute(this, ctx); + return; + } + + throw new BstVMException("No matching identifier found: " + name); + } + + @Override + public Integer visitBstFunction(BstParser.BstFunctionContext ctx) { + String name = ctx.getChild(0).getText(); + if (bstVMContext.functions().containsKey(name)) { + bstVMContext.functions().get(name).execute(this, ctx, selectedBstEntry); + } else { + visit(ctx.getChild(0)); + } + + return BstVM.TRUE; + } + + @Override + public Integer visitStackitem(BstParser.StackitemContext ctx) { + for (ParseTree childNode : ctx.children) { + try { + if (childNode instanceof TerminalNode token) { + switch (token.getSymbol().getType()) { + case BstParser.STRING -> { + String s = token.getText(); + bstVMContext.stack().push(s.substring(1, s.length() - 1)); + } + case BstParser.INTEGER -> + bstVMContext.stack().push(Integer.parseInt(token.getText().substring(1))); + case BstParser.QUOTED -> + bstVMContext.stack().push(new Identifier(token.getText().substring(1))); + } + } else if (childNode instanceof BstParser.StackContext) { + bstVMContext.stack().push(childNode); + } else { + this.visit(childNode); + } + } catch (BstVMException e) { + bstVMContext.path().ifPresentOrElse( + (path) -> LOGGER.error("{} ({})", e.getMessage(), path), + () -> LOGGER.error(e.getMessage())); + throw e; + } + } + return BstVM.TRUE; + } +} diff --git a/src/main/java/org/jabref/logic/bst/ChangeCaseFunction.java b/src/main/java/org/jabref/logic/bst/ChangeCaseFunction.java deleted file mode 100644 index 65f4f64c619..00000000000 --- a/src/main/java/org/jabref/logic/bst/ChangeCaseFunction.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.jabref.logic.bst; - -import java.util.Locale; -import java.util.Stack; - -import org.jabref.logic.bst.BibtexCaseChanger.FORMAT_MODE; -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.BstFunction; - -/** - * From the Bibtex manual: - * - * Pops the top two (string) literals; it changes the case of the second - * according to the specifications of the first, as follows. (Note: The word - * `letters' in the next sentence refers only to those at brace-level 0, the - * top-most brace level; no other characters are changed, except perhaps for - * \special characters", described in Section 4.) If the first literal is the - * string `t', it converts to lower case all letters except the very first - * character in the string, which it leaves alone, and except the first - * character following any colon and then nonnull white space, which it also - * leaves alone; if it's the string `l', it converts all letters to lower case; - * and if it's the string `u', it converts all letters to upper case. It then - * pushes this resulting string. If either type is incorrect, it complains and - * pushes the null string; however, if both types are correct but the - * specification string (i.e., the first string) isn't one of the legal ones, it - * merely pushes the second back onto the stack, after complaining. (Another - * note: It ignores case differences in the specification string; for example, - * the strings t and T are equivalent for the purposes of this built-in - * function.) - * - * Christopher: I think this should be another grammar! This parser is horrible. - * - */ -public class ChangeCaseFunction implements BstFunction { - - private final VM vm; - - public ChangeCaseFunction(VM vm) { - this.vm = vm; - } - - @Override - public void execute(BstEntry context) { - Stack stack = vm.getStack(); - - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation change.case$"); - } - - Object o1 = stack.pop(); - if (!((o1 instanceof String) && (((String) o1).length() == 1))) { - throw new VMException("A format string of length 1 is needed for change.case$"); - } - - Object o2 = stack.pop(); - if (!(o2 instanceof String)) { - throw new VMException("A string is needed as second parameter for change.case$"); - } - - char format = ((String) o1).toLowerCase(Locale.ROOT).charAt(0); - String s = (String) o2; - - stack.push(BibtexCaseChanger.changeCase(s, FORMAT_MODE.getFormatModeForBSTFormat(format))); - } -} diff --git a/src/main/java/org/jabref/logic/bst/FormatNameFunction.java b/src/main/java/org/jabref/logic/bst/FormatNameFunction.java deleted file mode 100644 index b2845c89be7..00000000000 --- a/src/main/java/org/jabref/logic/bst/FormatNameFunction.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.jabref.logic.bst; - -import java.util.Stack; - -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.BstFunction; -import org.jabref.model.entry.Author; -import org.jabref.model.entry.AuthorList; - -/** - * From Bibtex: - * - * "The |built_in| function {\.{format.name\$}} pops the - * top three literals (they are a string, an integer, and a string - * literal, in that order). The last string literal represents a - * name list (each name corresponding to a person), the integer - * literal specifies which name to pick from this list, and the - * first string literal specifies how to format this name, as - * described in the \BibTeX\ documentation. Finally, this function - * pushes the formatted name. If any of the types is incorrect, it - * complains and pushes the null string." - * - * All the pain is encapsulated in BibtexNameFormatter. :-) - * - */ -public class FormatNameFunction implements BstFunction { - - private final VM vm; - - public FormatNameFunction(VM vm) { - this.vm = vm; - } - - @Override - public void execute(BstEntry context) { - Stack stack = vm.getStack(); - - if (stack.size() < 3) { - throw new VMException("Not enough operands on stack for operation format.name$"); - } - Object o1 = stack.pop(); - Object o2 = stack.pop(); - Object o3 = stack.pop(); - - if (!(o1 instanceof String) && !(o2 instanceof Integer) && !(o3 instanceof String)) { - // warning("A string is needed for change.case$"); - stack.push(""); - return; - } - - String format = (String) o1; - Integer name = (Integer) o2; - String names = (String) o3; - - if (names == null) { - stack.push(""); - } else { - AuthorList a = AuthorList.parse(names); - if (name > a.getNumberOfAuthors()) { - throw new VMException("Author Out of Bounds. Number " + name + " invalid for " + names); - } - Author author = a.getAuthor(name - 1); - - stack.push(BibtexNameFormatter.formatName(author, format, vm)); - } - } -} diff --git a/src/main/java/org/jabref/logic/bst/PurifyFunction.java b/src/main/java/org/jabref/logic/bst/PurifyFunction.java deleted file mode 100644 index b0df9df3735..00000000000 --- a/src/main/java/org/jabref/logic/bst/PurifyFunction.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.jabref.logic.bst; - -import java.util.Stack; - -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.BstFunction; - -/** - * - * The |built_in| function {\.{purify\$}} pops the top (string) literal, removes - * nonalphanumeric characters except for |white_space| and |sep_char| characters - * (these get converted to a |space|) and removes certain alphabetic characters - * contained in the control sequences associated with a special character, and - * pushes the resulting string. If the literal isn't a string, it complains and - * pushes the null string. - * - */ -public class PurifyFunction implements BstFunction { - - private final VM vm; - - public PurifyFunction(VM vm) { - this.vm = vm; - } - - @Override - public void execute(BstEntry context) { - Stack stack = vm.getStack(); - - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation purify$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - vm.warn("A string is needed for purify$"); - stack.push(""); - return; - } - - stack.push(BibtexPurify.purify((String) o1, vm)); - } -} diff --git a/src/main/java/org/jabref/logic/bst/TextPrefixFunction.java b/src/main/java/org/jabref/logic/bst/TextPrefixFunction.java deleted file mode 100644 index 415ee97498b..00000000000 --- a/src/main/java/org/jabref/logic/bst/TextPrefixFunction.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.jabref.logic.bst; - -import java.util.Stack; - -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.BstFunction; - -/** - * The |built_in| function {\.{text.prefix\$}} pops the top two literals - * (the integer literal |pop_lit1| and a string literal, in that order). - * It pushes the substring of the (at most) |pop_lit1| consecutive text - * characters starting from the beginning of the string. This function - * is similar to {\.{substring\$}}, but this one considers an accented - * character (or more precisely, a ``special character''$\!$, even if - * it's missing its matching |right_brace|) to be a single text character - * (rather than however many |ASCII_code| characters it actually - * comprises), and this function doesn't consider braces to be text - * characters; furthermore, this function appends any needed matching - * |right_brace|s. If any of the types is incorrect, it complains and - * pushes the null string. - */ -public class TextPrefixFunction implements BstFunction { - - private final VM vm; - - public TextPrefixFunction(VM vm) { - this.vm = vm; - } - - @Override - public void execute(BstEntry context) { - Stack stack = vm.getStack(); - - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation text.prefix$"); - } - - Object o1 = stack.pop(); - if (!(o1 instanceof Integer)) { - vm.warn("An integer is needed as first parameter to text.prefix$"); - stack.push(""); - return; - } - - Object o2 = stack.pop(); - if (!(o2 instanceof String)) { - vm.warn("A string is needed as second parameter to text.prefix$"); - stack.push(""); - return; - } - - stack.push(BibtexTextPrefix.textPrefix((Integer) o1, (String) o2, vm)); - } -} diff --git a/src/main/java/org/jabref/logic/bst/VM.java b/src/main/java/org/jabref/logic/bst/VM.java deleted file mode 100644 index caaf9078f64..00000000000 --- a/src/main/java/org/jabref/logic/bst/VM.java +++ /dev/null @@ -1,1245 +0,0 @@ -package org.jabref.logic.bst; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Objects; -import java.util.Stack; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.jabref.logic.bibtex.FieldContentFormatterPreferences; -import org.jabref.logic.bibtex.FieldWriter; -import org.jabref.logic.bibtex.FieldWriterPreferences; -import org.jabref.logic.bibtex.InvalidFieldValueException; -import org.jabref.model.database.BibDatabase; -import org.jabref.model.entry.AuthorList; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.Month; -import org.jabref.model.entry.field.Field; -import org.jabref.model.entry.field.FieldFactory; -import org.jabref.model.entry.field.StandardField; - -import org.antlr.runtime.ANTLRFileStream; -import org.antlr.runtime.ANTLRStringStream; -import org.antlr.runtime.CharStream; -import org.antlr.runtime.CommonTokenStream; -import org.antlr.runtime.RecognitionException; -import org.antlr.runtime.tree.CommonTree; -import org.antlr.runtime.tree.Tree; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A BibTeX Virtual machine that can execute .bst files. - *

- * Documentation can be found in the original bibtex distribution: - *

- * https://www.ctan.org/pkg/bibtex - */ -public class VM implements Warn { - - public static final Integer FALSE = 0; - - public static final Integer TRUE = 1; - - private static final Pattern ADD_PERIOD_PATTERN = Pattern.compile("([^\\.\\?\\!\\}\\s])(\\}|\\s)*$"); - - private static final Logger LOGGER = LoggerFactory.getLogger(VM.class); - - private List entries; - - private Map strings = new HashMap<>(); - - private Map integers = new HashMap<>(); - - private Map functions = new HashMap<>(); - - private Stack stack = new Stack<>(); - - private final Map buildInFunctions; - - private File file; - - private final CommonTree tree; - - private StringBuilder bbl; - - private String preamble = ""; - - public static class Identifier { - - public final String name; - - public Identifier(String name) { - this.name = name; - } - - public String getName() { - return name; - } - } - - public static class Variable { - - public final String name; - - public Variable(String name) { - this.name = name; - } - - public String getName() { - return name; - } - } - - @FunctionalInterface - public interface BstFunction { - void execute(BstEntry context); - } - - public VM(File f) throws RecognitionException, IOException { - this(new ANTLRFileStream(f.getPath())); - this.file = f; - } - - public VM(String s) throws RecognitionException { - this(new ANTLRStringStream(s)); - } - - private VM(CharStream bst) throws RecognitionException { - this(VM.charStream2CommonTree(bst)); - } - - private VM(CommonTree tree) { - this.tree = tree; - - this.buildInFunctions = new HashMap<>(37); - - /* - * Pops the top two (integer) literals, compares them, and pushes - * the integer 1 if the second is greater than the first, 0 - * otherwise. - */ - buildInFunctions.put(">", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation >"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { - throw new VMException("Can only compare two integers with >"); - } - - stack.push(((Integer) o1).compareTo((Integer) o2) > 0 ? VM.TRUE : VM.FALSE); - }); - - /* Analogous to >. */ - buildInFunctions.put("<", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation <"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { - throw new VMException("Can only compare two integers with <"); - } - - stack.push(((Integer) o1).compareTo((Integer) o2) < 0 ? VM.TRUE : VM.FALSE); - }); - - /* - * Pops the top two (both integer or both string) literals, compares - * them, and pushes the integer 1 if they're equal, 0 otherwise. - */ - buildInFunctions.put("=", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation ="); - } - Object o1 = stack.pop(); - Object o2 = stack.pop(); - - if ((o1 == null) ^ (o2 == null)) { - stack.push(VM.FALSE); - return; - } - - if ((o1 == null) && (o2 == null)) { - stack.push(VM.TRUE); - return; - } - - stack.push(o1.equals(o2) ? VM.TRUE : VM.FALSE); - }); - - /* Pops the top two (integer) literals and pushes their sum. */ - buildInFunctions.put("+", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation +"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { - throw new VMException("Can only compare two integers with +"); - } - - stack.push((Integer) o1 + (Integer) o2); - }); - - /* - * Pops the top two (integer) literals and pushes their difference - * (the first subtracted from the second). - */ - buildInFunctions.put("-", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation -"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { - throw new VMException("Can only subtract two integers with -"); - } - - stack.push((Integer) o1 - (Integer) o2); - }); - - /* - * Pops the top two (string) literals, concatenates them (in reverse - * order, that is, the order in which pushed), and pushes the - * resulting string. - */ - buildInFunctions.put("*", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation *"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (o1 == null) { - o1 = ""; - } - if (o2 == null) { - o2 = ""; - } - - if (!((o1 instanceof String) && (o2 instanceof String))) { - LOGGER.error("o1: {} ({})", o1, o1.getClass()); - LOGGER.error("o2: {} ({})", o2, o2.getClass()); - throw new VMException("Can only concatenate two String with *"); - } - - stack.push(o1.toString() + o2); - }); - - /* - * Pops the top two literals and assigns to the first (which must be - * a global or entry variable) the value of the second. - */ - buildInFunctions.put(":=", context -> { - if (stack.size() < 2) { - throw new VMException("Invalid call to operation :="); - } - Object o1 = stack.pop(); - Object o2 = stack.pop(); - assign(context, o1, o2); - }); - - /* - * Pops the top (string) literal, adds a `.' to it if the last non - * '}' character isn't a `.', `?', or `!', and pushes this resulting - * string. - */ - buildInFunctions.put("add.period$", context -> addPeriodFunction()); - - /* - * Executes the function whose name is the entry type of an entry. - * For example if an entry is of type book, this function executes - * the book function. When given as an argument to the ITERATE - * command, call.type$ actually produces the output for the entries. - * For an entry with an unknown type, it executes the function - * default.type. Thus you should define (before the READ command) - * one function for each standard entry type as well as a - * default.type function. - */ - buildInFunctions.put("call.type$", context -> { - if (context == null) { - throw new VMException("Call.type$ can only be called from within a context (ITERATE or REVERSE)."); - } - VM.this.execute(context.entry.getType().getName(), context); - }); - - buildInFunctions.put("change.case$", new ChangeCaseFunction(this)); - - /* - * Pops the top (string) literal, makes sure it's a single - * character, converts it to the corresponding ASCII integer, and - * pushes this integer. - */ - buildInFunctions.put("chr.to.int$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation chr.to.int$"); - } - Object o1 = stack.pop(); - - if (!((o1 instanceof String) && (((String) o1).length() == 1))) { - throw new VMException("Can only perform chr.to.int$ on string with length 1"); - } - - String s = (String) o1; - - stack.push((int) s.charAt(0)); - }); - - /* - * Pushes the string that was the \cite-command argument for this - * entry. - */ - buildInFunctions.put("cite$", context -> { - if (context == null) { - throw new VMException("Must have an entry to cite$"); - } - stack.push(context.entry.getCitationKey().orElse(null)); - }); - - /* - * Pops the top literal from the stack and pushes two copies of it. - */ - buildInFunctions.put("duplicate$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation duplicate$"); - } - Object o1 = stack.pop(); - - stack.push(o1); - stack.push(o1); - }); - - /* - * Pops the top literal and pushes the integer 1 if it's a missing - * field or a string having no non-white-space characters, 0 - * otherwise. - */ - buildInFunctions.put("empty$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation empty$"); - } - Object o1 = stack.pop(); - - if (o1 == null) { - stack.push(VM.TRUE); - return; - } - - if (!(o1 instanceof String)) { - throw new VMException("Operand does not match function empty$"); - } - - String s = (String) o1; - - stack.push("".equals(s.trim()) ? VM.TRUE : VM.FALSE); - }); - - buildInFunctions.put("format.name$", new FormatNameFunction(this)); - - /* - * Pops the top three literals (they are two function literals and - * an integer literal, in that order); if the integer is greater - * than 0, it executes the second literal, else it executes the - * first. - */ - buildInFunctions.put("if$", context -> { - if (stack.size() < 3) { - throw new VMException("Not enough operands on stack for operation ="); - } - Object f1 = stack.pop(); - Object f2 = stack.pop(); - Object i = stack.pop(); - - if (!((f1 instanceof Identifier) || (f1 instanceof Tree)) - && ((f2 instanceof Identifier) || (f2 instanceof Tree)) && (i instanceof Integer)) { - throw new VMException("Expecting two functions and an integer for if$."); - } - - if ((Integer) i > 0) { - VM.this.executeInContext(f2, context); - } else { - VM.this.executeInContext(f1, context); - } - }); - - /* - * Pops the top (integer) literal, interpreted as the ASCII integer - * value of a single character, converts it to the corresponding - * single-character string, and pushes this string. - */ - buildInFunctions.put("int.to.chr$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation int.to.chr$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof Integer)) { - throw new VMException("Can only perform operation int.to.chr$ on an Integer"); - } - - Integer i = (Integer) o1; - - stack.push(String.valueOf((char) i.intValue())); - }); - - /* - * Pops the top (integer) literal, converts it to its (unique) - * string equivalent, and pushes this string. - */ - buildInFunctions.put("int.to.str$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation int.to.str$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof Integer)) { - throw new VMException("Can only transform an integer to an string using int.to.str$"); - } - - stack.push(o1.toString()); - }); - - /* - * Pops the top literal and pushes the integer 1 if it's a missing - * field, 0 otherwise. - */ - buildInFunctions.put("missing$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation missing$"); - } - Object o1 = stack.pop(); - - if (o1 == null) { - stack.push(VM.TRUE); - return; - } - - if (!(o1 instanceof String)) { - warn("Not a string or missing field in operation missing$"); - stack.push(VM.TRUE); - return; - } - - stack.push(VM.FALSE); - }); - - /* - * Writes onto the bbl file what is accumulated in the output buffer. - * It writes a blank line if and only if the output buffer is empty. - * Since write$ does reasonable line breaking, you should use this - * function only when you want a blank line or an explicit line - * break. - */ - buildInFunctions.put("newline$", context -> VM.this.bbl.append('\n')); - - /* - * Pops the top (string) literal and pushes the number of names the - * string represents one plus the number of occurrences of the - * substring "and" (ignoring case differences) surrounded by - * non-null white-space at the top brace level. - */ - buildInFunctions.put("num.names$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation num.names$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - throw new VMException("Need a string at the top of the stack for num.names$"); - } - String s = (String) o1; - - stack.push(AuthorList.parse(s).getNumberOfAuthors()); - }); - - /* - * Pops the top of the stack but doesn't print it; this gets rid of - * an unwanted stack literal. - */ - buildInFunctions.put("pop$", context -> stack.pop()); - - /* - * The |built_in| function {\.{preamble\$}} pushes onto the stack - * the concatenation of all the \.{preamble} strings read from the - * database files. (or the empty string if there where none) - * - * @PREAMBLE strings read from the database files. - */ - buildInFunctions.put("preamble$", context -> { - stack.push(preamble); - }); - - /* - * Pops the top (string) literal, removes nonalphanumeric characters - * except for white-space characters and hyphens and ties (these all get - * converted to a space), removes certain alphabetic characters - * contained in the control sequences associated with a \special - * character", and pushes the resulting string. - */ - buildInFunctions.put("purify$", new PurifyFunction(this)); - - /* - * Pushes the string consisting of the double-quote character. - */ - buildInFunctions.put("quote$", context -> stack.push("\"")); - - /* - * Is a no-op. - */ - buildInFunctions.put("skip$", context -> { - // Nothing to do! Yeah! - }); - - /* - * Pops and prints the whole stack; it's meant to be used for style - * designers while debugging. - */ - buildInFunctions.put("stack$", context -> { - while (!stack.empty()) { - LOGGER.debug("Stack entry {}", stack.pop()); - } - }); - - /* - * Pops the top three literals (they are the two integers literals - * len and start, and a string literal, in that order). It pushes - * the substring of the (at most) len consecutive characters - * starting at the startth character (assuming 1-based indexing) if - * start is positive, and ending at the start-th character - * (including) from the end if start is negative (where the first - * character from the end is the last character). - */ - buildInFunctions.put("substring$", context -> substringFunction()); - - /* - * Swaps the top two literals on the stack. text.length$ Pops the - * top (string) literal, and pushes the number of text characters - * it contains, where an accented character (more precisely, a - * \special character", defined in Section 4) counts as a single - * text character, even if it's missing its matching right brace, - * and where braces don't count as text characters. - */ - buildInFunctions.put("swap$", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation swap$"); - } - Object f1 = stack.pop(); - Object f2 = stack.pop(); - - stack.push(f1); - stack.push(f2); - }); - - /* - * text.length$ Pops the top (string) literal, and pushes the number - * of text characters it contains, where an accented character (more - * precisely, a "special character", defined in Section 4) counts as - * a single text character, even if it's missing its matching right - * brace, and where braces don't count as text characters. - * - * From BibTeXing: For the purposes of counting letters in labels, - * BibTEX considers everything contained inside the braces as a - * single letter. - */ - buildInFunctions.put("text.length$", context -> textLengthFunction()); - - /* - * Pops the top two literals (the integer literal len and a string - * literal, in that order). It pushes the substring of the (at most) len - * consecutive text characters starting from the beginning of the - * string. This function is similar to substring$, but this one - * considers a \special character", even if it's missing its matching - * right brace, to be a single text character (rather than however many - * ASCII characters it actually comprises), and this function doesn't - * consider braces to be text characters; furthermore, this function - * appends any needed matching right braces. - */ - buildInFunctions.put("text.prefix$", new TextPrefixFunction(this)); - - /* - * Pops and prints the top of the stack to the log file. It's useful for debugging. - */ - buildInFunctions.put("top$", context -> LOGGER.debug("Stack entry {}", stack.pop())); - - /* - * Pushes the current entry's type (book, article, etc.), but pushes - * the null string if the type is either unknown or undefined. - */ - buildInFunctions.put("type$", context -> { - if (context == null) { - throw new VMException("type$ need a context."); - } - - stack.push(context.entry.getType().getName()); - }); - - /* - * Pops the top (string) literal and prints it following a warning - * message. This also increments a count of the number of warning - * messages issued. - */ - buildInFunctions.put("warning$", new BstFunction() { - int warning = 1; - - @Override - public void execute(BstEntry context) { - LOGGER.warn("Warning (#" + (warning++) + "): " + stack.pop()); - } - }); - - /* - * Pops the top two (function) literals, and keeps executing the - * second as long as the (integer) literal left on the stack by - * executing the first is greater than 0. - */ - buildInFunctions.put("while$", this::whileFunction); - - buildInFunctions.put("width$", new WidthFunction(this)); - - /* - * Pops the top (string) literal and writes it on the output buffer - * (which will result in stuff being written onto the bbl file when - * the buffer fills up). - */ - buildInFunctions.put("write$", context -> { - String s = (String) stack.pop(); - VM.this.bbl.append(s); - }); - } - - private void textLengthFunction() { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation text.length$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - throw new VMException("Can only perform operation on a string text.length$"); - } - - String s = (String) o1; - char[] c = s.toCharArray(); - int result = 0; - - // Comments from bibtex.web: - - // sp_ptr := str_start[pop_lit1]; - int i = 0; - - // sp_end := str_start[pop_lit1+1]; - int n = s.length(); - - // sp_brace_level := 0; - int braceLevel = 0; - - // while (sp_ptr < sp_end) do begin - while (i < n) { - // incr(sp_ptr); - i++; - // if (str_pool[sp_ptr-1] = left_brace) then - // begin - if (c[i - 1] == '{') { - // incr(sp_brace_level); - braceLevel++; - // if ((sp_brace_level = 1) and (sp_ptr < sp_end)) then - if ((braceLevel == 1) && (i < n)) { - // if (str_pool[sp_ptr] = backslash) then - // begin - if (c[i] == '\\') { - // incr(sp_ptr); {skip over the |backslash|} - i++; // skip over backslash - // while ((sp_ptr < sp_end) and (sp_brace_level - // > 0)) do begin - while ((i < n) && (braceLevel > 0)) { - // if (str_pool[sp_ptr] = right_brace) then - if (c[i] == '}') { - // decr(sp_brace_level) - braceLevel--; - } else if (c[i] == '{') { - // incr(sp_brace_level); - braceLevel++; - } - // incr(sp_ptr); - i++; - // end; - } - // incr(num_text_chars); - result++; - // end; - } - // end - } - - // else if (str_pool[sp_ptr-1] = right_brace) then - // begin - } else if (c[i - 1] == '}') { - // if (sp_brace_level > 0) then - if (braceLevel > 0) { - // decr(sp_brace_level); - braceLevel--; - // end - } - } else { // else - // incr(num_text_chars); - result++; - } - } - stack.push(result); - } - - private void whileFunction(BstEntry context) { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation while$"); - } - Object f2 = stack.pop(); - Object f1 = stack.pop(); - - if (!((f1 instanceof Identifier) || (f1 instanceof Tree)) - && ((f2 instanceof Identifier) || (f2 instanceof Tree))) { - throw new VMException("Expecting two functions for while$."); - } - - do { - VM.this.executeInContext(f1, context); - - Object i = stack.pop(); - if (!(i instanceof Integer)) { - throw new VMException("First parameter to while has to return an integer but was " + i); - } - if ((Integer) i <= 0) { - break; - } - VM.this.executeInContext(f2, context); - } while (true); - } - - private void substringFunction() { - if (stack.size() < 3) { - throw new VMException("Not enough operands on stack for operation substring$"); - } - Object o1 = stack.pop(); - Object o2 = stack.pop(); - Object o3 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer) && (o3 instanceof String))) { - throw new VMException("Expecting two integers and a string for substring$"); - } - - Integer len = (Integer) o1; - Integer start = (Integer) o2; - - int lenI = len; - int startI = start; - - if (lenI > (Integer.MAX_VALUE / 2)) { - lenI = Integer.MAX_VALUE / 2; - } - - if (startI > (Integer.MAX_VALUE / 2)) { - startI = Integer.MAX_VALUE / 2; - } - - if (startI < (Integer.MIN_VALUE / 2)) { - startI = -Integer.MIN_VALUE / 2; - } - - String s = (String) o3; - - if (startI < 0) { - startI += s.length() + 1; - startI = Math.max(1, (startI + 1) - lenI); - } - stack.push(s.substring(startI - 1, Math.min((startI - 1) + lenI, s.length()))); - } - - private void addPeriodFunction() { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation add.period$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - throw new VMException("Can only add a period to a string for add.period$"); - } - - String s = (String) o1; - Matcher m = ADD_PERIOD_PATTERN.matcher(s); - - if (m.find()) { - StringBuilder sb = new StringBuilder(); - m.appendReplacement(sb, m.group(1)); - sb.append('.'); - String group2 = m.group(2); - if (group2 != null) { - sb.append(m.group(2)); - } - stack.push(sb.toString()); - } else { - stack.push(s); - } - } - - private static CommonTree charStream2CommonTree(CharStream bst) throws RecognitionException { - BstLexer lex = new BstLexer(bst); - CommonTokenStream tokens = new CommonTokenStream(lex); - BstParser parser = new BstParser(tokens); - BstParser.program_return r = parser.program(); - return (CommonTree) r.getTree(); - } - - private boolean assign(BstEntry context, Object o1, Object o2) { - if (!(o1 instanceof Identifier) || !((o2 instanceof String) || (o2 instanceof Integer))) { - throw new VMException("Invalid parameters"); - } - - String name = ((Identifier) o1).getName(); - - if (o2 instanceof String) { - if ((context != null) && context.localStrings.containsKey(name)) { - context.localStrings.put(name, (String) o2); - return true; - } - - if (strings.containsKey(name)) { - strings.put(name, (String) o2); - return true; - } - return false; - } - - if ((context != null) && context.localIntegers.containsKey(name)) { - context.localIntegers.put(name, (Integer) o2); - return true; - } - - if (integers.containsKey(name)) { - integers.put(name, (Integer) o2); - return true; - } - return false; - } - - public String run(BibDatabase db) { - preamble = db.getPreamble().orElse(""); - return run(db.getEntries()); - } - - public String run(Collection bibtex) { - return this.run(bibtex, null); - } - - /** - * Transforms the given list of BibEntries to a rendered list of references using the underlying bst file - * - * @param bibEntries list of entries to convert - * @param bibDatabase (may be null) the bibDatabase used for resolving strings / crossref - * @return list of references in plain text form - */ - public String run(Collection bibEntries, BibDatabase bibDatabase) { - Objects.requireNonNull(bibEntries); - - // Reset - bbl = new StringBuilder(); - - strings = new HashMap<>(); - - integers = new HashMap<>(); - integers.put("entry.max$", Integer.MAX_VALUE); - integers.put("global.max$", Integer.MAX_VALUE); - - functions = new HashMap<>(); - functions.putAll(buildInFunctions); - - stack = new Stack<>(); - - // Create entries - entries = new ArrayList<>(bibEntries.size()); - for (BibEntry entry : bibEntries) { - entries.add(new BstEntry(entry)); - } - - // Go - for (int i = 0; i < tree.getChildCount(); i++) { - Tree child = tree.getChild(i); - switch (child.getType()) { - case BstParser.STRINGS: - strings(child); - break; - case BstParser.INTEGERS: - integers(child); - break; - case BstParser.FUNCTION: - function(child); - break; - case BstParser.EXECUTE: - execute(child); - break; - case BstParser.SORT: - sort(); - break; - case BstParser.ITERATE: - iterate(child); - break; - case BstParser.REVERSE: - reverse(child); - break; - case BstParser.ENTRY: - entry(child); - break; - case BstParser.READ: - read(bibDatabase); - break; - case BstParser.MACRO: - macro(child); - break; - default: - LOGGER.info("Unknown type: {}", child.getType()); - break; - } - } - - return bbl.toString(); - } - - /** - * Dredges up from the database file the field values for each entry in the list. It has no arguments. If a database - * entry doesn't have a value for a field (and probably no database entry will have a value for every field), that - * field variable is marked as missing for the entry. - *

- * We use null for the missing entry designator. - */ - private void read(BibDatabase bibDatabase) { - FieldWriter fieldWriter = new FieldWriter(new FieldWriterPreferences(true, List.of(StandardField.MONTH), new FieldContentFormatterPreferences())); - for (BstEntry e : entries) { - for (Map.Entry mEntry : e.fields.entrySet()) { - Field field = FieldFactory.parseField(mEntry.getKey()); - String fieldValue = e.entry.getResolvedFieldOrAlias(field, bibDatabase) - .map(content -> { - try { - String result = fieldWriter.write(field, content); - if (result.startsWith("{")) { - // Strip enclosing {} from the output - return result.substring(1, result.length() - 1); - } - if (field == StandardField.MONTH) { - // We don't have the internal BibTeX strings at hand. - // We nevertheless want to have the full month name. - // Thus, we lookup the full month name here. - return Month.parse(result) - .map(month -> month.getFullName()) - .orElse(result); - } - return result; - } catch (InvalidFieldValueException invalidFieldValueException) { - // in case there is something wrong with the content, just return the content itself - return content; - } - }) - .orElse(null); - mEntry.setValue(fieldValue); - } - } - - for (BstEntry e : entries) { - if (!e.fields.containsKey(StandardField.CROSSREF.getName())) { - e.fields.put(StandardField.CROSSREF.getName(), null); - } - } - } - - /** - * Defines a string macro. It has two arguments; the first is the macro's name, which is treated like any other - * variable or function name, and the second is its definition, which must be double-quote-delimited. You must have - * one for each three-letter month abbreviation; in addition, you should have one for common journal names. The - * user's database may override any definition you define using this command. If you want to define a string the - * user can't touch, use the FUNCTION command, which has a compatible syntax. - */ - private void macro(Tree child) { - String name = child.getChild(0).getText(); - String replacement = child.getChild(1).getText(); - functions.put(name, new MacroFunction(replacement)); - } - - public class MacroFunction implements BstFunction { - - private final String replacement; - - public MacroFunction(String replacement) { - this.replacement = replacement; - } - - @Override - public void execute(BstEntry context) { - VM.this.push(replacement); - } - } - - /** - * Declares the fields and entry variables. It has three arguments, each a (possibly empty) list of variable names. - * The three lists are of: fields, integer entry variables, and string entry variables. There is an additional field - * that BibTEX automatically declares, crossref, used for cross referencing. And there is an additional string entry - * variable automatically declared, sort.key$, used by the SORT command. Each of these variables has a value for - * each entry on the list. - */ - private void entry(Tree child) { - // Fields first - Tree t = child.getChild(0); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - - for (BstEntry entry : entries) { - entry.fields.put(name, null); - } - } - - // Integers - t = child.getChild(1); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - - for (BstEntry entry : entries) { - entry.localIntegers.put(name, 0); - } - } - // Strings - t = child.getChild(2); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - for (BstEntry entry : entries) { - entry.localStrings.put(name, null); - } - } - for (BstEntry entry : entries) { - entry.localStrings.put("sort.key$", null); - } - } - - private void reverse(Tree child) { - BstFunction f = functions.get(child.getChild(0).getText()); - - ListIterator i = entries.listIterator(entries.size()); - while (i.hasPrevious()) { - f.execute(i.previous()); - } - } - - private void iterate(Tree child) { - BstFunction f = functions.get(child.getChild(0).getText()); - - for (BstEntry entry : entries) { - f.execute(entry); - } - } - - /** - * Sorts the entry list using the values of the string entry variable sort.key$. It has no arguments. - */ - private void sort() { - entries.sort(Comparator.comparing(o -> (o.localStrings.get("sort.key$")))); - } - - private void executeInContext(Object o, BstEntry context) { - if (o instanceof Tree) { - Tree t = (Tree) o; - new StackFunction(t).execute(context); - } else if (o instanceof Identifier) { - execute(((Identifier) o).getName(), context); - } - } - - private void execute(Tree child) { - execute(child.getChild(0).getText(), null); - } - - public class StackFunction implements BstFunction { - - private final Tree localTree; - - public StackFunction(Tree stack) { - localTree = stack; - } - - public Tree getTree() { - return localTree; - } - - @Override - public void execute(BstEntry context) { - for (int i = 0; i < localTree.getChildCount(); i++) { - Tree c = localTree.getChild(i); - try { - - switch (c.getType()) { - case BstParser.STRING: - String s = c.getText(); - push(s.substring(1, s.length() - 1)); - break; - case BstParser.INTEGER: - push(Integer.parseInt(c.getText().substring(1))); - break; - case BstParser.QUOTED: - push(new Identifier(c.getText().substring(1))); - break; - case BstParser.STACK: - push(c); - break; - default: - VM.this.execute(c.getText(), context); - break; - } - } catch (VMException e) { - if (file == null) { - LOGGER.error("ERROR " + e.getMessage() + " (" + c.getLine() + ")"); - } else { - LOGGER.error("ERROR " + e.getMessage() + " (" + file.getPath() + ":" - + c.getLine() + ")"); - } - throw e; - } - } - } - } - - private void push(Tree t) { - stack.push(t); - } - - private void execute(String name, BstEntry context) { - if (context != null) { - if (context.fields.containsKey(name)) { - stack.push(context.fields.get(name)); - return; - } - if (context.localStrings.containsKey(name)) { - stack.push(context.localStrings.get(name)); - return; - } - if (context.localIntegers.containsKey(name)) { - stack.push(context.localIntegers.get(name)); - return; - } - } - if (strings.containsKey(name)) { - stack.push(strings.get(name)); - return; - } - if (integers.containsKey(name)) { - stack.push(integers.get(name)); - return; - } - - if (functions.containsKey(name)) { - // OK to have a null context - functions.get(name).execute(context); - return; - } - - throw new VMException("No matching identifier found: " + name); - } - - private void function(Tree child) { - String name = child.getChild(0).getText(); - Tree localStack = child.getChild(1); - functions.put(name, new StackFunction(localStack)); - } - - /** - * Declares global integer variables. It has one argument, a list of variable names. There are two such - * automatically-declared variables, entry.max$ and global.max$, used for limiting the lengths of string vari- - * ables. You may have any number of these commands, but a variable's declaration must precede its use. - */ - private void integers(Tree child) { - Tree t = child.getChild(0); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - integers.put(name, 0); - } - } - - /** - * Declares global string variables. It has one argument, a list of variable names. You may have any number of these - * commands, but a variable's declaration must precede its use. - * - * @param child - */ - private void strings(Tree child) { - Tree t = child.getChild(0); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - strings.put(name, null); - } - } - - public static class BstEntry { - - public final BibEntry entry; - - public final Map localStrings = new HashMap<>(); - - // keys filled by org.jabref.logic.bst.VM.entry based on the contents of the bst file - public final Map fields = new HashMap<>(); - - public final Map localIntegers = new HashMap<>(); - - public BstEntry(BibEntry e) { - this.entry = e; - } - } - - private void push(Integer integer) { - stack.push(integer); - } - - private void push(String string) { - stack.push(string); - } - - private void push(Identifier identifier) { - stack.push(identifier); - } - - public Map getStrings() { - return strings; - } - - public Map getIntegers() { - return integers; - } - - public List getEntries() { - return entries; - } - - public Map getFunctions() { - return functions; - } - - public Stack getStack() { - return stack; - } - - @Override - public void warn(String string) { - LOGGER.warn(string); - } -} diff --git a/src/main/java/org/jabref/logic/bst/VMException.java b/src/main/java/org/jabref/logic/bst/VMException.java deleted file mode 100644 index f46c5cd5277..00000000000 --- a/src/main/java/org/jabref/logic/bst/VMException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.jabref.logic.bst; - -public class VMException extends RuntimeException { - - public VMException(String string) { - super(string); - } -} diff --git a/src/main/java/org/jabref/logic/bst/Warn.java b/src/main/java/org/jabref/logic/bst/Warn.java deleted file mode 100644 index 7a524ad9834..00000000000 --- a/src/main/java/org/jabref/logic/bst/Warn.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.jabref.logic.bst; - -@FunctionalInterface -public interface Warn { - - void warn(String s); -} diff --git a/src/main/java/org/jabref/logic/bst/WidthFunction.java b/src/main/java/org/jabref/logic/bst/WidthFunction.java deleted file mode 100644 index 06784267bf0..00000000000 --- a/src/main/java/org/jabref/logic/bst/WidthFunction.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.jabref.logic.bst; - -import java.util.Stack; - -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.BstFunction; - -/** - * The |built_in| function {\.{width\$}} pops the top (string) literal and - * pushes the integer that represents its width in units specified by the - * |char_width| array. This function takes the literal literally; that is, it - * assumes each character in the string is to be printed as is, regardless of - * whether the character has a special meaning to \TeX, except that special - * characters (even without their |right_brace|s) are handled specially. If the - * literal isn't a string, it complains and pushes~0. - * - */ -public class WidthFunction implements BstFunction { - - private final VM vm; - - public WidthFunction(VM vm) { - this.vm = vm; - } - - @Override - public void execute(BstEntry context) { - Stack stack = vm.getStack(); - - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation width$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - vm.warn("A string is needed for change.case$"); - stack.push(0); - return; - } - - stack.push(BibtexWidth.width((String) o1)); - } -} diff --git a/src/main/java/org/jabref/logic/bst/BibtexCaseChanger.java b/src/main/java/org/jabref/logic/bst/util/BstCaseChanger.java similarity index 83% rename from src/main/java/org/jabref/logic/bst/BibtexCaseChanger.java rename to src/main/java/org/jabref/logic/bst/util/BstCaseChanger.java index e05ce998e80..dd3e7c7b05c 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexCaseChanger.java +++ b/src/main/java/org/jabref/logic/bst/util/BstCaseChanger.java @@ -1,4 +1,4 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import java.util.Locale; import java.util.Optional; @@ -6,9 +6,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public final class BibtexCaseChanger { +public final class BstCaseChanger { - private static final Logger LOGGER = LoggerFactory.getLogger(BibtexCaseChanger.class); + private static final Logger LOGGER = LoggerFactory.getLogger(BstCaseChanger.class); // stores whether the char before the current char was a colon private boolean prevColon = true; @@ -16,7 +16,7 @@ public final class BibtexCaseChanger { // global variable to store the current brace level private int braceLevel; - public enum FORMAT_MODE { + public enum FormatMode { // First character and character after a ":" as upper case - everything else in lower case. Obey {}. TITLE_LOWERS('t'), @@ -40,7 +40,7 @@ public enum FORMAT_MODE { private final char asChar; - FORMAT_MODE(char asChar) { + FormatMode(char asChar) { this.asChar = asChar; } @@ -53,17 +53,21 @@ public char asChar() { * * @throws IllegalArgumentException if char is not 't', 'l', 'u' */ - public static FORMAT_MODE getFormatModeForBSTFormat(final char bstFormat) { - for (FORMAT_MODE mode : FORMAT_MODE.values()) { + public static FormatMode of(final char bstFormat) { + for (FormatMode mode : FormatMode.values()) { if (mode.asChar == bstFormat) { return mode; } } throw new IllegalArgumentException(); } + + public static FormatMode of(final String bstFormat) { + return of(bstFormat.toLowerCase(Locale.ROOT).charAt(0)); + } } - private BibtexCaseChanger() { + private BstCaseChanger() { } /** @@ -72,11 +76,11 @@ private BibtexCaseChanger() { * @param s the string to handle * @param format the format */ - public static String changeCase(String s, FORMAT_MODE format) { - return (new BibtexCaseChanger()).doChangeCase(s, format); + public static String changeCase(String s, FormatMode format) { + return (new BstCaseChanger()).doChangeCase(s, format); } - private String doChangeCase(String s, FORMAT_MODE format) { + private String doChangeCase(String s, FormatMode format) { char[] c = s.toCharArray(); StringBuilder sb = new StringBuilder(); @@ -93,7 +97,7 @@ private String doChangeCase(String s, FORMAT_MODE format) { i++; continue; } - if ((format == FORMAT_MODE.TITLE_LOWERS) && ((i == 0) || (prevColon && Character.isWhitespace(c[i - 1])))) { + if ((format == FormatMode.TITLE_LOWERS) && ((i == 0) || (prevColon && Character.isWhitespace(c[i - 1])))) { sb.append('{'); i++; prevColon = false; @@ -136,12 +140,9 @@ private String doChangeCase(String s, FORMAT_MODE format) { * is other stuff, too, between braces, but it doesn't try to do anything * special with |colon|s. * - * @param c * @param start the current position. It points to the opening brace - * @param format - * @return */ - private int convertSpecialChar(StringBuilder sb, char[] c, int start, FORMAT_MODE format) { + private int convertSpecialChar(StringBuilder sb, char[] c, int start, FormatMode format) { int i = start; sb.append(c[i]); @@ -152,7 +153,7 @@ private int convertSpecialChar(StringBuilder sb, char[] c, int start, FORMAT_MOD i++; // skip over the |backslash| - Optional s = BibtexCaseChanger.findSpecialChar(c, i); + Optional s = BstCaseChanger.findSpecialChar(c, i); if (s.isPresent()) { i = convertAccented(c, i, s.get(), sb, format); } @@ -174,14 +175,9 @@ private int convertSpecialChar(StringBuilder sb, char[] c, int start, FORMAT_MOD * up) and append the result to the stringBuffer, return the updated * position. * - * @param c - * @param start - * @param s - * @param sb - * @param format * @return the new position */ - private int convertAccented(char[] c, int start, String s, StringBuilder sb, FORMAT_MODE format) { + private int convertAccented(char[] c, int start, String s, StringBuilder sb, FormatMode format) { int pos = start; pos += s.length(); @@ -214,29 +210,27 @@ private int convertAccented(char[] c, int start, String s, StringBuilder sb, FOR return pos; } - private int convertNonControl(char[] c, int start, StringBuilder sb, FORMAT_MODE format) { + private int convertNonControl(char[] c, int start, StringBuilder sb, FormatMode format) { int pos = start; switch (format) { - case TITLE_LOWERS: - case ALL_LOWERS: + case TITLE_LOWERS, ALL_LOWERS -> { sb.append(Character.toLowerCase(c[pos])); pos++; - break; - case ALL_UPPERS: + } + case ALL_UPPERS -> { sb.append(Character.toUpperCase(c[pos])); pos++; - break; - default: - LOGGER.info("convertNonControl - Unknown format: " + format); - break; + } + default -> + LOGGER.info("convertNonControl - Unknown format: " + format); } return pos; } - private int convertCharIfBraceLevelIsZero(char[] c, int start, StringBuilder sb, FORMAT_MODE format) { + private int convertCharIfBraceLevelIsZero(char[] c, int start, StringBuilder sb, FormatMode format) { int i = start; switch (format) { - case TITLE_LOWERS: + case TITLE_LOWERS -> { if ((i == 0) || (prevColon && Character.isWhitespace(c[i - 1]))) { sb.append(c[i]); } else { @@ -247,16 +241,13 @@ private int convertCharIfBraceLevelIsZero(char[] c, int start, StringBuilder sb, } else if (!Character.isWhitespace(c[i])) { prevColon = false; } - break; - case ALL_LOWERS: - sb.append(Character.toLowerCase(c[i])); - break; - case ALL_UPPERS: - sb.append(Character.toUpperCase(c[i])); - break; - default: - LOGGER.info("convertCharIfBraceLevelIsZero - Unknown format: " + format); - break; + } + case ALL_LOWERS -> + sb.append(Character.toLowerCase(c[i])); + case ALL_UPPERS -> + sb.append(Character.toUpperCase(c[i])); + default -> + LOGGER.info("convertCharIfBraceLevelIsZero - Unknown format: " + format); } i++; return i; diff --git a/src/main/java/org/jabref/logic/bst/BibtexNameFormatter.java b/src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java similarity index 76% rename from src/main/java/org/jabref/logic/bst/BibtexNameFormatter.java rename to src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java index aecf571bdf4..c4986dc12dd 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexNameFormatter.java +++ b/src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java @@ -1,13 +1,17 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import java.util.Arrays; import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; +import org.jabref.logic.bst.BstVMException; import org.jabref.model.entry.Author; import org.jabref.model.entry.AuthorList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * From Bibtex: * @@ -24,9 +28,10 @@ * Sounds easy - is a nightmare... X-( * */ -public class BibtexNameFormatter { +public class BstNameFormatter { + private static final Logger LOGGER = LoggerFactory.getLogger(BstNameFormatter.class); - private BibtexNameFormatter() { + private BstNameFormatter() { } /** @@ -35,23 +40,18 @@ private BibtexNameFormatter() { * @param authorsNameList The string from an author field * @param whichName index of the list, starting with 1 * @param formatString TODO - * @param warn collects the warnings, may-be-null - * @return */ - public static String formatName(String authorsNameList, int whichName, String formatString, Warn warn) { + public static String formatName(String authorsNameList, int whichName, String formatString) { AuthorList al = AuthorList.parse(authorsNameList); if ((whichName < 1) && (whichName > al.getNumberOfAuthors())) { - warn.warn("AuthorList " + authorsNameList + " does not contain an author with number " + whichName); + LOGGER.warn("AuthorList {} does not contain an author with number {}", authorsNameList, whichName); return ""; } - return BibtexNameFormatter.formatName(al.getAuthor(whichName - 1), formatString, warn); + return BstNameFormatter.formatName(al.getAuthor(whichName - 1), formatString); } - /** - * @param warn collects the warnings, may-be-null - */ - public static String formatName(Author author, String format, Warn warn) { + public static String formatName(Author author, String format) { StringBuilder sb = new StringBuilder(); char[] c = format.toCharArray(); @@ -81,11 +81,7 @@ public static String formatName(Author author, String format, Warn warn) { } if ((braceLevel == 1) && Character.isLetter(c[i])) { if ("fvlj".indexOf(c[i]) == -1) { - if (warn != null) { - warn.warn( - "Format string in format.name$ may only contain fvlj on brace level 1 in group " - + group + ": " + format); - } + LOGGER.warn("Format string in format.name$ may only contain fvlj on brace level 1 in group {}: {}", group, format); } else { level1Chars.append(c[i]); } @@ -99,31 +95,26 @@ public static String formatName(Author author, String format, Warn warn) { continue; } - if ((control.length() > 2) && (warn != null)) { - warn.warn("Format string in format.name$ may only be one or two character long on brace level 1 in group " + group + ": " + format); + if ((control.length() > 2)) { + LOGGER.warn("Format string in format.name$ may only be one or two character long on brace level 1 in group {}: {}", group, format); } char type = control.charAt(0); - Optional tokenS; - switch (type) { - case 'f': - tokenS = author.getFirst(); - break; - case 'v': - tokenS = author.getVon(); - break; - case 'l': - tokenS = author.getLast(); - break; - case 'j': - tokenS = author.getJr(); - break; - default: - throw new VMException("Internal error"); - } - - if (!tokenS.isPresent()) { + Optional tokenS = switch (type) { + case 'f' -> + author.getFirst(); + case 'v' -> + author.getVon(); + case 'l' -> + author.getLast(); + case 'j' -> + author.getJr(); + default -> + throw new BstVMException("Internal error"); + }; + + if (tokenS.isEmpty()) { i++; continue; } @@ -135,9 +126,7 @@ public static String formatName(Author author, String format, Warn warn) { if (control.charAt(1) == control.charAt(0)) { abbreviateThatIsSingleLetter = false; } else { - if (warn != null) { - warn.warn("Format string in format.name$ may only contain one type of vlfj on brace level 1 in group " + group + ": " + format); - } + LOGGER.warn("Format string in format.name$ may only contain one type of vlfj on brace level 1 in group {}: {}", group, format); } } @@ -162,7 +151,7 @@ public static String formatName(Author author, String format, Warn warn) { } if (((j + 1) < d.length) && (d[j + 1] == '{')) { StringBuilder interTokenSb = new StringBuilder(); - j = BibtexNameFormatter.consumeToMatchingBrace(interTokenSb, d, j + 1); + j = BstNameFormatter.consumeToMatchingBrace(interTokenSb, d, j + 1); interToken = interTokenSb.substring(1, interTokenSb.length() - 1); } @@ -171,7 +160,7 @@ public static String formatName(Author author, String format, Warn warn) { if (abbreviateThatIsSingleLetter) { String[] dashes = token.split("-"); - token = Arrays.asList(dashes).stream().map(BibtexNameFormatter::getFirstCharOfString) + token = Arrays.stream(dashes).map(BstNameFormatter::getFirstCharOfString) .collect(Collectors.joining(".-")); } @@ -187,7 +176,7 @@ public static String formatName(Author author, String format, Warn warn) { // No clue what this means (What the hell are tokens anyway??? // if (lex_class[name_sep_char[cur_token]] = sep_char) then // append_ex_buf_char_and_check (name_sep_char[cur_token]) - if ((k == (tokens.length - 2)) || (BibtexNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 3) < 3)) { + if ((k == (tokens.length - 2)) || (BstNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 3) < 3)) { sb.append('~'); } else { sb.append(' '); @@ -212,7 +201,7 @@ public static String formatName(Author author, String format, Warn warn) { if (sb.length() > 0) { boolean noDisTie = false; if ((sb.charAt(sb.length() - 1) == '~') && - ((BibtexNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 4) >= 4) || + ((BstNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 4) >= 4) || ((sb.length() > 1) && (noDisTie = sb.charAt(sb.length() - 2) == '~')))) { sb.deleteCharAt(sb.length() - 1); if (!noDisTie) { @@ -221,16 +210,14 @@ public static String formatName(Author author, String format, Warn warn) { } } } else if (c[i] == '}') { - if (warn != null) { - warn.warn("Unmatched brace in format string: " + format); - } + LOGGER.warn("Unmatched brace in format string: {}", format); } else { sb.append(c[i]); // verbatim } i++; } - if ((braceLevel != 0) && (warn != null)) { - warn.warn("Unbalanced brace in format string for nameFormat: " + format); + if ((braceLevel != 0)) { + LOGGER.warn("Unbalanced brace in format string for nameFormat: {}", format); } return sb.toString(); @@ -268,7 +255,7 @@ public static String getFirstCharOfString(String s) { } if ((c[i] == '{') && ((i + 1) < c.length) && (c[i + 1] == '\\')) { StringBuilder sb = new StringBuilder(); - BibtexNameFormatter.consumeToMatchingBrace(sb, c, i); + BstNameFormatter.consumeToMatchingBrace(sb, c, i); return sb.toString(); } } diff --git a/src/main/java/org/jabref/logic/bst/BibtexPurify.java b/src/main/java/org/jabref/logic/bst/util/BstPurifier.java similarity index 77% rename from src/main/java/org/jabref/logic/bst/BibtexPurify.java rename to src/main/java/org/jabref/logic/bst/util/BstPurifier.java index 08cb14db207..90818a0749f 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexPurify.java +++ b/src/main/java/org/jabref/logic/bst/util/BstPurifier.java @@ -1,4 +1,7 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @@ -10,17 +13,13 @@ * pushes the null string. * */ -public class BibtexPurify { +public class BstPurifier { + private static final Logger LOGGER = LoggerFactory.getLogger(BstPurifier.class); - private BibtexPurify() { + private BstPurifier() { } - /** - * @param toPurify - * @param warn may-be-null - * @return - */ - public static String purify(String toPurify, Warn warn) { + public static String purify(String toPurify) { StringBuilder sb = new StringBuilder(); char[] cs = toPurify.toCharArray(); @@ -41,7 +40,7 @@ public static String purify(String toPurify, Warn warn) { i++; // skip brace while ((i < n) && (braceLevel > 0)) { i++; // skip backslash - BibtexCaseChanger.findSpecialChar(cs, i).ifPresent(sb::append); + BstCaseChanger.findSpecialChar(cs, i).ifPresent(sb::append); while ((i < n) && Character.isLetter(cs[i])) { i++; @@ -63,15 +62,13 @@ public static String purify(String toPurify, Warn warn) { if (braceLevel > 0) { braceLevel--; } else { - if (warn != null) { - warn.warn("Unbalanced brace in string for purify$: " + toPurify); - } + LOGGER.warn("Unbalanced brace in string for purify$: {}", toPurify); } } i++; } - if ((braceLevel != 0) && (warn != null)) { - warn.warn("Unbalanced brace in string for purify$: " + toPurify); + if ((braceLevel != 0)) { + LOGGER.warn("Unbalanced brace in string for purify$: {}", toPurify); } return sb.toString(); diff --git a/src/main/java/org/jabref/logic/bst/BibtexTextPrefix.java b/src/main/java/org/jabref/logic/bst/util/BstTextPrefixer.java similarity index 84% rename from src/main/java/org/jabref/logic/bst/BibtexTextPrefix.java rename to src/main/java/org/jabref/logic/bst/util/BstTextPrefixer.java index 721838e29fe..31cbc71de9b 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexTextPrefix.java +++ b/src/main/java/org/jabref/logic/bst/util/BstTextPrefixer.java @@ -1,4 +1,7 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The |built_in| function {\.{text.prefix\$}} pops the top two literals (the @@ -14,15 +17,13 @@ * complains and pushes the null string. * */ -public class BibtexTextPrefix { +public class BstTextPrefixer { + private static final Logger LOGGER = LoggerFactory.getLogger(BstTextPrefixer.class); - private BibtexTextPrefix() { + private BstTextPrefixer() { } - /** - * @param warn may-be-null - */ - public static String textPrefix(int inNumOfChars, String toPrefix, Warn warn) { + public static String textPrefix(int inNumOfChars, String toPrefix) { int numOfChars = inNumOfChars; StringBuilder sb = new StringBuilder(); @@ -53,15 +54,13 @@ public static String textPrefix(int inNumOfChars, String toPrefix, Warn warn) { if (braceLevel > 0) { braceLevel--; } else { - if (warn != null) { - warn.warn("Unbalanced brace in string for purify$: " + toPrefix); - } + LOGGER.warn("Unbalanced brace in string for purify$: {}", toPrefix); } } else { numOfChars--; } } - sb.append(toPrefix.substring(0, i)); + sb.append(toPrefix, 0, i); while (braceLevel > 0) { sb.append('}'); braceLevel--; diff --git a/src/main/java/org/jabref/logic/bst/util/BstWidthCalculator.java b/src/main/java/org/jabref/logic/bst/util/BstWidthCalculator.java new file mode 100644 index 00000000000..c286fe1497b --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/util/BstWidthCalculator.java @@ -0,0 +1,241 @@ +package org.jabref.logic.bst.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * The |built_in| function {\.{purify\$}} pops the top (string) literal, removes + * nonalphanumeric characters except for |white_space| and |sep_char| characters + * (these get converted to a |space|) and removes certain alphabetic characters + * contained in the control sequences associated with a special character, and + * pushes the resulting string. If the literal isn't a string, it complains and + * pushes the null string. + * + */ +public class BstWidthCalculator { + + private static final Logger LOGGER = LoggerFactory.getLogger(BstWidthCalculator.class); + + /* + * Quoted from Bibtex: + * + * Now we initialize the system-dependent |char_width| array, for which + * |space| is the only |white_space| character given a nonzero printing + * width. The widths here are taken from Stanford's June~'87 $cmr10$~font + * and represent hundredths of a point (rounded), but since they're used + * only for relative comparisons, the units have no meaning. + */ + + private static int[] widths; + + static { + if (BstWidthCalculator.widths == null) { + BstWidthCalculator.widths = new int[128]; + + for (int i = 0; i < 128; i++) { + BstWidthCalculator.widths[i] = 0; + } + BstWidthCalculator.widths[32] = 278; + BstWidthCalculator.widths[33] = 278; + BstWidthCalculator.widths[34] = 500; + BstWidthCalculator.widths[35] = 833; + BstWidthCalculator.widths[36] = 500; + BstWidthCalculator.widths[37] = 833; + BstWidthCalculator.widths[38] = 778; + BstWidthCalculator.widths[39] = 278; + BstWidthCalculator.widths[40] = 389; + BstWidthCalculator.widths[41] = 389; + BstWidthCalculator.widths[42] = 500; + BstWidthCalculator.widths[43] = 778; + BstWidthCalculator.widths[44] = 278; + BstWidthCalculator.widths[45] = 333; + BstWidthCalculator.widths[46] = 278; + BstWidthCalculator.widths[47] = 500; + BstWidthCalculator.widths[48] = 500; + BstWidthCalculator.widths[49] = 500; + BstWidthCalculator.widths[50] = 500; + BstWidthCalculator.widths[51] = 500; + BstWidthCalculator.widths[52] = 500; + BstWidthCalculator.widths[53] = 500; + BstWidthCalculator.widths[54] = 500; + BstWidthCalculator.widths[55] = 500; + BstWidthCalculator.widths[56] = 500; + BstWidthCalculator.widths[57] = 500; + BstWidthCalculator.widths[58] = 278; + BstWidthCalculator.widths[59] = 278; + BstWidthCalculator.widths[60] = 278; + BstWidthCalculator.widths[61] = 778; + BstWidthCalculator.widths[62] = 472; + BstWidthCalculator.widths[63] = 472; + BstWidthCalculator.widths[64] = 778; + BstWidthCalculator.widths[65] = 750; + BstWidthCalculator.widths[66] = 708; + BstWidthCalculator.widths[67] = 722; + BstWidthCalculator.widths[68] = 764; + BstWidthCalculator.widths[69] = 681; + BstWidthCalculator.widths[70] = 653; + BstWidthCalculator.widths[71] = 785; + BstWidthCalculator.widths[72] = 750; + BstWidthCalculator.widths[73] = 361; + BstWidthCalculator.widths[74] = 514; + BstWidthCalculator.widths[75] = 778; + BstWidthCalculator.widths[76] = 625; + BstWidthCalculator.widths[77] = 917; + BstWidthCalculator.widths[78] = 750; + BstWidthCalculator.widths[79] = 778; + BstWidthCalculator.widths[80] = 681; + BstWidthCalculator.widths[81] = 778; + BstWidthCalculator.widths[82] = 736; + BstWidthCalculator.widths[83] = 556; + BstWidthCalculator.widths[84] = 722; + BstWidthCalculator.widths[85] = 750; + BstWidthCalculator.widths[86] = 750; + BstWidthCalculator.widths[87] = 1028; + BstWidthCalculator.widths[88] = 750; + BstWidthCalculator.widths[89] = 750; + BstWidthCalculator.widths[90] = 611; + BstWidthCalculator.widths[91] = 278; + BstWidthCalculator.widths[92] = 500; + BstWidthCalculator.widths[93] = 278; + BstWidthCalculator.widths[94] = 500; + BstWidthCalculator.widths[95] = 278; + BstWidthCalculator.widths[96] = 278; + BstWidthCalculator.widths[97] = 500; + BstWidthCalculator.widths[98] = 556; + BstWidthCalculator.widths[99] = 444; + BstWidthCalculator.widths[100] = 556; + BstWidthCalculator.widths[101] = 444; + BstWidthCalculator.widths[102] = 306; + BstWidthCalculator.widths[103] = 500; + BstWidthCalculator.widths[104] = 556; + BstWidthCalculator.widths[105] = 278; + BstWidthCalculator.widths[106] = 306; + BstWidthCalculator.widths[107] = 528; + BstWidthCalculator.widths[108] = 278; + BstWidthCalculator.widths[109] = 833; + BstWidthCalculator.widths[110] = 556; + BstWidthCalculator.widths[111] = 500; + BstWidthCalculator.widths[112] = 556; + BstWidthCalculator.widths[113] = 528; + BstWidthCalculator.widths[114] = 392; + BstWidthCalculator.widths[115] = 394; + BstWidthCalculator.widths[116] = 389; + BstWidthCalculator.widths[117] = 556; + BstWidthCalculator.widths[118] = 528; + BstWidthCalculator.widths[119] = 722; + BstWidthCalculator.widths[120] = 528; + BstWidthCalculator.widths[121] = 528; + BstWidthCalculator.widths[122] = 444; + BstWidthCalculator.widths[123] = 500; + BstWidthCalculator.widths[124] = 1000; + BstWidthCalculator.widths[125] = 500; + BstWidthCalculator.widths[126] = 500; + } + } + + private BstWidthCalculator() { + } + + private static int getSpecialCharWidth(char[] c, int pos) { + if ((pos + 1) < c.length) { + if ((c[pos] == 'o') && (c[pos + 1] == 'e')) { + return 778; + } + if ((c[pos] == 'O') && (c[pos + 1] == 'E')) { + return 1014; + } + if ((c[pos] == 'a') && (c[pos + 1] == 'e')) { + return 722; + } + if ((c[pos] == 'A') && (c[pos + 1] == 'E')) { + return 903; + } + if ((c[pos] == 's') && (c[pos + 1] == 's')) { + return 500; + } + } + return BstWidthCalculator.getCharWidth(c[pos]); + } + + public static int getCharWidth(char c) { + if ((c >= 0) && (c < 128)) { + return BstWidthCalculator.widths[c]; + } else { + return 0; + } + } + + public static int width(String toMeasure) { + /* + * From Bibtex: We use the natural width for all but special characters, + * and we complain if the string isn't brace-balanced. + */ + + int i = 0; + int n = toMeasure.length(); + int braceLevel = 0; + char[] c = toMeasure.toCharArray(); + int result = 0; + + /* + * From Bibtex: + * + * We use the natural widths of all characters except that some + * characters have no width: braces, control sequences (except for the + * usual 13 accented and foreign characters, whose widths are given in + * the next module), and |white_space| following control sequences (even + * a null control sequence). + * + */ + while (i < n) { + if (c[i] == '{') { + braceLevel++; + if ((braceLevel == 1) && ((i + 1) < n) && (c[i + 1] == '\\')) { + i++; // skip brace + while ((i < n) && (braceLevel > 0)) { + i++; // skip backslash + + int afterBackslash = i; + while ((i < n) && Character.isLetter(c[i])) { + i++; + } + if ((i < n) && (i == afterBackslash)) { + i++; // Skip non-alpha control seq + } else { + if (BstCaseChanger.findSpecialChar(c, afterBackslash).isPresent()) { + result += BstWidthCalculator.getSpecialCharWidth(c, afterBackslash); + } + } + while ((i < n) && Character.isWhitespace(c[i])) { + i++; + } + while ((i < n) && (braceLevel > 0) && (c[i] != '\\')) { + if (c[i] == '}') { + braceLevel--; + } else if (c[i] == '{') { + braceLevel++; + } else { + result += BstWidthCalculator.getCharWidth(c[i]); + } + i++; + } + } + continue; + } + } else if (c[i] == '}') { + if (braceLevel > 0) { + braceLevel--; + } else { + LOGGER.warn("Too many closing braces in string: " + toMeasure); + } + } + result += BstWidthCalculator.getCharWidth(c[i]); + i++; + } + if (braceLevel > 0) { + LOGGER.warn("No enough closing braces in string: " + toMeasure); + } + return result; + } +} diff --git a/src/main/java/org/jabref/logic/layout/format/NameFormatter.java b/src/main/java/org/jabref/logic/layout/format/NameFormatter.java index 919ff4df2d5..e8812235435 100644 --- a/src/main/java/org/jabref/logic/layout/format/NameFormatter.java +++ b/src/main/java/org/jabref/logic/layout/format/NameFormatter.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.Objects; -import org.jabref.logic.bst.BibtexNameFormatter; +import org.jabref.logic.bst.util.BstNameFormatter; import org.jabref.logic.layout.LayoutFormatter; import org.jabref.model.entry.AuthorList; @@ -86,7 +86,7 @@ private static String format(String toFormat, AuthorList al, String[] formats) { for (int i = 1; i <= al.getNumberOfAuthors(); i++) { for (int j = 1; j < formats.length; j += 2) { if ("*".equals(formats[j])) { - sb.append(BibtexNameFormatter.formatName(toFormat, i, formats[j + 1], null)); + sb.append(BstNameFormatter.formatName(toFormat, i, formats[j + 1])); break; } else { String[] range = formats[j].split("\\.\\."); @@ -112,7 +112,7 @@ private static String format(String toFormat, AuthorList al, String[] formats) { } if ((s <= i) && (i <= e)) { - sb.append(BibtexNameFormatter.formatName(toFormat, i, formats[j + 1], null)); + sb.append(BstNameFormatter.formatName(toFormat, i, formats[j + 1])); break; } } diff --git a/src/test/java/org/jabref/logic/bst/BstFunctionsTest.java b/src/test/java/org/jabref/logic/bst/BstFunctionsTest.java new file mode 100644 index 00000000000..1b63e26cbb3 --- /dev/null +++ b/src/test/java/org/jabref/logic/bst/BstFunctionsTest.java @@ -0,0 +1,666 @@ +package org.jabref.logic.bst; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.jabref.logic.bst.util.BstCaseChangersTest; +import org.jabref.logic.bst.util.BstNameFormatterTest; +import org.jabref.logic.bst.util.BstPurifierTest; +import org.jabref.logic.bst.util.BstTextPrefixerTest; +import org.jabref.logic.bst.util.BstWidthCalculatorTest; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.antlr.v4.runtime.RecognitionException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * For additional tests see for + *
    + *
  • purify: {@link BstPurifierTest}
  • + *
  • width: {@link BstWidthCalculatorTest}
  • + *
  • format.name: {@link BstNameFormatterTest}
  • + *
  • change.case: {@link BstCaseChangersTest}
  • + *
  • prefix: {@link BstTextPrefixerTest}
  • + *
+ */ +class BstFunctionsTest { + @Test + public void testCompareFunctions() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test.compare } { + #5 #5 = % TRUE + #1 #2 = % FALSE + #3 #4 < % TRUE + #4 #3 < % FALSE + #4 #4 < % FALSE + #3 #4 > % FALSE + #4 #3 > % TRUE + #4 #4 > % FALSE + "H" "H" = % TRUE + "H" "Ha" = % FALSE + } + EXECUTE { test.compare } + """); + + vm.render(Collections.emptyList()); + + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testArithmeticFunctions() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + #1 #1 + % 2 + #5 #2 - % 3 + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(3, vm.getStack().pop()); + assertEquals(2, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testArithmeticFunctionTypeMismatch() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + #1 "HELLO" + % Should throw exception + } + EXECUTE { test } + """); + + assertThrows(BstVMException.class, () -> vm.render(Collections.emptyList())); + } + + @Test + public void testStringOperations() throws RecognitionException { + // Test for concat (*) and add.period + BstVM vm = new BstVM(""" + FUNCTION { test } { + "H" "ello" * % Hello + "Johnny" add.period$ % Johnny. + "Johnny." add.period$ % Johnny. + "Johnny!" add.period$ % Johnny! + "Johnny?" add.period$ % Johnny? + "Johnny} }}}" add.period$ % Johnny.} + "Johnny!}" add.period$ % Johnny!} + "Johnny?}" add.period$ % Johnny?} + "Johnny.}" add.period$ % Johnny.} + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("Johnny.}", vm.getStack().pop()); + assertEquals("Johnny?}", vm.getStack().pop()); + assertEquals("Johnny!}", vm.getStack().pop()); + assertEquals("Johnny.}", vm.getStack().pop()); + assertEquals("Johnny?", vm.getStack().pop()); + assertEquals("Johnny!", vm.getStack().pop()); + assertEquals("Johnny.", vm.getStack().pop()); + assertEquals("Johnny.", vm.getStack().pop()); + assertEquals("Hello", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testMissing() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + READ + SORT + FUNCTION { test } { title missing$ cite$ } + ITERATE { test } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "No title")); + + vm.render(testEntries); + + assertEquals("test", vm.getStack().pop()); // cite + assertEquals(BstVM.TRUE, vm.getStack().pop()); // missing title + assertEquals("canh05", vm.getStack().pop()); // cite + assertEquals(BstVM.FALSE, vm.getStack().pop()); // missing title + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testNumNames() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + "Johnny Foo { and } Mary Bar" num.names$ + "Johnny Foo and Mary Bar" num.names$ + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(2, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testSubstring() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + "123456789" #2 #1 substring$ % 2 + "123456789" #4 global.max$ substring$ % 456789 + "123456789" #1 #9 substring$ % 123456789 + "123456789" #1 #10 substring$ % 123456789 + "123456789" #1 #99 substring$ % 123456789 + "123456789" #-7 #3 substring$ % 123 + "123456789" #-1 #1 substring$ % 9 + "123456789" #-1 #3 substring$ % 789 + "123456789" #-2 #2 substring$ % 78 + } EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("78", vm.getStack().pop()); + assertEquals("789", vm.getStack().pop()); + assertEquals("9", vm.getStack().pop()); + assertEquals("123", vm.getStack().pop()); + assertEquals("123456789", vm.getStack().pop()); + assertEquals("123456789", vm.getStack().pop()); + assertEquals("123456789", vm.getStack().pop()); + assertEquals("456789", vm.getStack().pop()); + assertEquals("2", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testEmpty() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } { } { } + READ + STRINGS { s } + FUNCTION { test } { + s empty$ % TRUE + "" empty$ % TRUE + " " empty$ % TRUE + title empty$ % TRUE + " HALLO " empty$ % FALSE + } + ITERATE { test } + """); + List testEntry = List.of(new BibEntry(StandardEntryType.Article)); + + vm.render(testEntry); + + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testFormatNameStatic() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { format }{ "Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin" #1 "{vv~}{ll}{, jj}{, f}?" format.name$ } + EXECUTE { format } + """); + List v = Collections.emptyList(); + + vm.render(v); + + assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J?", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testFormatNameInEntries() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { author } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + READ + SORT + FUNCTION { format }{ author #2 "{vv~}{ll}{, jj}{, f}?" format.name$ } + ITERATE { format } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Book) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Jonathan Meyer and Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin")); + + vm.render(testEntries); + + assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J?", vm.getStack().pop()); + assertEquals("Annabi, H?", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testChangeCase() throws RecognitionException { + BstVM vm = new BstVM(""" + STRINGS { title } + READ + FUNCTION { format.title } { + duplicate$ empty$ + { pop$ "" } + { "t" change.case$ } + if$ + } + FUNCTION { test } { + "hello world" "u" change.case$ format.title + "Hello World" format.title + "" format.title + "{A}{D}/{C}ycle: {I}{B}{M}'s {F}ramework for {A}pplication {D}evelopment and {C}ase" "u" change.case$ format.title + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("{A}{D}/{C}ycle: {I}{B}{M}'s {F}ramework for {A}pplication {D}evelopment and {C}ase", + vm.getStack().pop()); + assertEquals("", vm.getStack().pop()); + assertEquals("Hello world", vm.getStack().pop()); + assertEquals("Hello world", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testTextLength() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + "hello world" text.length$ % 11 + "Hello {W}orld" text.length$ % 11 + "" text.length$ % 0 + "{A}{D}/{Cycle}" text.length$ % 8 + "{\\This is one character}" text.length$ % 1 + "{\\This {is} {one} {c{h}}aracter as well}" text.length$ % 1 + "{\\And this too" text.length$ % 1 + "These are {\\11}" text.length$ % 11 + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(11, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(8, vm.getStack().pop()); + assertEquals(0, vm.getStack().pop()); + assertEquals(11, vm.getStack().pop()); + assertEquals(11, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testIntToStr() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { #3 int.to.str$ #9999 int.to.str$ } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("9999", vm.getStack().pop()); + assertEquals("3", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testChrToInt() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { "H" chr.to.int$ } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(72, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testChrToIntIntToChr() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { "H" chr.to.int$ int.to.chr$ } + EXECUTE {test} + """); + + vm.render(Collections.emptyList()); + + assertEquals("H", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testType() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + SORT + FUNCTION { test } { type$ } + ITERATE { test } + """); + List testEntries = List.of( + new BibEntry(StandardEntryType.Article).withCitationKey("a"), + new BibEntry(StandardEntryType.Book).withCitationKey("b"), + new BibEntry(StandardEntryType.Misc).withCitationKey("c"), + new BibEntry(StandardEntryType.InProceedings).withCitationKey("d")); + + vm.render(testEntries); + + assertEquals("inproceedings", vm.getStack().pop()); + assertEquals("misc", vm.getStack().pop()); + assertEquals("book", vm.getStack().pop()); + assertEquals("article", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testCallType() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + READ + SORT + FUNCTION { inproceedings }{ "InProceedings called on " title * } + FUNCTION { book }{ "Book called on " title * } + ITERATE { call.type$ } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Book) + .withCitationKey("test") + .withField(StandardField.TITLE, "Test")); + + vm.render(testEntries); + + assertEquals("Book called on Test", vm.getStack().pop()); + assertEquals( + "InProceedings called on Effective work practices for floss development: A model and propositions", + vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testSwap() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { a } { #3 "Hallo" swap$ } + EXECUTE { a } + """); + + List v = Collections.emptyList(); + vm.render(v); + + assertEquals(3, vm.getStack().pop()); + assertEquals("Hallo", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + void testAssignFunction() { + BstVM vm = new BstVM(""" + INTEGERS { test.var } + FUNCTION { test.func } { #1 'test.var := } + EXECUTE { test.func } + """); + + vm.render(Collections.emptyList()); + + Map functions = vm.latestContext.functions(); + assertTrue(functions.containsKey("test.func")); + assertNotNull(functions.get("test.func")); + assertEquals(1, vm.latestContext.integers().get("test.var")); + } + + @Test + void testSimpleIf() { + BstVM vm = new BstVM(""" + FUNCTION { path1 } { #1 } + FUNCTION { path0 } { #0 } + FUNCTION { test } { + #1 path1 path0 if$ + #0 path1 path0 if$ + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(0, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + void testSimpleWhile() { + BstVM vm = new BstVM(""" + INTEGERS { i } + FUNCTION { test } { + #3 'i := + { i } + { + i + i #1 - + 'i := + } + while$ + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(1, vm.getStack().pop()); + assertEquals(2, vm.getStack().pop()); + assertEquals(3, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testNestedControlFunctions() throws RecognitionException { + BstVM vm = new BstVM(""" + STRINGS { t } + FUNCTION { not } { { #0 } { #1 } if$ } + FUNCTION { n.dashify } { + "HELLO-WORLD" 't := + "" + { t empty$ not } % while + { + t #1 #1 substring$ "-" = % if + { + t #1 #2 substring$ "--" = not % if + { + "--" * + t #2 global.max$ substring$ 't := + } + { + { t #1 #1 substring$ "-" = } % while + { + "-" * + t #2 global.max$ substring$ 't := + } + while$ + } + if$ + } + { + t #1 #1 substring$ * + t #2 global.max$ substring$ 't := + } + if$ + } + while$ + } + EXECUTE { n.dashify } + """); + List v = Collections.emptyList(); + + vm.render(v); + + assertEquals(1, vm.getStack().size()); + assertEquals("HELLO--WORLD", vm.getStack().pop()); + } + + @Test + public void testLogic() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { not } { { #0 } { #1 } if$ } + FUNCTION { and } { 'skip$ { pop$ #0 } if$ } + FUNCTION { or } { { pop$ #1 } 'skip$ if$ } + FUNCTION { test } { + #1 #1 and + #0 #1 and + #1 #0 and + #0 #0 and + #0 not + #1 not + #1 #1 or + #0 #1 or + #1 #0 or + #0 #0 or + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + /** + * See also {@link BstWidthCalculatorTest} + */ + + @Test + public void testWidth() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { address author title type } { } { label } + STRINGS { longest.label } + INTEGERS { number.label longest.label.width } + FUNCTION { initialize.longest.label } { + "" 'longest.label := + #1 'number.label := + #0 'longest.label.width := + } + FUNCTION {longest.label.pass} { + number.label int.to.str$ 'label := + number.label #1 + 'number.label := + label width$ longest.label.width > + { + label 'longest.label := + label width$ 'longest.label.width := + } + 'skip$ + if$ + } + EXECUTE { initialize.longest.label } + ITERATE { longest.label.pass } + FUNCTION { begin.bib } { + preamble$ empty$ + 'skip$ + { preamble$ write$ newline$ } + if$ + "\\begin{thebibliography}{" longest.label * "}" * + } + EXECUTE {begin.bib} + """); + + List testEntries = List.of(BstVMTest.defaultTestEntry()); + + vm.render(testEntries); + + assertTrue(vm.latestContext.integers().containsKey("longest.label.width")); + assertEquals("\\begin{thebibliography}{1}", vm.getStack().pop()); + } + + @Test + public void testDuplicateEmptyPopSwapIf() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { emphasize } { + duplicate$ empty$ + { pop$ "" } + { "{\\em " swap$ * "}" * } + if$ + } + FUNCTION { test } { + "" emphasize + "Hello" emphasize + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("{\\em Hello}", vm.getStack().pop()); + assertEquals("", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testPreambleWriteNewlineQuote() { + BstVM vm = new BstVM(""" + FUNCTION { test } { + preamble$ + write$ + newline$ + "hello" + write$ + quote$ "quoted" * quote$ * + write$ + } + EXECUTE { test } + """); + + BibDatabase testDatabase = new BibDatabase(); + testDatabase.setPreamble("A Preamble"); + + String result = vm.render(Collections.emptyList(), testDatabase); + + assertEquals("A Preamble\nhello\"quoted\"", result); + } +} diff --git a/src/test/java/org/jabref/logic/bst/BstVMTest.java b/src/test/java/org/jabref/logic/bst/BstVMTest.java new file mode 100644 index 00000000000..4a66a8d5836 --- /dev/null +++ b/src/test/java/org/jabref/logic/bst/BstVMTest.java @@ -0,0 +1,220 @@ +package org.jabref.logic.bst; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.antlr.v4.runtime.RecognitionException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BstVMTest { + + public static BibEntry defaultTestEntry() { + return new BibEntry(StandardEntryType.InProceedings) + .withCitationKey("canh05") + .withField(StandardField.AUTHOR, "Crowston, K. and Annabi, H. and Howison, J. and Masango, C.") + .withField(StandardField.TITLE, "Effective work practices for floss development: A model and propositions") + .withField(StandardField.BOOKTITLE, "Hawaii International Conference On System Sciences (HICSS)") + .withField(StandardField.YEAR, "2005") + .withField(StandardField.OWNER, "oezbek") + .withField(StandardField.TIMESTAMP, "2006.05.29") + .withField(StandardField.URL, "http://james.howison.name/publications.html"); + } + + @Test + public void testAbbrv() throws RecognitionException, IOException { + BstVM vm = new BstVM(Path.of("src/test/resources/org/jabref/logic/bst/abbrv.bst")); + List testEntries = List.of(defaultTestEntry()); + + String expected = "\\begin{thebibliography}{1}\\bibitem{canh05}K.~Crowston, H.~Annabi, J.~Howison, and C.~Masango.\\newblock Effective work practices for floss development: A model and propositions.\\newblock In {\\em Hawaii International Conference On System Sciences (HICSS)}, 2005.\\end{thebibliography}"; + String result = vm.render(testEntries); + + assertEquals( + expected.replaceAll("\\s", ""), + result.replaceAll("\\s", "")); + } + + @Test + public void testSimple() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { address author title type } { } { label } + INTEGERS { output.state before.all mid.sentence after.sentence after.block } + FUNCTION { init.state.consts }{ + #0 'before.all := + #1 'mid.sentence := + #2 'after.sentence := + #3 'after.block := + } + STRINGS { s t } + READ + """); + List testEntries = List.of(defaultTestEntry()); + + vm.render(testEntries); + + assertEquals(2, vm.latestContext.strings().size()); + assertEquals(7, vm.latestContext.integers().size()); + assertEquals(1, vm.latestContext.entries().size()); + assertEquals(5, vm.latestContext.entries().get(0).fields.size()); + assertEquals(38, vm.latestContext.functions().size()); + } + + @Test + public void testLabel() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } {} { label } + FUNCTION { test } { + label #0 = + title 'label := + #5 label #6 pop$ } + READ + ITERATE { test } + """); + List testEntries = List.of(defaultTestEntry()); + + vm.render(testEntries); + + assertEquals( + "Effective work practices for floss development: A model and propositions", + vm.latestContext.stack().pop()); + } + + @Test + public void testQuote() throws RecognitionException { + BstVM vm = new BstVM("FUNCTION { a }{ quote$ quote$ * } EXECUTE { a }"); + + vm.render(Collections.emptyList()); + assertEquals("\"\"", vm.latestContext.stack().pop()); + } + + @Test + public void testBuildIn() throws RecognitionException { + BstVM vm = new BstVM("EXECUTE { global.max$ }"); + + vm.render(Collections.emptyList()); + + assertEquals(Integer.MAX_VALUE, vm.latestContext.stack().pop()); + assertTrue(vm.latestContext.stack().empty()); + } + + @Test + public void testVariables() throws RecognitionException { + BstVM vm = new BstVM(""" + STRINGS { t } + FUNCTION { not } { + { #0 } { #1 } if$ + } + FUNCTION { n.dashify } { + "HELLO-WORLD" 't := + t empty$ not + } + EXECUTE { n.dashify } + """); + + vm.render(Collections.emptyList()); + + assertEquals(BstVM.TRUE, vm.latestContext.stack().pop()); + } + + @Test + public void testHypthenatedName() throws RecognitionException, IOException { + BstVM vm = new BstVM(Path.of("src/test/resources/org/jabref/logic/bst/abbrv.bst")); + List testEntries = List.of( + new BibEntry(StandardEntryType.Article) + .withCitationKey("canh05") + .withField(StandardField.AUTHOR, "Jean-Paul Sartre") + ); + + String result = vm.render(testEntries); + + assertTrue(result.contains("J.-P. Sartre")); + } + + @Test + void testAbbrevStyleChopWord() { + BstVM vm = new BstVM(""" + STRINGS { s } + INTEGERS { len } + + FUNCTION { chop.word } + { + 's := + 'len := + s #1 len substring$ = + { s len #1 + global.max$ substring$ } + 's + if$ + } + + FUNCTION { test } { + "A " #2 + "A Colorful Morning" + chop.word + + "An " #3 + "A Colorful Morning" + chop.word + } + + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("A Colorful Morning", vm.latestContext.stack().pop()); + assertEquals("Colorful Morning", vm.latestContext.stack().pop()); + assertEquals(0, vm.latestContext.stack().size()); + } + + @Test + void testAbbrevStyleSortFormatTitle() { + BstVM vm = new BstVM(""" + STRINGS { s t } + INTEGERS { len } + FUNCTION { sortify } { + purify$ + "l" change.case$ + } + + FUNCTION { chop.word } + { + 's := + 'len := + s #1 len substring$ = + { s len #1 + global.max$ substring$ } + 's + if$ + } + + FUNCTION { sort.format.title } + { 't := + "A " #2 + "An " #3 + "The " #4 t chop.word + chop.word + chop.word + sortify + #1 global.max$ substring$ + } + + FUNCTION { test } { + "A Colorful Morning" + sort.format.title + } + + EXECUTE {test} + """); + + vm.render(Collections.emptyList()); + + assertEquals("colorful morning", vm.latestContext.stack().pop()); + } +} diff --git a/src/test/java/org/jabref/logic/bst/BstVMVisitorTest.java b/src/test/java/org/jabref/logic/bst/BstVMVisitorTest.java new file mode 100644 index 00000000000..366ba0a9d55 --- /dev/null +++ b/src/test/java/org/jabref/logic/bst/BstVMVisitorTest.java @@ -0,0 +1,249 @@ +package org.jabref.logic.bst; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.types.StandardEntryType; + +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.tree.ParseTree; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BstVMVisitorTest { + + @Test + public void testVisitStringsCommand() { + BstVM vm = new BstVM("STRINGS { test.string1 test.string2 test.string3 }"); + + vm.render(Collections.emptyList()); + + Map strList = vm.latestContext.strings(); + assertTrue(strList.containsKey("test.string1")); + assertNull(strList.get("test.string1")); + assertTrue(strList.containsKey("test.string2")); + assertNull(strList.get("test.string2")); + assertTrue(strList.containsKey("test.string3")); + assertNull(strList.get("test.string3")); + } + + @Test + public void testVisitIntegersCommand() { + BstVM vm = new BstVM("INTEGERS { variable.a variable.b variable.c }"); + + vm.render(Collections.emptyList()); + + Map integersList = vm.latestContext.integers(); + assertTrue(integersList.containsKey("variable.a")); + assertEquals(0, integersList.get("variable.a")); + assertTrue(integersList.containsKey("variable.b")); + assertEquals(0, integersList.get("variable.b")); + assertTrue(integersList.containsKey("variable.c")); + assertEquals(0, integersList.get("variable.c")); + } + + @Test + void testVisitFunctionCommand() { + BstVM vm = new BstVM(""" + FUNCTION { test.func } { #1 'test.var := } + EXECUTE { test.func } + """); + + vm.render(Collections.emptyList()); + + Map functions = vm.latestContext.functions(); + assertTrue(functions.containsKey("test.func")); + assertNotNull(functions.get("test.func")); + } + + @Test + void testVisitMacroCommand() { + BstVM vm = new BstVM(""" + MACRO { jan } { "January" } + EXECUTE { jan } + """); + + vm.render(Collections.emptyList()); + + Map functions = vm.latestContext.functions(); + assertTrue(functions.containsKey("jan")); + assertNotNull(functions.get("jan")); + assertEquals("January", vm.latestContext.stack().pop()); + assertTrue(vm.latestContext.stack().isEmpty()); + } + + @Test + void testVisitEntryCommand() { + BstVM vm = new BstVM("ENTRY { address author title type } { variable } { label }"); + List testEntries = List.of(BstVMTest.defaultTestEntry()); + + vm.render(testEntries); + + BstEntry bstEntry = vm.latestContext.entries().get(0); + assertTrue(bstEntry.fields.containsKey("address")); + assertTrue(bstEntry.fields.containsKey("author")); + assertTrue(bstEntry.fields.containsKey("title")); + assertTrue(bstEntry.fields.containsKey("type")); + assertTrue(bstEntry.localIntegers.containsKey("variable")); + assertTrue(bstEntry.localStrings.containsKey("label")); + assertTrue(bstEntry.localStrings.containsKey("sort.key$")); + } + + @Test + void testVisitReadCommand() { + BstVM vm = new BstVM(""" + ENTRY { author title booktitle year owner timestamp url } { } { } + READ + """); + List testEntries = List.of(BstVMTest.defaultTestEntry()); + + vm.render(testEntries); + + Map fields = vm.latestContext.entries().get(0).fields; + assertEquals("Crowston, K. and Annabi, H. and Howison, J. and Masango, C.", fields.get("author")); + assertEquals("Effective work practices for floss development: A model and propositions", fields.get("title")); + assertEquals("Hawaii International Conference On System Sciences (HICSS)", fields.get("booktitle")); + assertEquals("2005", fields.get("year")); + assertEquals("oezbek", fields.get("owner")); + assertEquals("2006.05.29", fields.get("timestamp")); + assertEquals("http://james.howison.name/publications.html", fields.get("url")); + } + + @Test + public void testVisitExecuteCommand() throws RecognitionException { + BstVM vm = new BstVM(""" + INTEGERS { variable.a } + FUNCTION { init.state.consts } { #5 'variable.a := } + EXECUTE { init.state.consts } + """); + + vm.render(Collections.emptyList()); + + assertEquals(5, vm.latestContext.integers().get("variable.a")); + } + + @Test + public void testVisitIterateCommand() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { } { } { } + FUNCTION { test } { cite$ } + READ + ITERATE { test } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Article) + .withCitationKey("test")); + + vm.render(testEntries); + + assertEquals(2, vm.getStack().size()); + assertEquals("test", vm.getStack().pop()); + assertEquals("canh05", vm.getStack().pop()); + } + + @Test + public void testVisitReverseCommand() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { } { } { } + FUNCTION { test } { cite$ } + READ + REVERSE { test } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Article) + .withCitationKey("test")); + + vm.render(testEntries); + + assertEquals(2, vm.getStack().size()); + assertEquals("canh05", vm.getStack().pop()); + assertEquals("test", vm.getStack().pop()); + } + + @Test + public void testVisitSortCommand() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + SORT + """); + List testEntries = List.of( + new BibEntry(StandardEntryType.Article).withCitationKey("c"), + new BibEntry(StandardEntryType.Article).withCitationKey("b"), + new BibEntry(StandardEntryType.Article).withCitationKey("d"), + new BibEntry(StandardEntryType.Article).withCitationKey("a")); + + vm.render(testEntries); + + List sortedEntries = vm.latestContext.entries(); + assertEquals(Optional.of("a"), sortedEntries.get(0).entry.getCitationKey()); + assertEquals(Optional.of("b"), sortedEntries.get(1).entry.getCitationKey()); + assertEquals(Optional.of("c"), sortedEntries.get(2).entry.getCitationKey()); + assertEquals(Optional.of("d"), sortedEntries.get(3).entry.getCitationKey()); + } + + @Test + void testVisitIdentifier() { + BstVM vm = new BstVM(""" + ENTRY { } { local.variable } { local.label } + READ + STRINGS { label } + INTEGERS { variable } + FUNCTION { test } { + #1 'local.variable := + #2 'variable := + "TEST" 'local.label := + "TEST-GLOBAL" 'label := + local.label local.variable + label variable + } + ITERATE { test } + """); + List testEntries = List.of(BstVMTest.defaultTestEntry()); + + vm.render(testEntries); + + assertEquals(2, vm.getStack().pop()); + assertEquals("TEST-GLOBAL", vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals("TEST", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + void testVisitStackitem() { + BstVM vm = new BstVM(""" + STRINGS { t } + FUNCTION { test2 } { #3 } + FUNCTION { test } { + "HELLO" + #1 + 't + { #2 } + test2 + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(3, vm.getStack().pop()); + assertTrue(vm.getStack().pop() instanceof ParseTree); + assertEquals(new BstVMVisitor.Identifier("t"), vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals("HELLO", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + // stackitem +} diff --git a/src/test/java/org/jabref/logic/bst/TestVM.java b/src/test/java/org/jabref/logic/bst/TestVM.java deleted file mode 100644 index bb9fc8ad193..00000000000 --- a/src/test/java/org/jabref/logic/bst/TestVM.java +++ /dev/null @@ -1,634 +0,0 @@ -package org.jabref.logic.bst; - -import java.io.File; -import java.io.IOException; -import java.io.StringReader; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.StackFunction; -import org.jabref.logic.importer.ImportFormatPreferences; -import org.jabref.logic.importer.ParserResult; -import org.jabref.logic.importer.fileformat.BibtexParser; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.util.DummyFileUpdateMonitor; - -import org.antlr.runtime.RecognitionException; -import org.junit.jupiter.api.Test; -import org.mockito.Answers; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; - -public class TestVM { - - @Test - public void testAbbrv() throws RecognitionException, IOException { - VM vm = new VM(new File("src/test/resources/org/jabref/logic/bst/abbrv.bst")); - List v = List.of(t1BibtexEntry()); - - String expected = "\\begin{thebibliography}{1}\\bibitem{canh05}K.~Crowston, H.~Annabi, J.~Howison, and C.~Masango.\\newblock Effective work practices for floss development: A model and propositions.\\newblock In {\\em Hawaii International Conference On System Sciences (HICSS)}, 2005.\\end{thebibliography}"; - - assertEquals(expected.replaceAll("\\s", ""), vm.run(v).replaceAll("\\s", "")); - } - - @Test - public void testVMSimple() throws RecognitionException, IOException { - - VM vm = new VM("ENTRY { " + " address " + " author " + " title " + " type " - + "} {} { label }" + "INTEGERS { output.state before.all" - + " mid.sentence after.sentence after.block }" - + "FUNCTION {init.state.consts}{ #0 'before.all := " - + " #1 'mid.sentence := #2 'after.sentence := #3 'after.block := } " - + "STRINGS { s t } " + "READ"); - - List v = List.of(t1BibtexEntry()); - - vm.run(v); - - assertEquals(2, vm.getStrings().size()); - assertEquals(7, vm.getIntegers().size()); - assertEquals(1, vm.getEntries().size()); - assertEquals(5, vm.getEntries().get(0).fields.size()); - assertEquals(38, vm.getFunctions().size()); - } - - @Test - public void testLabel() throws RecognitionException, IOException { - VM vm = new VM("ENTRY { title } {} { label } " - + "FUNCTION { test } { label #0 = title 'label := #5 label #6 pop$ } " + "READ " - + "ITERATE { test }"); - - List v = List.of(t1BibtexEntry()); - - vm.run(v); - - assertEquals("Effective work practices for floss development: A model and propositions", vm - .getStack() - .pop()); - } - - @Test - public void testQuote() throws RecognitionException { - VM vm = new VM("FUNCTION {a}{ quote$ quote$ * } EXECUTE {a}"); - - vm.run(Collections.emptyList()); - assertEquals("\"\"", vm.getStack().pop()); - } - - @Test - public void testVMFunction1() throws RecognitionException { - VM vm = new VM("FUNCTION {init.state.consts}{ #0 'before.all := } "); - - vm.run(Collections.emptyList()); - - assertEquals(38, vm.getFunctions().size()); - - assertTrue(vm.getFunctions().get("init.state.consts") instanceof StackFunction); - - StackFunction fun = (StackFunction) vm.getFunctions().get("init.state.consts"); - assertEquals(3, fun.getTree().getChildCount()); - } - - @Test - public void testVMExecuteSimple() throws RecognitionException { - VM vm = new VM("INTEGERS { variable.a } " + "FUNCTION {init.state.consts}{ #5 'variable.a := } " - + "EXECUTE {init.state.consts}"); - - vm.run(Collections.emptyList()); - - assertEquals(Integer.valueOf(5), vm.getIntegers().get("variable.a")); - } - - @Test - public void testVMExecuteSimple2() throws RecognitionException { - VM vm = new VM("FUNCTION {a}{ #5 #5 = " + "#1 #2 = " + "#3 #4 < " + "#4 #3 < " - + "#4 #4 < " + "#3 #4 > " + "#4 #3 > " + "#4 #4 > " + "\"H\" \"H\" = " - + "\"H\" \"Ha\" = } " + "EXECUTE {a}"); - - vm.run(Collections.emptyList()); - - assertEquals(VM.FALSE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(VM.FALSE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(VM.FALSE, vm.getStack().pop()); - assertEquals(VM.FALSE, vm.getStack().pop()); - assertEquals(VM.FALSE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(VM.FALSE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testVMIfSkipPop() throws RecognitionException { - VM vm = new VM("FUNCTION {not} { { #0 } { #1 } if$ }" - + "FUNCTION {and} { 'skip$ { pop$ #0 } if$ }" - + "FUNCTION {or} { { pop$ #1 } 'skip$ if$ }" + "FUNCTION {test} { " - + "#1 #1 and #0 #1 and #1 #0 and #0 #0 and " + "#0 not #1 not " - + "#1 #1 or #0 #1 or #1 #0 or #0 #0 or }" + "EXECUTE {test}"); - - vm.run(Collections.emptyList()); - - assertEquals(VM.FALSE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(VM.FALSE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(VM.FALSE, vm.getStack().pop()); - assertEquals(VM.FALSE, vm.getStack().pop()); - assertEquals(VM.FALSE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testVMArithmetic() throws RecognitionException { - VM vm = new VM("FUNCTION {test} { " + "#1 #1 + #5 #2 - }" + "EXECUTE {test}"); - - vm.run(Collections.emptyList()); - - assertEquals(3, vm.getStack().pop()); - assertEquals(2, vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testVMArithmetic2() throws RecognitionException { - VM vm = new VM("FUNCTION {test} { " + "#1 \"HELLO\" + #5 #2 - }" + "EXECUTE {test}"); - assertThrows(VMException.class, () -> vm.run(Collections.emptyList())); - } - - @Test - public void testNumNames() throws RecognitionException { - VM vm = new VM("FUNCTION {test} { \"Johnny Foo and Mary Bar\" num.names$ }" + "EXECUTE {test}"); - - vm.run(Collections.emptyList()); - - assertEquals(2, vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testNumNames2() throws RecognitionException { - VM vm = new VM("FUNCTION {test} { \"Johnny Foo { and } Mary Bar\" num.names$ }" - + "EXECUTE {test}"); - - vm.run(Collections.emptyList()); - - assertEquals(1, vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testVMStringOps1() throws RecognitionException { - VM vm = new VM( - "FUNCTION {test} { \"H\" \"allo\" * \"Johnny\" add.period$ \"Johnny.\" add.period$" - + "\"Johnny!\" add.period$ \"Johnny?\" add.period$ \"Johnny} }}}\" add.period$" - + "\"Johnny!}\" add.period$ \"Johnny?}\" add.period$ \"Johnny.}\" add.period$ }" - + "EXECUTE {test}"); - - vm.run(Collections.emptyList()); - - assertEquals("Johnny.}", vm.getStack().pop()); - assertEquals("Johnny?}", vm.getStack().pop()); - assertEquals("Johnny!}", vm.getStack().pop()); - assertEquals("Johnny.}", vm.getStack().pop()); - assertEquals("Johnny?", vm.getStack().pop()); - assertEquals("Johnny!", vm.getStack().pop()); - assertEquals("Johnny.", vm.getStack().pop()); - assertEquals("Johnny.", vm.getStack().pop()); - assertEquals("Hallo", vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testSubstring() throws RecognitionException { - VM vm = new VM("FUNCTION {test} " + "{ \"123456789\" #2 #1 substring$ " + // 2 - " \"123456789\" #4 global.max$ substring$ " + // 456789 - " \"123456789\" #1 #9 substring$ " + // 123456789 - " \"123456789\" #1 #10 substring$ " + // 123456789 - " \"123456789\" #1 #99 substring$ " + // 123456789 - - " \"123456789\" #-7 #3 substring$ " + // 123 - " \"123456789\" #-1 #1 substring$ " + // 9 - " \"123456789\" #-1 #3 substring$ " + // 789 - " \"123456789\" #-2 #2 substring$ " + // 78 - - "} EXECUTE {test} "); - - vm.run(Collections.emptyList()); - - assertEquals("78", vm.getStack().pop()); - assertEquals("789", vm.getStack().pop()); - assertEquals("9", vm.getStack().pop()); - assertEquals("123", vm.getStack().pop()); - - assertEquals("123456789", vm.getStack().pop()); - assertEquals("123456789", vm.getStack().pop()); - assertEquals("123456789", vm.getStack().pop()); - assertEquals("456789", vm.getStack().pop()); - assertEquals("2", vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testEmpty() throws RecognitionException, IOException { - VM vm = new VM("ENTRY {title}{}{} READ STRINGS { s } FUNCTION {test} " + "{ s empty$ " + // FALSE - "\"\" empty$ " + // FALSE - "\" \" empty$ " + // FALSE - " title empty$ " + // FALSE - " \" HALLO \" empty$ } ITERATE {test} "); - - List v = List.of(TestVM.bibtexString2BibtexEntry("@article{a, author=\"AAA\"}")); - vm.run(v); - assertEquals(VM.FALSE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(VM.TRUE, vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testDuplicateEmptyPopSwapIf() throws RecognitionException { - VM vm = new VM("FUNCTION {emphasize} " + "{ duplicate$ empty$ " + " { pop$ \"\" } " - + " { \"{\\em \" swap$ * \"}\" * } " + " if$ " + "} " + "FUNCTION {test} {" - + " \"\" emphasize " + " \"Hello\" emphasize " + "}" + "EXECUTE {test} "); - - vm.run(Collections.emptyList()); - - assertEquals("{\\em Hello}", vm.getStack().pop()); - assertEquals("", vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testChangeCase() throws RecognitionException { - VM vm = new VM( - "STRINGS { title } " - + "READ " - + "FUNCTION {format.title}" - + " { duplicate$ empty$ " - + " { pop$ \"\" } " - + " { \"t\" change.case$ } " - + " if$ " - + "} " - + "FUNCTION {test} {" - + " \"hello world\" \"u\" change.case$ format.title " - + " \"Hello World\" format.title " - + " \"\" format.title " - + " \"{A}{D}/{C}ycle: {I}{B}{M}'s {F}ramework for {A}pplication {D}evelopment and {C}ase\" \"u\" change.case$ format.title " - + "}" + "EXECUTE {test} "); - - vm.run(Collections.emptyList()); - - assertEquals( - "{A}{D}/{C}ycle: {I}{B}{M}'s {F}ramework for {A}pplication {D}evelopment and {C}ase", - vm.getStack().pop()); - assertEquals("", vm.getStack().pop()); - assertEquals("Hello world", vm.getStack().pop()); - assertEquals("Hello world", vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testTextLength() throws RecognitionException { - VM vm = new VM("FUNCTION {test} {" + " \"hello world\" text.length$ " - + " \"Hello {W}orld\" text.length$ " + " \"\" text.length$ " - + " \"{A}{D}/{Cycle}\" text.length$ " - + " \"{\\This is one character}\" text.length$ " - + " \"{\\This {is} {one} {c{h}}aracter as well}\" text.length$ " - + " \"{\\And this too\" text.length$ " + " \"These are {\\11}\" text.length$ " + "} " - + "EXECUTE {test} "); - - vm.run(Collections.emptyList()); - - assertEquals(11, vm.getStack().pop()); - assertEquals(1, vm.getStack().pop()); - assertEquals(1, vm.getStack().pop()); - assertEquals(1, vm.getStack().pop()); - assertEquals(8, vm.getStack().pop()); - assertEquals(0, vm.getStack().pop()); - assertEquals(11, vm.getStack().pop()); - assertEquals(11, vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testVMIntToStr() throws RecognitionException { - VM vm = new VM("FUNCTION {test} { #3 int.to.str$ #9999 int.to.str$}" + "EXECUTE {test}"); - - vm.run(Collections.emptyList()); - - assertEquals("9999", vm.getStack().pop()); - assertEquals("3", vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testVMChrToInt() throws RecognitionException { - VM vm = new VM("FUNCTION {test} { \"H\" chr.to.int$ }" + "EXECUTE {test}"); - - vm.run(Collections.emptyList()); - - assertEquals(72, vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testVMChrToIntIntToChr() throws RecognitionException { - VM vm = new VM("FUNCTION {test} { \"H\" chr.to.int$ int.to.chr$ }" + "EXECUTE {test}"); - - vm.run(Collections.emptyList()); - - assertEquals("H", vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testSort() throws RecognitionException, IOException { - VM vm = new VM("ENTRY { title } { } { label }" - + "FUNCTION {presort} { cite$ 'sort.key$ := } ITERATE { presort } SORT"); - - List v = List.of( - TestVM.bibtexString2BibtexEntry("@article{a, author=\"AAA\"}"), - TestVM.bibtexString2BibtexEntry("@article{b, author=\"BBB\"}"), - TestVM.bibtexString2BibtexEntry("@article{d, author=\"DDD\"}"), - TestVM.bibtexString2BibtexEntry("@article{c, author=\"CCC\"}")); - vm.run(v); - - List v2 = vm.getEntries(); - assertEquals(Optional.of("a"), v2.get(0).entry.getCitationKey()); - assertEquals(Optional.of("b"), v2.get(1).entry.getCitationKey()); - assertEquals(Optional.of("c"), v2.get(2).entry.getCitationKey()); - assertEquals(Optional.of("d"), v2.get(3).entry.getCitationKey()); - } - - @Test - public void testBuildIn() throws RecognitionException { - VM vm = new VM("EXECUTE {global.max$}"); - - vm.run(Collections.emptyList()); - - assertEquals(Integer.MAX_VALUE, vm.getStack().pop()); - assertTrue(vm.getStack().empty()); - } - - @Test - public void testVariables() throws RecognitionException { - VM vm = new VM(" STRINGS { t } " - + " FUNCTION {not} { { #0 } { #1 } if$ } " - + " FUNCTION {n.dashify} { \"HELLO-WORLD\" 't := t empty$ not } " - + " EXECUTE {n.dashify} "); - - vm.run(Collections.emptyList()); - - assertEquals(VM.TRUE, vm.getStack().pop()); - } - - @Test - public void testWhile() throws RecognitionException { - VM vm = new VM( - "STRINGS { t } " - + "FUNCTION {not} { " - + " { #0 } { #1 } if$ } " - + "FUNCTION {n.dashify} " - + "{ \"HELLO-WORLD\" " - + " 't := " - + " \"\" " - + " { t empty$ not } " - + " { t #1 #1 substring$ \"-\" = " - + " { t #1 #2 substring$ \"--\" = not " - + " { \"--\" * " - + " t #2 global.max$ substring$ 't := " - + " } " - + " { { t #1 #1 substring$ \"-\" = } " - + " { \"-\" * " - + " t #2 global.max$ substring$ 't := " - + " } " - + " while$ " - + " } " - + " if$ " - + " } " - + " { t #1 #1 substring$ * " - + " t #2 global.max$ substring$ 't := " - + " } " - + " if$ " - + " } " - + " while$ " - + " } " - + " EXECUTE {n.dashify} "); - - List v = Collections.emptyList(); - vm.run(v); - - assertEquals(1, vm.getStack().size()); - assertEquals("HELLO--WORLD", vm.getStack().pop()); - } - - @Test - public void testType() throws RecognitionException, IOException { - VM vm = new VM("ENTRY { title } { } { label }" - + "FUNCTION {presort} { cite$ 'sort.key$ := } ITERATE { presort } SORT FUNCTION {test} { type$ } ITERATE { test }"); - - List v = List.of( - TestVM.bibtexString2BibtexEntry("@article{a, author=\"AAA\"}"), - TestVM.bibtexString2BibtexEntry("@book{b, author=\"BBB\"}"), - TestVM.bibtexString2BibtexEntry("@misc{c, author=\"CCC\"}"), - TestVM.bibtexString2BibtexEntry("@inproceedings{d, author=\"DDD\"}")); - vm.run(v); - - assertEquals(4, vm.getStack().size()); - assertEquals("inproceedings", vm.getStack().pop()); - assertEquals("misc", vm.getStack().pop()); - assertEquals("book", vm.getStack().pop()); - assertEquals("article", vm.getStack().pop()); - } - - @Test - public void testMissing() throws RecognitionException, IOException { - VM vm = new VM( - "ENTRY { title } { } { label } " + - "FUNCTION {presort} { cite$ 'sort.key$ := } " + - "ITERATE {presort} " + - "READ SORT " + - "FUNCTION {test}{ title missing$ cite$ } " + - "ITERATE { test }"); - - List v = List.of( - t1BibtexEntry(), - TestVM.bibtexString2BibtexEntry("@article{test, author=\"No title\"}")); - vm.run(v); - - assertEquals(4, vm.getStack().size()); - - assertEquals("test", vm.getStack().pop()); - assertEquals(1, vm.getStack().pop()); - assertEquals("canh05", vm.getStack().pop()); - assertEquals(0, vm.getStack().pop()); - } - - @Test - public void testFormatName() throws RecognitionException { - VM vm = new VM( - "FUNCTION {format}{ \"Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin\" #1 \"{vv~}{ll}{, jj}{, f}?\" format.name$ }" - + "EXECUTE {format}"); - - List v = Collections.emptyList(); - vm.run(v); - assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J?", vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testFormatName2() throws RecognitionException, IOException { - VM vm = new VM("ENTRY { author } { } { label } " + "FUNCTION {presort} { cite$ 'sort.key$ := } " - + "ITERATE { presort } " + "READ " + "SORT " - + "FUNCTION {format}{ author #2 \"{vv~}{ll}{, jj}{, f}?\" format.name$ }" + "ITERATE {format}"); - - List v = List.of( - t1BibtexEntry(), - TestVM.bibtexString2BibtexEntry( - "@book{test, author=\"Jonathan Meyer and Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin\"}")); - vm.run(v); - assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J?", vm.getStack().pop()); - assertEquals("Annabi, H?", vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testCallType() throws RecognitionException, IOException { - VM vm = new VM( - "ENTRY { title } { } { label } FUNCTION {presort} { cite$ 'sort.key$ := } ITERATE { presort } READ SORT " - + "FUNCTION {inproceedings}{ \"InProceedings called on \" title * } " - + "FUNCTION {book}{ \"Book called on \" title * } " + " ITERATE { call.type$ }"); - - List v = List.of( - t1BibtexEntry(), - TestVM.bibtexString2BibtexEntry("@book{test, title=\"Test\"}")); - - vm.run(v); - - assertEquals(2, vm.getStack().size()); - - assertEquals("Book called on Test", vm.getStack().pop()); - assertEquals( - "InProceedings called on Effective work practices for floss development: A model and propositions", - vm.getStack().pop()); - assertEquals(0, vm.getStack().size()); - } - - @Test - public void testIterate() throws RecognitionException, IOException { - VM vm = new VM("ENTRY { " + " address " + " author " + " title " + " type " - + "} {} { label } " + "FUNCTION {test}{ cite$ } " + "READ " + "ITERATE { test }"); - - List v = List.of( - t1BibtexEntry(), - TestVM.bibtexString2BibtexEntry("@article{test, title=\"BLA\"}")); - - vm.run(v); - - assertEquals(2, vm.getStack().size()); - - String s1 = (String) vm.getStack().pop(); - String s2 = (String) vm.getStack().pop(); - - if ("canh05".equals(s1)) { - assertEquals("test", s2); - } else { - assertEquals("canh05", s2); - assertEquals("test", s1); - } - } - - @Test - public void testWidth() throws RecognitionException, IOException { - VM vm = new VM("ENTRY { " + " address " + " author " + " title " + " type " - + "} {} { label } " + - "STRINGS { longest.label } " + - "INTEGERS { number.label longest.label.width } " + - "FUNCTION {initialize.longest.label} " + - "{ \"\" 'longest.label := " + - " #1 'number.label := " + - " #0 'longest.label.width := " + - "} " + - " " + - " FUNCTION {longest.label.pass} " + - " { number.label int.to.str$ 'label := " + - " number.label #1 + 'number.label := " + - " label width$ longest.label.width > " + - " { label 'longest.label := " + - " label width$ 'longest.label.width := " + - " } " + - " 'skip$ " + - " if$ " + - " } " + - " " + - " EXECUTE {initialize.longest.label} " + - " " + - " ITERATE {longest.label.pass} " + - "FUNCTION {begin.bib} " + - "{ preamble$ empty$" + - " 'skip$" + - " { preamble$ write$ newline$ }" + - " if$" + - " \"\\begin{thebibliography}{\" longest.label * \"}\" *" + - "}" + - "EXECUTE {begin.bib}"); - - List v = List.of(t1BibtexEntry()); - - vm.run(v); - - assertTrue(vm.getIntegers().containsKey("longest.label.width")); - assertEquals("\\begin{thebibliography}{1}", vm.getStack().pop()); - } - - @Test - public void testVMSwap() throws RecognitionException { - VM vm = new VM("FUNCTION {a}{ #3 \"Hallo\" swap$ } EXECUTE { a }"); - - List v = Collections.emptyList(); - vm.run(v); - - assertEquals(2, vm.getStack().size()); - assertEquals(3, vm.getStack().pop()); - assertEquals("Hallo", vm.getStack().pop()); - } - - @Test - public void testHypthenatedName() throws RecognitionException, IOException { - VM vm = new VM(new File("src/test/resources/org/jabref/logic/bst/abbrv.bst")); - List v = List.of(TestVM.bibtexString2BibtexEntry("@article{canh05, author = \"Jean-Paul Sartre\" }")); - assertTrue(vm.run(v).contains("J.-P. Sartre")); - } - - private static BibEntry bibtexString2BibtexEntry(String s) throws IOException { - ParserResult result = new BibtexParser(mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS), new DummyFileUpdateMonitor()).parse(new StringReader(s)); - Collection c = result.getDatabase().getEntries(); - assertEquals(1, c.size()); - return c.iterator().next(); - } - - private static String t1BibtexString() { - return "@inproceedings{canh05,\n" - + " author = {Crowston, K. and Annabi, H. and Howison, J. and Masango, C.},\n" - + " title = {Effective work practices for floss development: A model and propositions},\n" - + " booktitle = {Hawaii International Conference On System Sciences (HICSS)},\n" - + " year = {2005},\n" + " owner = {oezbek},\n" + " timestamp = {2006.05.29},\n" - + " url = {http://james.howison.name/publications.html}}\n"; - } - - private static BibEntry t1BibtexEntry() throws IOException { - return TestVM.bibtexString2BibtexEntry(t1BibtexString()); - } -} diff --git a/src/test/java/org/jabref/logic/bst/BibtexCaseChangersTest.java b/src/test/java/org/jabref/logic/bst/util/BstCaseChangersTest.java similarity index 93% rename from src/test/java/org/jabref/logic/bst/BibtexCaseChangersTest.java rename to src/test/java/org/jabref/logic/bst/util/BstCaseChangersTest.java index 5349be3ef3f..3bf615093e4 100644 --- a/src/test/java/org/jabref/logic/bst/BibtexCaseChangersTest.java +++ b/src/test/java/org/jabref/logic/bst/util/BstCaseChangersTest.java @@ -1,8 +1,8 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import java.util.stream.Stream; -import org.jabref.logic.bst.BibtexCaseChanger.FORMAT_MODE; +import org.jabref.logic.bst.util.BstCaseChanger.FormatMode; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -12,12 +12,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -public class BibtexCaseChangersTest { +public class BstCaseChangersTest { @ParameterizedTest @MethodSource("provideStringsForTitleLowers") public void testChangeCaseTitleLowers(String expected, String toBeFormatted) { - assertEquals(expected, BibtexCaseChanger.changeCase(toBeFormatted, FORMAT_MODE.TITLE_LOWERS)); + assertEquals(expected, BstCaseChanger.changeCase(toBeFormatted, FormatMode.TITLE_LOWERS)); } private static Stream provideStringsForTitleLowers() { @@ -59,7 +59,7 @@ private static Stream provideStringsForTitleLowers() { @ParameterizedTest @MethodSource("provideStringsForAllLowers") public void testChangeCaseAllLowers(String expected, String toBeFormatted) { - assertEquals(expected, BibtexCaseChanger.changeCase(toBeFormatted, FORMAT_MODE.ALL_LOWERS)); + assertEquals(expected, BstCaseChanger.changeCase(toBeFormatted, FormatMode.ALL_LOWERS)); } private static Stream provideStringsForAllLowers() { @@ -90,7 +90,7 @@ private static Stream provideStringsForAllLowers() { @ParameterizedTest @MethodSource("provideStringsForAllUppers") public void testChangeCaseAllUppers(String expected, String toBeFormatted) { - assertEquals(expected, BibtexCaseChanger.changeCase(toBeFormatted, FORMAT_MODE.ALL_UPPERS)); + assertEquals(expected, BstCaseChanger.changeCase(toBeFormatted, FormatMode.ALL_UPPERS)); } private static Stream provideStringsForAllUppers() { @@ -121,7 +121,7 @@ private static Stream provideStringsForAllUppers() { @ParameterizedTest @MethodSource("provideTitleCaseAllLowers") public void testTitleCaseAllLowers(String expected, String toBeFormatted) { - assertEquals(expected, BibtexCaseChanger.changeCase(toBeFormatted, FORMAT_MODE.ALL_LOWERS)); + assertEquals(expected, BstCaseChanger.changeCase(toBeFormatted, FormatMode.ALL_LOWERS)); } private static Stream provideTitleCaseAllLowers() { diff --git a/src/test/java/org/jabref/logic/bst/BibtexNameFormatterTest.java b/src/test/java/org/jabref/logic/bst/util/BstNameFormatterTest.java similarity index 52% rename from src/test/java/org/jabref/logic/bst/BibtexNameFormatterTest.java rename to src/test/java/org/jabref/logic/bst/util/BstNameFormatterTest.java index a8000752edb..4912af099bf 100644 --- a/src/test/java/org/jabref/logic/bst/BibtexNameFormatterTest.java +++ b/src/test/java/org/jabref/logic/bst/util/BstNameFormatterTest.java @@ -1,48 +1,42 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import org.jabref.model.entry.AuthorList; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -public class BibtexNameFormatterTest { +public class BstNameFormatterTest { @Test public void testUmlautsFullNames() { - AuthorList al = AuthorList - .parse("Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin"); + AuthorList list = AuthorList.parse("Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin"); - assertEquals("de~laVall{\\'e}e~PoussinCharles Louis Xavier~Joseph", BibtexNameFormatter.formatName(al - .getAuthor(0), "{vv}{ll}{jj}{ff}", Assertions::fail)); + assertEquals("de~laVall{\\'e}e~PoussinCharles Louis Xavier~Joseph", + BstNameFormatter.formatName(list.getAuthor(0), "{vv}{ll}{jj}{ff}")); } @Test public void testUmlautsAbbreviations() { - AuthorList al = AuthorList - .parse("Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin"); + AuthorList list = AuthorList.parse("Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin"); - assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J.", BibtexNameFormatter.formatName(al - .getAuthor(0), "{vv~}{ll}{, jj}{, f.}", Assertions::fail)); + assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J.", + BstNameFormatter.formatName(list.getAuthor(0), "{vv~}{ll}{, jj}{, f.}")); } @Test public void testUmlautsAbbreviationsWithQuestionMark() { - AuthorList al = AuthorList - .parse("Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin"); + AuthorList list = AuthorList.parse("Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin"); - assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J?", BibtexNameFormatter.formatName(al - .getAuthor(0), "{vv~}{ll}{, jj}{, f}?", Assertions::fail)); + assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J?", + BstNameFormatter.formatName(list.getAuthor(0), "{vv~}{ll}{, jj}{, f}?")); } @Test public void testFormatName() { - AuthorList al = AuthorList - .parse("Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin"); + AuthorList list = AuthorList.parse("Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin"); - assertEquals("dlVP", BibtexNameFormatter.formatName(al.getAuthor(0), "{v{}}{l{}}", - Assertions::fail)); + assertEquals("dlVP", BstNameFormatter.formatName(list.getAuthor(0), "{v{}}{l{}}")); assertNameFormatA("Meyer, J?", "Jonathan Meyer and Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin"); assertNameFormatB("J.~Meyer", "Jonathan Meyer and Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin"); @@ -60,8 +54,7 @@ public void testFormatName() { } private void assertNameFormat(String string, String string2, int which, String format) { - assertEquals(string, BibtexNameFormatter.formatName(string2, which, format, - Assertions::fail)); + assertEquals(string, BstNameFormatter.formatName(string2, which, format)); } private void assertNameFormatC(String string, String string2) { @@ -79,44 +72,41 @@ private void assertNameFormatA(String string, String string2) { @Test public void matchingBraceConsumedForCompleteWords() { StringBuilder sb = new StringBuilder(); - assertEquals(6, BibtexNameFormatter.consumeToMatchingBrace(sb, "{HELLO} {WORLD}" - .toCharArray(), 0)); + assertEquals(6, BstNameFormatter.consumeToMatchingBrace(sb, "{HELLO} {WORLD}".toCharArray(), 0)); assertEquals("{HELLO}", sb.toString()); } @Test public void matchingBraceConsumedForBracesInWords() { StringBuilder sb = new StringBuilder(); - assertEquals(18, BibtexNameFormatter.consumeToMatchingBrace(sb, "{HE{L{}L}O} {WORLD}" - .toCharArray(), 12)); + assertEquals(18, BstNameFormatter.consumeToMatchingBrace(sb, "{HE{L{}L}O} {WORLD}".toCharArray(), 12)); assertEquals("{WORLD}", sb.toString()); } @Test public void testConsumeToMatchingBrace() { StringBuilder sb = new StringBuilder(); - assertEquals(10, BibtexNameFormatter.consumeToMatchingBrace(sb, "{HE{L{}L}O} {WORLD}" - .toCharArray(), 0)); + assertEquals(10, BstNameFormatter.consumeToMatchingBrace(sb, "{HE{L{}L}O} {WORLD}".toCharArray(), 0)); assertEquals("{HE{L{}L}O}", sb.toString()); } @Test public void testGetFirstCharOfString() { - assertEquals("C", BibtexNameFormatter.getFirstCharOfString("Charles")); - assertEquals("V", BibtexNameFormatter.getFirstCharOfString("Vall{\\'e}e")); - assertEquals("{\\'e}", BibtexNameFormatter.getFirstCharOfString("{\\'e}")); - assertEquals("{\\'e", BibtexNameFormatter.getFirstCharOfString("{\\'e")); - assertEquals("E", BibtexNameFormatter.getFirstCharOfString("{E")); + assertEquals("C", BstNameFormatter.getFirstCharOfString("Charles")); + assertEquals("V", BstNameFormatter.getFirstCharOfString("Vall{\\'e}e")); + assertEquals("{\\'e}", BstNameFormatter.getFirstCharOfString("{\\'e}")); + assertEquals("{\\'e", BstNameFormatter.getFirstCharOfString("{\\'e")); + assertEquals("E", BstNameFormatter.getFirstCharOfString("{E")); } @Test public void testNumberOfChars() { - assertEquals(6, BibtexNameFormatter.numberOfChars("Vall{\\'e}e", -1)); - assertEquals(2, BibtexNameFormatter.numberOfChars("Vall{\\'e}e", 2)); - assertEquals(1, BibtexNameFormatter.numberOfChars("Vall{\\'e}e", 1)); - assertEquals(6, BibtexNameFormatter.numberOfChars("Vall{\\'e}e", 6)); - assertEquals(6, BibtexNameFormatter.numberOfChars("Vall{\\'e}e", 7)); - assertEquals(8, BibtexNameFormatter.numberOfChars("Vall{e}e", -1)); - assertEquals(6, BibtexNameFormatter.numberOfChars("Vall{\\'e this will be skipped}e", -1)); + assertEquals(6, BstNameFormatter.numberOfChars("Vall{\\'e}e", -1)); + assertEquals(2, BstNameFormatter.numberOfChars("Vall{\\'e}e", 2)); + assertEquals(1, BstNameFormatter.numberOfChars("Vall{\\'e}e", 1)); + assertEquals(6, BstNameFormatter.numberOfChars("Vall{\\'e}e", 6)); + assertEquals(6, BstNameFormatter.numberOfChars("Vall{\\'e}e", 7)); + assertEquals(8, BstNameFormatter.numberOfChars("Vall{e}e", -1)); + assertEquals(6, BstNameFormatter.numberOfChars("Vall{\\'e this will be skipped}e", -1)); } } diff --git a/src/test/java/org/jabref/logic/bst/BibtexPurifyTest.java b/src/test/java/org/jabref/logic/bst/util/BstPurifierTest.java similarity index 80% rename from src/test/java/org/jabref/logic/bst/BibtexPurifyTest.java rename to src/test/java/org/jabref/logic/bst/util/BstPurifierTest.java index 684e4366225..295b735fd37 100644 --- a/src/test/java/org/jabref/logic/bst/BibtexPurifyTest.java +++ b/src/test/java/org/jabref/logic/bst/util/BstPurifierTest.java @@ -1,4 +1,4 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import java.util.stream.Stream; @@ -7,14 +7,13 @@ import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; -public class BibtexPurifyTest { +public class BstPurifierTest { @ParameterizedTest @MethodSource("provideTestStrings") public void testPurify(String expected, String toBePurified) { - assertEquals(expected, BibtexPurify.purify(toBePurified, s -> fail("Should not Warn (" + s + ")! purify should be " + expected + " for " + toBePurified))); + assertEquals(expected, BstPurifier.purify(toBePurified)); } private static Stream provideTestStrings() { diff --git a/src/test/java/org/jabref/logic/bst/TextPrefixFunctionTest.java b/src/test/java/org/jabref/logic/bst/util/BstTextPrefixerTest.java similarity index 73% rename from src/test/java/org/jabref/logic/bst/TextPrefixFunctionTest.java rename to src/test/java/org/jabref/logic/bst/util/BstTextPrefixerTest.java index d91eadd8920..cc857101822 100644 --- a/src/test/java/org/jabref/logic/bst/TextPrefixFunctionTest.java +++ b/src/test/java/org/jabref/logic/bst/util/BstTextPrefixerTest.java @@ -1,11 +1,10 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; -public class TextPrefixFunctionTest { +public class BstTextPrefixerTest { @Test public void testPrefix() { @@ -21,6 +20,6 @@ public void testPrefix() { } private static void assertPrefix(final String string, final String string2) { - assertEquals(string, BibtexTextPrefix.textPrefix(5, string2, s -> fail("Should not Warn! text.prefix$ should be " + string + " for (5) " + string2))); + assertEquals(string, BstTextPrefixer.textPrefix(5, string2)); } } diff --git a/src/test/java/org/jabref/logic/bst/BibtexWidthTest.java b/src/test/java/org/jabref/logic/bst/util/BstWidthCalculatorTest.java similarity index 91% rename from src/test/java/org/jabref/logic/bst/BibtexWidthTest.java rename to src/test/java/org/jabref/logic/bst/util/BstWidthCalculatorTest.java index 79cb00b3eb8..30f2f6770e5 100644 --- a/src/test/java/org/jabref/logic/bst/BibtexWidthTest.java +++ b/src/test/java/org/jabref/logic/bst/util/BstWidthCalculatorTest.java @@ -1,4 +1,4 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import java.util.stream.Stream; @@ -35,12 +35,12 @@ * \bibcite{canh05}{CMM{$^{+}$}05} * */ -public class BibtexWidthTest { +public class BstWidthCalculatorTest { @ParameterizedTest @MethodSource("provideTestWidth") public void testWidth(int i, String str) { - assertEquals(i, BibtexWidth.width(str)); + assertEquals(i, BstWidthCalculator.width(str)); } private static Stream provideTestWidth() { @@ -60,7 +60,7 @@ private static Stream provideTestWidth() { @ParameterizedTest @MethodSource("provideTestGetCharWidth") public void testGetCharWidth(int i, Character c) { - assertEquals(i, BibtexWidth.getCharWidth(c)); + assertEquals(i, BstWidthCalculator.getCharWidth(c)); } private static Stream provideTestGetCharWidth() {