├── .flake8
├── .github
├── FUNDING.yml
├── media
│ └── demo.gif
└── workflows
│ ├── codeql-analysis.yml
│ ├── stale.yml
│ └── test.yml
├── .gitignore
├── LICENSE.md
├── Makefile
├── README.md
├── README.zh.md
├── academic
├── cli.py
├── generate_markdown.py
├── import_bibtex.py
├── import_notebook.py
├── jupyter_whitespace_remover.py
├── publication_type.py
├── templates
│ └── publication.md
└── utils.py
├── poetry.lock
├── pyproject.toml
└── tests
├── data
├── article.bib
├── book.bib
├── notebooks
│ ├── blog-with-jupyter.ipynb
│ └── test.ipynb
├── report.bib
└── thesis.bib
├── test_bibtex_import.py
└── test_notebook_import.py
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 150
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: gcushen
2 | custom: https://hugoblox.com/sponsor/
3 |
--------------------------------------------------------------------------------
/.github/media/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetRD/academic-file-converter/e2ccb0cbcf3842a5e5817f22472ee00b8375652b/.github/media/demo.gif
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '33 22 * * 6'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'python' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v2
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: 'Close stale issues and PRs'
2 | on:
3 | schedule:
4 | - cron: '30 1 * * *'
5 |
6 | permissions:
7 | contents: write # only for delete-branch option
8 | issues: write
9 | pull-requests: write
10 |
11 | jobs:
12 | stale:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/stale@v9
16 | with:
17 | repo-token: ${{ secrets.GITHUB_TOKEN }}
18 | stale-issue-message: |
19 | This issue is stale because it has not had any recent activity. The resources of the project maintainers are limited, and so we are asking for your help.
20 |
21 | If this is a **bug** and you can still reproduce this error on the main
branch, consider contributing a Pull Request with a fix.
22 |
23 | If this is a **feature request**, and you feel that it is still relevant and valuable, consider contributing a Pull Request for review.
24 |
25 | This issue will automatically close soon if no further activity occurs. Thank you for your contributions.
26 | stale-pr-message: |
27 | This PR is stale because it has not had any recent activity. The resources of the project maintainers are limited, and so we are asking for your help.
28 |
29 | If you feel that the PR is still relevant in the latest release, consider making the PR easier to review and finding developers to help review the PR.
30 |
31 | Please be _mindful_ that although we encourage PRs, we cannot expand the scope of the project in every possible direction. There will be requests that don't make the roadmap.
32 |
33 | This PR will automatically close soon if no further activity occurs. Thank you for your contributions.
34 | days-before-stale: 30
35 | days-before-close: 5
36 | stale-issue-label: stale
37 | stale-pr-label: stale
38 | exempt-issue-labels: 'keep,enhancement,bug,documentation'
39 | exempt-pr-labels: 'keep,enhancement,bug,documentation'
40 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test, lint, format, type check
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | python-version: [3.11]
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Set up Python ${{ matrix.python-version }}
16 | uses: actions/setup-python@v4
17 | with:
18 | python-version: ${{ matrix.python-version }}
19 | - name: Install Poetry
20 | uses: snok/install-poetry@v1
21 | - name: Install dependencies
22 | run: |
23 | poetry install
24 | - name: Lint with flake8
25 | run: |
26 | poetry run flake8 . --count --show-source --statistics
27 | - name: Test with pytest
28 | run: |
29 | poetry run pytest
30 | - name: Formatting checks
31 | run: |
32 | poetry run isort --profile black --diff .
33 | poetry run black --check .
34 | - name: Type checks
35 | run: |
36 | poetry run pyright
37 | # - name: Report Coverage
38 | # if: matrix.python-version == '3.11'
39 | # uses: codecov/codecov-action@v3
40 | # with:
41 | # verbose: true
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | .DS_Store
3 | *.egg-info/
4 | build/
5 | dist/
6 | site/
7 | .idea/
8 | .tox/
9 | .cache/
10 |
11 | # Test data - temp files
12 | .ipynb_checkpoints
13 |
14 | # Trial runs
15 | output/
16 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-present George Cushen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: black lint test type publish
2 |
3 | format:
4 | poetry run isort --profile black .
5 | poetry run black .
6 |
7 | lint:
8 | poetry run flake8
9 |
10 | test:
11 | poetry run pytest -v
12 |
13 | type:
14 | poetry run pyright
15 |
16 | publish:
17 | poetry publish --build
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [**中文**](./README.zh.md)
2 |
3 | # [Academic File Converter](https://github.com/GetRD/academic-file-converter)
4 |
5 | [](https://pypi.python.org/pypi/academic)
6 | [](https://discord.com/channels/722225264733716590/742892432458252370/742895548159492138)
7 | [](https://github.com/sponsors/gcushen)
8 | [](https://twitter.com/GeorgeCushen)
9 | [](https://github.com/gcushen)
10 |
11 |
12 | ### 📚 Easily import publications and Jupyter notebooks to your Markdown-formatted website or book
13 |
14 | 
15 |
16 | **Features**
17 |
18 | * **Import Jupyter notebooks** as blog posts or book chapters
19 | * **Import publications** (such as **books, conference proceedings, and journals**) from your reference manager to your Markdown-formatted website or book
20 | * Simply export a BibTeX file from your reference manager, such as [Zotero](https://www.zotero.org), and provide this as the input to the converter tool
21 | * **Compatible with all static website generators** such as Next, Astro, Gatsby, Hugo, etc.
22 | * **Easy to use** - 100% Python, no dependency on complex software such as Pandoc
23 | * **Automate** file conversions using a [GitHub Action](https://github.com/HugoBlox/hugo-blox-builder/blob/main/starters/blog/.github/workflows/import-notebooks.yml)
24 |
25 | **Community**
26 |
27 | - 📚 [View the **documentation** below](#installation)
28 | - 💬 [Chat live with the **community** on Discord](https://discord.gg/z8wNYzb)
29 | - 🐦 Twitter: [@GetResearchDev](https://twitter.com/GetResearchDev) [@GeorgeCushen](https://twitter.com/GeorgeCushen) [#MadeWithAcademic](https://twitter.com/search?q=%23MadeWithAcademic&src=typed_query)
30 |
31 | ## ❤️ Support Open Research & Open Source
32 |
33 | We are on a mission to foster **open research** by developing **open source** tools like this.
34 |
35 | To help us develop this open source software sustainably under the MIT license, we ask all individuals and businesses that use it to help support its ongoing maintenance and development via sponsorship and contributing.
36 |
37 | Support the open research movement:
38 |
39 | - ⭐️ [**Star** this project on GitHub](https://github.com/GetRD/academic-file-converter)
40 | - ❤️ [Become a **GitHub Sponsor** and **unlock perks**](https://github.com/sponsors/gcushen)
41 | - ☕️ [**Donate a coffee**](https://github.com/sponsors/gcushen?frequency=one-time)
42 | - 👩💻 [**Contribute**](#contribute)
43 |
44 | ## Installation
45 |
46 | Open your **Terminal** or **Command Prompt** app and enter one of the installation commands below.
47 |
48 | ### With Pipx
49 |
50 | For the **easiest** installation, install with [Pipx](https://pypa.github.io/pipx/):
51 |
52 | pipx install academic
53 |
54 | Pipx will **automatically install the required Python version for you** in a dedicated environment.
55 |
56 | ### With Pip
57 |
58 | To install using the Python's Pip tool, ensure you have [Python 3.11+](https://realpython.com/installing-python/) installed and then run:
59 |
60 | pip3 install -U academic
61 |
62 | ## Usage
63 |
64 | Open your Command Line or Terminal app and use the `cd` command to navigate to the folder containing the files you wish to convert, for example:
65 |
66 | cd ~/Documents/my_website
67 |
68 | ### Import publications
69 |
70 | Download references from your reference manager, such as Zotero, in the Bibtex format.
71 |
72 | Say we downloaded our publications to a file named `my_publications.bib` within the website folder, let's import them into the `content/publication/` folder:
73 |
74 | academic import my_publications.bib content/publication/ --compact
75 |
76 | Optional arguments:
77 |
78 | * `--compact` Generate minimal markdown without comments or empty keys
79 | * `--overwrite` Overwrite any existing publications in the output folder
80 | * `--normalize` Normalize tags by converting them to lowercase and capitalizing the first letter (e.g. "sciEnCE" -> "Science")
81 | * `--featured` Flag these publications as *featured* (to appear in your website's *Featured Publications* section)
82 | * `--verbose` or `-v` Show verbose messages
83 | * `--help` Help
84 |
85 | ### Import full text and cover image
86 |
87 | After importing publications, we suggest you:
88 | - Edit the Markdown body of each publication to add the full text directly to the page (if the publication is open access), or otherwise, to add supplementary notes for each publication
89 | - Add an image named `featured` to each publication's folder to visually represent your publication on the page and for sharing on social media
90 | - Add the publication PDF to each publication folder (for open access publications), to enable your website visitors to download your publication
91 |
92 | [Learn more in the Hugo Blox Docs](https://docs.hugoblox.com/reference/content-types/).
93 |
94 | ### Import blog posts from Jupyter Notebooks
95 |
96 | Say we have our notebooks in a `notebooks` folder within the website folder, let's import them into the `content/post/` folder:
97 |
98 | academic import 'notebooks/*.ipynb' content/post/ --verbose
99 |
100 | Optional arguments:
101 |
102 | * `--overwrite` Overwrite any existing blog posts in the output folder
103 | * `--verbose` or `-v` Show verbose messages
104 | * `--help` Help
105 |
106 | ## Contribute
107 |
108 | Interested in contributing to **open source** and **open research**?
109 |
110 | Learn [how to contribute code on Github](https://codeburst.io/a-step-by-step-guide-to-making-your-first-github-contribution-5302260a2940).
111 |
112 | Check out the [open issues](https://github.com/GetRD/academic-file-converter/issues) and contribute a [Pull Request](https://github.com/GetRD/academic-file-converter/pulls).
113 |
114 | For local development, clone this repository and use Poetry to install and run the converter using the following commands:
115 |
116 | git clone https://github.com/GetRD/academic-file-converter.git
117 | cd academic-file-converter
118 | poetry install
119 | poetry run academic import tests/data/article.bib output/publication/ --overwrite --compact
120 | poetry run academic import 'tests/data/**/*.ipynb' output/post/ --overwrite --verbose
121 |
122 | When preparing a contribution, run the following checks and ensure that they all pass:
123 |
124 | - Lint: `make lint`
125 | - Format: `make format`
126 | - Test: `make test`
127 | - Type check: `make type`
128 |
129 | ### Help beta test the dev version
130 |
131 | You can help test the latest development version by installing the latest `main` branch from GitHub:
132 |
133 | pip3 install -U git+https://github.com/GetRD/academic-file-converter.git
134 |
135 | ## License
136 |
137 | Copyright 2018-present [George Cushen](https://georgecushen.com).
138 |
139 | Licensed under the [MIT License](https://github.com/GetRD/academic-file-converter/blob/main/LICENSE.md).
140 |
141 | 
142 | [](https://github.com/GetRD/academic-file-converter/blob/main/LICENSE.md)
143 |
--------------------------------------------------------------------------------
/README.zh.md:
--------------------------------------------------------------------------------
1 | [**English**](./README.md)
2 |
3 | # [学术文件转换器](https://github.com/GetRD/academic-file-converter)
4 |
5 | [](https://pypi.python.org/pypi/academic)
6 | [](https://discord.com/channels/722225264733716590/742892432458252370/742895548159492138)
7 | [](https://github.com/sponsors/gcushen)
8 | [](https://twitter.com/GeorgeCushen)
9 | [](https://github.com/gcushen)
10 |
11 |
12 | ### 📚 将出版物和Jupyter笔记本轻松导入到您的Markdown格式的网站或书籍中
13 |
14 | 
15 |
16 | **特性**
17 |
18 | * 将 **Jupyter笔记本**导入为博客文章或书籍章节
19 | * 将出版物(如**书籍、会议论文集和期刊**)从您的参考文献管理器导入到您的Markdown格式的网站或书籍中
20 | * 只需从参考文献管理器(例如[Zotero](https://www.zotero.org))导出BibTeX文件,并将此文件作为转换工具的输入
21 | * **兼容所有静态网站生成器**,如Next、Astro、Gatsby、Hugo等
22 | * **易于使用** - 100% Python,无需依赖Pandoc等复杂软件
23 | * 使用[GitHub Action](https://github.com/HugoBlox/hugo-blox-builder/blob/main/starters/blog/.github/workflows/import-notebooks.yml) **自动化** 文件转换
24 |
25 | **社区**
26 |
27 | - 📚 [查看以下的**文档**](#安装)
28 | - 💬 [在Discord上与**社区**实时聊天](https://discord.gg/z8wNYzb)
29 | - 🐦 推特:[@GetResearchDev](https://twitter.com/GetResearchDev) [@GeorgeCushen](https://twitter.com/GeorgeCushen) [#MadeWithAcademic](https://twitter.com/search?q=%23MadeWithAcademic&src=typed_query)
30 |
31 | ## ❤️ 支持开放研究和开源软件
32 |
33 | 我们的使命是通过开发像这样的**开源**工具来促进**开放研究**。
34 |
35 | 为了帮助我们根据MIT许可证在可持续地进行这个开源软件的开发,我们请求所有使用它的个人和企业支持它的维护和发展,通过赞助和贡献来实现。
36 |
37 | 支持开放研究运动:
38 |
39 | - ⭐️ [在GitHub上给这个项目**加星标**](https://github.com/GetRD/academic-file-converter)
40 | - ❤️ [成为**GitHub赞助商**并**解锁特权**](https://github.com/sponsors/gcushen)
41 | - ☕️ [**捐赠一杯咖啡**](https://github.com/sponsors/gcushen?frequency=one-time)
42 | - 👩💻 [**贡献**](#contribute)
43 |
44 | ## 安装
45 |
46 | 打开您的**终端**或**命令提示符**应用程序并输入以下的安装命令之一。
47 |
48 | ### 使用Pipx
49 |
50 | 对于**最简单**的安装,使用[Pipx](https://pypa.github.io/pipx/)进行安装:
51 |
52 | pipx install academic
53 |
54 | Pipx将会**自动在一个专用环境中为您安装所需的Python版本**。
55 |
56 | ### 使用Pip
57 |
58 | 使用Python的Pip工具进行安装,请确保您已安装[Python 3.11+](https://realpython.com/installing-python/),然后运行:
59 |
60 | pip3 install -U academic
61 |
62 | ## 使用方式
63 |
64 | 打开您的命令行或终端应用程序,使用`cd`命令导航至包含您希望转换的文件的文件夹,例如:
65 |
66 | cd ~/Documents/my_website
67 |
68 | ### 导入出版物
69 |
70 | 从您的参考文献管理器中(例如Zotero)下载参考文献,使用Bibtex格式。
71 |
72 | 假设我们将我们的出版物下载到了网站文件夹内名为`my_publications.bib`的文件中,让我们将它们导入到 `content/publication/`文件夹中:
73 |
74 | academic import my_publications.bib content/publication/ --compact
75 |
76 | 可选参数:
77 |
78 | * `--compact` 生成没有注释或空键的最小化markdown
79 | * `--overwrite` 覆盖输出文件夹中的任何现有出版物
80 | * `--normalize` 标准化标签,将其转换为小写并将第一个字母大写(例如 "sciEnCE" -> "Science")
81 | * `--featured` 将这些出版物标记为*特色出版物*(以便在您的网站的*特色出版物*部分显示)
82 | * `--verbose` 或 `-v`显示详细消息
83 | * `--help` 帮助
84 |
85 | ### 导入全文和封面图像
86 |
87 | 导入出版物后,我们建议您:
88 | - 编辑每个出版物的Markdown正文,直接在页面上添加全文(如果出版物是开放访问的),或者添加每个出版物的补充笔记
89 | - 将名为`featured`的图像添加到每个出版物的文件夹中,以在页面上可视化地代表您的出版物,并用于在社交媒体上分享
90 | - 将出版物的PDF添加到每个出版物的文件夹中(对于开放访问的出版物),以便您的网站访问者可以下载您的出版物
91 |
92 | [在Hugo Blox文档中学习更多知识](https://docs.hugoblox.com/reference/content-types/).
93 |
94 | ### 从Jupyter笔记本中导入博客文章
95 |
96 | 设想我们有一个笔记本在网站文件夹中的`notebooks`文件夹中,让我们将它们导入到`content/post/`文件夹中:
97 |
98 | academic import 'notebooks/*.ipynb' content/post/ --verbose
99 |
100 | 可选参数:
101 |
102 | * `--overwrite` 覆盖输出文件夹中的任何现有博客文章
103 | * `--verbose` 或 `-v` 显示详细消息
104 | * `--help` 帮助
105 |
106 | ## 贡献
107 |
108 | 对贡献**开源**和**开放研究**感兴趣吗?
109 |
110 | 了解[Github上如何贡献代码](https://codeburst.io/a-step-by-step-guide-to-making-your-first-github-contribution-5302260a2940)。
111 |
112 | 查看[开放的问题](https://github.com/GetRD/academic-file-converter/issues),并贡献一个[Pull Request](https://github.com/GetRD/academic-file-converter/pulls)。
113 |
114 | 对于本地开发,克隆此存储库并使用诗(Poetry)安装和运行转换器,使用以下命令: git clone https://github.com/GetRD/academic-file-converter.git
115 | cd academic-file-converter
116 | poetry install
117 | poetry run academic import tests/data/article.bib output/publication/ --overwrite --compact
118 | poetry run academic import 'tests/data/**/*.ipynb' output/post/ --overwrite --verbose
119 |
120 | 在准备投稿时,请运行以下检查并确保所有检查都通过:
121 |
122 | - Lint:`make lint`
123 | - Format:`make format`
124 | - Test:`make test`
125 | - Type check:`make type`
126 |
127 | ### 帮助测试开发者版本
128 |
129 | 您可以通过安装GitHub上的最新`main`分支来帮助测试最新的开发版本:
130 |
131 | pip3 install -U git+https://github.com/GetRD/academic-file-converter.git
132 |
133 | ## 许可证
134 |
135 | 版权所有2018-至今 [George Cushen](https://georgecushen.com)。
136 |
137 | 根据[MIT许可证](https://github.com/GetRD/academic-file-converter/blob/main/LICENSE.md)授权。
138 |
139 | 
140 | [](https://github.com/GetRD/academic-file-converter/blob/main/LICENSE.md)
141 |
--------------------------------------------------------------------------------
/academic/cli.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import argparse
4 | import importlib.metadata
5 | import logging
6 | import sys
7 | from argparse import RawTextHelpFormatter
8 |
9 | from academic.import_bibtex import import_bibtex
10 | from academic.import_notebook import import_notebook
11 |
12 | # Initialise logger.
13 | logging.basicConfig(
14 | format="%(asctime)s %(levelname)s: %(message)s",
15 | level=logging.WARNING,
16 | datefmt="%I:%M:%S%p",
17 | )
18 | log = logging.getLogger(__name__)
19 |
20 |
21 | def main():
22 | # Strip command name (currently `academic`) and feed arguments to the parser
23 | parse_args(sys.argv[1:])
24 |
25 |
26 | def parse_args(args):
27 | """Parse command-line arguments"""
28 |
29 | # Initialise command parser.
30 | version = importlib.metadata.version("academic")
31 | parser = argparse.ArgumentParser(
32 | description=f"Academic CLI v{version}\nhttps://github.com/GetRD/academic-file-converter",
33 | formatter_class=RawTextHelpFormatter,
34 | )
35 | subparsers = parser.add_subparsers(help="Sub-commands", dest="command")
36 |
37 | # Sub-parser for import command.
38 | parser_a = subparsers.add_parser("import", help="Import content into your website or book")
39 | parser_a.add_argument("input", type=str, help="File path to your BibTeX or Jupyter Notebook file(s)")
40 | parser_a.add_argument("output", type=str, help="Output path (e.g. `content/publication/`)")
41 | parser_a.add_argument("--featured", action="store_true", help="Flag publications as featured")
42 | parser_a.add_argument("--overwrite", action="store_true", help="Overwrite existing files in output path")
43 | parser_a.add_argument("--compact", action="store_true", help="Generate minimal markdown")
44 | parser_a.add_argument(
45 | "--normalize",
46 | action="store_true",
47 | help="Normalize each BibTeX keyword to lowercase with uppercase first letter",
48 | )
49 | parser_a.add_argument("-v", "--verbose", action="store_true", required=False, help="Verbose mode")
50 | parser_a.add_argument(
51 | "-dr",
52 | "--dry-run",
53 | action="store_true",
54 | required=False,
55 | help="Perform a dry run (e.g. for testing purposes)",
56 | )
57 |
58 | known_args, unknown = parser.parse_known_args(args)
59 |
60 | # If no arguments, show help.
61 | if len(args) == 0:
62 | parser.print_help()
63 | parser.exit()
64 | else:
65 | # The command has been recognised, proceed to parse it.
66 | if known_args.command:
67 | if known_args.verbose:
68 | # Set logging level to debug if verbose mode activated.
69 | logging.getLogger().setLevel(logging.INFO)
70 | if known_args.input.lower().endswith(".bib"):
71 | # Run command to import bibtex.
72 | import_bibtex(
73 | known_args.input,
74 | pub_dir=known_args.output,
75 | featured=known_args.featured,
76 | overwrite=known_args.overwrite,
77 | normalize=known_args.normalize,
78 | compact=known_args.compact,
79 | dry_run=known_args.dry_run,
80 | )
81 | elif known_args.input.lower().endswith(".ipynb"):
82 | # Run command to import bibtex.
83 | import_notebook(
84 | known_args.input,
85 | output_dir=known_args.output,
86 | overwrite=known_args.overwrite,
87 | dry_run=known_args.dry_run,
88 | )
89 |
90 |
91 | if __name__ == "__main__":
92 | main()
93 |
--------------------------------------------------------------------------------
/academic/generate_markdown.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import ruamel.yaml
4 |
5 |
6 | class GenerateMarkdown:
7 | """
8 | Load a Markdown file, enable its YAML front matter to be edited (currently, directly via `self.yaml[...]`), and then save it.
9 | """
10 |
11 | def __init__(self, base_path: Path, delim: str = "---", dry_run: bool = False, compact: bool = False):
12 | """
13 | Initialise the class.
14 |
15 | Args:
16 | base_path: the folder to save the Markdown file to
17 | delim: the front matter delimiter, i.e. `---` for YAML front matter
18 | dry_run: whether to actually save the output to file
19 | compact: whether to strip comments, line breaks, and empty keys from the generated Markdown
20 | """
21 | self.base_path = base_path
22 | if delim != "---":
23 | raise NotImplementedError("Currently, YAML is the only supported front-matter format.")
24 | self.delim = delim
25 | self.yaml = {}
26 | self.content = []
27 | self.path = ""
28 | self.dry_run = dry_run
29 | self.compact = compact
30 | # We use Ruamel's default round-trip loading to preserve key order and comments, rather than `YAML(typ='safe')`
31 | self.yaml_parser = ruamel.yaml.YAML()
32 |
33 | def load(self, file: Path):
34 | """
35 | Load the Markdown file to edit.
36 |
37 | Args:
38 | file: the Markdown filename to load. By default, it will be a copy of the Markdown template file saved to the output folder.
39 |
40 | Returns: n/a - directly saves output to `self.yaml`
41 |
42 | """
43 | front_matter_text = []
44 | self.yaml = {}
45 | self.content = []
46 | self.path = self.base_path / file
47 | if self.dry_run and not self.path.exists():
48 | self.yaml = dict()
49 | return
50 |
51 | with self.path.open("r", encoding="utf-8") as f:
52 | lines = f.readlines()
53 |
54 | # Detect both the YAML front matter and the Markdown content in the template
55 | delims_seen = 0
56 | for line in lines:
57 | if line.startswith(self.delim):
58 | delims_seen += 1
59 | else:
60 | if delims_seen < 2:
61 | front_matter_text.append(line)
62 | # In Compact mode, we don't add any placeholder content to the page
63 | elif not self.compact:
64 | # Append any Markdown content from the template body (after the YAML front matter)
65 | self.content.append(line)
66 |
67 | # Parse YAML, trying to preserve key order, comments, and whitespace
68 | self.yaml = self.yaml_parser.load("".join(front_matter_text))
69 |
70 | def recursive_delete_comment_attribs(self, d):
71 | """
72 | Delete comments from the YAML template for Compact mode
73 |
74 | Args:
75 | d: the named attribute to delete from the YAML dict
76 | """
77 | if isinstance(d, dict):
78 | for k, v in d.items():
79 | self.recursive_delete_comment_attribs(k)
80 | self.recursive_delete_comment_attribs(v)
81 | elif isinstance(d, list):
82 | for elem in d:
83 | self.recursive_delete_comment_attribs(elem)
84 | try:
85 | # literal scalarstring might have comment associated with them
86 | attr = "comment" if isinstance(d, ruamel.yaml.scalarstring.ScalarString) else ruamel.yaml.comments.Comment.attrib # type: ignore
87 | delattr(d, attr)
88 | except AttributeError:
89 | pass
90 |
91 | def dump(self):
92 | """
93 | Save the generated markdown to file.
94 | """
95 | assert self.path, "You need to `.load()` first."
96 | if self.dry_run:
97 | return
98 |
99 | with open(self.path, "w", encoding="utf-8") as f:
100 | f.write("{}\n".format(self.delim))
101 | if self.compact:
102 | # For compact output, strip comments, new lines, and empty keys
103 | # Strip `image` key in Compact mode as it cannot currently be set via Bibtex, it's just set in template.
104 | # Note: a better implementation may be just to start with a different template for Compact mode,
105 | # rather than remove items from the detailed template.
106 | self.recursive_delete_comment_attribs(self.yaml)
107 | elems_to_delete = []
108 | for elem in self.yaml:
109 | if (
110 | self.yaml[elem] is None
111 | or self.yaml[elem] == ""
112 | or self.yaml[elem] == []
113 | or (elem == "featured" and self.yaml[elem] is False)
114 | or (elem == "image")
115 | ):
116 | elems_to_delete.append(elem)
117 | for elem in elems_to_delete:
118 | del self.yaml[elem]
119 | del elems_to_delete
120 | self.yaml_parser.dump(self.yaml, f)
121 | f.write("{}\n".format(self.delim))
122 | f.writelines(self.content)
123 |
--------------------------------------------------------------------------------
/academic/import_bibtex.py:
--------------------------------------------------------------------------------
1 | import calendar
2 | import os
3 | import re
4 | from datetime import datetime
5 | from pathlib import Path
6 |
7 | import bibtexparser
8 | from bibtexparser.bibdatabase import BibDatabase
9 | from bibtexparser.bparser import BibTexParser
10 | from bibtexparser.bwriter import BibTexWriter
11 | from bibtexparser.customization import convert_to_unicode
12 |
13 | from academic.generate_markdown import GenerateMarkdown
14 | from academic.publication_type import PUB_TYPES_BIBTEX_TO_CSL
15 |
16 |
17 | def import_bibtex(
18 | bibtex,
19 | pub_dir=os.path.join("content", "publication"),
20 | featured=False,
21 | overwrite=False,
22 | normalize=False,
23 | compact=False,
24 | dry_run=False,
25 | ):
26 | """Import publications from BibTeX file"""
27 | from academic.cli import log
28 | from academic.utils import AcademicError
29 |
30 | # Check BibTeX file exists.
31 | if not Path(bibtex).is_file():
32 | err = "Please check the path to your BibTeX file and re-run"
33 | log.error(err)
34 | raise AcademicError(err)
35 |
36 | # Load BibTeX file for parsing.
37 | with open(bibtex, "r", encoding="utf-8") as bibtex_file:
38 | parser = BibTexParser(common_strings=True)
39 | parser.customization = convert_to_unicode
40 | parser.ignore_nonstandard_types = False
41 | bib_database = bibtexparser.load(bibtex_file, parser=parser)
42 | for entry in bib_database.entries:
43 | parse_bibtex_entry(
44 | entry,
45 | pub_dir=pub_dir,
46 | featured=featured,
47 | overwrite=overwrite,
48 | normalize=normalize,
49 | compact=compact,
50 | dry_run=dry_run,
51 | )
52 |
53 |
54 | def parse_bibtex_entry(
55 | entry,
56 | pub_dir=os.path.join("content", "publication"),
57 | featured=False,
58 | overwrite=False,
59 | normalize=False,
60 | compact=False,
61 | dry_run=False,
62 | ):
63 | """Parse a bibtex entry and generate corresponding publication bundle"""
64 | from academic.cli import log
65 |
66 | log.info(f"Parsing entry {entry['ID']}")
67 |
68 | bundle_path = os.path.join(pub_dir, slugify(entry["ID"]))
69 | markdown_path = os.path.join(bundle_path, "index.md")
70 | cite_path = os.path.join(bundle_path, "cite.bib")
71 | date = datetime.utcnow()
72 | timestamp = date.isoformat("T") + "Z" # RFC 3339 timestamp.
73 |
74 | # Do not overwrite publication bundle if it already exists.
75 | if not overwrite and os.path.isdir(bundle_path):
76 | log.warning(f"Skipping creation of {bundle_path} as it already exists. " f"To overwrite, add the `--overwrite` argument.")
77 | return
78 |
79 | # Create bundle dir.
80 | log.info(f"Creating folder {bundle_path}")
81 | if not dry_run:
82 | Path(bundle_path).mkdir(parents=True, exist_ok=True)
83 |
84 | # Save citation file.
85 | log.info(f"Saving citation to {cite_path}")
86 | db = BibDatabase()
87 | db.entries = [entry]
88 | writer = BibTexWriter()
89 | if not dry_run:
90 | with open(cite_path, "w", encoding="utf-8") as f:
91 | f.write(writer.write(db))
92 |
93 | # Prepare YAML front matter for Markdown file.
94 | if not dry_run:
95 | from importlib import resources as import_resources
96 |
97 | # Load the Markdown template from within the `templates` folder of the `academic` package
98 | template = import_resources.read_text(__package__ + ".templates", "publication.md")
99 |
100 | with open(markdown_path, "w") as f:
101 | f.write(template)
102 |
103 | page = GenerateMarkdown(Path(bundle_path), dry_run=dry_run, compact=compact)
104 | page.load(Path("index.md"))
105 |
106 | page.yaml["title"] = clean_bibtex_str(entry["title"])
107 |
108 | if "subtitle" in entry:
109 | page.yaml["subtitle"] = clean_bibtex_str(entry["subtitle"])
110 |
111 | year, month, day = "", "01", "01"
112 | if "date" in entry:
113 | date_parts = entry["date"].split("-")
114 | if len(date_parts) == 3:
115 | year, month, day = date_parts[0], date_parts[1], date_parts[2]
116 | elif len(date_parts) == 2:
117 | year, month = date_parts[0], date_parts[1]
118 | elif len(date_parts) == 1:
119 | year = date_parts[0]
120 | if "month" in entry and month == "01":
121 | month = month2number(entry["month"])
122 | if "year" in entry and year == "":
123 | year = entry["year"]
124 | if len(year) == 0:
125 | log.error(f'Invalid date for entry `{entry["ID"]}`.')
126 |
127 | page.yaml["date"] = f"{year}-{month}-{day}"
128 | page.yaml["publishDate"] = timestamp
129 |
130 | authors = None
131 | if "author" in entry:
132 | authors = entry["author"]
133 | elif "editor" in entry:
134 | authors = entry["editor"]
135 |
136 | if authors:
137 | authors = clean_bibtex_authors([i.strip() for i in authors.replace("\n", " ").split(" and ")])
138 | page.yaml["authors"] = authors
139 |
140 | # Convert Bibtex publication type to the universal CSL standard, defaulting to `manuscript`
141 | default_csl_type = "manuscript"
142 | pub_type = PUB_TYPES_BIBTEX_TO_CSL.get(entry["ENTRYTYPE"], default_csl_type)
143 | page.yaml["publication_types"] = [pub_type]
144 |
145 | if "abstract" in entry:
146 | page.yaml["abstract"] = clean_bibtex_str(entry["abstract"])
147 | else:
148 | page.yaml["abstract"] = ""
149 |
150 | page.yaml["featured"] = featured
151 |
152 | # Publication name.
153 | # This field is Markdown formatted, wrapping the publication name in `*` for italics
154 | if "booktitle" in entry:
155 | publication = "*" + clean_bibtex_str(entry["booktitle"]) + "*"
156 | elif "journal" in entry:
157 | publication = "*" + clean_bibtex_str(entry["journal"]) + "*"
158 | elif "publisher" in entry:
159 | publication = "*" + clean_bibtex_str(entry["publisher"]) + "*"
160 | else:
161 | publication = ""
162 | page.yaml["publication"] = publication
163 |
164 | if "keywords" in entry:
165 | page.yaml["tags"] = clean_bibtex_tags(entry["keywords"], normalize)
166 |
167 | if "doi" in entry:
168 | page.yaml["doi"] = clean_bibtex_str(entry["doi"])
169 |
170 | links = []
171 | if all(f in entry for f in ["archiveprefix", "eprint"]) and entry["archiveprefix"].lower() == "arxiv":
172 | links += [{"name": "arXiv", "url": "https://arxiv.org/abs/" + clean_bibtex_str(entry["eprint"])}]
173 |
174 | if "url" in entry:
175 | sane_url = clean_bibtex_str(entry["url"])
176 |
177 | if sane_url[-4:].lower() == ".pdf":
178 | page.yaml["url_pdf"] = sane_url
179 | else:
180 | links += [{"name": "URL", "url": sane_url}]
181 |
182 | if links:
183 | page.yaml["links"] = links
184 |
185 | # Save Markdown file.
186 | try:
187 | log.info(f"Saving Markdown to '{markdown_path}'")
188 | if not dry_run:
189 | page.dump()
190 | except IOError:
191 | log.error("Could not save file.")
192 | return page
193 |
194 |
195 | def slugify(s, lower=True):
196 | bad_symbols = (".", "_", ":") # Symbols to replace with hyphen delimiter.
197 | delimiter = "-"
198 | good_symbols = (delimiter,) # Symbols to keep.
199 | for r in bad_symbols:
200 | s = s.replace(r, delimiter)
201 |
202 | s = re.sub(r"(\D+)(\d+)", r"\1\-\2", s) # Delimit non-number, number.
203 | s = re.sub(r"(\d+)(\D+)", r"\1\-\2", s) # Delimit number, non-number.
204 | s = re.sub(r"((?<=[a-z])[A-Z]|(? str:
17 | return text.lower().replace(" ", "-")
18 |
19 |
20 | def import_notebook(
21 | input_path,
22 | output_dir=os.path.join("content", "post"),
23 | overwrite=False,
24 | dry_run=False,
25 | ):
26 | """Import blog posts from Jupyter Notebook files"""
27 | from academic.cli import log
28 |
29 | log.info(f"Searching for Jupyter notebooks in `{input_path}`")
30 | for filename in glob.glob(input_path, recursive=True):
31 | if not (filename.endswith(".ipynb") and os.path.basename(filename) != ".ipynb_checkpoints"):
32 | continue
33 |
34 | log.debug(f"Found notebook `{filename}`")
35 |
36 | # Read Notebook
37 | nb = nbf.read(open(filename, "r"), as_version=4)
38 |
39 | # Export Markdown
40 | nbc_config = Config()
41 | nbc_config.MarkdownExporter.preprocessors = [JupyterWhitespaceRemover]
42 | exporter = nbc.MarkdownExporter(config=nbc_config)
43 | if not dry_run:
44 | _export(nb, exporter, output_dir, filename, ".md", overwrite)
45 |
46 |
47 | def _export(nb, exporter, output_dir, filename, extension, overwrite):
48 | from academic.cli import log
49 |
50 | # Determine output path for page bundle
51 | filename_base = Path(filename).stem
52 | slug = _get_slug(filename_base)
53 | page_bundle_path = Path(output_dir) / slug
54 |
55 | # Do not overwrite blog post if it already exists
56 | if not overwrite and os.path.isdir(page_bundle_path):
57 | log.debug(f"Skipping creation of `{page_bundle_path}` as it already exists. To overwrite, add the `--overwrite` argument.")
58 | return
59 |
60 | log.info(f"Importing notebook `{filename}`")
61 |
62 | # Create page bundle folder
63 | if not os.path.exists(page_bundle_path):
64 | os.makedirs(page_bundle_path)
65 |
66 | # Check for front matter variables in notebook metadata
67 | if "front_matter" in nb["metadata"]:
68 | front_matter_from_file = dict(nb["metadata"]["front_matter"])
69 | log.info(f"Found front matter metadata in notebook: {json.dumps(front_matter_from_file)}")
70 | else:
71 | front_matter_from_file = {}
72 |
73 | # Convert notebook to markdown
74 | (body, resources) = exporter.from_notebook_node(nb)
75 |
76 | # Export notebook resources
77 | for name, data in resources.get("outputs", {}).items():
78 | output_filename = Path(page_bundle_path) / name
79 | with open(output_filename, "wb") as image_file:
80 | image_file.write(data)
81 |
82 | # Try to find title as top-level heading (h1), falling back to filename
83 | search = re.search("^#{1}(.*)", body)
84 | if search:
85 | title = search.group(1).strip()
86 | # Remove the h1 heading as static site generators expect the title to be defined via front matter instead.
87 | body = re.sub("^#{1}(.*)", "", body)
88 | else:
89 | # Fallback to using filename as title
90 | # Apply transformation as expect *nix-style file naming with hyphens/underscores separating words rather than spaces.
91 | title = filename_base.replace("-", " ").replace("_", " ").title()
92 |
93 | # Initialise front matter variables
94 | date = datetime.now().strftime("%Y-%m-%d")
95 | front_matter = {"title": title, "date": date}
96 | front_matter.update(front_matter_from_file)
97 | log.info(f"Generating page with title: {front_matter['title']}")
98 |
99 | # Unlike the Bibtex converter, we can't easily use Ruamel YAML library here as we need to output to string
100 | front_matter_yaml = yaml.safe_dump(front_matter, sort_keys=False, allow_unicode=True)
101 | # Strip final newline as our `output` will auto-add newlines below
102 | front_matter_yaml = front_matter_yaml.rstrip()
103 | # Wrap front matter variables with triple hyphens to represent Markdown front matter
104 | output = "\n".join(("---", front_matter_yaml, "---", clean_markdown(body)))
105 |
106 | # Write output file
107 | output_filename = os.path.join(page_bundle_path, "index" + extension)
108 | with open(output_filename, "w") as text_file:
109 | text_file.write(output)
110 |
111 |
112 | def clean_markdown(body: str) -> str:
113 | """
114 | `nbconvert` creates too much whitespace and newlines.
115 | Try to tidy up the output by removing multiple new lines.
116 | """
117 | return re.sub(r"\n+(?=\n)", "\n", body)
118 |
--------------------------------------------------------------------------------
/academic/jupyter_whitespace_remover.py:
--------------------------------------------------------------------------------
1 | from nbconvert.preprocessors import Preprocessor
2 |
3 |
4 | class JupyterWhitespaceRemover(Preprocessor):
5 | """
6 | Try to clean up a Jupyter notebook by:
7 | - removing blank code cells
8 | - removing unnecessary whitespace
9 | """
10 |
11 | def preprocess(self, nb, resources):
12 | """
13 | Remove blank `code` cells
14 | """
15 | for index, cell in enumerate(nb.cells):
16 | if cell.cell_type == "code" and not cell.source:
17 | nb.cells.pop(index)
18 | else:
19 | nb.cells[index], resources = self.preprocess_cell(cell, resources, index)
20 | return nb, resources
21 |
22 | def preprocess_cell(self, cell, resources, cell_index):
23 | """
24 | Remove extraneous whitespace from code cells' source code
25 | """
26 | if cell.cell_type == "code":
27 | cell.source = cell.source.strip()
28 |
29 | return cell, resources
30 |
--------------------------------------------------------------------------------
/academic/publication_type.py:
--------------------------------------------------------------------------------
1 | # Map BibTeX publication types to universal CSL types
2 | PUB_TYPES_BIBTEX_TO_CSL = {
3 | "article": "article-journal",
4 | "book": "book",
5 | "conference": "paper-conference",
6 | "inbook": "chapter",
7 | "incollection": "chapter",
8 | "inproceedings": "paper-conference",
9 | "manual": "book",
10 | "mastersthesis": "thesis",
11 | "patent": "patent",
12 | "phdthesis": "thesis",
13 | "proceedings": "book",
14 | "report": "report",
15 | "thesis": "thesis",
16 | "techreport": "report",
17 | "unpublished": "manuscript",
18 | }
19 |
--------------------------------------------------------------------------------
/academic/templates/publication.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Publication title'
3 |
4 | # Authors
5 | # A YAML list of author names
6 | # If you created a profile for a user (e.g. the default `admin` user at `content/authors/admin/`),
7 | # write the username (folder name) here, and it will be replaced with their full name and linked to their profile.
8 | authors: []
9 |
10 | # Author notes (such as 'Equal Contribution')
11 | # A YAML list of notes for each author in the above `authors` list
12 | author_notes: []
13 |
14 | date: '2013-07-01T00:00:00Z'
15 |
16 | # Date to publish webpage (NOT necessarily Bibtex publication's date).
17 | publishDate: '2017-01-01T00:00:00Z'
18 |
19 | # Publication type.
20 | # A single CSL publication type but formatted as a YAML list (for Hugo requirements).
21 | publication_types: ['paper-conference']
22 |
23 | # Publication name and optional abbreviated publication name.
24 | publication: ''
25 | publication_short: ''
26 |
27 | doi: ''
28 |
29 | abstract: ''
30 |
31 | # Summary. An optional shortened abstract.
32 | summary: ''
33 |
34 | tags: []
35 |
36 | # Display this page in a list of Featured pages?
37 | featured: false
38 |
39 | # Links
40 | url_pdf: ''
41 | url_code: ''
42 | url_dataset: ''
43 | url_poster: ''
44 | url_project: ''
45 | url_slides: ''
46 | url_source: ''
47 | url_video: ''
48 |
49 | # Custom links (uncomment lines below)
50 | # links:
51 | # - name: Custom Link
52 | # url: http://example.org
53 |
54 | # Publication image
55 | # Add an image named `featured.jpg/png` to your page's folder then add a caption below.
56 | image:
57 | caption: ''
58 | focal_point: ''
59 | preview_only: false
60 |
61 | # Associated Projects (optional).
62 | # Associate this publication with one or more of your projects.
63 | # Simply enter your project's folder or file name without extension.
64 | # E.g. `projects: ['internal-project']` links to `content/project/internal-project/index.md`.
65 | # Otherwise, set `projects: []`.
66 | projects: []
67 | ---
68 |
69 | Add the **full text** or **supplementary notes** for the publication here using Markdown formatting.
70 |
--------------------------------------------------------------------------------
/academic/utils.py:
--------------------------------------------------------------------------------
1 | class AcademicError(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | # See https://python-poetry.org/docs/pyproject/#poetry-and-pep-517
3 | requires = ["poetry-core>=1.0.0"]
4 | build-backend = "poetry.core.masonry.api"
5 |
6 | [tool.poetry]
7 | name = "academic"
8 | description = "Import Bibtex publications and Jupyter Notebook blog posts into your Markdown-formatted website or book."
9 | version = "0.11.2"
10 | authors = ["George Cushen"]
11 | readme = "README.md"
12 | homepage = "https://hugoblox.com/"
13 | documentation = "https://docs.hugoblox.com/reference/content-types/"
14 | repository = "https://github.com/GetRD/academic-file-converter"
15 | keywords = ["research", "Jupyter", "bibtex", "markdown", "latex", "publication", "reference", "academic", "converter"]
16 | license = "MIT"
17 | packages = [
18 | { include = "academic" },
19 | ]
20 | classifiers = [
21 | "Intended Audience :: Science/Research",
22 | "Topic :: Internet :: WWW/HTTP :: Site Management",
23 | "Topic :: Software Development :: Libraries :: Python Modules",
24 | ]
25 |
26 | [tool.poetry.scripts]
27 | academic = "academic.cli:main"
28 |
29 | [tool.poetry.dependencies]
30 | python = ">=3.11.5"
31 | bibtexparser = "~1.4"
32 | "ruamel.yaml" = "~0.17"
33 | nbconvert = "^7.10.0"
34 | pyyaml = "^6.0.1"
35 |
36 | [tool.poetry.group.dev.dependencies]
37 | black = "^23.9.1"
38 | flake8 = "^6.1.0"
39 | isort = "^5.12.0"
40 | pytest = "^7.4.2"
41 | pyright = "^1.1.329"
42 | jupyter = "^1.0.0"
43 |
44 | [tool.black]
45 | target-version = ['py311', 'py312']
46 | line-length = 150 # Match Flake8 setting in `.flake8` file
47 |
48 | [tool.pyright]
49 | include = ["academic"]
50 |
51 | [tool.pytest.ini_options]
52 | # Ignore warning from Jupyter package: `DeprecationWarning: Jupyter is migrating its paths to use standard platformdirs`
53 | filterwarnings =['ignore::DeprecationWarning']
54 |
--------------------------------------------------------------------------------
/tests/data/article.bib:
--------------------------------------------------------------------------------
1 | @article{articleID,
2 | author = {Nelson Bigetti},
3 | title = {The title of the article},
4 | journal = {The name of the journal},
5 | year = 2019,
6 | number = 2,
7 | pages = {100-105},
8 | month = 7,
9 | volume = 4
10 | }
--------------------------------------------------------------------------------
/tests/data/book.bib:
--------------------------------------------------------------------------------
1 | @book{book,
2 | author = {Nelson Bigetti},
3 | title = {The title of the book},
4 | year = 2019,
5 | abstract = {Paragraph one.
6 |
7 | Paragraph two.
8 |
9 | Paragraph three.},
10 | }
11 | @manual{MyManual,
12 | author = {Nelson Bigetti},
13 | title = {Long manual title},
14 | shorttitle = {Short manual title},
15 | date = {2019-08},
16 | }
17 |
--------------------------------------------------------------------------------
/tests/data/notebooks/blog-with-jupyter.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {
6 | "editable": true,
7 | "slideshow": {
8 | "slide_type": ""
9 | },
10 | "tags": []
11 | },
12 | "source": [
13 | "# Blog with Jupyter Notebooks!"
14 | ]
15 | },
16 | {
17 | "cell_type": "code",
18 | "execution_count": 1,
19 | "metadata": {
20 | "ExecuteTime": {
21 | "end_time": "2023-11-04T20:14:14.562747Z",
22 | "start_time": "2023-11-04T20:14:14.488817Z"
23 | }
24 | },
25 | "outputs": [
26 | {
27 | "data": {
28 | "image/png": "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",
29 | "text/plain": [
30 | ""
31 | ]
32 | },
33 | "execution_count": 1,
34 | "metadata": {},
35 | "output_type": "execute_result"
36 | }
37 | ],
38 | "source": [
39 | "from IPython.core.display import Image\n",
40 | "Image('https://www.python.org/static/community_logos/python-logo-master-v3-TM-flattened.png')"
41 | ]
42 | },
43 | {
44 | "cell_type": "code",
45 | "execution_count": 6,
46 | "metadata": {},
47 | "outputs": [
48 | {
49 | "name": "stdout",
50 | "output_type": "stream",
51 | "text": [
52 | "Welcome to Academic!\n"
53 | ]
54 | }
55 | ],
56 | "source": [
57 | "print(\"Welcome to Academic!\")"
58 | ]
59 | },
60 | {
61 | "cell_type": "markdown",
62 | "metadata": {
63 | "editable": true,
64 | "slideshow": {
65 | "slide_type": ""
66 | },
67 | "tags": []
68 | },
69 | "source": [
70 | "## Organize your notebooks\n",
71 | "\n",
72 | "Place the notebooks that you would like to publish in a `notebooks` folder at the root of your website."
73 | ]
74 | },
75 | {
76 | "cell_type": "markdown",
77 | "metadata": {
78 | "editable": true,
79 | "slideshow": {
80 | "slide_type": ""
81 | },
82 | "tags": []
83 | },
84 | "source": [
85 | "## Import the notebooks into your site\n",
86 | "\n",
87 | "```bash\n",
88 | "pipx install academic\n",
89 | "academic import 'notebooks/**.ipynb' content/post/ --verbose\n",
90 | "```"
91 | ]
92 | },
93 | {
94 | "cell_type": "markdown",
95 | "metadata": {
96 | "editable": true,
97 | "slideshow": {
98 | "slide_type": ""
99 | },
100 | "tags": []
101 | },
102 | "source": [
103 | "The notebooks will be published to the folder you specify above. In this case, they will be published to your `content/post/` folder."
104 | ]
105 | },
106 | {
107 | "cell_type": "code",
108 | "execution_count": null,
109 | "metadata": {
110 | "editable": true,
111 | "slideshow": {
112 | "slide_type": ""
113 | },
114 | "tags": []
115 | },
116 | "outputs": [],
117 | "source": []
118 | }
119 | ],
120 | "metadata": {
121 | "front_matter": {
122 | "summary": "Easily blog from Jupyter notebooks!"
123 | },
124 | "kernelspec": {
125 | "display_name": "Python 3 (ipykernel)",
126 | "language": "python",
127 | "name": "python3"
128 | },
129 | "language_info": {
130 | "codemirror_mode": {
131 | "name": "ipython",
132 | "version": 3
133 | },
134 | "file_extension": ".py",
135 | "mimetype": "text/x-python",
136 | "name": "python",
137 | "nbconvert_exporter": "python",
138 | "pygments_lexer": "ipython3",
139 | "version": "3.11.5"
140 | }
141 | },
142 | "nbformat": 4,
143 | "nbformat_minor": 4
144 | }
145 |
--------------------------------------------------------------------------------
/tests/data/notebooks/test.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "source": [
6 | "# Test\n",
7 | "\n",
8 | "Hello ** world ** 🌍"
9 | ],
10 | "metadata": {
11 | "collapsed": false
12 | }
13 | },
14 | {
15 | "cell_type": "code",
16 | "execution_count": null,
17 | "outputs": [],
18 | "source": [],
19 | "metadata": {
20 | "collapsed": false
21 | }
22 | }
23 | ],
24 | "metadata": {
25 | "kernelspec": {
26 | "display_name": "Python 3",
27 | "language": "python",
28 | "name": "python3"
29 | },
30 | "language_info": {
31 | "codemirror_mode": {
32 | "name": "ipython",
33 | "version": 3
34 | },
35 | "file_extension": ".py",
36 | "mimetype": "text/x-python",
37 | "name": "python",
38 | "nbconvert_exporter": "python",
39 | "pygments_lexer": "ipython3",
40 | "version": "3.7.3"
41 | }
42 | },
43 | "nbformat": 4,
44 | "nbformat_minor": 4
45 | }
46 |
--------------------------------------------------------------------------------
/tests/data/report.bib:
--------------------------------------------------------------------------------
1 | @techreport{TR-1234,
2 | author = {Nelson Bigetti},
3 | title = {Technical report 1234},
4 | shorttitle = {TR 1234},
5 | date = {2019-06},
6 | institution = {{University of Somewhere, Department of Something}},
7 | number = {TR-1234},
8 | type = {Technical Report}
9 | }
10 | @report{TR-2345,
11 | author = {Nelson Bigetti},
12 | title = {Technical report 2345},
13 | shorttitle = {TR 2345},
14 | date = {2019-07},
15 | institution = {{University of Somewhere, Department of Something}},
16 | number = {TR-2345},
17 | type = {Technical Report}
18 | }
19 |
--------------------------------------------------------------------------------
/tests/data/thesis.bib:
--------------------------------------------------------------------------------
1 | @thesis{MyBScThesis,
2 | author = {Nelson Bigetti},
3 | title = {An extremly interesting title},
4 | date = {2019-06},
5 | institution = {{University of Somewhere, Department of Something}},
6 | type = {BSc Thesis}
7 | }
8 | @mastersthesis{MyMScThesis,
9 | author = {Nelson Bigetti},
10 | title = {Another extremly interesting title},
11 | date = {2019-07},
12 | institution = {{University of Somewhere, Department of Something}},
13 | type = {MSc Thesis}
14 | }
15 | @phdthesis{MyPhDThesis,
16 | author = {Nelson Bigetti},
17 | title = {Yet another extremly interesting title},
18 | date = {2019-08},
19 | institution = {{University of Somewhere, Department of Something}},
20 | }
21 |
--------------------------------------------------------------------------------
/tests/test_bibtex_import.py:
--------------------------------------------------------------------------------
1 | import typing
2 | from pathlib import Path
3 |
4 | import bibtexparser
5 | from bibtexparser.bparser import BibTexParser
6 |
7 | from academic import cli, import_bibtex
8 | from academic.generate_markdown import GenerateMarkdown
9 |
10 | bibtex_dir = Path(__file__).parent / "data"
11 |
12 |
13 | def test_bibtex_import():
14 | cli.parse_args(["import", "--dry-run", "tests/data/article.bib", "content/publication/"])
15 |
16 |
17 | def _process_bibtex(file, expected_count=1) -> "typing.List[GenerateMarkdown]":
18 | """
19 | Parse a BibTeX .bib file and return the parsed metadata
20 | :param file: The .bib file to parse
21 | :param expected_count: The expected number of entries inside the .bib
22 | :return: The parsed metadata as a list of EditableFM
23 | """
24 | parser = BibTexParser(common_strings=True)
25 | parser.customization = import_bibtex.convert_to_unicode
26 | parser.ignore_nonstandard_types = False
27 | with Path(bibtex_dir, file).open("r", encoding="utf-8") as bibtex_file:
28 | bib_database = bibtexparser.load(bibtex_file, parser=parser)
29 | results = []
30 | for entry in bib_database.entries:
31 | results.append(import_bibtex.parse_bibtex_entry(entry, dry_run=True))
32 | assert len(results) == expected_count
33 | return results
34 |
35 |
36 | def _test_publication_type(metadata: GenerateMarkdown, expected_type: str):
37 | """
38 | Check that the publication_types field of the parsed metadata is set to the expected type.
39 | """
40 | assert metadata.yaml["publication_types"] == [expected_type]
41 |
42 |
43 | def test_bibtex_types():
44 | """
45 | This test uses the import_bibtex functions to parse a .bib file and checks that the
46 | resulting metadata has the correct publication type set.
47 | """
48 | _test_publication_type(_process_bibtex("article.bib")[0], "article-journal")
49 | for metadata in _process_bibtex("report.bib", expected_count=2):
50 | _test_publication_type(metadata, "report")
51 | for metadata in _process_bibtex("thesis.bib", expected_count=3):
52 | _test_publication_type(metadata, "thesis")
53 | for metadata in _process_bibtex("book.bib", expected_count=2):
54 | _test_publication_type(metadata, "book")
55 |
--------------------------------------------------------------------------------
/tests/test_notebook_import.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from academic import cli
4 |
5 |
6 | def test_notebook_import_no_output(capfd):
7 | """
8 | The importer does not have a flag to determine what formats the user intends to convert.
9 | Instead, it relies on the file extension in the input string.
10 | If there is no file extension (e.g. just a wildcard), then expect no output.
11 | """
12 | cli.parse_args(["import", "--dry-run", "--verbose", "tests/data/notebooks/*", "content/post/"])
13 | out, err = capfd.readouterr()
14 | assert out == ""
15 | assert err == ""
16 |
17 |
18 | def test_notebook_import_info_level(caplog):
19 | caplog.set_level(logging.INFO)
20 |
21 | cli.parse_args(
22 | [
23 | "import",
24 | "tests/data/notebooks/*.ipynb",
25 | "content/post/",
26 | "--dry-run",
27 | ]
28 | )
29 | # assert "Found notebook `test.ipynb`" in out
30 | assert "Searching for Jupyter notebooks in `tests/data/notebooks/*.ipynb`" in caplog.text
31 |
32 |
33 | def test_notebook_import_debug_level(caplog):
34 | caplog.set_level(logging.DEBUG)
35 |
36 | cli.parse_args(
37 | [
38 | "import",
39 | "tests/data/notebooks/*.ipynb",
40 | "content/post/",
41 | "--dry-run",
42 | ]
43 | )
44 |
45 | # Note: this logging output should only be shown at DEBUG log level, so we set the corresponding level above
46 | assert "Found notebook `tests/data/notebooks/test.ipynb`" in caplog.text
47 | assert "Found notebook `tests/data/notebooks/blog-with-jupyter.ipynb`" in caplog.text
48 |
--------------------------------------------------------------------------------