├── .eslintrc.yml ├── .github ├── FUNDING.yml ├── pull-request-template.md └── workflows │ ├── build.yaml │ ├── lint.yaml │ └── release.yaml ├── .gitignore ├── .prettierrc.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── doc ├── arxiv2notion.gif ├── arxiv2notionplus_db.png ├── multiple_db.png ├── nerf_example1.png └── nerf_example2.png ├── manifest.json ├── package-lock.json ├── package.json ├── packages └── .gitkeep ├── src ├── html │ ├── options.html │ └── popup.html ├── images │ └── icon128.png ├── js │ ├── notion.js │ ├── options.js │ ├── parsers.js │ └── popup.js └── scss │ └── theme.scss └── webpack.config.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es6: true 3 | browser: true 4 | webextensions: true 5 | extends: eslint:recommended 6 | globals: 7 | chrome: true 8 | parserOptions: 9 | ecmaVersion: latest 10 | sourceType: module 11 | rules: {} 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [denkiwakame] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/pull-request-template.md: -------------------------------------------------------------------------------- 1 | ## 🔄 Changes 2 | 3 | 4 | 5 | ## ⚠️ Breaking Changes 6 | 7 | 8 | 9 | - [ ] None 10 | - [ ] Yes 11 | - **Details:** 12 | 13 | ## 🎯 Motivation 14 | 15 | 16 | 17 | ## 🔍 Reproduction Steps 18 | 19 | 20 | 21 | ```bash 22 | # Example: 23 | # npm run build 24 | # npm run dev 25 | # npm run lint 26 | # npm run pack 27 | ``` 28 | 29 | ## ✅ Results 30 | 31 | 32 | 33 | ## 📝 Additional Notes 34 | 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: '24.8.0' 17 | - name: install 18 | run: npm install 19 | - name: build 20 | run: npm run build 21 | - name: pack 22 | run: npm run pack 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: eslint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: '24.8.0' 15 | - uses: actions/cache@v3 16 | id: npm_cache_id 17 | with: 18 | path: "**/node_modules" 19 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 20 | - name: install 21 | if: steps.cache.outputs.cache-hit != 'true' 22 | run: npm install 23 | - name: eslint 24 | run: npm run lint 25 | - name: prettier 26 | run: npm run format:check 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Releases 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: '24.8.0' 15 | - name: install 16 | run: npm install 17 | - name: build 18 | run: npm run build 19 | - name: pack 20 | run: npm run pack 21 | - uses: ncipollo/release-action@v1 22 | with: 23 | artifacts: "packages/*" 24 | token: ${{secrets.GITHUB_TOKEN}} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: 'es5' 2 | tabWidth: 2 3 | semi: true 4 | singleQuote: true 5 | 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | All notable changes to this repository will be documented in this file. 4 | 5 | ## [1.4.1] - 2024-12-19 6 | 7 | ### Added 8 | 9 | - Support for www.arxiv.org URLs in addition to arxiv.org 10 | - Fixed arXiv API request with proper CORS headers 11 | - Additional host permissions for arxiv.org and www.arxiv.org domains 12 | 13 | ### Fixed 14 | 15 | - Updated arXiv API endpoint from HTTP to HTTPS for security 16 | - Improved error handling for arXiv API requests with status code logging 17 | - Enhanced URL detection to support both arxiv.org and www.arxiv.org formats 18 | 19 | ## [1.4.0] - 2024-07-18 20 | 21 | ### Fixed 22 | 23 | - updates for the latest Notion dashboard @denkiwakame [PR#19](https://github.com/denkiwakame/arxiv2notion/pull/19) 24 | 25 | ## [1.3.0] - 2024-05-28 26 | 27 | ### Added 28 | 29 | - 🚀 **New Feature:** ACL Anthology URL support (experimental) [PR#16](https://github.com/denkiwakame/arxiv2notion/pull/16) 30 | - 🚀 **New Feature:** support for old arXiv identifier [PR#15](https://github.com/denkiwakame/arxiv2notion/pull/15) @aralsea 31 | 32 | ## [1.2.0] - 2024-01-17 33 | 34 | ### Added 35 | 36 | - 🚀 **New Feature:** OpenReview URL support (experimental) [PR#11](https://github.com/denkiwakame/arxiv2notion/pull/11) [PR#12](https://github.com/denkiwakame/arxiv2notion/pull/12) 37 | 38 | ### Changed 39 | 40 | - restrict host_permission [PR#10](https://github.com/denkiwakame/arxiv2notion/pull/10) 41 | 42 | ## [1.1.0] - 2024-01-10 43 | 44 | ### Added 45 | 46 | - 🚀 **New Feature:** check duplicated entry [PR#9](https://github.com/wangjksjtu/arxiv2notionplus/issues/1) 47 | 48 | ### Changed 49 | 50 | - Migration to Notion API version (2022-06-28) [PR#8](https://github.com/denkiwakame/arxiv2notion/pull/8) 51 | 52 | ## [1.0.0] - 2024-01-08 53 | 54 | ### Added 55 | 56 | - integrates [arxiv2notionplus](https://github.com/wangjksjtu/arxiv2notionplus/issues/1) [PR#6](https://github.com/denkiwakame/arxiv2notion/pull/6) 57 | - add publication date for easier tracking @wangjksjtu 58 | - add comment parser for quick access to the potential project homepage or code link (if available) @wangjksjtu 59 | - Refactor the above codes & fix bugs by @denkiwakame 60 | - CONTRIBUTING.md @denkiwakame 61 | - CHANGELOG.md @denkiwakame 62 | - Linter / Formatter @denkiwakame 63 | 64 | ### Changed 65 | 66 | - Replace the author field from `text` to `multi-select` to fully leverage the search/filter in notion @wangjksjtu 67 | - Release a public notion database/table [here](https://denkiwakame.notion.site/597cdd58bded4375b1cbe073b2ed6f5d?v=63fcbfda57824b239b66e52dde841cdf) @denkiwakame 68 | - Update UI to improve the navigation to the button @denkiwakame 69 | 70 | ### Fixed 71 | 72 | - Migration to Manifest V3 @denkiwakame [PR#7](https://github.com/denkiwakame/arxiv2notion/pull/7) 73 | 74 | ## [0.0.1] - 2021-06-07 75 | 76 | ### Added 77 | 78 | - initial release from @denkiwakame 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing to arxiv2notion 3 | 4 | First off, thanks for taking the time to contribute! :rainbow: 5 | 6 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 7 | 8 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 9 | > - Star the project 10 | > - TweetX about it 11 | > - Refer this project in your project's readme 12 | 13 | 14 | ## Table of Contents 15 | 16 | - [I Have a Question](#i-have-a-question) 17 | - [I Want To Contribute](#i-want-to-contribute) 18 | - [How to Develop arxiv2notion](#how-to-develop-arxiv2notion) 19 | - [Suggesting Enhancements](#suggesting-enhancements) 20 | 21 | ## I Have a Question 22 | 23 | Before you ask a question, it is best to search for existing [Issues](https://github.com/denkiwakame/arxiv2notion/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. 24 | 25 | If you then still feel the need to ask a question and need clarification, we recommend the following: 26 | 27 | - Open an [Issue](https://github.com/denkiwakame/arxiv2notion/issues/new). 28 | - Provide as much context as you can about what you're running into. 29 | - Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant. 30 | 31 | We will then take care of the issue as soon as possible. 32 | 33 | ## I Want To Contribute 34 | ### How to Develop arxiv2notion 35 | 36 | #### How to build your extension locally 37 | 38 | ```bash 39 | $ git clone https://github.com/denkiwakame/arxiv2notion.md 40 | $ npm install 41 | $ npm run build 42 | $ npm run watch # debug locally 43 | $ npm run pack # pack to .zip extension 44 | ``` 45 | 46 | #### How to debug your extension 47 | - `$ npm run build` build the extension locally 48 | - navigate to `chrome://extension` 49 | - turn on `developer mode` 50 | - select `load unpacked` and open `arxiv2notion/dist` 51 | - you can see the extension ID like `aedplelmaaaldilfkeobdapccljxxxxx` in the loaded extension description 52 | - open `chrome-extension://aedplelmaaaldilfkeobdapccljxxxxx/popup.html` 53 | - open chrome developer tools in your browser (`Ctrl + Shift + i` w/Linux) 54 | - you can see debugging outputs (\eg `console.log()`) in the devtools. 55 | - once you `load unpacked` , the extension is automatically synced whenever you run `npm run build` and reload `chrome-extension://${your-extension-id}/popup.html` 56 | - you will be able to know more about how everything is working. 57 | 58 | ![image](https://user-images.githubusercontent.com/1871262/141605730-98917f70-f3cc-4d60-9068-29416474a086.png) 59 | 60 | #### Change Notion database scheme 61 | - If you attempt to add a new column like `published date` , you need to add it in your notion.so manually https://github.com/denkiwakame/arxiv2notion#getting-started or via Notion API https://developers.notion.com/reference/update-a-database 62 | 63 | #### Get arXiv published date 64 | - To retrieve arXiv paper information, this extension utilizes arXiv public API. 65 | - You can see properties available in the current API https://arxiv.org/help/api/basics 66 | - For example, if you need `published` , you can add some code to obtain the target property. 67 | - https://github.com/denkiwakame/arxiv2notion/blob/main/src/js/popup.js#L112-L124 68 | - `const published = entry.querySelector("published").textContent;` 69 | - The retrieved contents will be posted by this line https://github.com/denkiwakame/arxiv2notion/blob/main/src/js/popup.js#L49 70 | - Thus you need to add properties to https://github.com/denkiwakame/arxiv2notion/blob/main/src/js/popup.js#L127 71 | 72 | #### Post to notion.so 73 | - Add property to https://github.com/denkiwakame/arxiv2notion/blob/main/src/js/notion.js#L62-L118 74 | - You may need to follow the Notion API date format. https://developers.notion.com/reference/page#date-property-values 75 | 76 | ### Suggesting Enhancements 77 | 78 | This section guides you through submitting an enhancement suggestion for arxiv2notion, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. 79 | 80 | 81 | #### Before Submitting an Enhancement 82 | 83 | - Make sure that you are using the latest version. 84 | - Read the [documentation]() carefully and find out if the functionality is already covered, maybe by an individual configuration. 85 | - Perform a [search](https://github.com/denkiwakame/arxiv2notion/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 86 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. 87 | 88 | 89 | #### How Do I Submit a Good Enhancement Suggestion? 90 | 91 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/denkiwakame/arxiv2notion/issues). 92 | 93 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 94 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 95 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. 96 | - You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. 97 | - **Explain why this enhancement would be useful** to most arxiv2notion users. You may also want to point out the other projects that solved it better and which could serve as inspiration. 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 denkiwakame 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # arxiv2notion 2 | [![build](https://github.com/denkiwakame/arxiv2notion/actions/workflows/build.yaml/badge.svg)](https://github.com/denkiwakame/arxiv2notion/actions/workflows/build.yaml) [![lint](https://github.com/denkiwakame/arxiv2notion/actions/workflows/lint.yaml/badge.svg)](https://github.com/denkiwakame/arxiv2notion/actions/workflows/lint.yaml) 3 | [![Changelog](https://img.shields.io/badge/changelog-see%20here-blue.svg)](CHANGELOG.md) 4 | [![GitHub release](https://img.shields.io/github/release/denkiwakame/arxiv2notion.svg)](https://github.com/denkiwakame/arxiv2notion/releases) 5 | 6 | #### Supported Format 7 | [![arxiv](https://img.shields.io/badge/arxiv.org-API-red.svg)](https://info.arxiv.org/help/api/index.html) 8 | [![openreview](https://img.shields.io/badge/openreview.net-parser-purple.svg)](https://openreview.net/) 9 | [![acl](https://img.shields.io/badge/aclanthology.org-parser-purple.svg)](https://aclanthology.org/) 10 | 11 | Easy-to-use arXiv clipper for [Notion](https://www.notion.so) based on [Notion API](https://developers.notion.com/) 12 | 13 | ![demo](doc/arxiv2notion.gif) 14 | ![notion](doc/nerf_example2.png) 15 | 16 | ## ⬇️ Installation 17 | ### a. Install via Chrome Store 18 | - arxiv2notion is now available at [Chrome Store](https://chromewebstore.google.com/detail/arxiv2notion/jfgdgmjlakndggcpknmanlpgjgjbcbli) 🚀. 19 | 20 | ### b. Install Manually 21 | - download extension package from 22 | https://github.com/denkiwakame/arxiv2notion/releases/latest 23 | - for Chrome, navigate to `chrome://extension` 24 | - drag and drop the extension from your file manager anywhere onto the extensions page 25 | - or unzip the extension and `load unpacked` in developer mode 26 | 27 | ## ⚙️ Setup 28 | 29 | ### 1. Add arxiv2notion integration 30 | - navigate to [My Integrations](https://www.notion.so/profile/integrations) 31 | - `+ New Integration` 32 | - **associated workspace:** select your workspace where you save arXiv articles 33 | - **Type:** select `Internal` 34 | - **name:** set any name of your choice 35 | 36 | 37 | > [!NOTE] 38 | > For more detailed information about Notion integration, please refer to the official documentation at https://developers.notion.com/docs/getting-started. 39 | 40 | ### 2. Configure the extension 41 | - right-click on the extension icon > `Options` 42 | - copy **integration id (not the secret token!)** (see figures below) from `https://www.notion.so/my-integrations/internal/${integration-id}` 43 | - paste the `integration id` and click on `+` button. 44 | - if your entered id is valid, you can see the following callback messages. 45 | 46 | > [!NOTE] 47 | > To enhance security, arxiv2notion retrieves the Notion API key (integration secrets) on-demand through integration ID instead of storing it directly in Chrome local storage. **Please ensure you are logged into notion.so while using this extension.** 48 | 49 | 50 | 51 | 52 | ### 3. Create databases in Notion 53 | #### from template (recommended) 54 | - clone the public template [here](https://denkiwakame.notion.site/597cdd58bded4375b1cbe073b2ed6f5d?v=63fcbfda57824b239b66e52dde841cdf) to your own notion workspace 55 | - add connection to target databases via `...` > (scroll down...) `Connect to` > `{your integration name}` 56 | 57 | 58 | 59 | - Integration will have access writes to all child DBs. 60 | 61 | ![multiple_db](doc/multiple_db.png) 62 | 63 | 64 | #### or manually 65 | - alternatively, you can follow the following steps to create database from scratch in notion 66 | - login to [notion.so](https://www.notion.so) by admin user 67 | - create databases where you save arXiv articles 68 | - **follow this instruction** https://www.notion.so/guides/creating-a-database and **add properties listed below.** 69 | 70 | > [!CAUTION] 71 | > Do **NOT** create a new database by `/database` ! 72 | > Make sure to create properties with **exactly the same names and types as those listed.** 73 | 74 | |property|type| 75 | |-----|-----| 76 | |Title|Title| 77 | |URL|URL| 78 | |Authors|Multi-Select| 79 | |Abstract|Text| 80 | |Published|Date| 81 | |Comments|URL| 82 | |Publisher|Select| 83 | 84 | > [!NOTE] 85 | > **migration from v0.1.x → v1.0.0** 86 | - We changed `Authors` type and added `Published` `Comments` property from [v1.0.0](https://github.com/denkiwakame/arxiv2notion/releases/tag/v1.0.0). 87 | - Change your existing database properties as follows, you can easily integrate new features to your existing Notion database! 88 | 89 | |property|type(^v0.1.x)|type(v1.0.0+) | 90 | |-----|-----|-----| 91 | |Authors|Text| **Multi-Select**| 92 | |**Published**|--|**Date**| 93 | |**Comments**|--|**URL**| 94 | 95 | > [!TIP] 96 | > You can add extra columns of your choice alongside the default ones in your databases. 97 | 98 | #### :bulb: w/ Notion Formula (optional) 99 | - [Notion Formula](https://www.notion.so/help/formulas) allows you to add **custom autofill property** defined by formula. 100 | - For instance, `replace(URL, "arxiv", "ar5iv")` formula adds an [ar5iv link](https://ar5iv.labs.arxiv.org/) column by substituting "arxiv.org" with "ar5iv.org" 🚀 101 | 102 | 103 | #### :bulb: w/ Notion AI Property (optional) 104 | - [Notion AI Property](https://www.notion.so/ja-jp/help/guides/5-ai-prompts-to-surface-fresh-insights-from-your-databases) allows you to add **custom autofill property** to each DB record. 105 | - Add column to your Notion DB and select `AI custom autofill` 106 | - Set any prompt you like (e.g. summarization, extracting key ideas ...) 107 | 108 | - Save an article via `arxiv2notion` ,and then the preset `AI property` will be automatically generated. 109 | ![image](https://github.com/denkiwakame/arxiv2notion/assets/1871262/ad698cf0-dce0-4b29-8511-47f4c796a694) 110 | 111 | ## :technologist: Build locally (for Developers) 112 | - See also [CONTRIBUTING.md](CONTRIBUTING.md) 113 | 114 | ```bash 115 | $ git clone https://github.com/denkiwakame/arxiv2notion.git 116 | $ npm install 117 | $ npm run build 118 | $ npm run watch # debug locally 119 | $ npm run pack # packaging to zip 120 | ``` 121 | 122 | ## Contributors 123 | - Maintainers: [@denkiwakame](https://github.com/denkiwakame), [@wangjksjtu](https://github.com/wangjksjtu) 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /doc/arxiv2notion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denkiwakame/arxiv2notion/389d87bb79bf2937264a8d6ecb3bd4b93047180c/doc/arxiv2notion.gif -------------------------------------------------------------------------------- /doc/arxiv2notionplus_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denkiwakame/arxiv2notion/389d87bb79bf2937264a8d6ecb3bd4b93047180c/doc/arxiv2notionplus_db.png -------------------------------------------------------------------------------- /doc/multiple_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denkiwakame/arxiv2notion/389d87bb79bf2937264a8d6ecb3bd4b93047180c/doc/multiple_db.png -------------------------------------------------------------------------------- /doc/nerf_example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denkiwakame/arxiv2notion/389d87bb79bf2937264a8d6ecb3bd4b93047180c/doc/nerf_example1.png -------------------------------------------------------------------------------- /doc/nerf_example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denkiwakame/arxiv2notion/389d87bb79bf2937264a8d6ecb3bd4b93047180c/doc/nerf_example2.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "arxiv2notion", 4 | "description": "easy-to-use arXiv clipper for notion.so", 5 | "version": "1.4.1", 6 | "icons": { 7 | "128": "icon128.png" 8 | }, 9 | "action": { 10 | "default_popup": "popup.html", 11 | "default_icon": { 12 | "128": "icon128.png" 13 | } 14 | }, 15 | "options_ui": { 16 | "page": "options.html", 17 | "open_in_tab": false 18 | }, 19 | "permissions": ["tabs", "storage"], 20 | "host_permissions": [ 21 | "*://api.notion.com/*", 22 | "*://export.arxiv.org/*", 23 | "*://www.notion.so/*", 24 | "*://openreview.net/*", 25 | "*://aclanthology.org/*", 26 | "*://arxiv.org/*", 27 | "*://www.arxiv.org/*" 28 | ], 29 | "content_security_policy": { 30 | "extension_pages": "script-src 'self'; object-src 'self'" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arxiv2notion", 3 | "version": "1.4.1", 4 | "description": "easy-to-use arXiv clipper", 5 | "contributors": [ 6 | "denkiwakame", 7 | "wangjksjtu " 8 | ], 9 | "license": "MIT License", 10 | "scripts": { 11 | "dev": "webpack --mode development", 12 | "build": "webpack --mode production", 13 | "watch": "webpack serve --mode development", 14 | "lint": "eslint src/js", 15 | "format": "prettier --write src/js --check", 16 | "format:check": "prettier src/js --check", 17 | "pack": "npm run pack:keygen && npm run pack:zip", 18 | "pack:zip": "crx pack -p key.pem -o packages/arxiv2notion.crx --zip-output packages/arxiv2notion.chrome.zip dist", 19 | "pack:keygen": "if [ ! -f key.pem ] ; then crx keygen ./ ; fi" 20 | }, 21 | "devDependencies": { 22 | "copy-webpack-plugin": "^9.0.0", 23 | "crx": "^5.0.1", 24 | "css-loader": "^5.2.6", 25 | "eslint": "^8.14.0", 26 | "html-webpack-plugin": "^5.3.1", 27 | "prettier": "^3.1.1", 28 | "sass": "^1.34.1", 29 | "sass-loader": "^12.0.0", 30 | "style-loader": "^2.0.0", 31 | "webpack": "^5.37.0", 32 | "webpack-cli": "^4.7.0", 33 | "webpack-dev-server": "^4.8.1" 34 | }, 35 | "dependencies": { 36 | "mustache": "^4.2.0", 37 | "then-chrome": "^1.0.7", 38 | "uikit": "^3.6.22" 39 | }, 40 | "engines": { 41 | "node": ">=24.8.0", 42 | "npm": ">=11.6.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denkiwakame/arxiv2notion/389d87bb79bf2937264a8d6ecb3bd4b93047180c/packages/.gitkeep -------------------------------------------------------------------------------- /src/html/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | Notion Integration ID 8 | https://www.notion.so/profile/integrations/ 15 |
16 | 21 | 27 |
28 |
34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /src/html/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 |
13 |
14 | 15 | 16 |
17 |
18 | 24 |
25 |
26 | 27 | 33 |
34 |
35 | 36 | 42 |
43 |
44 | 45 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /src/images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denkiwakame/arxiv2notion/389d87bb79bf2937264a8d6ecb3bd4b93047180c/src/images/icon128.png -------------------------------------------------------------------------------- /src/js/notion.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2021 denkiwakame 3 | 4 | export default class Notion { 5 | constructor() { 6 | this.token = null; 7 | this.apiBase = 'https://api.notion.com/v1/'; 8 | } 9 | 10 | torkenizedHeaders() { 11 | return { 12 | 'Content-Type': 'application/json', 13 | 'Notion-Version': '2022-06-28', 14 | Authorization: `Bearer ${this.token}`, 15 | }; 16 | } 17 | 18 | async requestToken(botId) { 19 | const url = 'https://www.notion.so/api/v3/getBotToken'; 20 | const body = { botId: botId }; 21 | const headers = { 22 | Accept: 'application/json, */*', 23 | 'Content-type': 'application/json', 24 | }; 25 | const res = await fetch(url, { 26 | method: 'POST', 27 | mode: 'cors', 28 | headers: headers, 29 | credentials: 'include', 30 | body: JSON.stringify(body), 31 | }); 32 | const data = await res.json(); 33 | return data; 34 | } 35 | 36 | async retrievePage(pageId) { 37 | try { 38 | const url = this.apiBase + `pages/${pageId}`; 39 | const res = await fetch(url, { 40 | method: 'GET', 41 | mode: 'cors', 42 | headers: this.torkenizedHeaders(), 43 | }); 44 | const data = await res.json(); 45 | console.log(data); 46 | } catch (err) { 47 | console.error(err); 48 | throw err; 49 | } 50 | } 51 | 52 | async checkDuplicateEntry(paperId, databaseId) { 53 | const entries = await this.retrieveEntry(paperId, databaseId); 54 | if (entries.length != 0) { 55 | document.dispatchEvent( 56 | new CustomEvent('msg', { 57 | detail: { 58 | type: 'warning', 59 | msg: 'This item is already bookmarked. Opening existing entry...', 60 | }, 61 | }) 62 | ); 63 | await new Promise((resolve) => setTimeout(resolve, 1000)); 64 | } 65 | return entries; 66 | } 67 | 68 | async retrieveEntry(paperId, databaseId) { 69 | const filter = { 70 | property: 'URL', 71 | rich_text: { 72 | contains: `${paperId}`, 73 | }, 74 | }; 75 | 76 | try { 77 | const url = this.apiBase + `databases/${databaseId}/query`; 78 | const body = { 79 | filter: filter, 80 | }; 81 | const res = await fetch(url, { 82 | method: 'POST', 83 | mode: 'cors', 84 | headers: this.torkenizedHeaders(), 85 | body: JSON.stringify(body), 86 | }); 87 | const data = await res.json(); 88 | return data.results; 89 | } catch (err) { 90 | console.error(err); 91 | throw err; 92 | } 93 | } 94 | 95 | async createPage(_data) { 96 | const data = await _data; 97 | const databaseId = document.getElementById('js-select-database').value; 98 | 99 | // XXX check if the entry has already been bookmarked 100 | const duplicateEntries = await this.checkDuplicateEntry( 101 | data.id, 102 | databaseId 103 | ); 104 | if (duplicateEntries.length != 0) return duplicateEntries[0]; 105 | 106 | const title = data.title; 107 | const abst = data.abst; 108 | const paperUrl = data.url; 109 | const authorsFormatted = data.authors.join(', '); 110 | const published = data.published; 111 | const publisher = data.publisher; 112 | const comment = data.comment; 113 | const authors = authorsFormatted.split(', '); 114 | const authorsMultiSelect = authors.map((author) => { 115 | return { name: author }; 116 | }); 117 | 118 | try { 119 | const url = this.apiBase + 'pages'; 120 | const parent = { 121 | type: 'database_id', 122 | database_id: databaseId, 123 | }; 124 | const properties = { 125 | Title: { 126 | id: 'title', 127 | type: 'title', 128 | title: [{ text: { content: title } }], 129 | }, 130 | Publisher: { 131 | id: 'conference', 132 | type: 'select', 133 | select: { name: publisher }, 134 | }, 135 | URL: { 136 | id: 'url', 137 | type: 'url', 138 | url: paperUrl, 139 | }, 140 | Abstract: { 141 | id: 'abstract', 142 | type: 'rich_text', 143 | rich_text: [ 144 | { 145 | type: 'text', 146 | text: { content: abst, link: null }, 147 | annotations: { 148 | bold: false, 149 | italic: true, 150 | strikethrough: false, 151 | underline: false, 152 | code: false, 153 | color: 'default', 154 | }, 155 | plain_text: abst, 156 | href: null, 157 | }, 158 | ], 159 | }, 160 | Authors: { 161 | id: 'authors', 162 | type: 'multi_select', 163 | multi_select: authorsMultiSelect, 164 | }, 165 | Published: { 166 | id: 'published', 167 | type: 'date', 168 | date: { start: published, end: null }, 169 | }, 170 | Comments: { 171 | id: 'comment', 172 | type: 'url', 173 | url: comment, 174 | }, 175 | }; 176 | 177 | const body = { 178 | parent: parent, 179 | properties: properties, 180 | }; 181 | const res = await fetch(url, { 182 | method: 'POST', 183 | mode: 'cors', 184 | headers: this.torkenizedHeaders(), 185 | body: JSON.stringify(body), 186 | }); 187 | const data = await res.json(); 188 | console.log(data); 189 | return data; 190 | } catch (err) { 191 | console.error(err); 192 | throw err; 193 | } 194 | } 195 | 196 | async retrieveDatabase() { 197 | try { 198 | // /v1/databases is deprecated since Notion API version: 2022-06-28 199 | // https://developers.notion.com/reference/get-databases 200 | // https://developers.notion.com/reference/post-search 201 | const url = this.apiBase + 'search'; 202 | const headers = this.torkenizedHeaders(); 203 | const body = { filter: { value: 'database', property: 'object' } }; 204 | const res = await fetch(url, { 205 | method: 'POST', 206 | mode: 'cors', 207 | headers: headers, 208 | body: JSON.stringify(body), 209 | }); 210 | const data = await res.json(); 211 | data.results?.forEach((result) => { 212 | const option = ``; 213 | document 214 | .getElementById('js-select-database') 215 | .insertAdjacentHTML('beforeend', option); 216 | }); 217 | } catch (err) { 218 | console.error(err); 219 | throw err; 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2021 denkiwakame 3 | 4 | import '../scss/theme.scss'; 5 | import UIKit from 'uikit'; 6 | import Icons from 'uikit/dist/js/uikit-icons'; 7 | import Mustache from 'mustache'; 8 | import NotionClient from './notion.js'; 9 | 10 | UIKit.use(Icons); 11 | 12 | class TokenManager { 13 | constructor() { 14 | this.storageKey = 'botId'; 15 | this.setupInput(); 16 | this.setupSaveButton(); 17 | this.client = new NotionClient(); 18 | } 19 | toggleVisible() { 20 | if (this.input.type == 'password') { 21 | this.input.type = 'text'; 22 | this.visibleButton.setAttribute('uk-icon', 'unlock'); 23 | } else { 24 | this.input.type = 'password'; 25 | this.visibleButton.setAttribute('uk-icon', 'lock'); 26 | } 27 | } 28 | setupSaveButton() { 29 | this.saveButton = document.getElementById('js-save-btn'); 30 | this.saveButton.addEventListener('click', () => { 31 | this.saveIntegrationId(); 32 | }); 33 | this.visibleButton = document.getElementById('js-visible-btn'); 34 | this.visibleButton.addEventListener('click', () => { 35 | this.toggleVisible(); 36 | }); 37 | } 38 | setupInput() { 39 | this.input = document.getElementById('js-token-input'); 40 | if (!chrome.storage) return; 41 | chrome.storage.local.get(this.storageKey, (d) => { 42 | if (!d) return; 43 | this.input.value = d.botId; 44 | }); 45 | } 46 | async saveIntegrationId() { 47 | const botId = this.input.value; 48 | if (!botId.trim().length || botId.length != 36) { 49 | console.log('invalid!'); 50 | this.renderMessage('danger', 'Invalid integration ID (36 char).'); 51 | return; 52 | } 53 | console.log(botId); 54 | await chrome.storage.local.set({ 55 | botId: botId, 56 | }); 57 | chrome.storage.local.get(this.storageKey, (d) => { 58 | console.log('chrome storage', d); 59 | this.renderMessage('success', 'integration ID is successfully saved.'); 60 | this.connectionTest(); 61 | }); 62 | } 63 | async connectionTest() { 64 | chrome.storage.local.get(this.storageKey, (d) => { 65 | const botId = d.botId; 66 | const data = this.client.requestToken(botId); 67 | console.log(data); 68 | if (data.name == 'UnauthorizedError') { 69 | this.renderMessage('danger', 'You are not logged in notion.so.'); 70 | } else { 71 | this.renderMessage('success', 'Successfully connected with notion.so.'); 72 | } 73 | }); 74 | } 75 | renderMessage(type, message, overwrite = false) { 76 | // type: warning, danger, success, primary 77 | const template = `

{{message}}

`; 78 | const rendered = Mustache.render(template, { 79 | type: type, 80 | message: message, 81 | }); 82 | if (overwrite) { 83 | document.getElementById('js-message-container').innerHTML = rendered; 84 | } else { 85 | document 86 | .getElementById('js-message-container') 87 | .insertAdjacentHTML('beforeend', rendered); 88 | } 89 | } 90 | } 91 | 92 | new TokenManager(); 93 | -------------------------------------------------------------------------------- /src/js/parsers.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 denkiwakame 3 | 4 | class URLParser { 5 | constructor() { 6 | this.parsers = []; 7 | } 8 | 9 | addParser(domain, handler) { 10 | this.parsers.push({ domain, handler }); 11 | } 12 | 13 | async parse(url) { 14 | for (let { domain, handler } of this.parsers) { 15 | if (url?.startsWith(domain)) return handler(url); 16 | } 17 | throw new Error('No perser found for the given URL'); 18 | } 19 | } 20 | 21 | const arXivParser = async (url) => { 22 | const ARXIV_API = 'https://export.arxiv.org/api/query/search_query'; 23 | // ref: https://info.arxiv.org/help/arxiv_identifier.html 24 | // e.g. (new id format: 2404.16782) | (old id format: hep-th/0702063) 25 | const parseArXivId = (str) => str.match(/(\d+\.\d+$)|((\w|-)+\/\d+$)/)?.[0]; 26 | 27 | const paperId = parseArXivId(url); 28 | const res = await fetch(ARXIV_API + '?id_list=' + paperId.toString(), { 29 | method: 'GET', 30 | mode: 'cors', 31 | headers: { 32 | Accept: 'application/atom+xml', 33 | }, 34 | }); 35 | if (res.status != 200) { 36 | console.error('arXiv API request failed with status:', res.status); 37 | return; 38 | } 39 | const data = await res.text(); // TODO: error handling 40 | console.log(res.status); 41 | const xmlData = new window.DOMParser().parseFromString(data, 'text/xml'); 42 | console.log(xmlData); 43 | 44 | const entry = xmlData.querySelector('entry'); 45 | const id = parseArXivId(entry.querySelector('id')?.textContent); 46 | const paperTitle = entry.querySelector('title').textContent; 47 | const abst = entry.querySelector('summary').textContent; 48 | const authors = Array.from(entry.querySelectorAll('author')).map((author) => { 49 | return author.textContent.trim(); 50 | }); 51 | const published = entry.querySelector('published').textContent; 52 | const comment = entry.querySelector('comment')?.textContent ?? 'none'; 53 | 54 | return { 55 | id: id, 56 | title: paperTitle, 57 | abst: abst, 58 | authors: authors, 59 | url: url, 60 | published: published, 61 | comment: comment, 62 | publisher: 'arXiv', 63 | }; 64 | }; 65 | 66 | const openReviewParser = async (url) => { 67 | const id = new URLSearchParams(new URL(url).search).get('id'); 68 | const res = await fetch(url); 69 | const html = await res.text(); 70 | const parser = new DOMParser(); 71 | const xml = parser.parseFromString(html, 'text/html'); 72 | 73 | const authorsArray = Array.from( 74 | xml.querySelectorAll('meta[name="citation_author"]'), 75 | (author) => author.getAttribute('content') 76 | ); 77 | const authors = authorsArray.length ? authorsArray : ['Anonymous']; 78 | 79 | const paperTitle = xml 80 | .querySelector('meta[name="citation_title"]') 81 | .getAttribute('content'); 82 | 83 | const abst = xml 84 | .querySelector('meta[name="citation_abstract"]') 85 | .getAttribute('content'); 86 | 87 | const date = xml 88 | .querySelector('meta[name="citation_online_date"]') 89 | .getAttribute('content'); 90 | // -> ISO 8601 date string 91 | const published = new Date(date).toISOString().split('T')[0]; 92 | const comment = 'none'; 93 | 94 | return { 95 | id: id, 96 | title: paperTitle, 97 | abst: abst, 98 | authors: authors, 99 | url: url, 100 | published: published, 101 | comment: comment, 102 | publisher: 'OpenReview', 103 | }; 104 | }; 105 | 106 | const aclAnthologyParser = async (url) => { 107 | const res = await fetch(url); 108 | const html = await res.text(); 109 | const parser = new DOMParser(); 110 | const xml = parser.parseFromString(html, 'text/html'); 111 | 112 | const id = xml 113 | .querySelector('meta[name="citation_doi"]') 114 | .getAttribute('content'); 115 | const authors = Array.from( 116 | xml.querySelectorAll('meta[name="citation_author"]'), 117 | (author) => author.getAttribute('content') 118 | ); 119 | 120 | const paperTitle = xml 121 | .querySelector('meta[name="citation_title"]') 122 | .getAttribute('content'); 123 | 124 | const abst = 'none'; 125 | const date = xml 126 | .querySelector('meta[name="citation_publication_date"]') 127 | .getAttribute('content'); 128 | // -> ISO 8601 date string 129 | const published = new Date(date).toISOString().split('T')[0]; 130 | const publisher = xml 131 | .querySelectorAll('.acl-paper-details dd')[6] 132 | .textContent.replaceAll('\n', ''); 133 | const comment = xml 134 | .querySelector('meta[name="citation_pdf_url"]') 135 | .getAttribute('content'); 136 | return { 137 | id: id, 138 | title: paperTitle, 139 | abst: abst, 140 | authors: authors, 141 | url: url, 142 | published: published, 143 | comment: comment, 144 | publisher: publisher, 145 | }; 146 | }; 147 | 148 | const urlParser = new URLParser(); 149 | urlParser.addParser('https://openreview.net/', openReviewParser); 150 | urlParser.addParser('https://arxiv.org', arXivParser); 151 | urlParser.addParser('https://www.arxiv.org', arXivParser); 152 | urlParser.addParser('https://aclanthology.org', aclAnthologyParser); 153 | 154 | export default urlParser; 155 | -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2021 denkiwakame 3 | 4 | import '../scss/theme.scss'; 5 | import UIKit from 'uikit'; 6 | import Icons from 'uikit/dist/js/uikit-icons'; 7 | import Mustache from 'mustache'; 8 | import NotionClient from './notion.js'; 9 | import thenChrome from 'then-chrome'; 10 | import urlParser from './parsers.js'; 11 | 12 | UIKit.use(Icons); 13 | 14 | //const TEST_URL = 'https://arxiv.org/abs/2308.04079'; 15 | const TEST_URL = 'https://www.arxiv.org/abs/2508.20324'; 16 | // const TEST_URL = 'https://aclanthology.org/2023.ijcnlp-main.1/'; 17 | 18 | class UI { 19 | constructor() { 20 | this.setupProgressBar(); 21 | this.setupSaveButton(); 22 | this.client = new NotionClient(); 23 | this.connectionTest(); 24 | this.getCurrentTabUrl(); 25 | this.setupMsgHandler(); 26 | } 27 | 28 | getCurrentTabUrl() { 29 | document.addEventListener('DOMContentLoaded', () => { 30 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 31 | const url = tabs[0].url; 32 | this.data = this.isDebugUrl(url) 33 | ? this.getPaperInfo(TEST_URL) 34 | : this.getPaperInfo(url); 35 | }); 36 | }); 37 | } 38 | 39 | async connectionTest() { 40 | chrome.storage.local.get('botId', async (d) => { 41 | if (!this.client.token) { 42 | const botId = d.botId; 43 | const data = await this.client.requestToken(botId); 44 | if (data.name == 'UnauthorizedError') { 45 | this.renderMessage('danger', 'You are not logged in notion.so.'); 46 | } else { 47 | this.client.token = data.token; 48 | } 49 | } 50 | this.client.retrieveDatabase(); 51 | }); 52 | } 53 | 54 | setupSaveButton() { 55 | document.getElementById('js-save').addEventListener('click', async () => { 56 | this.showProgressBar(); 57 | const data = await this.client.createPage(this.data); 58 | if (data.status && data.status == 400) { 59 | this.renderMessage('danger', `[${data.code}] ${data.message}`); 60 | return; 61 | } else { 62 | thenChrome.tabs.create({ 63 | url: `https://notion.so/${data.id.replaceAll('-', '')}`, 64 | }); 65 | } 66 | }); 67 | } 68 | 69 | setupMsgHandler() { 70 | document.addEventListener('msg', (evt) => { 71 | console.error(evt); 72 | this.renderMessage(evt.detail.type, evt.detail.msg); 73 | }); 74 | } 75 | 76 | setupProgressBar() { 77 | this.progressBar = document.getElementById('js-progressbar'); 78 | } 79 | 80 | showProgressBar() { 81 | clearInterval(this._animate); 82 | this.progressBar.value = 0; 83 | this._animate = setInterval(() => { 84 | this.progressBar.value += 20; 85 | if (this.progressBar.value >= this.progressBar.max) { 86 | clearInterval(this._animate); 87 | } 88 | }, 200); 89 | } 90 | isDebugUrl(url) { 91 | return url?.startsWith('chrome-extension://') || false; 92 | } 93 | isArxivUrl(url) { 94 | return ( 95 | url?.startsWith('https://arxiv.org') || 96 | url?.startsWith('https://www.arxiv.org') || 97 | false 98 | ); 99 | } 100 | isOpenReviewUrl(url) { 101 | return url?.startsWith('https://openreview.net/') || false; 102 | } 103 | isPDF(url) { 104 | return url && url.split('.').pop() === 'pdf'; 105 | } 106 | async getPaperInfo(url) { 107 | this.showProgressBar(); 108 | const data = await urlParser.parse(url); 109 | this.setFormContents(data.title, data.abst, data.comment, data.authors); 110 | return data; 111 | } 112 | setFormContents(paperTitle, abst, comment, authors) { 113 | document.getElementById('js-title').value = paperTitle; 114 | document.getElementById('js-abst').value = abst; 115 | // document.getElementById('js-published').value = published; 116 | document.getElementById('js-comment').value = comment; 117 | authors.forEach((author) => { 118 | const template = `{{ text }}`; 119 | const rendered = Mustache.render(template, { text: author }); 120 | document 121 | .getElementById('js-chip-container') 122 | .insertAdjacentHTML('beforeend', rendered); 123 | }); 124 | } 125 | 126 | renderMessage(type, message, overwrite = false) { 127 | // type: warning, danger, success, primary 128 | const template = `

{{message}}

`; 129 | const rendered = Mustache.render(template, { 130 | type: type, 131 | message: message, 132 | }); 133 | if (overwrite) { 134 | document.getElementById('js-message-container').innerHTML = rendered; 135 | } else { 136 | document 137 | .getElementById('js-message-container') 138 | .insertAdjacentHTML('beforeend', rendered); 139 | } 140 | } 141 | } 142 | 143 | new UI(); 144 | -------------------------------------------------------------------------------- /src/scss/theme.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Titillium+Web:wght@400;600&display=swap'); 2 | $global-font-family: 'Titillium Web'; 3 | // ref: uikit/src/scss/variables.scss 4 | $progress-height: 5px; 5 | $progress-bar-background: #9f9f9f; 6 | $badge-color: $progress-bar-background; 7 | $badge-background: #ddd; 8 | $form-label-color: #aaa; 9 | $form-label-font-size: 0.7rem; 10 | // default variables and mixins 11 | @import "uikit/src/scss/variables-theme.scss"; 12 | @import "uikit/src/scss/mixins-theme.scss"; 13 | // import uikit 14 | @import "uikit/src/scss/uikit.scss"; 15 | 16 | body { width: 400px; } 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | 5 | module.exports = { 6 | target: "web", 7 | entry: { 8 | popup: path.join(__dirname, "src", "js", "popup.js"), 9 | options: path.join(__dirname, "src", "js", "options.js"), 10 | }, 11 | output: { 12 | path: path.join(__dirname, "dist"), 13 | filename: "[name].bundle.js", 14 | }, 15 | devServer: { 16 | hot: true, 17 | liveReload: true, 18 | port: 8080, 19 | static: { 20 | directory: path.join(__dirname, "dist"), 21 | }, 22 | }, 23 | plugins: [ 24 | new HtmlWebpackPlugin({ 25 | filename: "popup.html", 26 | chunks: ["popup"], 27 | template: path.join(__dirname, "src", "html", "popup.html"), 28 | }), 29 | new HtmlWebpackPlugin({ 30 | filename: "options.html", 31 | chunks: ["options"], 32 | template: path.join(__dirname, "src", "html", "options.html"), 33 | }), 34 | new CopyWebpackPlugin({ 35 | patterns: [ 36 | { 37 | from: path.join(__dirname, "manifest.json"), 38 | to: path.join(__dirname, "dist"), 39 | }, 40 | { 41 | from: path.join(__dirname, "src", "images", "icon128.png"), 42 | to: path.join(__dirname, "dist"), 43 | }, 44 | ], 45 | }), 46 | ], 47 | module: { 48 | rules: [ 49 | { 50 | test: /\.css$/i, 51 | use: ["style-loader", "css-loader"], 52 | }, 53 | { 54 | test: /\.s[ac]ss$/i, 55 | use: ["style-loader", "css-loader", "sass-loader"], 56 | }, 57 | { 58 | test: /\.(jpe?g|png|gif|svg|json)$/i, 59 | loader: "file-loader", 60 | options: { 61 | name: "/dist/[name].[ext]", 62 | esModule: false, 63 | }, 64 | }, 65 | ], 66 | }, 67 | }; 68 | --------------------------------------------------------------------------------