package org.jabref.logic.bibtex; import org.jabref.logic.util.OS; import org.jabref.model.entry.InternalBibtexFields; import org.jabref.model.strings.StringUtil; /** * Currently the only implementation of org.jabref.exporter.FieldFormatter * <p> * Obeys following settings: * * JabRefPreferences.RESOLVE_STRINGS_ALL_FIELDS * * JabRefPreferences.DO_NOT_RESOLVE_STRINGS_FOR * * JabRefPreferences.WRITEFIELD_WRAPFIELD */ public class LatexFieldFormatter { // "Fieldname" to indicate that a field should be treated as a bibtex string. Used when writing database to file. public static final String BIBTEX_STRING = "__string"; private static final char FIELD_START = '{'; private static final char FIELD_END = '}'; private final boolean neverFailOnHashes; private final LatexFieldFormatterPreferences prefs; private final FieldContentParser parser; private StringBuilder stringBuilder; public LatexFieldFormatter(LatexFieldFormatterPreferences prefs) { this(true, prefs); } private LatexFieldFormatter(boolean neverFailOnHashes, LatexFieldFormatterPreferences prefs) { this.neverFailOnHashes = neverFailOnHashes; this.prefs = prefs; parser = new FieldContentParser(prefs.getFieldContentParserPreferences()); } public static LatexFieldFormatter buildIgnoreHashes(LatexFieldFormatterPreferences prefs) { return new LatexFieldFormatter(true, prefs); } private static void checkBraces(String text) throws InvalidFieldValueException { int left = 0; int right = 0; // First we collect all occurrences: for (int i = 0; i < text.length(); i++) { char item = text.charAt(i); boolean charBeforeIsEscape = false; if (i > 0 && text.charAt(i - 1) == '\\') { charBeforeIsEscape = true; } if (!charBeforeIsEscape && item == '{') { left++; } else if (!charBeforeIsEscape && item == '}') { right++; } } // Then we throw an exception if the error criteria are met. if (!(right == 0) && (left == 0)) { throw new InvalidFieldValueException("Unescaped '}' character without opening bracket ends string prematurely."); } if (!(right == 0) && (right < left)) { throw new InvalidFieldValueException("Unescaped '}' character without opening bracket ends string prematurely."); } if (left != right) { throw new InvalidFieldValueException("Braces don't match."); } } /** * Formats the content of a field. * * @param content the content of the field * @param fieldName the name of the field - used to trigger different serializations, e.g., turning off resolution for some strings * @return a formatted string suitable for output * @throws InvalidFieldValueException if s is not a correct bibtex string, e.g., because of improperly balanced braces or using # not paired */ public String format(String content, String fieldName) throws InvalidFieldValueException { if (content == null) { return FIELD_START + String.valueOf(FIELD_END); } String result = content; // normalize newlines boolean shouldNormalizeNewlines = !result.contains(OS.NEWLINE) && result.contains("\n"); if (shouldNormalizeNewlines) { // if we don't have real new lines, but pseudo newlines, we replace them // On Win 8.1, this is always true for multiline fields result = result.replace("\n", OS.NEWLINE); } // If the field is non-standard, we will just append braces, // wrap and write. boolean resolveStrings = shouldResolveStrings(fieldName); if (!resolveStrings) { return formatWithoutResolvingStrings(result, fieldName); } // Trim whitespace result = result.trim(); return formatAndResolveStrings(result, fieldName); } /** * This method handles # in the field content to get valid bibtex strings * * For instance, <code>#jan# - #feb#</code> gets <code>jan #{ - } # feb</code> (see @link{org.jabref.logic.bibtex.LatexFieldFormatterTests#makeHashEnclosedWordsRealStringsInMonthField()}) */ private String formatAndResolveStrings(String content, String fieldName) throws InvalidFieldValueException { stringBuilder = new StringBuilder(); checkBraces(content); // Here we assume that the user encloses any bibtex strings in #, e.g.: // #jan# - #feb# // ...which will be written to the file like this: // jan # { - } # feb int pivot = 0; while (pivot < content.length()) { int goFrom = pivot; int pos1 = pivot; while (goFrom == pos1) { pos1 = content.indexOf('#', goFrom); if ((pos1 > 0) && (content.charAt(pos1 - 1) == '\\')) { goFrom = pos1 + 1; pos1++; } else { goFrom = pos1 - 1; // Ends the loop. } } int pos2; if (pos1 == -1) { pos1 = content.length(); // No more occurrences found. pos2 = -1; } else { pos2 = content.indexOf('#', pos1 + 1); if (pos2 == -1) { if (neverFailOnHashes) { pos1 = content.length(); // just write out the rest of the text, and throw no exception } else { throw new InvalidFieldValueException( "The # character is not allowed in BibTeX strings unless escaped as in '\\#'.\n" + "In JabRef, use pairs of # characters to indicate a string.\n" + "Note that the entry causing the problem has been selected."); } } } if (pos1 > pivot) { writeText(content, pivot, pos1); } if ((pos1 < content.length()) && ((pos2 - 1) > pos1)) { // We check that the string label is not empty. That means // an occurrence of ## will simply be ignored. Should it instead // cause an error message? writeStringLabel(content, pos1 + 1, pos2, pos1 == pivot, (pos2 + 1) == content.length()); } if (pos2 > -1) { pivot = pos2 + 1; } else { pivot = pos1 + 1; } } return parser.format(stringBuilder, fieldName); } private boolean shouldResolveStrings(String fieldName) { boolean resolveStrings = true; if (prefs.isResolveStringsAllFields()) { // Resolve strings for all fields except some: for (String exception : prefs.getDoNotResolveStringsFor()) { if (exception.equals(fieldName)) { resolveStrings = false; break; } } } else { // Default operation - we only resolve strings for standard fields: resolveStrings = InternalBibtexFields.isStandardField(fieldName) || BIBTEX_STRING.equals(fieldName); } return resolveStrings; } private String formatWithoutResolvingStrings(String content, String fieldName) throws InvalidFieldValueException { checkBraces(content); stringBuilder = new StringBuilder( String.valueOf(FIELD_START)); stringBuilder.append(parser.format(content, fieldName)); stringBuilder.append(FIELD_END); return stringBuilder.toString(); } private void writeText(String text, int startPos, int endPos) { stringBuilder.append(FIELD_START); boolean escape = false; boolean inCommandName = false; boolean inCommand = false; boolean inCommandOption = false; int nestedEnvironments = 0; StringBuilder commandName = new StringBuilder(); for (int i = startPos; i < endPos; i++) { char c = text.charAt(i); // Track whether we are in a LaTeX command of some sort. if (Character.isLetter(c) && (escape || inCommandName)) { inCommandName = true; if (!inCommandOption) { commandName.append(c); } } else if (Character.isWhitespace(c) && (inCommand || inCommandOption)) { // Whitespace } else if (inCommandName) { // This means the command name is ended. // Perhaps the beginning of an argument: if (c == '[') { inCommandOption = true; } else if (inCommandOption && (c == ']')) { // Or the end of an argument: inCommandOption = false; } else if (!inCommandOption && (c == '{')) { inCommandName = false; inCommand = true; } else { // Or simply the end of this command alltogether: commandName.delete(0, commandName.length()); inCommandName = false; } } // If we are in a command body, see if it has ended: if (inCommand && (c == '}')) { if ("begin".equals(commandName.toString())) { nestedEnvironments++; } if ((nestedEnvironments > 0) && "end".equals(commandName.toString())) { nestedEnvironments--; } commandName.delete(0, commandName.length()); inCommand = false; } // We add a backslash before any ampersand characters, with one exception: if // we are inside an \\url{...} command, we should write it as it is. Maybe. if ((c == '&') && !escape && !(inCommand && "url".equals(commandName.toString())) && (nestedEnvironments == 0)) { stringBuilder.append("\\&"); } else { stringBuilder.append(c); } escape = c == '\\'; } stringBuilder.append(FIELD_END); } private void writeStringLabel(String text, int startPos, int endPos, boolean first, boolean last) { putIn((first ? "" : " # ") + text.substring(startPos, endPos) + (last ? "" : " # ")); } private void putIn(String s) { stringBuilder.append(StringUtil.wrap(s, prefs.getLineLength(), OS.NEWLINE)); } }