package org.jabref.logic.bst;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
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."
*
* Sounds easy - is a nightmare... X-(
*
*/
public class BibtexNameFormatter {
private BibtexNameFormatter() {
}
public static String formatName(String authorsNameList, int whichName, String formatString, Warn warn) {
AuthorList al = AuthorList.parse(authorsNameList);
if ((whichName < 1) && (whichName > al.getNumberOfAuthors())) {
warn.warn("AuthorList " + authorsNameList + " does not contain an author with number " + whichName);
return "";
}
return BibtexNameFormatter.formatName(al.getAuthor(whichName - 1), formatString, warn);
}
/**
*
* @param author
* @param format
* @param warn may-be-null
* @return
*/
public static String formatName(Author author, String format, Warn warn) {
StringBuilder sb = new StringBuilder();
char[] c = format.toCharArray();
int n = c.length;
int braceLevel = 0;
int group = 0;
int i = 0;
while (i < n) {
if (c[i] == '{') {
group++;
i++;
braceLevel++;
StringBuilder level1Chars = new StringBuilder();
StringBuilder wholeChar = new StringBuilder();
while ((i < n) && (braceLevel > 0)) {
wholeChar.append(c[i]);
if (c[i] == '{') {
braceLevel++;
i++;
continue;
}
if (c[i] == '}') {
braceLevel--;
i++;
continue;
}
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);
}
} else {
level1Chars.append(c[i]);
}
}
i++;
}
i--; // unskip last brace (for last i++ at the end)
String control = level1Chars.toString().toLowerCase(Locale.ROOT);
if (control.isEmpty()) {
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);
}
char type = control.charAt(0);
Optional<String> 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()) {
i++;
continue;
}
String[] tokens = tokenS.get().split(" ");
boolean abbreviateThatIsSingleLetter = true;
if (control.length() == 2) {
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);
}
}
}
// Now we know what to do
if ((braceLevel == 0) && (wholeChar.charAt(wholeChar.length() - 1) == '}')) {
wholeChar.deleteCharAt(wholeChar.length() - 1);
}
char[] d = wholeChar.toString().toCharArray();
int bLevel = 1;
String interToken = null;
int groupStart = sb.length();
for (int j = 0; j < d.length; j++) {
if (Character.isLetter(d[j]) && (bLevel == 1)) {
groupStart = sb.length();
if (!abbreviateThatIsSingleLetter) {
j++;
}
if (((j + 1) < d.length) && (d[j + 1] == '{')) {
StringBuilder interTokenSb = new StringBuilder();
j = BibtexNameFormatter.consumeToMatchingBrace(interTokenSb, d, j + 1);
interToken = interTokenSb.substring(1, interTokenSb.length() - 1);
}
for (int k = 0; k < tokens.length; k++) {
String token = tokens[k];
if (abbreviateThatIsSingleLetter) {
String[] dashes = token.split("-");
token = Arrays.asList(dashes).stream().map(BibtexNameFormatter::getFirstCharOfString)
.collect(Collectors.joining(".-"));
}
// Output token
sb.append(token);
if (k < (tokens.length - 1)) {
// Output Intertoken String
if (interToken == null) {
if (abbreviateThatIsSingleLetter) {
sb.append('.');
}
// 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)) {
sb.append('~');
} else {
sb.append(' ');
}
} else {
sb.append(interToken);
}
}
}
} else if (d[j] == '}') {
bLevel--;
if (bLevel > 0) {
sb.append('}');
}
} else if (d[j] == '{') {
bLevel++;
sb.append('{');
} else {
sb.append(d[j]);
}
}
if (sb.length() > 0) {
boolean noDisTie = false;
if ((sb.charAt(sb.length() - 1) == '~') &&
((BibtexNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 4) >= 4) ||
((sb.length() > 1) && (noDisTie = sb.charAt(sb.length() - 2) == '~')))) {
sb.deleteCharAt(sb.length() - 1);
if (!noDisTie) {
sb.append(' ');
}
}
}
} else if (c[i] == '}') {
if (warn != null) {
warn.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);
}
return sb.toString();
}
/**
* Including the matching brace.
*
* @param interTokenSb
* @param c
* @param pos
* @return
*/
public static int consumeToMatchingBrace(StringBuilder interTokenSb, char[] c, int pos) {
int braceLevel = 0;
for (int i = pos; i < c.length; i++) {
if (c[i] == '}') {
braceLevel--;
if (braceLevel == 0) {
interTokenSb.append('}');
return i;
}
} else if (c[i] == '{') {
braceLevel++;
}
interTokenSb.append(c[i]);
}
return c.length;
}
/**
* Takes care of special characters too
*
* @param s
* @return
*/
public static String getFirstCharOfString(String s) {
char[] c = s.toCharArray();
for (int i = 0; i < c.length; i++) {
if (Character.isLetter(c[i])) {
return String.valueOf(c[i]);
}
if ((c[i] == '{') && ((i + 1) < c.length) && (c[i + 1] == '\\')) {
StringBuilder sb = new StringBuilder();
BibtexNameFormatter.consumeToMatchingBrace(sb, c, i);
return sb.toString();
}
}
return "";
}
public static int numberOfChars(String token, int inStop) {
int stop = inStop;
if (stop < 0) {
stop = Integer.MAX_VALUE;
}
int result = 0;
int i = 0;
char[] c = token.toCharArray();
int n = c.length;
int braceLevel = 0;
while ((i < n) && (result < stop)) {
i++;
if (c[i - 1] == '{') {
braceLevel++;
if ((braceLevel == 1) && (i < n) && (c[i] == '\\')) {
i++;
while ((i < n) && (braceLevel > 0)) {
if (c[i] == '}') {
braceLevel--;
} else if (c[i] == '{') {
braceLevel++;
}
i++;
}
}
} else if (c[i - 1] == '}') {
braceLevel--;
}
result++;
}
return result;
}
}