Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added option to export in CFF (Citation File Format) #10917

Merged
merged 11 commits into from
Feb 27, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- When pasting HTML into the abstract or a comment field, the hypertext is automatically converted to Markdown. [#10558](https://github.com/JabRef/jabref/issues/10558)
- We added the possibility to redownload files that had been present but are no longer in the specified location. [#10848](https://github.com/JabRef/jabref/issues/10848)
- We added the citation key pattern `[camelN]`. Equivalent to the first N words of the `[camel]` pattern.
- We added ability to export in CFF (Citation File Format) [#10661](https://github.com/JabRef/jabref/issues/10661).

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public static ExporterFactory create(PreferencesService preferencesService,
exporters.add(new TemplateExporter("MIS Quarterly", "misq", "misq", "misq", StandardFileType.RTF, layoutPreferences, saveOrder));
exporters.add(new TemplateExporter("CSL YAML", "yaml", "yaml", null, StandardFileType.YAML, layoutPreferences, saveOrder, BlankLineBehaviour.DELETE_BLANKS));
exporters.add(new TemplateExporter("Hayagriva YAML", "hayagrivayaml", "hayagrivayaml", null, StandardFileType.YAML, layoutPreferences, saveOrder, BlankLineBehaviour.DELETE_BLANKS));
exporters.add(new TemplateExporter("CFF", "cff", "cff", null, StandardFileType.CFF, layoutPreferences, saveOrder, BlankLineBehaviour.DELETE_BLANKS));
exporters.add(new OpenOfficeDocumentCreator());
exporters.add(new OpenDocumentSpreadsheetCreator());
exporters.add(new MSBibExporter());
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/org/jabref/logic/layout/LayoutEntry.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
import org.jabref.logic.layout.format.AuthorOrgSci;
import org.jabref.logic.layout.format.Authors;
import org.jabref.logic.layout.format.CSLType;
import org.jabref.logic.layout.format.CffDate;
import org.jabref.logic.layout.format.CffType;
import org.jabref.logic.layout.format.CompositeFormat;
import org.jabref.logic.layout.format.CreateBibORDFAuthors;
import org.jabref.logic.layout.format.CreateDocBook4Authors;
Expand Down Expand Up @@ -486,6 +488,8 @@ private LayoutFormatter getLayoutFormatterByName(String name) {
case "ShortMonth" -> new ShortMonthFormatter();
case "ReplaceWithEscapedDoubleQuotes" -> new ReplaceWithEscapedDoubleQuotes();
case "HayagrivaType" -> new HayagrivaType();
case "CffType" -> new CffType();
case "CffDate" -> new CffDate();
default -> null;
};
}
Expand Down
70 changes: 70 additions & 0 deletions src/main/java/org/jabref/logic/layout/format/CffDate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.jabref.logic.layout.format;

import java.time.LocalDate;
import java.time.Year;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

import org.jabref.logic.layout.LayoutFormatter;
import org.jabref.logic.util.OS;

/**
* This class is used to parse dates for CFF exports. Since we do not know if the input String contains
* year, month and day, we must go through all these cases to return the best CFF format possible.
* Different cases are stated below.
* <p>
* Year, Month and Day contained => preferred-citation:
* date-released: yyyy-mm-dd
* <p>
* Year and Month contained => preferred-citation
* ...
* month: mm
* year: yyyy
* <p>
* Year contained => preferred-citation:
* ...
* year: yyyy
* <p>
* Poorly formatted => preferred-citation:
* ...
* issue-date: text-as-is
*/
public class CffDate implements LayoutFormatter {
@Override
public String format(String fieldText) {
StringBuilder builder = new StringBuilder();
String formatString = "yyyy-MM-dd";
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString);
LocalDate date = LocalDate.parse(fieldText, DateTimeFormatter.ISO_LOCAL_DATE);
builder.append("date-released: ");
builder.append(date.format(formatter));
} catch (DateTimeParseException e) {
try {
formatString = "yyyy-MM";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString);
YearMonth yearMonth = YearMonth.parse(fieldText, formatter);
int month = yearMonth.getMonth().getValue();
int year = yearMonth.getYear();
builder.append("month: ");
builder.append(month);
builder.append(OS.NEWLINE);
builder.append(" year: "); // Account for indent since we are in `preferred-citation` indentation block
builder.append(year);
} catch (DateTimeParseException f) {
try {
formatString = "yyyy";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString);
int year = Year.parse(fieldText, formatter).getValue();
builder.append("year: ");
builder.append(year);
} catch (DateTimeParseException g) {
builder.append("issue-date: ");
builder.append(fieldText);
}
}
}
return builder.toString();
}
}
24 changes: 24 additions & 0 deletions src/main/java/org/jabref/logic/layout/format/CffType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.jabref.logic.layout.format;

