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