diff --git a/CHANGELOG.md b/CHANGELOG.md index 08c81da59b7..457ebf9cb08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/main/java/org/jabref/logic/exporter/ExporterFactory.java b/src/main/java/org/jabref/logic/exporter/ExporterFactory.java index b38bb57dc95..31ec6edb281 100644 --- a/src/main/java/org/jabref/logic/exporter/ExporterFactory.java +++ b/src/main/java/org/jabref/logic/exporter/ExporterFactory.java @@ -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()); diff --git a/src/main/java/org/jabref/logic/layout/LayoutEntry.java b/src/main/java/org/jabref/logic/layout/LayoutEntry.java index 7d0cf3d3a4d..211d9b02172 100644 --- a/src/main/java/org/jabref/logic/layout/LayoutEntry.java +++ b/src/main/java/org/jabref/logic/layout/LayoutEntry.java @@ -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; @@ -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; }; } diff --git a/src/main/java/org/jabref/logic/layout/format/CffDate.java b/src/main/java/org/jabref/logic/layout/format/CffDate.java new file mode 100644 index 00000000000..1e81697a049 --- /dev/null +++ b/src/main/java/org/jabref/logic/layout/format/CffDate.java @@ -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. + *

+ * Year, Month and Day contained => preferred-citation: + * date-released: yyyy-mm-dd + *

+ * Year and Month contained => preferred-citation + * ... + * month: mm + * year: yyyy + *

+ * Year contained => preferred-citation: + * ... + * year: yyyy + *

+ * 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(); + } +} diff --git a/src/main/java/org/jabref/logic/layout/format/CffType.java b/src/main/java/org/jabref/logic/layout/format/CffType.java new file mode 100644 index 00000000000..5de168b77ba --- /dev/null +++ b/src/main/java/org/jabref/logic/layout/format/CffType.java @@ -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"; + }; + } +} + diff --git a/src/main/resources/resource/layout/cff.layout b/src/main/resources/resource/layout/cff.layout new file mode 100644 index 00000000000..7ed334cdd07 --- /dev/null +++ b/src/main/resources/resource/layout/cff.layout @@ -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} diff --git a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java new file mode 100644 index 00000000000..f9314e11b56 --- /dev/null +++ b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java @@ -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 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 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 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 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)); + } +} diff --git a/src/test/java/org/jabref/logic/layout/format/CffDateTest.java b/src/test/java/org/jabref/logic/layout/format/CffDateTest.java new file mode 100644 index 00000000000..fd73612bbe4 --- /dev/null +++ b/src/test/java/org/jabref/logic/layout/format/CffDateTest.java @@ -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")); + } +}