import org.jabref.logic.layout.LayoutFormatter;
import org.jabref.model.entry.types.StandardEntryType;

public class CffType implements LayoutFormatter {
@Override
public String format(String value) {
return switch (StandardEntryType.valueOf(value)) {
case Article, Conference -> "article";
case Book -> "book";
case Booklet -> "pamphlet";
case InProceedings -> "conference-paper";
case Proceedings -> "proceedings";
case Misc -> "misc";
case Manual -> "manual";
case Software -> "software";
case Report, TechReport -> "report";
case Unpublished -> "unpublished";
default -> "generic";
};
}
}

17 changes: 17 additions & 0 deletions src/main/resources/resource/layout/cff.layout
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
cff-version: 1.2.0
message: "If you use this, please cite the work from preferred-citation."
authors:
- name: \format[Default(No author specified.)]{\author}
title: \format[Default(No title specified.)]{\title}
preferred-citation:
type: \format[CffType, Default(generic)]{\entrytype}
authors:
- name: \format[Default(No author specified.)]{\author}
title: \format[Default(No title specified.)]{\title}
\begin{date}
\format[CffDate]{\date}
\end{date}
\begin{abstract} abstract: \abstract\end{abstract}
\begin{doi} doi: \doi\end{doi}
\begin{volume} volume: \volume\end{volume}
\begin{url} url: "\url"\end{url}
159 changes: 159 additions & 0 deletions src/test/java/org/jabref/logic/exporter/CffExporterTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package org.jabref.logic.exporter;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;

import org.jabref.logic.layout.LayoutFormatterPreferences;
import org.jabref.logic.util.StandardFileType;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.types.StandardEntryType;
import org.jabref.model.metadata.SaveOrder;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Answers;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;

