-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Added a custom Export for academicpages.github.io #14463
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
base: main
Are you sure you want to change the base?
Changes from 17 commits
48450d4
65bbc44
a4e2fea
3e34b92
8e76f57
2799f1d
be96c94
1421e98
e4a987c
df7aa05
9b12666
f588da4
de65527
2d8afcf
4ea372d
3972b34
9b8a86d
9087390
655afe3
4d7dac1
30e7626
6e07a52
01f43cc
ad339be
aaa5855
3547cd0
7b55f27
ec19bb9
05792cb
7ccbc6f
6f25e5c
3c24b32
b38bed2
fe03fc4
c2a06e0
280a1fb
6a4c5b0
100fe95
07088a8
fcd3db4
1efe4a2
488620d
5d6e3a2
91a6a05
6ad9b7a
d9ab1a6
a587653
43576c3
fc3e5bd
47dc573
219a5c5
3e156fb
305135a
370b680
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| package org.jabref.logic.exporter; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| import org.jabref.logic.journals.JournalAbbreviationLoader; | ||
| import org.jabref.logic.journals.JournalAbbreviationRepository; | ||
| import org.jabref.logic.layout.LayoutFormatterPreferences; | ||
| import org.jabref.logic.layout.format.HTMLChars; | ||
| import org.jabref.logic.layout.format.RemoveLatexCommandsFormatter; | ||
| import org.jabref.logic.layout.format.Replace; | ||
| import org.jabref.logic.layout.format.SafeFileName; | ||
| import org.jabref.logic.util.StandardFileType; | ||
| import org.jabref.logic.util.io.FileUtil; | ||
| import org.jabref.model.database.BibDatabaseContext; | ||
| import org.jabref.model.entry.BibEntry; | ||
| import org.jabref.model.metadata.SelfContainedSaveOrder; | ||
|
|
||
| import org.jetbrains.annotations.NotNull; | ||
| import org.jspecify.annotations.NonNull; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * A custom exporter to write multiple bib entries as AcademicPages Markdown format. | ||
| */ | ||
| public class AcademicPagesExporter extends Exporter { | ||
| private static final String BLANK_LINE_PATTERN = "\\r\\n|\\n"; | ||
| private static final String LAYOUT_PREFIX = "/resource/layout/"; | ||
| private static final String LAYOUT_EXTENSION = ".layout"; | ||
| private static final String FORMATTERS_EXTENSION = ".formatters"; | ||
| private static final String BEGIN_INFIX = ".begin"; | ||
| private static final String END_INFIX = ".end"; | ||
|
|
||
| private static final Logger LOGGER = LoggerFactory.getLogger(TemplateExporter.class); | ||
|
|
||
| private final String lfFileName; | ||
|
||
| private final String directory; | ||
| private final LayoutFormatterPreferences layoutPreferences; | ||
| private final SelfContainedSaveOrder saveOrder; | ||
| private boolean customExport; | ||
| private List<BibEntry> entries; | ||
| private TemplateExporter academicPagesTemplate; | ||
|
|
||
| /** | ||
| * Initialize another export format based on templates stored in dir with layoutFile lfFilename. | ||
| * | ||
GreLucie marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| */ | ||
|
Comment on lines
39
to
41
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment is strange. What is |
||
| public AcademicPagesExporter(LayoutFormatterPreferences layoutPreferences, SelfContainedSaveOrder saveOrder) { | ||
| super("academicpages", "academic pages markdowns", StandardFileType.MARKDOWN); | ||
| this.lfFileName = "academicpages"; | ||
| this.directory = "academicpages"; | ||
| this.layoutPreferences = layoutPreferences; | ||
| this.saveOrder = saveOrder; | ||
| String consoleName = "academicpages"; | ||
| this.academicPagesTemplate = new TemplateExporter("academicpages", consoleName, lfFileName, directory, StandardFileType.MARKDOWN, layoutPreferences, saveOrder); | ||
| } | ||
|
|
||
| @Override | ||
| public void export(@NonNull final BibDatabaseContext databaseContext, | ||
| final Path exportDirectory, | ||
| @NonNull List<BibEntry> entries) throws SaveException { | ||
| export(databaseContext, exportDirectory, entries, List.of(), JournalAbbreviationLoader.loadBuiltInRepository()); | ||
| } | ||
|
|
||
| /** | ||
| * The method that performs the export of all entries by iterating on the entries. | ||
| * | ||
| * @param databaseContext the database to export from | ||
| * @param file the directory to write to | ||
| * @param entries a list containing all entries that should be exported | ||
| * @param abbreviationRepository the built-in repository | ||
|
Comment on lines
62
to
65
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure why not all comments start at the same column |
||
| * @throws SaveException Exception thrown if saving goes wrong | ||
| */ | ||
| @Override | ||
| public void export(@NonNull final BibDatabaseContext databaseContext, | ||
| final Path file, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure that Annoate all or nothing. Proposal: Add |
||
| @NonNull List<BibEntry> entries, | ||
| List<Path> fileDirForDataBase, | ||
| JournalAbbreviationRepository abbreviationRepository) throws SaveException { | ||
| if (entries.isEmpty()) { // Only export if entries exist | ||
GreLucie marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return; | ||
| } | ||
| try { | ||
| // convert what the ExportCommand gives as a file parameter to a directory | ||
| Path baseDir = file; | ||
| String exportDirectoryString = FileUtil.getBaseName(file); | ||
| Path exportDirectory = baseDir.getParent().resolve(exportDirectoryString); | ||
|
|
||
| // Ensure the directory exists. This is important: AtomicFileWriter will fail if parent dirs are missing. | ||
| Files.createDirectories(exportDirectory); | ||
|
|
||
| for (BibEntry entry : entries) { | ||
koppor marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (entry.getType() == null) { | ||
|
||
| LOGGER.warn("Skipping entry with no type: {}", entry); | ||
| continue; | ||
| } | ||
| // formatting the title of each entry to match the file names format demanded by academic pages (applying the same formatters applied to the title in the academicpages.layout) | ||
| Path path = getPath(entry, exportDirectory); | ||
|
|
||
| List<BibEntry> individual_entry = new ArrayList<BibEntry>(); | ||
| individual_entry.add(entry); | ||
| academicPagesTemplate.export(databaseContext, path, individual_entry, fileDirForDataBase, abbreviationRepository); | ||
| } | ||
| } catch (IOException e) { | ||
| throw new SaveException("could not export"); | ||
GreLucie marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| private static @NotNull Path getPath(BibEntry entry, Path exportDirectory) { | ||
| Replace replace_formatter = new Replace(); | ||
|
||
| replace_formatter.setArgument(" ,-"); | ||
|
||
| RemoveLatexCommandsFormatter commands_formatter = new RemoveLatexCommandsFormatter(); | ||
| HTMLChars html_formatter = new HTMLChars(); | ||
|
||
| String title = entry.getTitle().get(); | ||
| String formatted_title = commands_formatter.format(html_formatter.format(replace_formatter.format(title))); | ||
| SafeFileName safe_formatter = new SafeFileName(); // added custom formatter to remove all characters that are not allowed in filenames | ||
|
||
| String safe_title = safe_formatter.format(formatted_title); | ||
| return exportDirectory.resolve(safe_title + ".md"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package org.jabref.logic.layout.format; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.jabref.logic.layout.LayoutFormatter; | ||
| import org.jabref.model.entry.Month; | ||
|
|
||
| /** | ||
| * Convert the month name into the corresponding number and return 01 by default | ||
| */ | ||
| public class NumberMonthFormatter implements LayoutFormatter { | ||
|
||
|
|
||
| @Override | ||
| public String format(String fieldText) { | ||
| Optional<Month> month = Month.parse(fieldText); | ||
| return month.map(Month::getTwoDigitNumber).orElse("01"); | ||
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package org.jabref.logic.layout.format; | ||
|
|
||
| import org.jabref.logic.layout.LayoutFormatter; | ||
|
|
||
| /** | ||
| * Remove all the characters that are not allowed by the OS in file names | ||
| */ | ||
| public class SafeFileName implements LayoutFormatter { | ||
|
|
||
| @Override | ||
| public String format(String fieldText) { | ||
| return fieldText.replaceAll("[\\\\/:*?\"<>|]", ""); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use org.jabref.logic.util.io.FileUtil#getValidFileName |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| title: "\format[RemoveLatexCommands,HTMLChars]{\title}" | ||
| collection: publications | ||
| category: \format{\entrytype} | ||
| permalink: /publication/\format[RemoveLatexCommands,HTMLChars,Replace(\s,-),SafeFileName]{\title} | ||
| excerpt: '\begin{note}\format[RemoveLatexCommands,HTMLChars]{\note}\end{note}' | ||
| date: \format{\year}-\begin{month}\format[NumberMonth]{\month}\end{month}\begin{!month}01\end{!month}-\begin{day}\format{\day}\end{day}\begin{!day}01\end{!day} | ||
| venue: '\format[RemoveLatexCommands,HTMLChars]{\journal}\begin{!journal}Unknown\end{!journal}' | ||
| slidesurl: '\begin{file}\format[FileLink(pdf)]{\file}\end{file}\begin{!file}https://[insert username].github.io/files/[insert filename].pdf\end{!file}' | ||
| paperurl: '\begin{file}\format[FileLink(pdf)]{\file}\end{file}\begin{!file}https://[insert username].github.io/files/[insert filename].pdf\end{!file}' | ||
| bibtexurl: 'https://[insert username].github.io/files/[insert filename].bib' | ||
| citation: '\format[HTMLChars]{\author}. (\format{\year}). ""\format[RemoveLatexCommands,HTMLChars]{\title}." <i>\format[RemoveLatexCommands,HTMLChars]{\journal}</i>.' | ||
| --- | ||
| \begin{abstract}\format{\abstract}\end{abstract} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| package org.jabref.logic.exporter; | ||
|
|
||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.util.List; | ||
|
|
||
| import org.jabref.logic.layout.LayoutFormatterPreferences; | ||
| 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.jabref.model.metadata.SelfContainedSaveOrder; | ||
|
|
||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.io.TempDir; | ||
| import org.mockito.Answers; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| import static org.mockito.Mockito.mock; | ||
|
|
||
| class AcademicPagesExporterTest { | ||
|
|
||
| private AcademicPagesExporter exporter; | ||
| private BibDatabaseContext databaseContext; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| exporter = new AcademicPagesExporter( | ||
| mock(LayoutFormatterPreferences.class, Answers.RETURNS_DEEP_STUBS), | ||
| new SelfContainedSaveOrder(SaveOrder.OrderType.SPECIFIED, List.of())); | ||
| databaseContext = new BibDatabaseContext(); | ||
| } | ||
|
|
||
| @Test | ||
| void exportArticleWithFullDateAndRequiredFieldsGeneratesCorrectFileNameAndContent(@TempDir Path tempDir) throws Exception { | ||
| BibEntry entry = new BibEntry(StandardEntryType.Article) | ||
| .withCitationKey("testKey") | ||
| .withField(StandardField.TITLE, "Test Title") | ||
| .withField(StandardField.AUTHOR, "Test Author") | ||
| .withField(StandardField.YEAR, "2023") | ||
| .withField(StandardField.MONTH, "05") | ||
| .withField(StandardField.DAY, "12") | ||
| .withField(StandardField.JOURNAL, "Test Journal"); | ||
|
|
||
| exporter.export(databaseContext, tempDir, List.of(entry)); | ||
|
|
||
| // Verify file name follows pattern: YYYY-MM-DD-title.md | ||
| Path expectedFile = tempDir.resolve("2023-05-12-test-title.md"); | ||
| assertTrue(Files.exists(expectedFile)); | ||
|
|
||
| String content = Files.readString(expectedFile); | ||
|
|
||
| // Verify YAML front matter fields | ||
|
||
| assertTrue(content.contains("title: \"Test Title\"")); | ||
| assertTrue(content.contains("date: 2023-05-12")); | ||
| assertTrue(content.contains("venue: 'Test Journal'")); | ||
| assertTrue(content.contains("citation: 'Test Author (2023). \"Test Title.\" <i>Test Journal</i>.'")); | ||
| } | ||
|
|
||
| @Test | ||
| void exportArticleWithMissingMonthAndDayDefaultsToJanuaryFirst(@TempDir Path tempDir) throws Exception { | ||
| BibEntry entry = new BibEntry(StandardEntryType.Article) | ||
| .withCitationKey("testKey") | ||
| .withField(StandardField.TITLE, "No Date") | ||
| .withField(StandardField.YEAR, "2023"); | ||
|
|
||
| exporter.export(databaseContext, tempDir, List.of(entry)); | ||
|
|
||
| // Expect default date 2023-01-01 | ||
| Path expectedFile = tempDir.resolve("2023-01-01-no-date.md"); | ||
| assertTrue(Files.exists(expectedFile)); | ||
|
|
||
| String content = Files.readString(expectedFile); | ||
| assertTrue(content.contains("date: 2023-01-01")); | ||
| } | ||
|
|
||
| @Test | ||
| void exportArticleWithAbstractAppendsAbstractAfterYamlFrontMatter(@TempDir Path tempDir) throws Exception { | ||
| BibEntry entry = new BibEntry(StandardEntryType.Article) | ||
| .withCitationKey("testKey") | ||
| .withField(StandardField.TITLE, "Abstract Paper") | ||
| .withField(StandardField.YEAR, "2023") | ||
| .withField(StandardField.ABSTRACT, "This is a test abstract."); | ||
|
|
||
| exporter.export(databaseContext, tempDir, List.of(entry)); | ||
|
|
||
| Path expectedFile = tempDir.resolve("2023-01-01-abstract-paper.md"); | ||
| assertTrue(Files.exists(expectedFile)); | ||
|
|
||
| String content = Files.readString(expectedFile); | ||
|
|
||
| // Abstract should be outside the YAML block (after the second '---') | ||
| assertTrue(content.contains("---")); | ||
| assertTrue(content.endsWith("\nThis is a test abstract.\n")); | ||
| } | ||
|
|
||
| @Test | ||
| void exportMultipleEntriesGeneratesMultipleIndividualMarkdownFiles(@TempDir Path tempDir) throws Exception { | ||
| BibEntry entry1 = new BibEntry(StandardEntryType.Article) | ||
| .withCitationKey("key1") | ||
| .withField(StandardField.TITLE, "Paper One") | ||
| .withField(StandardField.YEAR, "2023"); | ||
|
|
||
| BibEntry entry2 = new BibEntry(StandardEntryType.Book) | ||
| .withCitationKey("key2") | ||
| .withField(StandardField.TITLE, "Book Two") | ||
| .withField(StandardField.YEAR, "2022"); | ||
|
|
||
| exporter.export(databaseContext, tempDir, List.of(entry1, entry2)); | ||
|
|
||
| // Verify both files exist | ||
| assertTrue(Files.exists(tempDir.resolve("2023-01-01-paper-one.md"))); | ||
| assertTrue(Files.exists(tempDir.resolve("2022-01-01-book-two.md"))); | ||
| } | ||
|
|
||
| @Test | ||
| void exportInProceedingsWithBooktitleUsesBooktitleAsVenueAlias(@TempDir Path tempDir) throws Exception { | ||
| BibEntry entry = new BibEntry(StandardEntryType.InProceedings) | ||
| .withCitationKey("testKey") | ||
| .withField(StandardField.TITLE, "Conference Paper") | ||
| .withField(StandardField.YEAR, "2023") | ||
| .withField(StandardField.BOOKTITLE, "Conference Proceedings"); | ||
|
|
||
| exporter.export(databaseContext, tempDir, List.of(entry)); | ||
|
|
||
| Path expectedFile = tempDir.resolve("2023-01-01-conference-paper.md"); | ||
| assertTrue(Files.exists(expectedFile)); | ||
|
|
||
| String content = Files.readString(expectedFile); | ||
|
|
||
| // 'venue' should be populated from 'booktitle' since 'journal' is missing | ||
| assertTrue(content.contains("venue: 'Conference Proceedings'")); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use jspecify nonnull