public class CffExporterTest {

private static Exporter cffExporter;
private static BibDatabaseContext databaseContext;

@BeforeAll
static void setUp() {
cffExporter = new TemplateExporter(
"CFF",
"cff",
"cff",
null,
StandardFileType.CFF,
mock(LayoutFormatterPreferences.class, Answers.RETURNS_DEEP_STUBS),
SaveOrder.getDefaultSaveOrder(),
BlankLineBehaviour.DELETE_BLANKS);

databaseContext = new BibDatabaseContext();
}

@Test
public final void exportForNoEntriesWritesNothing(@TempDir Path tempFile) throws Exception {
Path file = tempFile.resolve("ThisIsARandomlyNamedFile");
Files.createFile(file);
cffExporter.export(databaseContext, tempFile, Collections.emptyList());
assertEquals(Collections.emptyList(), Files.readAllLines(file));
}

@Test
public final void exportsCorrectContent(@TempDir Path tempFile) throws Exception {
BibEntry entry = new BibEntry(StandardEntryType.Article)
.withCitationKey("test")
.withField(StandardField.AUTHOR, "Test Author")
.withField(StandardField.TITLE, "Test Title")
.withField(StandardField.URL, "http://example.com");

Path file = tempFile.resolve("RandomFileName");
Files.createFile(file);
cffExporter.export(databaseContext, file, Collections.singletonList(entry));

List<String> expected = List.of(
"cff-version: 1.2.0",
"message: \"If you use this, please cite the work from preferred-citation.\"",
"authors:",
" - name: Test Author",
"title: Test Title",
"preferred-citation:",
" type: article",
" authors:",
" - name: Test Author",
" title: Test Title",
" url: \"http://example.com\"");

assertEquals(expected, Files.readAllLines(file));
}

@Test
public final void usesCorrectType(@TempDir Path tempFile) throws Exception {
BibEntry entry = new BibEntry(StandardEntryType.InProceedings)
.withCitationKey("test")
.withField(StandardField.AUTHOR, "Test Author")
.withField(StandardField.TITLE, "Test Title")
.withField(StandardField.DOI, "random_doi_value");

Path file = tempFile.resolve("RandomFileName");
Files.createFile(file);
cffExporter.export(databaseContext, file, Collections.singletonList(entry));

List<String> expected = List.of(
"cff-version: 1.2.0",
"message: \"If you use this, please cite the work from preferred-citation.\"",
"authors:",
" - name: Test Author",
"title: Test Title",
"preferred-citation:",
" type: conference-paper",
" authors:",
" - name: Test Author",
" title: Test Title",
" doi: random_doi_value");

assertEquals(expected, Files.readAllLines(file));
}

@Test
public final void usesCorrectDefaultValues(@TempDir Path tempFile) throws Exception {
BibEntry entry = new BibEntry(StandardEntryType.Thesis)
.withCitationKey("test");

Path file = tempFile.resolve("RandomFileName");
Files.createFile(file);
cffExporter.export(databaseContext, file, Collections.singletonList(entry));

List<String> expected = List.of(
"cff-version: 1.2.0",
"message: \"If you use this, please cite the work from preferred-citation.\"",
"authors:",
" - name: No author specified.",
"title: No title specified.",
"preferred-citation:",
" type: generic",
" authors:",
" - name: No author specified.",
" title: No title specified.");

assertEquals(expected, Files.readAllLines(file));
}

@Test
void passesModifiedCharset(@TempDir Path tempFile) throws Exception {
BibEntry entry = new BibEntry(StandardEntryType.Article)
.withCitationKey("test")
.withField(StandardField.AUTHOR, "谷崎 潤一郎")
.withField(StandardField.TITLE, "細雪")
.withField(StandardField.URL, "http://example.com");

Path file = tempFile.resolve("RandomFileName");
Files.createFile(file);
cffExporter.export(databaseContext, file, Collections.singletonList(entry));

List<String> expected = List.of(
"cff-version: 1.2.0",
"message: \"If you use this, please cite the work from preferred-citation.\"",
"authors:",
" - name: 谷崎 潤一郎",
"title: 細雪",
"preferred-citation:",
" type: article",
" authors:",
" - name: 谷崎 潤一郎",
" title: 細雪",
" url: \"http://example.com\"");

assertEquals(expected, Files.readAllLines(file));
}
}
45 changes: 45 additions & 0 deletions src/test/java/org/jabref/logic/layout/format/CffDateTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.jabref.logic.layout.format;

import org.jabref.logic.layout.LayoutFormatter;
import org.jabref.logic.util.OS;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class CffDateTest {

private LayoutFormatter formatter;
private String newLine;

@BeforeEach
public void setUp() {
formatter = new CffDate();
newLine = OS.NEWLINE;
}

@Test
public void dayMonthYear() {
String expected = "date-released: 2003-11-06";
assertEquals(expected, formatter.format("2003-11-06"));
}

@Test
public void monthYear() {
String expected = "month: 7" + newLine + " " + "year: 2016";
assertEquals(expected, formatter.format("2016-07"));
}

@Test
public void year() {
String expected = "year: 2021";
assertEquals(expected, formatter.format("2021"));
}

@Test
public void poorlyFormatted() {
String expected = "issue-date: -2023";
assertEquals(expected, formatter.format("-2023"));
}
}
Loading