├── tests ├── end-to-end-test │ ├── requirements.txt │ ├── Dockerfile │ ├── test_interactive.py │ ├── compose.yml │ ├── test_default.py │ └── test_navigation.py ├── unit-test │ ├── babel.config.js │ ├── Dockerfile │ ├── install-and-run.sh │ ├── compose.yml │ ├── jest.config.js │ ├── package.json │ ├── navigation.test.js │ └── .gitignore ├── screenshots │ ├── edge │ │ ├── 03-pinned.jpeg │ │ ├── 01-default.jpeg │ │ └── 02-extensions.jpeg │ ├── chrome │ │ ├── 03-pinned.jpeg │ │ ├── 01-default.jpeg │ │ └── 02-extensions.jpeg │ ├── firefox │ │ ├── 01-default.jpeg │ │ ├── 04-pinned.jpeg │ │ ├── 02-extensions.jpeg │ │ ├── 03-extensions-settings.jpeg │ │ ├── 05-details-manifest-v3.jpeg │ │ ├── 06-permissions-manifest-v3.jpeg │ │ └── 03-extensions-settings-manifest-v3.jpeg │ └── README.md ├── scripts │ ├── docker-utils │ │ ├── exec-selenium.sh │ │ └── test-connection.sh │ └── docker-compose.sh └── testcases │ └── testcases.yaml ├── firefox ├── background.html ├── icon128.png ├── icon16.png ├── icon32.png ├── icon48.png ├── pdfviewer.html ├── manifest.json ├── options.html ├── target_url_regexp_replace.js ├── pdfviewer.js ├── options.js ├── background.js └── content.js ├── chrome ├── icon16.png ├── icon19.png ├── icon38.png ├── icon48.png ├── icon128.png ├── options.html ├── options.js ├── manifest.json ├── target_url_regexp_replace.js ├── background.js └── content.js ├── icons ├── icon128.png ├── icon16.png ├── icon19.png ├── icon256.png ├── icon32.png ├── icon38.png ├── icon48.png ├── icon64.png └── icon.svg ├── screenshots ├── 02-pdf.png ├── 03-pdf2.png ├── 01-abstract.png ├── 06-onetab.png ├── 07-search.png ├── 04-abstract2.png ├── 05-bookmarks.png ├── unedited │ ├── pdf.png │ ├── onetab.png │ ├── search.png │ ├── abstract.png │ ├── abstract2.png │ ├── bookmarks.png │ ├── vertical-tabs.png │ ├── filename-format-chrome.png │ └── filename-format-firefox.png ├── 08-vertical-tabs.png ├── 09-filename-format-chrome.png ├── 10-filename-format-firefox.png └── social-preview │ └── abstract-wide.png ├── .gitattributes ├── .github ├── workflows │ ├── test-with-jest.yaml │ ├── build-and-publish.yaml │ └── test-with-selenium.yaml └── ISSUE_TEMPLATE │ └── bug_report.md ├── LICENSE └── README.md /tests/end-to-end-test/requirements.txt: -------------------------------------------------------------------------------- 1 | selenium==4.7.2 2 | pyyaml==6.0.1 3 | -------------------------------------------------------------------------------- /firefox/background.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chrome/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/chrome/icon16.png -------------------------------------------------------------------------------- /chrome/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/chrome/icon19.png -------------------------------------------------------------------------------- /chrome/icon38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/chrome/icon38.png -------------------------------------------------------------------------------- /chrome/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/chrome/icon48.png -------------------------------------------------------------------------------- /icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/icons/icon128.png -------------------------------------------------------------------------------- /icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/icons/icon16.png -------------------------------------------------------------------------------- /icons/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/icons/icon19.png -------------------------------------------------------------------------------- /icons/icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/icons/icon256.png -------------------------------------------------------------------------------- /icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/icons/icon32.png -------------------------------------------------------------------------------- /icons/icon38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/icons/icon38.png -------------------------------------------------------------------------------- /icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/icons/icon48.png -------------------------------------------------------------------------------- /icons/icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/icons/icon64.png -------------------------------------------------------------------------------- /tests/unit-test/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {presets: ['@babel/preset-env']} 2 | -------------------------------------------------------------------------------- /chrome/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/chrome/icon128.png -------------------------------------------------------------------------------- /firefox/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/firefox/icon128.png -------------------------------------------------------------------------------- /firefox/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/firefox/icon16.png -------------------------------------------------------------------------------- /firefox/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/firefox/icon32.png -------------------------------------------------------------------------------- /firefox/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/firefox/icon48.png -------------------------------------------------------------------------------- /screenshots/02-pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/02-pdf.png -------------------------------------------------------------------------------- /screenshots/03-pdf2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/03-pdf2.png -------------------------------------------------------------------------------- /screenshots/01-abstract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/01-abstract.png -------------------------------------------------------------------------------- /screenshots/06-onetab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/06-onetab.png -------------------------------------------------------------------------------- /screenshots/07-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/07-search.png -------------------------------------------------------------------------------- /tests/unit-test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.16.0-alpine3.20 2 | WORKDIR /app 3 | CMD ["sleep", "infinity"] 4 | -------------------------------------------------------------------------------- /screenshots/04-abstract2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/04-abstract2.png -------------------------------------------------------------------------------- /screenshots/05-bookmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/05-bookmarks.png -------------------------------------------------------------------------------- /screenshots/unedited/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/unedited/pdf.png -------------------------------------------------------------------------------- /screenshots/08-vertical-tabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/08-vertical-tabs.png -------------------------------------------------------------------------------- /screenshots/unedited/onetab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/unedited/onetab.png -------------------------------------------------------------------------------- /screenshots/unedited/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/unedited/search.png -------------------------------------------------------------------------------- /tests/unit-test/install-and-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd /app/tests/unit-test 4 | npm install 5 | npm run test 6 | -------------------------------------------------------------------------------- /screenshots/unedited/abstract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/unedited/abstract.png -------------------------------------------------------------------------------- /screenshots/unedited/abstract2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/unedited/abstract2.png -------------------------------------------------------------------------------- /screenshots/unedited/bookmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/unedited/bookmarks.png -------------------------------------------------------------------------------- /tests/screenshots/edge/03-pinned.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/edge/03-pinned.jpeg -------------------------------------------------------------------------------- /screenshots/unedited/vertical-tabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/unedited/vertical-tabs.png -------------------------------------------------------------------------------- /tests/screenshots/chrome/03-pinned.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/chrome/03-pinned.jpeg -------------------------------------------------------------------------------- /tests/screenshots/edge/01-default.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/edge/01-default.jpeg -------------------------------------------------------------------------------- /screenshots/09-filename-format-chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/09-filename-format-chrome.png -------------------------------------------------------------------------------- /screenshots/10-filename-format-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/10-filename-format-firefox.png -------------------------------------------------------------------------------- /tests/screenshots/chrome/01-default.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/chrome/01-default.jpeg -------------------------------------------------------------------------------- /tests/screenshots/edge/02-extensions.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/edge/02-extensions.jpeg -------------------------------------------------------------------------------- /tests/screenshots/firefox/01-default.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/firefox/01-default.jpeg -------------------------------------------------------------------------------- /tests/screenshots/firefox/04-pinned.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/firefox/04-pinned.jpeg -------------------------------------------------------------------------------- /screenshots/social-preview/abstract-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/social-preview/abstract-wide.png -------------------------------------------------------------------------------- /tests/screenshots/chrome/02-extensions.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/chrome/02-extensions.jpeg -------------------------------------------------------------------------------- /tests/screenshots/firefox/02-extensions.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/firefox/02-extensions.jpeg -------------------------------------------------------------------------------- /screenshots/unedited/filename-format-chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/unedited/filename-format-chrome.png -------------------------------------------------------------------------------- /screenshots/unedited/filename-format-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/screenshots/unedited/filename-format-firefox.png -------------------------------------------------------------------------------- /tests/screenshots/firefox/03-extensions-settings.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/firefox/03-extensions-settings.jpeg -------------------------------------------------------------------------------- /tests/screenshots/firefox/05-details-manifest-v3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/firefox/05-details-manifest-v3.jpeg -------------------------------------------------------------------------------- /tests/screenshots/firefox/06-permissions-manifest-v3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/firefox/06-permissions-manifest-v3.jpeg -------------------------------------------------------------------------------- /tests/scripts/docker-utils/exec-selenium.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | docker exec -t end-to-end-test-selenium-tests-1 \ 4 | python "/app/tests/end-to-end-test/$@" 5 | -------------------------------------------------------------------------------- /tests/screenshots/firefox/03-extensions-settings-manifest-v3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j3soon/arxiv-utils/HEAD/tests/screenshots/firefox/03-extensions-settings-manifest-v3.jpeg -------------------------------------------------------------------------------- /tests/unit-test/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | jest-tests: 3 | build: . 4 | volumes: 5 | - "../..:/app:rw" # Source code 6 | # docker exec -it unit-test-jest-tests-1 sh 7 | -------------------------------------------------------------------------------- /tests/unit-test/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | transform: { 4 | '^.+\\.(ts|tsx)?$': 'ts-jest', 5 | '^.+\\.(js|jsx)$': 'babel-jest', 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /tests/scripts/docker-utils/test-connection.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | docker run --rm --network end-to-end-test_default curlimages/curl:latest -s --retry 20 --retry-delay 1 --retry-connrefused $1 4 | -------------------------------------------------------------------------------- /tests/screenshots/README.md: -------------------------------------------------------------------------------- 1 | All images are captured with the UltraVNC Viewer (Right-click the menu bar) 2 | 3 | These images are used to determine the button click locations for the meta Selenium driver. 4 | -------------------------------------------------------------------------------- /tests/end-to-end-test/Dockerfile: -------------------------------------------------------------------------------- 1 | # Reference: https://docs.docker.com/compose/gettingstarted/ 2 | FROM python:3.8-alpine 3 | WORKDIR /app 4 | COPY requirements.txt requirements.txt 5 | RUN pip install -r requirements.txt 6 | CMD ["sleep", "infinity"] 7 | -------------------------------------------------------------------------------- /tests/unit-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "jest" 4 | }, 5 | "dependencies": { 6 | "@babel/preset-env": "^7.25.3", 7 | "babel-jest": "^29.7.0", 8 | "jest": "^29.7.0", 9 | "js-yaml": "^4.1.0", 10 | "ts-jest": "^29.2.4" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ref: https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings?platform=linux 2 | 3 | # Set the default behavior, in case people don't have core.autocrlf set. 4 | * text=auto 5 | 6 | # Denote all files that are truly binary and should not be modified. 7 | *.png binary 8 | *.svg binary 9 | -------------------------------------------------------------------------------- /firefox/pdfviewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/scripts/docker-compose.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 4 | 5 | if [[ "$1" == "up" ]]; then 6 | docker compose -f $DIR/../end-to-end-test/compose.yml up --build "${@:2}" 7 | elif [[ "$1" == "pull" ]]; then 8 | docker compose -f $DIR/../end-to-end-test/compose.yml pull 9 | elif [[ "$1" == "down" ]]; then 10 | docker compose -f $DIR/../end-to-end-test/compose.yml down 11 | elif [[ "$1" == "shutdown" ]]; then 12 | docker compose -f $DIR/../end-to-end-test/compose.yml down -v --remove-orphans 13 | fi 14 | -------------------------------------------------------------------------------- /.github/workflows/test-with-jest.yaml: -------------------------------------------------------------------------------- 1 | name: unit-tests 2 | on: 3 | push: 4 | paths: 5 | - .github/workflows/test-with-jest.yaml 6 | - tests/testcases/** 7 | - tests/unit-test/** 8 | pull_request: 9 | jobs: 10 | test-with-jest: 11 | name: Unit Test with Jest 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Pull docker images 16 | run: docker compose -f tests/unit-test/compose.yml pull 17 | - name: Start test containers 18 | run: docker compose -f tests/unit-test/compose.yml up -d 19 | # Start testing 20 | - name: Test navigation 21 | run: docker exec -t unit-test-jest-tests-1 /app/tests/unit-test/install-and-run.sh 22 | # Clean up 23 | - name: Stop test containers 24 | if: always() 25 | run: docker compose -f tests/unit-test/compose.yml down 26 | -------------------------------------------------------------------------------- /.github/workflows/build-and-publish.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build-and-publish: 7 | name: Build and Publish 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Build zip files 12 | run: | 13 | cd chrome && zip -r "../arxiv-utils-chrome-${{ github.event.release.tag_name }}.zip" . && cd .. 14 | cd chrome && zip -r "../arxiv-utils-edge-${{ github.event.release.tag_name }}.zip" . && cd .. 15 | cd firefox && zip -r "../arxiv-utils-firefox-${{ github.event.release.tag_name }}.zip" . && cd .. 16 | - name: Publish to Stores 17 | uses: PlasmoHQ/bpp@v3 18 | with: 19 | keys: ${{ secrets.BPP_KEYS }} 20 | chrome-file: "arxiv-utils-chrome-${{ github.event.release.tag_name }}.zip" 21 | edge-file: "arxiv-utils-edge-${{ github.event.release.tag_name }}.zip" 22 | firefox-file: "arxiv-utils-firefox-${{ github.event.release.tag_name }}.zip" 23 | -------------------------------------------------------------------------------- /chrome/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Options for arxiv-utils 7 | 8 | 9 | 10 |

Options for arxiv-utils

11 |
12 |

Current filename format: (To be loaded)

13 | 14 |
15 |

Will open in new tab:

16 | 17 |
18 |
19 | 20 |
21 |

Note: You'll need to re-open the arXiv tabs for the new filename format to take effect.

22 |
23 | Help 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/end-to-end-test/test_interactive.py: -------------------------------------------------------------------------------- 1 | #%% 2 | from selenium import webdriver 3 | from selenium.common.exceptions import TimeoutException 4 | from selenium.webdriver.common.by import By 5 | from selenium.webdriver.support import expected_conditions as EC 6 | from selenium.webdriver.support.ui import WebDriverWait 7 | 8 | #%% 9 | # ['chrome', 'firefox', 'edge'] 10 | browser = 'firefox' 11 | print(f"Testing with browser: {browser}") 12 | options = { 13 | 'chrome': webdriver.ChromeOptions(), 14 | 'firefox': webdriver.FirefoxOptions(), 15 | 'edge': webdriver.EdgeOptions(), 16 | }[browser] 17 | 18 | print(f"Launching webdriver...") 19 | driver = webdriver.Remote( 20 | command_executor='http://selenium-hub:4444/wd/hub', 21 | options=options 22 | ) 23 | wait = WebDriverWait(driver, 60) 24 | # The webdriver includes a default tab 25 | wait.until(EC.number_of_windows_to_be(1)) 26 | initial_window = driver.current_window_handle 27 | 28 | driver.get("https://duckduckgo.com") 29 | 30 | #%% 31 | # Make sure to quit before starting a new webdriver! 32 | driver.quit() 33 | 34 | # %% 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Johnson 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 | -------------------------------------------------------------------------------- /.github/workflows/test-with-selenium.yaml: -------------------------------------------------------------------------------- 1 | name: end-to-end-tests 2 | on: 3 | push: 4 | paths: 5 | - .github/workflows/test-with-selenium.yaml 6 | - chrome/** 7 | - firefox/** 8 | - tests/scripts/** 9 | - tests/testcases/** 10 | - tests/end-to-end-test/** 11 | pull_request: 12 | jobs: 13 | test-with-selenium: 14 | name: End-to-end Test with Selenium 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Pull docker images 19 | run: tests/scripts/docker-compose.sh pull 20 | - name: Start test containers 21 | run: tests/scripts/docker-compose.sh up -d 22 | - name: Test selenium hub connection 23 | run: tests/scripts/docker-utils/test-connection.sh http://selenium-hub:4444 24 | # Start testing 25 | - name: Test default 26 | run: tests/scripts/docker-utils/exec-selenium.sh test_default.py 27 | - name: Test navigation 28 | run: tests/scripts/docker-utils/exec-selenium.sh test_navigation.py 29 | # Clean up 30 | - name: Stop test containers 31 | if: always() 32 | run: tests/scripts/docker-compose.sh shutdown 33 | -------------------------------------------------------------------------------- /chrome/options.js: -------------------------------------------------------------------------------- 1 | async function saveOptionsAsync(e) { 2 | if (e.submitter.id === "revert") { 3 | await chrome.storage.sync.remove('filename_format'); 4 | await chrome.storage.sync.remove('open_in_new_tab'); 5 | } else if (e.submitter.id === "update") { 6 | await chrome.storage.sync.set({ 7 | 'filename_format': document.querySelector("#new-filename-format").value, 8 | 'open_in_new_tab': document.querySelector("#new-open-in-new-tab").checked, 9 | }); 10 | } 11 | e.preventDefault(); 12 | await restoreOptionsAsync(); 13 | } 14 | 15 | async function restoreOptionsAsync() { 16 | const result = await chrome.storage.sync.get({ 17 | 'filename_format': '${title}, ${firstAuthor} et al., ${publishedYear}, v${version}.pdf', 18 | 'open_in_new_tab': true, 19 | }); 20 | const filename_format = result.filename_format; 21 | document.querySelector("#filename-format").innerText = filename_format; 22 | document.querySelector("#new-filename-format").value = filename_format; 23 | const open_in_new_tab = result.open_in_new_tab; 24 | document.querySelector("#new-open-in-new-tab").checked = open_in_new_tab; 25 | document.querySelector("#open-in-new-tab").innerText = open_in_new_tab; 26 | } 27 | 28 | document.addEventListener('DOMContentLoaded', restoreOptionsAsync); 29 | const forms = [...document.getElementsByTagName("form")] 30 | forms.forEach(element => { 31 | element.addEventListener("submit", saveOptionsAsync); 32 | }) 33 | -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arxiv-utils", 3 | "version": "1.8.7", 4 | "description": "Easy access on ArXiv! Auto-rename tabs to paper title, Quick navigation via button/hotkey, Save PDFs by paper title, and more.", 5 | "background": { 6 | "service_worker": "background.js", 7 | "type": "module" 8 | }, 9 | "content_scripts": [{ 10 | "matches": [ 11 | "*://arxiv.org/*pdf*", 12 | "*://arxiv.org/abs/*", 13 | "*://export.arxiv.org/*pdf*", 14 | "*://export.arxiv.org/abs/*", 15 | "*://browse.arxiv.org/*pdf*", 16 | "*://browse.arxiv.org/abs/*", 17 | "*://www.arxiv.org/*pdf*", 18 | "*://www.arxiv.org/abs/*", 19 | "*://ar5iv.labs.arxiv.org/html/*" 20 | ], 21 | "js": [ "content.js" ], 22 | "run_at": "document_end" 23 | }], 24 | "action": { 25 | "default_icon": { 26 | "19": "icon19.png", 27 | "38": "icon38.png" 28 | }, 29 | "default_title": "Open Abstract / PDF" 30 | }, 31 | "commands": { 32 | "_execute_action": { 33 | "suggested_key": { 34 | "default": "Alt+A" 35 | } 36 | } 37 | }, 38 | "permissions": [ 39 | "tabs", 40 | "activeTab", 41 | "storage", 42 | "contextMenus", 43 | "scripting", 44 | "downloads" 45 | ], 46 | "host_permissions": [ 47 | "*://arxiv.org/*", 48 | "*://export.arxiv.org/*", 49 | "*://browse.arxiv.org/*", 50 | "*://www.arxiv.org/*", 51 | "*://ar5iv.labs.arxiv.org/*" 52 | ], 53 | "icons": { 54 | "16": "icon16.png", 55 | "48": "icon48.png", 56 | "128": "icon128.png" 57 | }, 58 | "options_page": "options.html", 59 | "manifest_version": 3 60 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. The sections below are not mandatory, if you're unsure about a certain section, feel free to remove the entire section. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. For example: 25 | - Upload a screenshot/video of the actual (incorrect) behavior. 26 | - Take a screenshot of the Developer Console (`Right Click > Inspect > Console`) right after reproducing the bug. 27 | - If there are no errors in the Developer Console, it would be beneficial to inspect the background script logs following [the steps here](https://github.com/j3soon/arxiv-utils#development), and also take a screenshot of the errors. 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: [e.g. iOS] 31 | - Browser [e.g. chrome, safari] 32 | - Version [e.g. 22] 33 | 34 | 41 | 42 | **Additional context** 43 | Add any other context about the problem here. 44 | 45 | Make sure you have reproduced the bug in a new browser profile with no other extensions installed. If you have not, please do so before submitting the bug report to prevent extensions from interfering with each other. 46 | -------------------------------------------------------------------------------- /tests/unit-test/navigation.test.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml'); 2 | const fs = require('fs'); 3 | 4 | import TARGET_URL_REGEXP_REPLACE from '../../firefox/target_url_regexp_replace.js'; 5 | 6 | function getTargetURL(url) { 7 | for (const [regexp, replacement] of TARGET_URL_REGEXP_REPLACE) { 8 | if (regexp.test(url)) 9 | return url.replace(regexp, replacement); 10 | } 11 | return null; 12 | } 13 | 14 | test('navigation rules', () => { 15 | const testcases_path = "/app/tests/testcases/testcases.yaml"; 16 | const testcases = yaml.load(fs.readFileSync(testcases_path, 'utf8')); 17 | let n_success = 0 18 | for (const testcase of testcases.navigation) { 19 | const { url, title, pdf_url, pdf_title, url2, title2, description, abs2pdf = true, pdf2abs = true } = testcase; 20 | if (!abs2pdf && !pdf2abs) { 21 | throw new Error("Both `abs2pdf` and `pdf2abs` are False."); 22 | } 23 | console.log(`Running navigation testcase: 24 | - URL: ${url} 25 | - Title: \`${title}\` 26 | - PDF URL: ${pdf_url} 27 | - PDF Title: \`${pdf_title}\` 28 | - URL2: ${url2} 29 | - Title2: \`${title2}\` 30 | - Description: ${description} 31 | - Tests 32 | - Test abs2pdf? ${abs2pdf} 33 | - Test pdf2abs? ${pdf2abs}` 34 | ); 35 | if (abs2pdf) { 36 | if (pdf_url) { 37 | console.log("Checking (abs) url -> pdf_url...") 38 | expect(getTargetURL(url)).toBe(pdf_url); 39 | } else if (url2) { 40 | console.log("Checking url -> url2...") 41 | expect(getTargetURL(url)).toBe(url2); 42 | } 43 | } 44 | if (pdf2abs) { 45 | if (pdf_url) { 46 | console.log("Checking pdf_url -> (abs) url...") 47 | expect(getTargetURL(pdf_url)).toBe(url); 48 | } 49 | } 50 | console.log("Testcase Succeeded") 51 | n_success += 1 52 | } 53 | console.log("All tests passed successfully!\n" + 54 | `Success: ${n_success}/${n_success}`); 55 | }); 56 | -------------------------------------------------------------------------------- /firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arxiv-utils", 3 | "version": "1.8.7", 4 | "description": "Easy access on ArXiv! Auto-rename tabs to paper title, Quick navigation via button/hotkey, Save PDFs by paper title, and more.", 5 | "background": { 6 | "page": "background.html" 7 | }, 8 | "content_scripts": [{ 9 | "matches": [ 10 | "*://arxiv.org/abs/*", 11 | "*://export.arxiv.org/abs/*", 12 | "*://browse.arxiv.org/abs/*", 13 | "*://www.arxiv.org/abs/*", 14 | "*://ar5iv.labs.arxiv.org/html/*", 15 | "*://mozilla.github.io/pdf.js/web/viewer.html*" 16 | ], 17 | "js": [ "content.js" ], 18 | "run_at": "document_end" 19 | }], 20 | "browser_action": { 21 | "default_icon": { 22 | "16": "icon16.png", 23 | "32": "icon32.png" 24 | }, 25 | "default_title": "Open Abstract / PDF" 26 | }, 27 | "commands": { 28 | "_execute_browser_action": { 29 | "suggested_key": { 30 | "default": "Alt+A" 31 | } 32 | } 33 | }, 34 | "permissions": [ 35 | "tabs", 36 | "activeTab", 37 | "storage", 38 | "contextMenus", 39 | "webRequest", 40 | "webRequestBlocking", 41 | "bookmarks", 42 | "downloads", 43 | "*://arxiv.org/*pdf*", 44 | "*://export.arxiv.org/*pdf*", 45 | "*://browse.arxiv.org/*pdf*", 46 | "*://www.arxiv.org/*pdf*" 47 | ], 48 | "content_security_policy": 49 | "script-src 'self'; object-src 'self' https://arxiv.org https://export.arxiv.org https://browse.arxiv.org https://www.arxiv.org;", 50 | "web_accessible_resources": [ 51 | "pdfviewer.html" 52 | ], 53 | "icons": { 54 | "16": "icon16.png", 55 | "48": "icon48.png", 56 | "128": "icon128.png" 57 | }, 58 | "options_ui": { 59 | "page": "options.html", 60 | "browser_style": true 61 | }, 62 | "browser_specific_settings": { 63 | "gecko": { 64 | "id": "{ab779d78-7270-4ee8-9ee8-369d73508298}" 65 | } 66 | }, 67 | "manifest_version": 2 68 | } -------------------------------------------------------------------------------- /tests/end-to-end-test/compose.yml: -------------------------------------------------------------------------------- 1 | # Modified From: https://github.com/SeleniumHQ/docker-selenium/blob/c299c323c8e70227b9c57a3618aeda7b5d615692/docker-compose-v3.yml 2 | # To execute this docker-compose yml file use `docker-compose -f docker-compose-v3.yml up` 3 | # Add the `-d` flag at the end for detached execution 4 | # To stop the execution, hit Ctrl+C, and then `docker-compose -f docker-compose-v3.yml down` 5 | services: 6 | chrome-node: 7 | image: selenium/node-chrome:4.8.3-20230404 8 | shm_size: 2gb 9 | depends_on: 10 | - selenium-hub 11 | environment: 12 | - SE_EVENT_BUS_HOST=selenium-hub 13 | - SE_EVENT_BUS_PUBLISH_PORT=4442 14 | - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 15 | # - VNC_NO_PASSWORD=1 16 | volumes: 17 | - "../..:/app:ro" # Source code 18 | - "data:/data" # For testing downloads 19 | ports: 20 | - "6900:5900" # VNC port, default password: secret 21 | - "7900:7900" # noVNC port, default password: secret 22 | 23 | edge-node: 24 | image: selenium/node-edge:4.8.3-20230404 25 | shm_size: 2gb 26 | depends_on: 27 | - selenium-hub 28 | environment: 29 | - SE_EVENT_BUS_HOST=selenium-hub 30 | - SE_EVENT_BUS_PUBLISH_PORT=4442 31 | - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 32 | # - VNC_NO_PASSWORD=1 33 | volumes: 34 | - "../..:/app:ro" # Source code 35 | - "data:/data" # For testing downloads 36 | ports: 37 | - "6901:5900" # VNC port, default password: secret 38 | - "7901:7900" # noVNC port, default password: secret 39 | 40 | firefox-node: 41 | image: selenium/node-firefox:4.8.3-20230404 42 | shm_size: 2gb 43 | depends_on: 44 | - selenium-hub 45 | environment: 46 | - SE_EVENT_BUS_HOST=selenium-hub 47 | - SE_EVENT_BUS_PUBLISH_PORT=4442 48 | - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 49 | # - VNC_NO_PASSWORD=1 50 | volumes: 51 | - "../..:/app:ro" # Source code 52 | - "data:/data" # For testing downloads 53 | ports: 54 | - "6902:5900" # VNC port, default password: secret 55 | - "7902:7900" # noVNC port, default password: secret 56 | 57 | selenium-hub: 58 | image: selenium/hub:4.8.3-20230404 59 | container_name: selenium-hub 60 | ports: 61 | - "4442:4442" # publish port 62 | - "4443:4443" # subscribe port 63 | - "4444:4444" # HTTP interface 64 | 65 | selenium-tests: 66 | build: . 67 | volumes: 68 | - "../..:/app:ro" # Source code 69 | - "data:/data" # For testing downloads 70 | # Attach to the container with: 71 | # docker exec -it end-to-end-test-selenium-tests-1 sh 72 | 73 | volumes: 74 | data: # For testing downloads 75 | -------------------------------------------------------------------------------- /tests/unit-test/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /firefox/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

Current filename format: (To be loaded)

11 | 12 |
13 |

Will open in new tab:

14 | 15 |
16 |

Using PDF redirection:

17 | 18 |

19 | 20 |
21 |

Note: You'll need to re-open the arXiv tabs for the new filename format to take effect.

22 |

Note: Disabling PDF redirection will disallow renaming for PDF tabs.

23 |
24 | Experimental Features: 25 |
26 |

Current external PDF viewer URL prefix:

27 | 28 |
29 | 30 |
31 |

Note: Setting an external PDF viewer enables Screenshots and Go Back.

32 |
33 |
34 |

Current PDF viewer default zoom:

35 | 36 |
50 | 54 | 55 |
56 |

Note: This setting only applies when "Using PDF redirection" is enabled.

57 |
58 | Help 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /chrome/target_url_regexp_replace.js: -------------------------------------------------------------------------------- 1 | // Regular expressions for parsing target navigation URL from URLs. 2 | // Ref: https://info.arxiv.org/help/arxiv_identifier_for_services.html#urls-for-standard-arxiv-functions 3 | // Note: `https://arxiv.org/pdf/.pdf` will be redirected to `https://arxiv.org/pdf/` by arXiv. 4 | // Regexp info: 5 | // - Leading `^.*:\/\/` matches the protocol part of the URL such as `http://` or `https://`. 6 | // - Trailing `(\?.*?)?(\#.*?)?$` matches the query string and fragment 7 | // - Trailing `\?(?:.*?&)?` matches the leading extra query strings 8 | // - Trailing `(&.*?)?` matches the extra query strings 9 | export default [ 10 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/abs\/(\S*?)\/*(\?.*?)?(\#.*?)?$/, "https://arxiv.org/pdf/$1"], 11 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/pdf\/(\S*?)(?:\.pdf)?\/*(\?.*?)?(\#.*?)?$/, "https://arxiv.org/abs/$1"], 12 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/ftp\/(?:arxiv\/|([^\/]*\/))papers\/.*?([^\/]*?)\.pdf(\?.*?)?(\#.*?)?$/, "https://arxiv.org/abs/$1$2"], 13 | [/^.*:\/\/(?:browse\.|www\.)?arxiv\.org\/html\/(\S*?)\/*(\?.*?)?(\#.*?)?$/, "https://arxiv.org/abs/$1"], 14 | [/^.*:\/\/ar5iv\.labs\.arxiv\.org\/html\/(\S*?)\/*(\?.*?)?(\#.*?)?$/, "https://arxiv.org/abs/$1"], 15 | [/^.*:\/\/huggingface\.co\/papers\/(\S*?)(\?.*?)?(\#.*?)?$/, "https://arxiv.org/abs/$1"], 16 | [/^.*:\/\/openreview\.net\/forum\?(?:.*?&)?id=(\S*?)(&.*?)?(\#.*?)?$/, "https://openreview.net/pdf?id=$1"], 17 | [/^.*:\/\/openreview\.net\/pdf\?(?:.*?&)?id=(\S*?)(&.*?)?(\#.*?)?$/, "https://openreview.net/forum?id=$1"], 18 | // Starting from 2022, NIPS urls may end with a `-Conference` suffix 19 | [/^.*:\/\/(papers|proceedings)\.(nips|neurips)\.cc\/paper_files\/paper\/(\d*)\/(?:[^\/]*)\/(.*?)-Abstract(-Conference)?\.html(\?.*?)?(\#.*?)?$/, 20 | "https://$1.$2.cc/paper_files/paper/$3/file/$4-Paper$5.pdf"], 21 | [/^.*:\/\/(papers|proceedings)\.(nips|neurips)\.cc\/paper_files\/paper\/(\d*)\/(?:[^\/]*)\/(.*?)-.*?(-Conference)?\..*?(\?.*?)?(\#.*?)?$/, 22 | "https://$1.$2.cc/paper_files/paper/$3/hash/$4-Abstract$5.html"], 23 | [/^.*:\/\/proceedings\.mlr\.press\/(.*?)\/(.*?)(?:\/.*?)?(?:-supp)?\.pdf$/, "https://proceedings.mlr.press/$1/$2.html"], 24 | [/^.*:\/\/proceedings\.mlr\.press\/(.*?)\/(.*?)(?:\.html)?(\?.*?)?(\#.*?)?$/, "https://proceedings.mlr.press/$1/$2/$2.pdf"], 25 | [/^.*:\/\/openaccess\.thecvf\.com\/(.*?)\/html\/(.*?)\.html(\?.*?)?(\#.*?)?$/, "https://openaccess.thecvf.com/$1/papers/$2.pdf"], 26 | [/^.*:\/\/openaccess\.thecvf\.com\/(.*?)\/papers\/(.*?)\.pdf(\?.*?)?(\#.*?)?$/, "https://openaccess.thecvf.com/$1/html/$2.html"], 27 | [/^.*:\/\/www\.jmlr\.org\/papers\/v(\d+)\/(.*?)\.html(\?.*?)?(\#.*?)?$/, "https://www.jmlr.org/papers/volume$1/$2/$2.pdf"], 28 | [/^.*:\/\/www\.jmlr\.org\/papers\/volume(\d+)\/(.*?)\/(.*?)\.pdf(\?.*?)?(\#.*?)?$/, "https://www.jmlr.org/papers/v$1/$2.html"], 29 | [/^.*:\/\/ieeexplore\.ieee\.org\/document\/(\d+)(\?.*?)?(\#.*?)?$/, "https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=$1"], 30 | [/^.*:\/\/ieeexplore\.ieee\.org\/stamp\/stamp\.jsp\?(?:.*?&)?arnumber=(\d+)(&.*?)?(\#.*?)?$/, "https://ieeexplore.ieee.org/document/$1"], 31 | [/^.*:\/\/aclanthology\.org\/([^\/]+)\.pdf(\?.*?)?(\#.*?)?$/, "https://aclanthology.org/$1/"], 32 | [/^.*:\/\/aclanthology\.org\/([^\/]+)\/(\?.*?)?(\#.*?)?$/, "https://aclanthology.org/$1.pdf"], 33 | ]; 34 | -------------------------------------------------------------------------------- /firefox/target_url_regexp_replace.js: -------------------------------------------------------------------------------- 1 | // Regular expressions for parsing target navigation URL from URLs. 2 | // Ref: https://info.arxiv.org/help/arxiv_identifier_for_services.html#urls-for-standard-arxiv-functions 3 | // Note: `https://arxiv.org/pdf/.pdf` will be redirected to `https://arxiv.org/pdf/` by arXiv. 4 | // Regexp info: 5 | // - Leading `^.*:\/\/` matches the protocol part of the URL such as `http://` or `https://`. 6 | // - Trailing `(\?.*?)?(\#.*?)?$` matches the query string and fragment 7 | // - Trailing `\?(?:.*?&)?` matches the leading extra query strings 8 | // - Trailing `(&.*?)?` matches the extra query strings 9 | export default [ 10 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/abs\/(\S*?)\/*(\?.*?)?(\#.*?)?$/, "https://arxiv.org/pdf/$1"], 11 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/pdf\/(\S*?)(?:\.pdf)?\/*(\?.*?)?(\#.*?)?$/, "https://arxiv.org/abs/$1"], 12 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/ftp\/(?:arxiv\/|([^\/]*\/))papers\/.*?([^\/]*?)\.pdf(\?.*?)?(\#.*?)?$/, "https://arxiv.org/abs/$1$2"], 13 | [/^.*:\/\/(?:browse\.|www\.)?arxiv\.org\/html\/(\S*?)\/*(\?.*?)?(\#.*?)?$/, "https://arxiv.org/abs/$1"], 14 | [/^.*:\/\/ar5iv\.labs\.arxiv\.org\/html\/(\S*?)\/*(\?.*?)?(\#.*?)?$/, "https://arxiv.org/abs/$1"], 15 | [/^.*:\/\/huggingface\.co\/papers\/(\S*?)(\?.*?)?(\#.*?)?$/, "https://arxiv.org/abs/$1"], 16 | [/^.*:\/\/openreview\.net\/forum\?(?:.*?&)?id=(\S*?)(&.*?)?(\#.*?)?$/, "https://openreview.net/pdf?id=$1"], 17 | [/^.*:\/\/openreview\.net\/pdf\?(?:.*?&)?id=(\S*?)(&.*?)?(\#.*?)?$/, "https://openreview.net/forum?id=$1"], 18 | // Starting from 2022, NIPS urls may end with a `-Conference` suffix 19 | [/^.*:\/\/(papers|proceedings)\.(nips|neurips)\.cc\/paper_files\/paper\/(\d*)\/(?:[^\/]*)\/(.*?)-Abstract(-Conference)?\.html(\?.*?)?(\#.*?)?$/, 20 | "https://$1.$2.cc/paper_files/paper/$3/file/$4-Paper$5.pdf"], 21 | [/^.*:\/\/(papers|proceedings)\.(nips|neurips)\.cc\/paper_files\/paper\/(\d*)\/(?:[^\/]*)\/(.*?)-.*?(-Conference)?\..*?(\?.*?)?(\#.*?)?$/, 22 | "https://$1.$2.cc/paper_files/paper/$3/hash/$4-Abstract$5.html"], 23 | [/^.*:\/\/proceedings\.mlr\.press\/(.*?)\/(.*?)(?:\/.*?)?(?:-supp)?\.pdf$/, "https://proceedings.mlr.press/$1/$2.html"], 24 | [/^.*:\/\/proceedings\.mlr\.press\/(.*?)\/(.*?)(?:\.html)?(\?.*?)?(\#.*?)?$/, "https://proceedings.mlr.press/$1/$2/$2.pdf"], 25 | [/^.*:\/\/openaccess\.thecvf\.com\/(.*?)\/html\/(.*?)\.html(\?.*?)?(\#.*?)?$/, "https://openaccess.thecvf.com/$1/papers/$2.pdf"], 26 | [/^.*:\/\/openaccess\.thecvf\.com\/(.*?)\/papers\/(.*?)\.pdf(\?.*?)?(\#.*?)?$/, "https://openaccess.thecvf.com/$1/html/$2.html"], 27 | [/^.*:\/\/www\.jmlr\.org\/papers\/v(\d+)\/(.*?)\.html(\?.*?)?(\#.*?)?$/, "https://www.jmlr.org/papers/volume$1/$2/$2.pdf"], 28 | [/^.*:\/\/www\.jmlr\.org\/papers\/volume(\d+)\/(.*?)\/(.*?)\.pdf(\?.*?)?(\#.*?)?$/, "https://www.jmlr.org/papers/v$1/$2.html"], 29 | [/^.*:\/\/ieeexplore\.ieee\.org\/document\/(\d+)(\?.*?)?(\#.*?)?$/, "https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=$1"], 30 | [/^.*:\/\/ieeexplore\.ieee\.org\/stamp\/stamp\.jsp\?(?:.*?&)?arnumber=(\d+)(&.*?)?(\#.*?)?$/, "https://ieeexplore.ieee.org/document/$1"], 31 | [/^.*:\/\/aclanthology\.org\/([^\/]+)\.pdf(\?.*?)?(\#.*?)?$/, "https://aclanthology.org/$1/"], 32 | [/^.*:\/\/aclanthology\.org\/([^\/]+)\/(\?.*?)?(\#.*?)?$/, "https://aclanthology.org/$1.pdf"], 33 | ]; 34 | -------------------------------------------------------------------------------- /firefox/pdfviewer.js: -------------------------------------------------------------------------------- 1 | // This script modifies the title of the PDF in the container once it has finished loading. 2 | 3 | // Regular expressions for parsing arXiv IDs from URLs. 4 | // Ref: https://info.arxiv.org/help/arxiv_identifier_for_services.html#urls-for-standard-arxiv-functions 5 | const ID_REGEXP_REPLACE = [ 6 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/pdf\/(\S*?)(?:\.pdf)?\/*(\?.*?)?(\#.*?)?$/, "$1"], 7 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/ftp\/(?:arxiv\/|([^\/]*\/))papers\/.*?([^\/]*?)\.pdf(\?.*?)?(\#.*?)?$/, "$1$2"], 8 | ]; 9 | // All console logs should start with this prefix. 10 | const LOG_PREFIX = "[arXiv-utils]"; 11 | 12 | // Return the id parsed from the url. 13 | function getId(url) { 14 | for (const [regexp, replacement] of ID_REGEXP_REPLACE) { 15 | if (regexp.test(url)) 16 | return url.replace(regexp, replacement); 17 | } 18 | return null; 19 | } 20 | // Get article information through arXiv API asynchronously. 21 | // Ref: https://info.arxiv.org/help/api/user-manual.html#31-calling-the-api 22 | async function getArticleInfoAsync(id, pageType) { 23 | console.log(LOG_PREFIX, "Retrieving title through ArXiv API request..."); 24 | const response = await fetch(`https://export.arxiv.org/api/query?id_list=${id}`); 25 | if (!response.ok) { 26 | console.error(LOG_PREFIX, "Error: ArXiv API request failed."); 27 | return; 28 | } 29 | const xmlDoc = await response.text(); 30 | const parsedXML = new DOMParser().parseFromString(xmlDoc, 'text/xml'); 31 | // title[0] is query string, title[1] is paper name. 32 | const title = parsedXML.getElementsByTagName("title")[1].textContent; 33 | // Long titles will be split into multiple lines, with all lines except the first one starting with two spaces. 34 | const escapedTitle = title.replace("\n", "").replace(" ", " "); 35 | // TODO: May need to escape special characters in title? 36 | const newTitle = `${escapedTitle} | ${pageType}`; 37 | return { 38 | escapedTitle, 39 | newTitle, 40 | } 41 | } 42 | 43 | async function mainAsync() { 44 | // Extract the pdf url from 'pdfviewer.html?target='. 45 | const url = new URL(window.location.href).searchParams.get("target"); 46 | 47 | // Get zoom setting from storage 48 | const result = await browser.storage.sync.get({ 49 | 'pdf_viewer_default_zoom': 'auto' 50 | }); 51 | const zoom = result.pdf_viewer_default_zoom; 52 | 53 | // Construct the final URL with zoom parameter 54 | let finalUrl = url; 55 | if (zoom !== 'auto') { 56 | finalUrl += '#zoom=' + zoom; 57 | } 58 | 59 | // Inject PDF before querying the API to load the PDF as soon as 60 | // possible in the case of slow response from the API. 61 | const elContainer = document.getElementById("container"); 62 | elContainer.innerHTML += ``; 63 | console.log(LOG_PREFIX, "Injected PDF: " + finalUrl); 64 | // Query the API to get the title. 65 | const pageType = url.includes("abs") ? "Abstract" : "PDF"; 66 | const id = getId(url); 67 | if (!id) { 68 | console.error(LOG_PREFIX, "Error: Failed to get paper ID, aborted."); 69 | return; 70 | } 71 | const articleInfo = await getArticleInfoAsync(id, pageType); 72 | document.title = articleInfo.newTitle; 73 | console.log(LOG_PREFIX, `Set document title to: ${articleInfo.newTitle}.`); 74 | } 75 | 76 | // Execute main logic. 77 | mainAsync(); 78 | -------------------------------------------------------------------------------- /tests/end-to-end-test/test_default.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | import yaml 4 | from selenium import webdriver 5 | from selenium.common.exceptions import TimeoutException 6 | from selenium.webdriver.common.by import By 7 | from selenium.webdriver.support import expected_conditions as EC 8 | from selenium.webdriver.support.ui import WebDriverWait 9 | 10 | testcases_path = "/app/tests/testcases/testcases.yaml" 11 | with open(testcases_path, "r") as f: 12 | testcases = yaml.safe_load(f) 13 | 14 | n_success = 0 15 | n_skipped = 0 16 | 17 | for browser in ['chrome', 'firefox', 'edge']: 18 | print(f"Testing with browser: {browser}") 19 | options = { 20 | 'chrome': webdriver.ChromeOptions(), 21 | 'firefox': webdriver.FirefoxOptions(), 22 | 'edge': webdriver.EdgeOptions(), 23 | }[browser] 24 | 25 | print(f"Launching webdriver...") 26 | driver = webdriver.Remote( 27 | command_executor='http://selenium-hub:4444/wd/hub', 28 | options=options 29 | ) 30 | wait = WebDriverWait(driver, 60) 31 | # The webdriver includes a default tab 32 | wait.until(EC.number_of_windows_to_be(1)) 33 | initial_window = driver.current_window_handle 34 | 35 | for testcase in testcases['default']: 36 | url, title, description = itemgetter('url', 'title', 'description')(testcase) 37 | print(f"Running testcase:") 38 | print(f"- URL: {url}") 39 | print(f"- Title: `{title}`") 40 | print(f"- Description: {description}") 41 | 42 | if 'pdf' in url: 43 | # The current page title is `about:blank` with `application/pdf` as the content type. 44 | # The pdf is stored in a embed frame `/html/body/embed`. 45 | # However, I don't think it's possible to switch to that frame, since the PDF viewer itself is an extension. 46 | # There are several potential solutions: 47 | # - One way forward is to somehow access the pdf frame content and retrieve the title. 48 | # - Another way is to somehow use JavaScript to retrieve the pdf frame title and expose it through the browser. 49 | # - The last way is to give up accessing the pdf title, and instead only check the html title for non-default testcases. 50 | # The first two solutions cannot be implmented, so we apply the third solution. 51 | # Ref: https://stackoverflow.com/a/29817526 52 | # Ref: https://stackoverflow.com/a/4693418 53 | # Ref: https://stackoverflow.com/a/68041520 54 | print("Testcase Skipped (Ends with .pdf)") 55 | n_skipped += 1 56 | continue 57 | 58 | print(f"Opening webpage...") 59 | driver.switch_to.new_window('tab') 60 | assert len(driver.window_handles) == 2 61 | driver.get(url) 62 | 63 | print(f"Checking title...") 64 | try: 65 | wait.until(EC.title_is(title)) 66 | except TimeoutException as e: 67 | print(f"Title mismatch: `{driver.title}`; URL: `{driver.current_url}`.") 68 | assert driver.title == title 69 | 70 | print(f"Closing webpage...") 71 | driver.close() 72 | assert len(driver.window_handles) == 1 73 | driver.switch_to.window(initial_window) 74 | 75 | print("Testcase Succeeded") 76 | n_success += 1 77 | 78 | print(f"Closing webdriver...") 79 | driver.quit() 80 | print(f"{browser.capitalize()} Tests Succeeded") 81 | 82 | print("All tests passed successfully!") 83 | n = n_success + n_skipped 84 | print(f"Success: {n_success}/{n}; Skipped: {n_skipped}/{n}") 85 | -------------------------------------------------------------------------------- /firefox/options.js: -------------------------------------------------------------------------------- 1 | async function saveOptionsAsync(e) { 2 | if (e.submitter.id === "revert") { 3 | await browser.storage.sync.remove('filename_format'); 4 | await browser.storage.sync.remove('open_in_new_tab'); 5 | await browser.storage.sync.remove('redirect_pdf'); 6 | } else if (e.submitter.id === "update") { 7 | await browser.storage.sync.set({ 8 | 'filename_format': document.querySelector("#new-filename-format").value, 9 | 'open_in_new_tab': document.querySelector("#new-open-in-new-tab").checked, 10 | 'redirect_pdf': document.querySelector("#new-redirect-pdf").checked, 11 | }); 12 | } else if (e.submitter.id === "revert-pdf-viewer-url-prefix") { 13 | await browser.storage.sync.remove('pdf_viewer_url_prefix'); 14 | } else if (e.submitter.id === "update-pdf-viewer-url-prefix") { 15 | await browser.storage.sync.set({ 16 | 'pdf_viewer_url_prefix': document.querySelector("#new-pdf-viewer-url-prefix").value, 17 | }); 18 | } else if (e.submitter.id === "revert-pdf-viewer-default-zoom") { 19 | await browser.storage.sync.remove('pdf_viewer_default_zoom'); 20 | } else if (e.submitter.id === "update-pdf-viewer-default-zoom") { 21 | const zoomSelect = document.querySelector("#new-pdf-viewer-default-zoom"); 22 | let zoomValue = zoomSelect.value; 23 | 24 | if (zoomValue === "custom") { 25 | const customZoomInput = document.querySelector("#custom-pdf-viewer-zoom"); 26 | const customZoomValue = customZoomInput.value; 27 | if (customZoomValue && !isNaN(customZoomValue)) { 28 | zoomValue = customZoomValue; 29 | } else { 30 | // If invalid, fallback to 'auto' 31 | zoomValue = 'auto'; 32 | } 33 | } 34 | 35 | await browser.storage.sync.set({ 36 | 'pdf_viewer_default_zoom': zoomValue, 37 | }); 38 | } 39 | e.preventDefault(); 40 | await restoreOptionsAsync(); 41 | } 42 | 43 | async function restoreOptionsAsync() { 44 | const result = await browser.storage.sync.get({ 45 | 'filename_format': '${title}, ${firstAuthor} et al., ${publishedYear}, v${version}.pdf', 46 | 'open_in_new_tab': true, 47 | 'redirect_pdf': true, 48 | 'pdf_viewer_url_prefix': '', 49 | 'pdf_viewer_default_zoom': 'auto', 50 | }); 51 | const filename_format = result.filename_format; 52 | document.querySelector("#filename-format").innerText = filename_format; 53 | document.querySelector("#new-filename-format").value = filename_format; 54 | const open_in_new_tab = result.open_in_new_tab; 55 | document.querySelector("#new-open-in-new-tab").checked = open_in_new_tab; 56 | document.querySelector("#open-in-new-tab").innerText = open_in_new_tab; 57 | const redirect_pdf = result.redirect_pdf; 58 | document.querySelector("#new-redirect-pdf").checked = redirect_pdf; 59 | document.querySelector("#redirect-pdf").innerText = redirect_pdf; 60 | const pdf_viewer_url_prefix = result.pdf_viewer_url_prefix; 61 | document.querySelector("#pdf-viewer-url-prefix").innerText = pdf_viewer_url_prefix; 62 | if (pdf_viewer_url_prefix !== '') 63 | document.querySelector("#new-pdf-viewer-url-prefix").value = pdf_viewer_url_prefix; 64 | else 65 | document.querySelector("#new-pdf-viewer-url-prefix").value = "https://mozilla.github.io/pdf.js/web/viewer.html?file="; 66 | const pdf_viewer_default_zoom = result.pdf_viewer_default_zoom; 67 | document.querySelector("#pdf-viewer-default-zoom").innerText = pdf_viewer_default_zoom + (pdf_viewer_default_zoom !== 'auto' && pdf_viewer_default_zoom !== 'page-fit' && pdf_viewer_default_zoom !== 'page-width' && !['25', '50', '75', '100', '125', '150', '200'].includes(pdf_viewer_default_zoom) ? '%' : ''); 68 | 69 | // Check if it's a predefined value or custom 70 | const predefinedValues = ['auto', 'page-fit', 'page-width', '50', '75', '100', '125', '150', '200', '300', '400']; 71 | if (predefinedValues.includes(pdf_viewer_default_zoom)) { 72 | document.querySelector("#new-pdf-viewer-default-zoom").value = pdf_viewer_default_zoom; 73 | toggleCustomZoomInput('hide'); 74 | } else { 75 | // It's a custom value 76 | document.querySelector("#new-pdf-viewer-default-zoom").value = 'custom'; 77 | document.querySelector("#custom-pdf-viewer-zoom").value = pdf_viewer_default_zoom; 78 | toggleCustomZoomInput('show'); 79 | } 80 | } 81 | 82 | function toggleCustomZoomInput(action) { 83 | const customZoomContainer = document.querySelector("#custom-zoom-container"); 84 | if (action === 'show') { 85 | customZoomContainer.style.display = 'block'; 86 | } else { 87 | customZoomContainer.style.display = 'none'; 88 | } 89 | } 90 | 91 | function handleZoomSelectChange() { 92 | const zoomSelect = document.querySelector("#new-pdf-viewer-default-zoom"); 93 | if (zoomSelect.value === 'custom') { 94 | toggleCustomZoomInput('show'); 95 | } else { 96 | toggleCustomZoomInput('hide'); 97 | } 98 | } 99 | 100 | document.addEventListener('DOMContentLoaded', async () => { 101 | await restoreOptionsAsync(); 102 | 103 | // Add event listener for zoom dropdown change 104 | const zoomSelect = document.querySelector("#new-pdf-viewer-default-zoom"); 105 | if (zoomSelect) { 106 | zoomSelect.addEventListener('change', handleZoomSelectChange); 107 | } 108 | }); 109 | 110 | const forms = [...document.getElementsByTagName("form")] 111 | forms.forEach(element => { 112 | element.addEventListener("submit", saveOptionsAsync); 113 | }) 114 | -------------------------------------------------------------------------------- /chrome/background.js: -------------------------------------------------------------------------------- 1 | // This background script implements the extension button, 2 | // and triggers the content script upon tab title change. 3 | import TARGET_URL_REGEXP_REPLACE from './target_url_regexp_replace.js'; 4 | 5 | // All console logs should start with this prefix. 6 | const LOG_PREFIX = "[arXiv-utils]"; 7 | 8 | // Return the target URL parsed from the url. 9 | function getTargetURL(url) { 10 | for (const [regexp, replacement] of TARGET_URL_REGEXP_REPLACE) { 11 | if (regexp.test(url)) 12 | return url.replace(regexp, replacement); 13 | } 14 | return null; 15 | } 16 | // Update the state of the extension button (i.e., browser action) 17 | async function updateActionStateAsync(tabId, url) { 18 | const id = getTargetURL(url); 19 | if (!id) { 20 | await chrome.action.disable(tabId); 21 | // console.log(LOG_PREFIX, `Disabled browser action for tab ${tabId} with url: ${url}.`); 22 | } else { 23 | await chrome.action.enable(tabId); 24 | // console.log(LOG_PREFIX, `Enabled browser action for tab ${tabId} with url: ${url}.`); 25 | } 26 | } 27 | // Update browser action state for the updated tab. 28 | function onTabUpdated(tabId, changeInfo, tab) { 29 | updateActionStateAsync(tabId, tab.url) 30 | const id = getTargetURL(tab.url); 31 | if (!id) return; 32 | if (changeInfo.title && tab.status == "complete") { 33 | // Send title changed message to content script. 34 | // Ref: https://stackoverflow.com/a/73151665 35 | console.log(LOG_PREFIX, "Title changed, sending message to content script."); 36 | chrome.tabs.sendMessage(tabId, tab); 37 | } 38 | } 39 | // Open the abstract / PDF page according to the current URL. 40 | async function onButtonClickedAsync(tab) { 41 | console.log(LOG_PREFIX, "Button clicked, opening abstract / PDF page."); 42 | const targetURL = getTargetURL(tab.url); 43 | if (!targetURL) { 44 | console.error(LOG_PREFIX, "Error: Failed to get paper ID, aborted."); 45 | return; 46 | } 47 | // Create the abstract / PDF page in existing / new tab. 48 | const openInNewTab = (await chrome.storage.sync.get({ 49 | 'open_in_new_tab': true 50 | })).open_in_new_tab; 51 | if (openInNewTab) { 52 | await chrome.tabs.create({ 53 | url: targetURL, 54 | index: tab.index + 1, 55 | }); 56 | } else { 57 | await chrome.tabs.update({ 58 | url: targetURL, 59 | }); 60 | } 61 | console.log(LOG_PREFIX, "Opened abstract / PDF page in existing / new tab."); 62 | } 63 | function onMessage(message, sender, sendResponse) { 64 | // Handle API query requests from content script (to avoid Chrome's CORS restrictions) 65 | if (message.type === 'fetchArticleInfo') { 66 | (async () => { 67 | try { 68 | console.log(LOG_PREFIX, `Fetching article info for id: ${message.id}`); 69 | const response = await fetch(`https://export.arxiv.org/api/query?id_list=${message.id}`); 70 | if (!response.ok) { 71 | console.error(LOG_PREFIX, "Error: ArXiv API request failed."); 72 | sendResponse({ success: false, error: 'API request failed' }); 73 | return; 74 | } 75 | const xmlDoc = await response.text(); 76 | console.log(LOG_PREFIX, "Successfully retrieved article info from ArXiv API."); 77 | sendResponse({ success: true, data: xmlDoc }); 78 | } catch (error) { 79 | console.error(LOG_PREFIX, "Error fetching article info:", error); 80 | sendResponse({ success: false, error: error.message }); 81 | } 82 | })(); 83 | // Tell Chrome we will send the response asynchronously 84 | return true; 85 | } 86 | 87 | // Handle download requests 88 | if (message.type === 'downloadFile') { 89 | chrome.downloads.download({ 90 | url: message.url, 91 | filename: message.filename, 92 | saveAs: false, 93 | }).then(() => { 94 | console.log(LOG_PREFIX, `Downloading file: ${message.filename} from ${message.url}.`) 95 | }); 96 | } 97 | } 98 | function onContextClicked(info, tab) { 99 | if (info.menuItemId === 'help') 100 | chrome.tabs.create({ 101 | url: "https://github.com/j3soon/arxiv-utils", 102 | }); 103 | } 104 | function onInstalled() { 105 | // Add Help menu item to extension button context menu. (Manifest v3) 106 | chrome.contextMenus.create({ 107 | id: "help", 108 | title: "Help", 109 | contexts: ["action"], 110 | }); 111 | } 112 | // Inject content scripts to pre-existing tabs. E.g., after installation or re-enable. 113 | // Firefox injects content scripts automatically, but Chrome does not. 114 | async function injectContentScriptsAsync() { 115 | // TODO: Fix errors: 116 | // - Injecting content scripts seems to cause error when 117 | // disabling and re-enabling the extension very quickly with existing arXiv tabs: 118 | // Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'sync') 119 | // - Another error seems to occur under unknown circumstances: 120 | // Uncaught SyntaxError: Identifier 'ABS_REGEXP' has already been declared 121 | // - Another error seems to occur under unknown circumstances: 122 | // Unchecked runtime.lastError: Cannot create item with duplicate id help 123 | for (const cs of chrome.runtime.getManifest().content_scripts) { 124 | for (const tab of await chrome.tabs.query({url: cs.matches})) { 125 | console.log(LOG_PREFIX, `Injecting content scripts for tab ${tab.id} with url: ${tab.url}.`); 126 | chrome.scripting.executeScript({ 127 | target: {tabId: tab.id}, 128 | files: cs.js, 129 | }); 130 | } 131 | } 132 | } 133 | 134 | // Update browser action state upon start (e.g., installation, enable). 135 | chrome.tabs.query({}, function(tabs) { 136 | if (!tabs) return; 137 | for (const tab of tabs) 138 | updateActionStateAsync(tab.id, tab.url) 139 | }); 140 | // Disable the extension button by default. (Manifest v3) 141 | chrome.action.disable(); 142 | // Listen to all tab updates. 143 | chrome.tabs.onUpdated.addListener(onTabUpdated); 144 | // Listen to extension button click. 145 | chrome.action.onClicked.addListener(onButtonClickedAsync); 146 | // Listen to extension button right-click. 147 | chrome.contextMenus.onClicked.addListener(onContextClicked) 148 | // Listen to download request 149 | chrome.runtime.onMessage.addListener(onMessage); 150 | 151 | // Listen to on extension install event. 152 | chrome.runtime.onInstalled.addListener(onInstalled); 153 | injectContentScriptsAsync(); 154 | -------------------------------------------------------------------------------- /firefox/background.js: -------------------------------------------------------------------------------- 1 | // This background script implements the extension button, 2 | // and redirects PDF pages to custom PDF container. 3 | import TARGET_URL_REGEXP_REPLACE from './target_url_regexp_replace.js'; 4 | 5 | // For our PDF container. 6 | const pdfViewerRelatedURL = "pdfviewer.html?target="; 7 | // The match pattern for the URLs to redirect 8 | const redirectPatterns = [ 9 | "*://arxiv.org/*.pdf*", "*://export.arxiv.org/*.pdf*", "*://browse.arxiv.org/*.pdf*", "*://www.arxiv.org/*.pdf*", 10 | "*://arxiv.org/*pdf*/*", "*://export.arxiv.org/*pdf*/*", "*://browse.arxiv.org/*pdf*/*", "*://www.arxiv.org/*pdf*/*", 11 | ]; 12 | // All console logs should start with this prefix. 13 | const LOG_PREFIX = "[arXiv-utils]"; 14 | 15 | // Get PDF viewer URL prefix 16 | async function getPDFViewerURLPrefixAsync() { 17 | const result = await browser.storage.sync.get({ 18 | 'pdf_viewer_url_prefix': '' 19 | }); 20 | var prefix; 21 | if (result.pdf_viewer_url_prefix === '') 22 | prefix = browser.runtime.getURL(pdfViewerRelatedURL); 23 | else 24 | prefix = result.pdf_viewer_url_prefix; 25 | return prefix; 26 | } 27 | // Construct PDF viewer URL with zoom parameter 28 | async function constructPDFViewerURLAsync(pdfURL) { 29 | const result = await browser.storage.sync.get({ 30 | 'pdf_viewer_url_prefix': '', 31 | 'pdf_viewer_default_zoom': 'auto' 32 | }); 33 | var prefix; 34 | if (result.pdf_viewer_url_prefix === '') { 35 | // Using custom PDF container, no zoom parameter needed 36 | prefix = browser.runtime.getURL(pdfViewerRelatedURL); 37 | return prefix + pdfURL; 38 | } else { 39 | // Using external PDF viewer, append zoom parameter 40 | prefix = result.pdf_viewer_url_prefix; 41 | const zoom = result.pdf_viewer_default_zoom; 42 | if (zoom === 'auto') { 43 | return prefix + pdfURL; 44 | } else { 45 | return prefix + pdfURL + '#zoom=' + zoom; 46 | } 47 | } 48 | } 49 | // Helper function to remove zoom suffix from URL 50 | function removeZoomSuffix(url) { 51 | // Remove #zoom= from the end of the URL 52 | return url.replace(/#zoom=.*$/, ''); 53 | } 54 | 55 | // Return the target URL parsed from the url. 56 | async function getTargetURLAsync(url) { 57 | // Remove the prefix for the custom PDF page. 58 | const prefix = await getPDFViewerURLPrefixAsync(); 59 | if (url.startsWith(prefix)) { 60 | url = url.substr(prefix.length); 61 | // Remove zoom suffix if present 62 | url = removeZoomSuffix(url); 63 | } 64 | for (const [regexp, replacement] of TARGET_URL_REGEXP_REPLACE) { 65 | if (regexp.test(url)) 66 | return url.replace(regexp, replacement); 67 | } 68 | return null; 69 | } 70 | // Update the state of the extension button (i.e., browser action) 71 | async function updateActionStateAsync(tabId, url) { 72 | const id = await getTargetURLAsync(url); 73 | if (!id) { 74 | await browser.browserAction.disable(tabId); 75 | // console.log(LOG_PREFIX, `Disabled browser action for tab ${tabId} with url: ${url}.`); 76 | } else { 77 | await browser.browserAction.enable(tabId); 78 | // console.log(LOG_PREFIX, `Enabled browser action for tab ${tabId} with url: ${url}.`); 79 | } 80 | } 81 | // Update browser action state for the updated tab. 82 | function onTabUpdated(tabId, changeInfo, tab) { 83 | updateActionStateAsync(tabId, tab.url) 84 | } 85 | // Open the abstract / PDF page according to the current URL. 86 | async function onButtonClickedAsync(tab) { 87 | console.log(LOG_PREFIX, "Button clicked, opening abstract / PDF page."); 88 | const targetURL = await getTargetURLAsync(tab.url); 89 | if (!targetURL) { 90 | console.error(LOG_PREFIX, "Error: Failed to get paper ID, aborted."); 91 | return; 92 | } 93 | // Create the abstract / PDF page in existing / new tab. 94 | const openInNewTab = (await browser.storage.sync.get({ 95 | 'open_in_new_tab': true 96 | })).open_in_new_tab; 97 | if (openInNewTab) { 98 | await browser.tabs.create({ 99 | url: targetURL, 100 | index: tab.index + 1, 101 | }); 102 | } else { 103 | await browser.tabs.update({ 104 | url: targetURL, 105 | }); 106 | } 107 | console.log(LOG_PREFIX, "Opened abstract / PDF page in existing / new tab."); 108 | } 109 | async function onMessage(message) { 110 | await browser.downloads.download({ 111 | url: message.url, 112 | filename: message.filename, 113 | saveAs: false, 114 | }); 115 | console.log(LOG_PREFIX, `Downloading file: ${message.filename} from ${message.url}.`) 116 | } 117 | // Redirect to custom PDF page. 118 | async function onBeforeWebRequestAsync(requestDetails) { 119 | if (requestDetails.documentUrl !== undefined) { 120 | // Request from this plugin itself (the embedded PDF). 121 | return; 122 | } 123 | const redirectPDF = (await browser.storage.sync.get({ 124 | 'redirect_pdf': true 125 | })).redirect_pdf; 126 | if (!redirectPDF) { 127 | // Redirection of PDF is disabled. 128 | return; 129 | } 130 | // Force HTTPS to avoid CSP (Content Security Policy) violation. 131 | const url = requestDetails.url.replace("http:", "https:"); 132 | // Redirect to custom PDF viewer or a external PDF viewer. 133 | const targetURL = await constructPDFViewerURLAsync(url); 134 | console.log(`${LOG_PREFIX} Redirecting: ${requestDetails.url} to ${targetURL}`); 135 | return { 136 | redirectUrl: targetURL 137 | }; 138 | } 139 | // If the custom PDF page is bookmarked, bookmark the original PDF link instead. 140 | async function onCreateBookmarkAsync(id, bookmarkInfo) { 141 | const prefix = await getPDFViewerURLPrefixAsync(); 142 | if (!bookmarkInfo.url.startsWith(prefix)) { 143 | return; 144 | } 145 | console.log(LOG_PREFIX, "Updating bookmark with id: " + id + ", url: " + bookmarkInfo.url); 146 | let url = bookmarkInfo.url.substr(prefix.length); 147 | // Remove zoom suffix if present 148 | url = removeZoomSuffix(url); 149 | browser.bookmarks.update(id, { 150 | url 151 | }, () => { 152 | console.log(LOG_PREFIX, "Updated bookmark with id: " + id + " to URL: " + url); 153 | }); 154 | } 155 | 156 | // Update browser action state upon start (e.g., installation, enable). 157 | browser.tabs.query({}, function(tabs) { 158 | if (!tabs) return; 159 | for (const tab of tabs) 160 | updateActionStateAsync(tab.id, tab.url) 161 | }); 162 | // Disable the extension button by default. (Manifest v2) 163 | browser.browserAction.disable(); 164 | // Listen to all tab updates. 165 | browser.tabs.onUpdated.addListener(onTabUpdated); 166 | // Listen to extension button click. 167 | browser.browserAction.onClicked.addListener(onButtonClickedAsync); 168 | // Add Help menu item to extension button context menu. (Manifest v2) 169 | browser.contextMenus.create({ 170 | title: "Help", 171 | contexts: ["browser_action"], 172 | onclick: () => { 173 | browser.tabs.create({ 174 | url: "https://github.com/j3soon/arxiv-utils", 175 | }); 176 | } 177 | }); 178 | // Listen to download request 179 | browser.runtime.onMessage.addListener(onMessage); 180 | 181 | // Redirect the PDF page to custom PDF container page. 182 | browser.webRequest.onBeforeRequest.addListener( 183 | onBeforeWebRequestAsync, 184 | { urls: redirectPatterns }, 185 | ["blocking"] 186 | ); 187 | // Capture bookmarking event of custom PDF page. 188 | browser.bookmarks.onCreated.addListener(onCreateBookmarkAsync); 189 | -------------------------------------------------------------------------------- /icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 27 | 30 | 34 | 35 | 36 | 55 | 57 | 58 | 60 | image/svg+xml 61 | 63 | 64 | 65 | 66 | 67 | 73 | 80 | 81 | 119 | 125 | 133 | 141 | 142 | 148 | 154 | 155 | 161 | 167 | 173 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /firefox/content.js: -------------------------------------------------------------------------------- 1 | // This content script modifies the title of the abstract page once it has finished loading. 2 | // The PDF page title is not modified here due to the limitations of Firefox. 3 | // Ref: https://bugzilla.mozilla.org/show_bug.cgi?id=1457500 4 | 5 | // Regular expressions for parsing arXiv IDs from URLs. 6 | // Ref: https://info.arxiv.org/help/arxiv_identifier_for_services.html#urls-for-standard-arxiv-functions 7 | const ID_REGEXP_REPLACE = [ 8 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/abs\/(\S*?)\/*(\?.*?)?(\#.*?)?$/, "$1", "Abstract"], 9 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/pdf\/(\S*?)(?:\.pdf)?\/*(\?.*?)?(\#.*?)?$/, "$1", "PDF"], 10 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/ftp\/(?:arxiv\/|([^\/]*\/))papers\/.*?([^\/]*?)\.pdf(\?.*?)?(\#.*?)?$/, "$1$2", "PDF"], 11 | [/^.*:\/\/ar5iv\.labs\.arxiv\.org\/html\/(\S*?)\/*(\?.*?)?(\#.*?)?$/, "$1", "HTML5"], 12 | // For external PDF viewer 13 | [/^.*:\/\/mozilla\.github\.io\/pdf\.js\/web\/viewer\.html\?file=https:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/pdf\/(\S*?)(?:\.pdf)?\/*(?:#zoom=.*)?$/, "$1"], 14 | ]; 15 | // All console logs should start with this prefix. 16 | const LOG_PREFIX = "[arXiv-utils]"; 17 | // Element IDs for injected links 18 | const DIRECT_DOWNLOAD_LI_ID = "arxiv-utils-direct-download-li"; 19 | const DIRECT_DOWNLOAD_A_ID = "arxiv-utils-direct-download-a"; 20 | const EXTRA_SERVICES_DIV_ID = "arxiv-utils-extra-services-div"; 21 | 22 | // Return the id parsed from the url. 23 | function getId(url) { 24 | for (const [regexp, replacement, pageType] of ID_REGEXP_REPLACE) { 25 | if (regexp.test(url)) 26 | return url.replace(regexp, replacement); 27 | } 28 | return null; 29 | } 30 | // Return the page type according to the URL. 31 | function getPageType(url) { 32 | for (const [regexp, replacement, pageType] of ID_REGEXP_REPLACE) { 33 | if (regexp.test(url)) 34 | return pageType; 35 | } 36 | return null; 37 | } 38 | // Get article information through arXiv API asynchronously. 39 | // Ref: https://info.arxiv.org/help/api/user-manual.html#31-calling-the-api 40 | async function getArticleInfoAsync(id, pageType) { 41 | console.log(LOG_PREFIX, "Retrieving title through ArXiv API request..."); 42 | const response = await fetch(`https://export.arxiv.org/api/query?id_list=${id}`); 43 | if (!response.ok) { 44 | console.error(LOG_PREFIX, "Error: ArXiv API request failed."); 45 | return; 46 | } 47 | const xmlDoc = await response.text(); 48 | const parsedXML = new DOMParser().parseFromString(xmlDoc, 'text/xml'); 49 | const entry = parsedXML.getElementsByTagName("entry")[0]; 50 | // title[0] is query string, title[1] is paper name. 51 | const title = entry.getElementsByTagName("title")[0].textContent; 52 | // Long titles will be split into multiple lines, with all lines except the first one starting with two spaces. 53 | const escapedTitle = title.replace("\n", "").replace(" ", " "); 54 | // TODO: May need to escape special characters in title? 55 | const newTitle = `${escapedTitle} | ${pageType}`; 56 | const firstAuthor = entry.getElementsByTagName("name")[0].textContent; 57 | const firstAuthorFamilyName = firstAuthor.split(' ').pop(); 58 | const firstAuthorFamilyNameLowerCase = firstAuthorFamilyName.toLowerCase(); 59 | const authors = [...entry.getElementsByTagName("name")].map((el) => el.textContent).join(", "); 60 | const publishedDateSplit = entry.getElementsByTagName("published")[0].textContent.split('-'); 61 | const updatedDateSplit = entry.getElementsByTagName("updated")[0].textContent.split('-'); 62 | const publishedYear = publishedDateSplit[0]; 63 | const updatedYear = updatedDateSplit[0]; 64 | const publishedYear2Digits = publishedYear.slice(-2); 65 | const updatedYear2Digits = updatedYear.slice(-2); 66 | const publishedMonth = publishedDateSplit[1]; 67 | const updatedMonth = updatedDateSplit[1]; 68 | const publishedDay = publishedDateSplit[2].split('T')[0]; 69 | const updatedDay = updatedDateSplit[2].split('T')[0]; 70 | const versionRegexp = /^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/abs\/.*v([0-9]*)$/; 71 | var version = ''; 72 | for (const el of entry.getElementsByTagName("link")) { 73 | const match = el.getAttribute("href").match(versionRegexp); 74 | if (match && match[1]) 75 | version = match[1]; 76 | } 77 | return { 78 | escapedTitle, 79 | newTitle, 80 | firstAuthor, 81 | firstAuthorFamilyName, 82 | firstAuthorFamilyNameLowerCase, 83 | authors, 84 | publishedYear, 85 | updatedYear, 86 | publishedYear2Digits, 87 | updatedYear2Digits, 88 | publishedMonth, 89 | updatedMonth, 90 | publishedDay, 91 | updatedDay, 92 | version, 93 | } 94 | } 95 | // Add custom links in abstract page. 96 | function addCustomLinksAsync(id) { 97 | document.getElementById(DIRECT_DOWNLOAD_LI_ID)?.remove(); 98 | const directDownloadHTML = ` \ 99 |
  • \ 100 | Direct Download \ 101 |
  • `; 102 | const downloadUL = document.querySelector(".full-text > ul"); 103 | if (!downloadUL) { 104 | console.error(LOG_PREFIX, "Error: Cannot find the unordered list inside the Download section at the right side of the abstract page."); 105 | return; 106 | } 107 | downloadUL.innerHTML += directDownloadHTML; 108 | console.log(LOG_PREFIX, "Added direct download link.") 109 | // Add extra services links. 110 | const elExtraRefCite = document.querySelector(".extra-ref-cite"); 111 | if (!elExtraRefCite) { 112 | console.error(LOG_PREFIX, "Error: Cannot find the References & Citations section at the right side of the abstract page."); 113 | return; 114 | } 115 | document.getElementById(EXTRA_SERVICES_DIV_ID)?.remove(); 116 | const extraServicesDiv = document.createElement("div"); 117 | extraServicesDiv.classList.add('extra-ref-cite'); 118 | extraServicesDiv.id = EXTRA_SERVICES_DIV_ID; 119 | extraServicesDiv.innerHTML = ` \ 120 |

    Extra Services

    \ 121 | `; 126 | elExtraRefCite.after(extraServicesDiv); 127 | console.log(LOG_PREFIX, "Added extra services links.") 128 | } 129 | 130 | async function enableDirectDownload(id, articleInfo) { 131 | // Add direct download link. 132 | const result = await browser.storage.sync.get({ 133 | 'filename_format': '${title}, ${firstAuthor} et al., ${publishedYear}, v${version}.pdf' 134 | }); 135 | const fileName = result.filename_format 136 | .replace('${title}', articleInfo.escapedTitle) 137 | .replace('${firstAuthor}', articleInfo.firstAuthor) 138 | .replace('${firstAuthorFamilyName}', articleInfo.firstAuthorFamilyName) 139 | .replace('${firstAuthorFamilyNameLowerCase}', articleInfo.firstAuthorFamilyNameLowerCase) 140 | .replace('${authors}', articleInfo.authors) 141 | .replace('${publishedYear}', articleInfo.publishedYear) 142 | .replace('${updatedYear}', articleInfo.updatedYear) 143 | .replace('${publishedYear2Digits}', articleInfo.publishedYear2Digits) 144 | .replace('${updatedYear2Digits}', articleInfo.updatedYear2Digits) 145 | .replace('${publishedMonth}', articleInfo.publishedMonth) 146 | .replace('${updatedMonth}', articleInfo.updatedMonth) 147 | .replace('${publishedDay}', articleInfo.publishedDay) 148 | .replace('${updatedDay}', articleInfo.updatedDay) 149 | .replace('${version}', articleInfo.version) 150 | .replace('${paperid}', id) 151 | // Replace invalid characters. 152 | // Ref: https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words 153 | // Ref: https://stackoverflow.com/a/42210346 154 | .replace(/[/:]/g, ',') 155 | .replace(/[/\\?*|"<>]/g, '_') 156 | .replace(/\n/g, '') // Replace newline, which exists in some titles that are too long. 157 | ; 158 | const directURL = `https://arxiv.org/pdf/${id}.pdf`; 159 | const downloadA = document.getElementById(DIRECT_DOWNLOAD_A_ID) 160 | downloadA.addEventListener('click', function (e) { 161 | browser.runtime.sendMessage({ 162 | url: directURL, 163 | filename: fileName, 164 | }); 165 | e.preventDefault(); 166 | console.log(LOG_PREFIX, `Sending download message to download: ${fileName} from ${directURL}.`) 167 | }); 168 | downloadA.href = "#"; 169 | console.log(LOG_PREFIX, "Enabled direct download.") 170 | } 171 | 172 | async function mainAsync() { 173 | console.log(LOG_PREFIX, "Extension initialized."); 174 | const url = location.href; 175 | const pageType = getPageType(url); 176 | const id = getId(url); 177 | if (!id) { 178 | console.error(LOG_PREFIX, "Error: Failed to get paper ID, aborted."); 179 | return; 180 | } 181 | if (pageType === "Abstract") 182 | addCustomLinksAsync(id); 183 | const articleInfo = await getArticleInfoAsync(id, pageType); 184 | document.title = articleInfo.newTitle; 185 | console.log(LOG_PREFIX, `Set document title to: ${articleInfo.newTitle}.`); 186 | if (pageType === "Abstract") 187 | await enableDirectDownload(id, articleInfo); 188 | } 189 | 190 | // Execute main logic. 191 | mainAsync(); 192 | -------------------------------------------------------------------------------- /chrome/content.js: -------------------------------------------------------------------------------- 1 | // This content script modifies the title of the abstract / PDF page once it has finished loading. 2 | 3 | // We intentionally use global `var` instead of `const` to prevent 'Identifier already declared' errors when Chrome injects the content script multiple times. 4 | 5 | // Regular expressions for parsing arXiv IDs from URLs. 6 | // Ref: https://info.arxiv.org/help/arxiv_identifier_for_services.html#urls-for-standard-arxiv-functions 7 | var ID_REGEXP_REPLACE = [ 8 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/abs\/(\S*?)\/*(\?.*?)?(\#.*?)?$/, "$1", "Abstract"], 9 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/pdf\/(\S*?)(?:\.pdf)?\/*(\?.*?)?(\#.*?)?$/, "$1", "PDF"], 10 | [/^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/ftp\/(?:arxiv\/|([^\/]*\/))papers\/.*?([^\/]*?)\.pdf(\?.*?)?(\#.*?)?$/, "$1$2", "PDF"], 11 | [/^.*:\/\/ar5iv\.labs\.arxiv\.org\/html\/(\S*?)\/*(\?.*?)?(\#.*?)?$/, "$1", "HTML5"], 12 | ]; 13 | // Store new title for onMessage to deal with Chrome PDF viewer bug. 14 | var newTitle = undefined; 15 | // Define onMessage countdown for Chrome PDF viewer bug. 16 | var messageCallbackCountdown = 3; 17 | // All console logs should start with this prefix. 18 | var LOG_PREFIX = "[arXiv-utils]"; 19 | // Element IDs for injected links 20 | var DIRECT_DOWNLOAD_LI_ID = "arxiv-utils-direct-download-li"; 21 | var DIRECT_DOWNLOAD_A_ID = "arxiv-utils-direct-download-a"; 22 | var EXTRA_SERVICES_DIV_ID = "arxiv-utils-extra-services-div"; 23 | 24 | // Return the id parsed from the url. 25 | function getId(url) { 26 | for (const [regexp, replacement, pageType] of ID_REGEXP_REPLACE) { 27 | if (regexp.test(url)) 28 | return url.replace(regexp, replacement); 29 | } 30 | return null; 31 | } 32 | // Return the page type according to the URL. 33 | function getPageType(url) { 34 | for (const [regexp, replacement, pageType] of ID_REGEXP_REPLACE) { 35 | if (regexp.test(url)) 36 | return pageType; 37 | } 38 | return null; 39 | } 40 | // Get article information through arXiv API asynchronously. 41 | // Ref: https://info.arxiv.org/help/api/user-manual.html#31-calling-the-api 42 | async function getArticleInfoAsync(id, pageType) { 43 | console.log(LOG_PREFIX, "Retrieving title through ArXiv API request (via background)..."); 44 | // Request article info from background script to avoid Chrome's stricter CORS restrictions 45 | const result = await chrome.runtime.sendMessage({ 46 | type: 'fetchArticleInfo', 47 | id: id 48 | }); 49 | if (!result.success) { 50 | console.error(LOG_PREFIX, "Error: ArXiv API request failed in background.", result.error); 51 | return; 52 | } 53 | const xmlDoc = result.data; 54 | const parsedXML = new DOMParser().parseFromString(xmlDoc, "text/xml"); 55 | const entry = parsedXML.getElementsByTagName("entry")[0]; 56 | // title[0] is query string, title[1] is paper name. 57 | const title = entry.getElementsByTagName("title")[0].textContent; 58 | // Long titles will be split into multiple lines, with all lines except the first one starting with two spaces. 59 | const escapedTitle = title.replace("\n", "").replace(" ", " "); 60 | // TODO: May need to escape special characters in title? 61 | const newTitle = `${escapedTitle} | ${pageType}`; 62 | const firstAuthor = entry.getElementsByTagName("name")[0].textContent; 63 | const firstAuthorFamilyName = firstAuthor.split(" ").pop(); 64 | const firstAuthorFamilyNameLowerCase = firstAuthorFamilyName.toLowerCase(); 65 | const authors = [...entry.getElementsByTagName("name")].map((el) => el.textContent).join(", "); 66 | const publishedDateSplit = entry.getElementsByTagName("published")[0].textContent.split("-"); 67 | const updatedDateSplit = entry.getElementsByTagName("updated")[0].textContent.split("-"); 68 | const publishedYear = publishedDateSplit[0]; 69 | const updatedYear = updatedDateSplit[0]; 70 | const publishedYear2Digits = publishedYear.slice(-2); 71 | const updatedYear2Digits = updatedYear.slice(-2); 72 | const publishedMonth = publishedDateSplit[1]; 73 | const updatedMonth = updatedDateSplit[1]; 74 | const publishedDay = publishedDateSplit[2].split("T")[0]; 75 | const updatedDay = updatedDateSplit[2].split("T")[0]; 76 | const versionRegexp = /^.*:\/\/(?:export\.|browse\.|www\.)?arxiv\.org\/abs\/.*v([0-9]*)$/; 77 | var version = ""; 78 | for (const el of entry.getElementsByTagName("link")) { 79 | const match = el.getAttribute("href").match(versionRegexp); 80 | if (match && match[1]) { 81 | version = match[1]; 82 | break; 83 | } 84 | } 85 | 86 | return { 87 | escapedTitle, 88 | newTitle, 89 | firstAuthor, 90 | firstAuthorFamilyName, 91 | firstAuthorFamilyNameLowerCase, 92 | authors, 93 | publishedYear, 94 | updatedYear, 95 | publishedYear2Digits, 96 | updatedYear2Digits, 97 | publishedMonth, 98 | updatedMonth, 99 | publishedDay, 100 | updatedDay, 101 | version, 102 | }; 103 | } 104 | 105 | // Add custom links in abstract page. 106 | function addCustomLinksAsync(id) { 107 | document.getElementById(DIRECT_DOWNLOAD_LI_ID)?.remove(); 108 | const directDownloadHTML = ` \ 109 |
  • \ 110 | Direct Download \ 111 |
  • `; 112 | const downloadUL = document.querySelector(".full-text > ul"); 113 | if (!downloadUL) { 114 | console.error(LOG_PREFIX, "Error: Cannot find the unordered list inside the Download section at the right side of the abstract page."); 115 | return; 116 | } 117 | downloadUL.innerHTML += directDownloadHTML; 118 | console.log(LOG_PREFIX, "Added direct download link.") 119 | // Add extra services links. 120 | const elExtraRefCite = document.querySelector(".extra-ref-cite"); 121 | if (!elExtraRefCite) { 122 | console.error(LOG_PREFIX, "Error: Cannot find the References & Citations section at the right side of the abstract page."); 123 | return; 124 | } 125 | document.getElementById(EXTRA_SERVICES_DIV_ID)?.remove(); 126 | const extraServicesDiv = document.createElement("div"); 127 | extraServicesDiv.classList.add('extra-ref-cite'); 128 | extraServicesDiv.id = EXTRA_SERVICES_DIV_ID; 129 | extraServicesDiv.innerHTML = ` \ 130 |

    Extra Services

    \ 131 | `; 136 | elExtraRefCite.after(extraServicesDiv); 137 | console.log(LOG_PREFIX, "Added extra services links.") 138 | } 139 | 140 | async function enableDirectDownload(id, articleInfo) { 141 | // Add direct download link. 142 | const result = await chrome.storage.sync.get({ 143 | 'filename_format': '${title}, ${firstAuthor} et al., ${publishedYear}, v${version}.pdf' 144 | }); 145 | const fileName = result.filename_format 146 | .replace('${title}', articleInfo.escapedTitle) 147 | .replace('${firstAuthor}', articleInfo.firstAuthor) 148 | .replace('${firstAuthorFamilyName}', articleInfo.firstAuthorFamilyName) 149 | .replace('${firstAuthorFamilyNameLowerCase}', articleInfo.firstAuthorFamilyNameLowerCase) 150 | .replace('${authors}', articleInfo.authors) 151 | .replace('${publishedYear}', articleInfo.publishedYear) 152 | .replace('${updatedYear}', articleInfo.updatedYear) 153 | .replace('${publishedYear2Digits}', articleInfo.publishedYear2Digits) 154 | .replace('${updatedYear2Digits}', articleInfo.updatedYear2Digits) 155 | .replace('${publishedMonth}', articleInfo.publishedMonth) 156 | .replace('${updatedMonth}', articleInfo.updatedMonth) 157 | .replace('${publishedDay}', articleInfo.publishedDay) 158 | .replace('${updatedDay}', articleInfo.updatedDay) 159 | .replace('${version}', articleInfo.version) 160 | .replace('${paperid}', id) 161 | // Replace invalid characters. 162 | // Ref: https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words 163 | // Ref: https://stackoverflow.com/a/42210346 164 | .replace(/[/:]/g, ',') 165 | .replace(/[/\\?*|"<>]/g, '_') 166 | .replace(/\n/g, '') // Replace newline, which exists in some titles that are too long. 167 | ; 168 | const directURL = `https://arxiv.org/pdf/${id}.pdf`; 169 | const downloadA = document.getElementById(DIRECT_DOWNLOAD_A_ID) 170 | downloadA.addEventListener('click', function (e) { 171 | chrome.runtime.sendMessage({ 172 | type: 'downloadFile', 173 | url: directURL, 174 | filename: fileName, 175 | }); 176 | e.preventDefault(); 177 | console.log(LOG_PREFIX, `Sending download message to download: ${fileName} from ${directURL}.`) 178 | }); 179 | downloadA.href = "#"; 180 | console.log(LOG_PREFIX, "Enabled direct download.") 181 | } 182 | 183 | // The PDF viewer in Chrome has a bug that will overwrite the title of the page after loading the PDF. 184 | // Change the PDF page title again if the loading process is long enough for this bug to occur. 185 | // Ref: https://stackoverflow.com/a/69408967 186 | async function onMessageAsync(tab, sender, sendResponse) { 187 | console.log(LOG_PREFIX, `Tab title changed to: ${tab.title}`); 188 | if (!newTitle || tab.title === newTitle) 189 | return; 190 | console.log(LOG_PREFIX, "Tab title has been changed by others!"); 191 | console.log(LOG_PREFIX, `Trying to change title to: ${newTitle} after 1 second`); 192 | await new Promise(r => setTimeout(r, 1000)); 193 | if (messageCallbackCountdown <= 0) { 194 | console.log(LOG_PREFIX, "Title insertion stopped. Assuming the title is correct now."); 195 | return; 196 | } 197 | messageCallbackCountdown--; 198 | // Setting `document.title` does not work when this bug occur. 199 | const elTitle = document.querySelector("title"); 200 | if (elTitle) { 201 | elTitle.innerText = newTitle; 202 | console.log(LOG_PREFIX, "Modify tag directly."); 203 | return; 204 | } 205 | console.error(LOG_PREFIX, "Error: Cannot insert title"); 206 | } 207 | 208 | async function mainAsync() { 209 | console.log(LOG_PREFIX, "Extension initialized."); 210 | const url = location.href; 211 | const pageType = getPageType(url); 212 | const id = getId(url); 213 | if (!id) { 214 | console.error(LOG_PREFIX, "Error: Failed to get paper ID, aborted."); 215 | return; 216 | } 217 | if (pageType === "Abstract") 218 | addCustomLinksAsync(id); 219 | const articleInfo = await getArticleInfoAsync(id, pageType); 220 | document.title = articleInfo.newTitle; 221 | console.log(LOG_PREFIX, `Set document title to: ${articleInfo.newTitle}.`); 222 | if (pageType === "Abstract") 223 | await enableDirectDownload(id, articleInfo); 224 | // Store new title for onMessage. 225 | newTitle = articleInfo.newTitle 226 | } 227 | 228 | // Execute main logic. 229 | mainAsync(); 230 | // Listen for title-change messages from the background script. 231 | chrome.runtime.onMessage.addListener(onMessageAsync); 232 | -------------------------------------------------------------------------------- /tests/end-to-end-test/test_navigation.py: -------------------------------------------------------------------------------- 1 | import time 2 | from operator import itemgetter 3 | from collections import defaultdict 4 | 5 | import yaml 6 | from selenium import webdriver 7 | from selenium.common.exceptions import TimeoutException 8 | from selenium.webdriver.common.action_chains import ActionChains 9 | from selenium.webdriver.common.by import By 10 | from selenium.webdriver.common.keys import Keys 11 | from selenium.webdriver.support import expected_conditions as EC 12 | from selenium.webdriver.support.ui import WebDriverWait 13 | 14 | testcases_path = "/app/tests/testcases/testcases.yaml" 15 | with open(testcases_path, "r") as f: 16 | testcases = yaml.safe_load(f) 17 | 18 | command_executor = 'http://selenium-hub:4444/wd/hub' 19 | 20 | n_success = 0 21 | n_skipped = 0 22 | 23 | for browser in ['chrome', 'firefox', 'edge']: 24 | print(f"Testing with browser: {browser}") 25 | if browser == 'chrome': 26 | options = webdriver.ChromeOptions() 27 | options.add_argument('load-extension=/app/chrome') 28 | options.add_argument("--window-size=1024,768") 29 | meta_options = webdriver.EdgeOptions() 30 | meta_options.add_argument("--window-size=1024,768") 31 | novnc_url = "http://chrome-node:7900" 32 | extensions_button_pos = (915, 65) # chrome/01-default.jpeg 33 | pin_button_pos = (865, 220) # chrome/02-extensions.jpeg 34 | arxiv_utils_button_pos = (880, 65) # chrome/03-pinned.jpeg 35 | elif browser == 'firefox': 36 | options = webdriver.FirefoxOptions() 37 | # Ref: https://stackoverflow.com/a/55878622 38 | # The `window-size` argument below doesn't seem to work for firefox 39 | # 40 | # options.add_argument("--window-size=1024,768") 41 | # 42 | options.add_argument("--width=800") 43 | options.add_argument("--height=600") 44 | meta_options = webdriver.EdgeOptions() 45 | meta_options.add_argument("--window-size=1024,768") 46 | novnc_url = "http://firefox-node:7900" 47 | extensions_button_pos = (745, 85) # firefox/01-default.jpeg 48 | extensions_settings_button_pos = (730, 185) # firefox/02-extensions.jpeg 49 | pin_button_pos = (590, 240) # firefox/03-extensions-settings.jpeg 50 | arxiv_utils_button_pos = (705, 85) # firefox/04-pinned.jpeg 51 | # Below are for add-ons using Manifest v3 52 | """ 53 | manage_button_pos = (520, 265) # firefox/03-extensions-settings.jpeg 54 | permissions_button_pos = (395, 340) # firefox/05-details.jpeg 55 | toggle_export_arxiv_button_pos = (750, 515) # firefox/06-permissions.jpeg 56 | toggle_arxiv_button_pos = (750, 540) # firefox/06-permissions.jpeg 57 | """ 58 | elif browser == 'edge': 59 | options = webdriver.EdgeOptions() 60 | options.add_argument('load-extension=/app/chrome') 61 | options.add_argument("--window-size=1024,768") 62 | meta_options = webdriver.ChromeOptions() 63 | meta_options.add_argument("--window-size=1024,768") 64 | novnc_url = "http://edge-node:7900" 65 | extensions_button_pos = (770, 65) # edge/01-default.jpeg 66 | pin_button_pos = (740, 140) # edge/02-extensions.jpeg 67 | arxiv_utils_button_pos = (725, 65) # edge/03-pinned.jpeg 68 | else: 69 | raise ValueError(f"Invalid browser: {browser}") 70 | 71 | print(f"Launching webdriver...") 72 | driver = webdriver.Remote( 73 | command_executor=command_executor, 74 | options=options 75 | ) 76 | driver.set_page_load_timeout(60) 77 | wait = WebDriverWait(driver, 60) 78 | 79 | # The webdriver includes a default tab 80 | wait.until(EC.number_of_windows_to_be(1)) 81 | windows_stack = [driver.current_window_handle] 82 | 83 | print(f"(Meta) Launching webdriver...") 84 | meta_driver = webdriver.Remote( 85 | command_executor=command_executor, 86 | options=meta_options 87 | ) 88 | meta_wait = WebDriverWait(meta_driver, 60) 89 | meta_viewport_size = ( 90 | meta_driver.execute_script("return window.innerWidth"), 91 | meta_driver.execute_script("return window.innerHeight") 92 | ) 93 | viewport_offset_x = -meta_viewport_size[0] // 2 94 | viewport_offset_y = -meta_viewport_size[1] // 2 95 | print(f"(Meta) Viewport size is {meta_viewport_size}") 96 | 97 | print("(Meta) Visiting noVNC") 98 | meta_driver.get(novnc_url) 99 | 100 | print("Waiting 1 second after visiting noVNC") 101 | time.sleep(1) 102 | 103 | print("(Meta) Clicking the Connect button") 104 | xpath = '//*[@id="noVNC_connect_button"]' 105 | element = meta_wait.until(EC.element_to_be_clickable((By.XPATH, xpath))) 106 | element.click() 107 | 108 | print("(Meta) Entering Password") 109 | xpath = '//*[@id="noVNC_password_input"]' 110 | password = 'secret' 111 | element = meta_wait.until(EC.element_to_be_clickable((By.XPATH, xpath))) 112 | element.click() 113 | element.send_keys(password + Keys.ENTER) 114 | 115 | # The following check doesn't seem reliable 116 | # 117 | # print("(Meta) Waiting until Connected") 118 | # xpath = '//*[@id="noVNC_status"]' 119 | # expected = 'Connected (unencrypted)' 120 | # meta_wait.until(EC.text_to_be_present_in_element_attribute((By.XPATH, xpath), 'innerHTML', expected)) 121 | # 122 | print("Waiting 1 second for VNC connection") 123 | time.sleep(1) 124 | 125 | print("(Meta) Locating Canvas") 126 | xpath = '//*[@id="noVNC_container"]/div/canvas' 127 | element_canvas = meta_wait.until(EC.presence_of_element_located((By.XPATH, xpath))) 128 | 129 | def meta_click_at(pos, wait=True): 130 | x, y = pos 131 | x, y = x + viewport_offset_x, y + viewport_offset_y 132 | ActionChains(meta_driver)\ 133 | .move_to_element_with_offset(element_canvas, x, y)\ 134 | .click()\ 135 | .perform() 136 | if wait: 137 | print("Waiting 1 second after click") 138 | time.sleep(1) 139 | 140 | def meta_setup_arxiv_utils(restore=False): 141 | global addon_id 142 | if browser == 'chrome' or browser == 'edge': 143 | if restore: 144 | print("(Meta) Unpinning arxiv-utils") 145 | else: 146 | print("(Meta) Pinning arxiv-utils") 147 | print("(Meta) Clicking Extensions Button (Open Dropdown)") 148 | meta_click_at(extensions_button_pos) 149 | print("(Meta) Clicking Pin (Unpinned -> Pinned) for arxiv-utils") 150 | meta_click_at(pin_button_pos) 151 | print("(Meta) Clicking Extensions Button (Close Dropdown)") 152 | meta_click_at(extensions_button_pos) 153 | elif browser == 'firefox': 154 | if not restore: 155 | print(f"Installing add-on...") 156 | addon_id = webdriver.Firefox.install_addon(driver, '/app/firefox', temporary=True) 157 | print("Waiting 1 second after installing add-on") 158 | time.sleep(1) 159 | 160 | # Below are for add-ons using Manifest v3 161 | """ 162 | print("(Meta) Setting arxiv-utils Permissions") 163 | print("(Meta) Clicking Extensions Button (Open Dropdown)") 164 | meta_click_at(extensions_button_pos) 165 | print("(Meta) Clicking Extensions Settings Button (Open Dropdown)") 166 | meta_click_at(extensions_settings_button_pos) 167 | print("(Meta) Clicking Manage Extension Button for arxiv-utils") 168 | meta_click_at(manage_button_pos) 169 | print("(Meta) Clicking Permissions Button for arxiv-utils") 170 | meta_click_at(permissions_button_pos) 171 | print("(Meta) Toggling export.arxiv.org for arxiv-utils") 172 | meta_click_at(toggle_export_arxiv_button_pos) 173 | print("(Meta) Toggling arxiv.org Button for arxiv-utils") 174 | meta_click_at(toggle_arxiv_button_pos) 175 | """ 176 | 177 | print("(Meta) Pinning arxiv-utils") 178 | print("(Meta) Clicking Extensions Button (Open Dropdown)") 179 | meta_click_at(extensions_button_pos) 180 | print("(Meta) Clicking Extensions Settings Button (Open Dropdown)") 181 | meta_click_at(extensions_settings_button_pos) 182 | print("(Meta) Clicking Pin (Unpinned -> Pinned) for arxiv-utils") 183 | meta_click_at(pin_button_pos) 184 | else: 185 | print(f"Uninstalling add-on...") 186 | webdriver.Firefox.uninstall_addon(driver, addon_id) 187 | else: 188 | raise ValueError(f"Invalid browser: {browser}") 189 | print("Waiting 1 second after setting up arxiv-utils") 190 | time.sleep(1) 191 | 192 | def meta_click_arxiv_utils(): 193 | print("(Meta) Clicking Open Abstract / PDF") 194 | meta_click_at(arxiv_utils_button_pos, wait=False) 195 | 196 | meta_setup_arxiv_utils() 197 | 198 | global_exception = None 199 | try: 200 | for testcase in testcases['navigation']: 201 | url, title, pdf_url, pdf_title, url2, title2, skip_selenium, description = \ 202 | itemgetter('url', 'title', 'pdf_url', 'pdf_title', 'url2', 'title2', 'skip_selenium', 'description')( 203 | defaultdict(lambda: None, testcase)) 204 | abs2pdf = testcase.get('abs2pdf', True) 205 | pdf2abs = testcase.get('pdf2abs', True) 206 | 207 | if not abs2pdf and not pdf2abs: 208 | raise ValueError("Both `abs2pdf` and `pdf2abs` are False.") 209 | 210 | print(f"Running navigation testcase:") 211 | print(f"- URL: {url}") 212 | print(f"- Title: `{title}`") 213 | print(f"- PDF URL: {pdf_url}") 214 | print(f"- PDF Title: `{pdf_title}`") 215 | print(f"- URL2: {url2}") 216 | print(f"- Title2: `{title2}`") 217 | print(f"- Description: {description}") 218 | print(f"- Tests") 219 | print(f" - Test abs2pdf? {abs2pdf}") 220 | print(f" - Test pdf2abs? {pdf2abs}") 221 | print(f" - skip_selenium? {skip_selenium}") 222 | 223 | if skip_selenium: 224 | print("Testcase Skipped") 225 | n_skipped += 1 226 | continue 227 | 228 | if abs2pdf: 229 | print(f"Opening (abs) webpage...") 230 | driver.switch_to.new_window('tab') 231 | windows_stack.append(driver.current_window_handle) 232 | assert len(windows_stack) == 2 233 | assert len(driver.window_handles) == 2 234 | try: 235 | driver.get(url) 236 | except TimeoutException as e: 237 | print(f"Page load timeout, continuing...") 238 | raise e 239 | if title: 240 | print(f"Checking (abs) title...") 241 | try: 242 | wait.until(EC.title_is(title)) 243 | except TimeoutException as e: 244 | print(f"Title mismatch: `{driver.title}`; URL: `{driver.current_url}`.") 245 | raise e 246 | # Please note that the tests may be flaky due to slow arxiv API response. 247 | assert driver.title == title 248 | meta_click_arxiv_utils() 249 | wait.until(EC.number_of_windows_to_be(3)) 250 | print(f"Closing (abs) webpage...") 251 | driver.close() 252 | windows_stack.pop() 253 | assert len(windows_stack) == 1 254 | assert len(driver.window_handles) == 2 255 | for window_handle in driver.window_handles: 256 | if window_handle not in windows_stack: 257 | driver.switch_to.window(window_handle) 258 | break 259 | windows_stack.append(driver.current_window_handle) 260 | assert len(windows_stack) == 2 261 | assert len(driver.window_handles) == 2 262 | if pdf_url: 263 | # Within arXiv domain 264 | print(f"Checking (pdf) url...") 265 | try: 266 | if browser == 'firefox': 267 | suffix = f"/pdfviewer.html?target={pdf_url}" 268 | wait.until(EC.url_contains(suffix)) 269 | else: 270 | wait.until(EC.url_to_be(pdf_url)) 271 | except TimeoutException as e: 272 | print(f"URL mismatch: `{driver.current_url}`.") 273 | raise e 274 | if browser == 'firefox': 275 | assert driver.current_url.startswith("moz-extension://") 276 | assert driver.current_url.endswith(suffix) 277 | xpath = '//*[@id="container"]/iframe' 278 | element = wait.until(EC.presence_of_element_located((By.XPATH, xpath))) 279 | assert element.get_attribute('src') == pdf_url 280 | else: 281 | assert driver.current_url == pdf_url 282 | elif url2: 283 | # Outside arXiv domain 284 | print(f"Checking (the second) url...") 285 | try: 286 | wait.until(EC.url_to_be(url2)) 287 | except TimeoutException as e: 288 | print(f"URL mismatch: `{driver.current_url}`.") 289 | raise e 290 | assert driver.current_url == url2 291 | else: 292 | print(f"Opening (pdf) webpage...") 293 | driver.switch_to.new_window('tab') 294 | windows_stack.append(driver.current_window_handle) 295 | assert len(windows_stack) == 2 296 | assert len(driver.window_handles) == 2 297 | try: 298 | driver.get(pdf_url) 299 | except TimeoutException as e: 300 | print(f"Page load timeout, continuing...") 301 | raise e 302 | 303 | if pdf_url: 304 | # Within arXiv domain 305 | print(f"Current state is an empty tab and an active pdf tab.") 306 | print(f"Checking (pdf) title...") 307 | try: 308 | wait.until(EC.title_is(pdf_title)) 309 | except TimeoutException as e: 310 | print(f"Title mismatch: `{driver.title}`; URL: `{driver.current_url}`.") 311 | raise e 312 | assert driver.title == pdf_title 313 | elif url2: 314 | if title2: 315 | # Outside arXiv domain 316 | print(f"Current state is an empty tab and an active url2 tab.") 317 | print(f"Checking (url2) title...") 318 | try: 319 | wait.until(EC.title_is(title2)) 320 | except TimeoutException as e: 321 | print(f"Title mismatch: `{driver.title}`; URL: `{driver.current_url}`.") 322 | raise e 323 | assert driver.title == title2 324 | 325 | if pdf_url and pdf2abs: 326 | meta_click_arxiv_utils() 327 | wait.until(EC.number_of_windows_to_be(3)) 328 | print(f"Closing (pdf) webpage...") 329 | driver.close() 330 | windows_stack.pop() 331 | assert len(windows_stack) == 1 332 | assert len(driver.window_handles) == 2 333 | for window_handle in driver.window_handles: 334 | if window_handle not in windows_stack: 335 | driver.switch_to.window(window_handle) 336 | break 337 | windows_stack.append(driver.current_window_handle) 338 | assert len(windows_stack) == 2 339 | assert len(driver.window_handles) == 2 340 | print(f"Checking (abs) url...") 341 | try: 342 | wait.until(EC.url_to_be(url)) 343 | except TimeoutException as e: 344 | print(f"URL mismatch: `{driver.current_url}`.") 345 | raise e 346 | assert driver.current_url == url 347 | print(f"Checking (abs) title...") 348 | try: 349 | wait.until(EC.title_is(title)) 350 | except TimeoutException as e: 351 | print(f"Title mismatch: `{driver.title}`; URL: `{driver.current_url}`.") 352 | raise e 353 | assert driver.title == title 354 | print(f"Closing (abs) webpage...") 355 | driver.close() 356 | windows_stack.pop() 357 | else: 358 | print(f"Closing (pdf) webpage...") 359 | driver.close() 360 | windows_stack.pop() 361 | 362 | assert len(windows_stack) == 1 363 | assert len(driver.window_handles) == 1 364 | driver.switch_to.window(windows_stack[-1]) 365 | 366 | print("Testcase Succeeded") 367 | n_success += 1 368 | except Exception as e: 369 | print(f"Exception: {e}") 370 | global_exception = e 371 | finally: 372 | meta_setup_arxiv_utils(restore=True) 373 | print(f"Closing webdriver...") 374 | driver.quit() 375 | print(f"(Meta) Closing webdriver...") 376 | meta_driver.quit() 377 | if global_exception: 378 | print("The tests have failed, but has terminated gracefully. Re-raising the exception...") 379 | print("") 380 | raise global_exception 381 | print(f"{browser.capitalize()} Tests Succeeded") 382 | 383 | print("All tests passed successfully!") 384 | n = n_success + n_skipped 385 | print(f"Success: {n_success}/{n}; Skipped: {n_skipped}/{n}") 386 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # arxiv-utils 2 | 3 | [![tests](https://img.shields.io/github/actions/workflow/status/j3soon/arxiv-utils/test-with-jest.yaml?label=unit-tests)](https://github.com/j3soon/arxiv-utils/actions/workflows/test-with-jest.yaml) 4 | [![tests](https://img.shields.io/github/actions/workflow/status/j3soon/arxiv-utils/test-with-selenium.yaml?label=end-to-end-tests)](https://github.com/j3soon/arxiv-utils/actions/workflows/test-with-selenium.yaml) 5 | [![build](https://img.shields.io/github/actions/workflow/status/j3soon/arxiv-utils/build-and-publish.yaml)](https://github.com/j3soon/arxiv-utils/actions/workflows/build-and-publish.yaml) 6 | 7 | [![](https://img.shields.io/chrome-web-store/v/mnhdpeipjhhkmlhlcljdjpgmilbmehij.svg)](https://chrome.google.com/webstore/detail/arxiv-utils/mnhdpeipjhhkmlhlcljdjpgmilbmehij) 8 | [![](https://img.shields.io/chrome-web-store/rating/mnhdpeipjhhkmlhlcljdjpgmilbmehij.svg)](https://chrome.google.com/webstore/detail/arxiv-utils/mnhdpeipjhhkmlhlcljdjpgmilbmehij) 9 | [![](https://img.shields.io/chrome-web-store/users/mnhdpeipjhhkmlhlcljdjpgmilbmehij.svg)](https://chrome.google.com/webstore/detail/arxiv-utils/mnhdpeipjhhkmlhlcljdjpgmilbmehij) 10 | 11 | [![](https://img.shields.io/amo/v/arxiv-utils.svg)](https://addons.mozilla.org/en-US/firefox/addon/arxiv-utils/) 12 | [![](https://img.shields.io/amo/rating/arxiv-utils.svg)](https://addons.mozilla.org/en-US/firefox/addon/arxiv-utils/) 13 | [![](https://img.shields.io/amo/users/arxiv-utils.svg)](https://addons.mozilla.org/en-US/firefox/addon/arxiv-utils/) 14 | 15 | [![](https://img.shields.io/badge/dynamic/json?label=edge%20add-on&prefix=v&query=%24.version&url=https%3A%2F%2Fmicrosoftedge.microsoft.com%2Faddons%2Fgetproductdetailsbycrxid%2Fngjpcfjabahdoadnajbhnikbemhmemdg)](https://microsoftedge.microsoft.com/addons/detail/arxivutils/ngjpcfjabahdoadnajbhnikbemhmemdg) 16 | [![](https://img.shields.io/badge/dynamic/json?label=rating&suffix=/5&query=%24.averageRating&url=https%3A%2F%2Fmicrosoftedge.microsoft.com%2Faddons%2Fgetproductdetailsbycrxid%2Fngjpcfjabahdoadnajbhnikbemhmemdg)](https://microsoftedge.microsoft.com/addons/detail/arxivutils/ngjpcfjabahdoadnajbhnikbemhmemdg) 17 | [![](https://img.shields.io/badge/dynamic/json?label=users&query=%24.activeInstallCount&url=https%3A%2F%2Fmicrosoftedge.microsoft.com%2Faddons%2Fgetproductdetailsbycrxid%2Fngjpcfjabahdoadnajbhnikbemhmemdg)](https://microsoftedge.microsoft.com/addons/detail/arxivutils/ngjpcfjabahdoadnajbhnikbemhmemdg) 18 | 19 | ![icon](icons/icon64.png) 20 | 21 | A collection of features that enhance your reading experience on ArXiv (and some other sites): 22 | 23 | - Renames the title of PDF page to the paper's title. 24 | - Adds a button and hotkey (`Alt+A`) to navigate back to Abstract page for arXiv, OpenReview, and more. 25 | - Download PDF with paper's title as filename. 26 | - Open the paper in extra services such as [ar5iv](https://ar5iv.labs.arxiv.org/). 27 | - Works with Native Tab Search, and other plugins! (See the [Solution Descriptions](#solution-descriptions) section for more details) 28 | - All required permissions are documented in detail. 29 | 30 | Please [open an issue](https://github.com/j3soon/arxiv-utils/issues) if you have any questions, feature requests, or bug reports. 31 | 32 | ## Download Links 33 | 34 | Supports Chrome, Firefox, Edge, Firefox on Android. (Not tested on Android) 35 | 36 | - [Chrome Web Store](https://chrome.google.com/webstore/detail/arxiv-utils/mnhdpeipjhhkmlhlcljdjpgmilbmehij) 37 | - [Firefox Add-on](https://addons.mozilla.org/en-US/firefox/addon/arxiv-utils/) 38 | - [Edge Add-on](https://microsoftedge.microsoft.com/addons/detail/arxivutils/ngjpcfjabahdoadnajbhnikbemhmemdg) 39 | 40 | Alternatively, these 3 browsers can also load arxiv-utils directly from source. First, download the source code release from [Releases](https://github.com/j3soon/arxiv-utils/releases), and then load the extension as an unpacked extension following the [Development Section](#development). 41 | 42 | ## Screenshots 43 | 44 | The paper id in the title has been removed automatically! 45 | A direct download link is added to download PDF with paper's title as the filename! 46 | Open in extra services such as ar5iv! 47 | ![](screenshots/01-abstract.png) 48 | Finally... Meaningful paper title instead of paper id! (For Firefox, this is achieved through a custom PDF container.) 49 | ![](screenshots/02-pdf.png) 50 | Difficult to get back to abstract page... 51 | Click to get back to abstract page! 52 | ![](screenshots/03-pdf2.png) 53 | TADA~ The abstract page is shown at the right of the PDF page! Both with meaningful title! 54 | ![](screenshots/04-abstract2.png) 55 | The button is disabled if not in ArXiv's domain. 56 | Meaningful bookmark titles. 57 | ![](screenshots/05-bookmarks.png) 58 | Meaningful OneTab entries! (Chrome & Edge only) 59 | ![](screenshots/06-onetab.png) 60 | Opened too many tabs? Search in terms of the paper title! 61 | ![](screenshots/07-search.png) 62 | Works well with vertical tabs. 63 | ![](screenshots/08-vertical-tabs.png) 64 | Right-click the extension icon and select `Options` to set your preference. (Chrome & Edge) 65 | ![](screenshots/09-filename-format-chrome.png) 66 | Go to add-ons page, click the extension select `Options` to set your preference. (Firefox) 67 | ![](screenshots/10-filename-format-firefox.png) 68 | 69 | ## Solution Descriptions 70 | 71 | For ArXiv PDF / abstract tabs: 72 | 73 | - Renames the title to paper's title automatically in the background. (Originally is meaningless paper id, or start with paper id) 74 | - Add an action button (or `Alt+A`) to open its corresponding abstract / PDF page. (Originally is hard to get back to abstract page from PDF page) 75 | - Add a direct download link on abstract page, click it to download the PDF with the title as filename. (Originally is paper id as filename) 76 | - Open the paper in extra services such as [ar5iv](https://ar5iv.labs.arxiv.org/). 77 | - Better title even for bookmarks and the [OneTab](https://www.one-tab.com/) plugin! 78 | - Firefox has [strict restrictions on PDF.js](https://bugzilla.mozilla.org/show_bug.cgi?id=1454760). So it doesn't work well with OneTab, the PDF renaming is achieved by intercepting requests and show the PDF in a container. The bookmark works well though. 79 | - Works well with native tab search (credits: [@The Rooler](https://addons.mozilla.org/en-US/firefox/addon/arxiv-utils/reviews/1674567/)) 80 | - [Tab search on Firefox](https://support.mozilla.org/en-US/kb/search-open-tabs-firefox) 81 | - [Enable Tab search on Chrome](https://www.howtogeek.com/722640/how-to-enable-or-disable-the-tab-search-icon-in-chrome/), [Tab search on Chrome](https://www.howtogeek.com/704212/how-to-search-open-tabs-on-google-chrome/) 82 | - [Enable Tab search on Edge](https://www.makeuseof.com/microsoft-edge-chrome-tab-search/) 83 | 84 | ## Options 85 | 86 | - `filename format`: 87 | - Default: `${title}, ${firstAuthor} et al., ${publishedYear}, v${version}.pdf` 88 | - `${title}` is replaced with the paper title. 89 | - `${firstAuthor}` is replaced with the first author of the paper. 90 | - `${firstAuthorFamilyName}` is replaced with the family name of the first author. 91 | - `${firstAuthorFamilyNameLowerCase}` is replaced with the family name of the first author in lowercase. 92 | - `${authors}` is replaced with all authors separated by commas. 93 | - `${publishedYear}` is replaced with the published year of the paper. (e.g., "2025") 94 | - `${updatedYear}` is replaced with the updated year of the current paper version. (e.g., "2025") 95 | - `${publishedYear2Digits}` is replaced with the last two digits of the published year. (e.g., "25") 96 | - `${updatedYear2Digits}` is replaced with the last two digits of the updated year. (e.g., "25") 97 | - `${publishedMonth}` is replaced with the published month of the paper (e.g., "12"). 98 | - `${updatedMonth}` is replaced with the updated month of the current paper version (e.g., "12"). 99 | - `${publishedDay}` is replaced with the published day of the paper (e.g., "25"). 100 | - `${updatedDay}` is replaced with the updated day of the current paper version (e.g., "25"). 101 | - `${version}` is replaced with the version of the current paper. 102 | - `${paperid}` is replaced with the arXiv paper id. 103 | - `Open in new tab`: 104 | - Default: `true` 105 | - Set to `false` to open in existing tab when clicking the action button. 106 | - (Firefox) `Enable PDF redirection`: 107 | - Default: `true` 108 | - Set to `false` to disable PDF redirection. This will disallow renaming for PDF tabs. 109 | - (Firefox, Experimental) `external PDF viewer URL prefix`: 110 | - Default: (empty), uses the custom PDF container. 111 | - Set to `https://mozilla.github.io/pdf.js/web/viewer.html?file=`, enables Screenshots and Go Back by using pdf.js as PDF viewer. 112 | - (Firefox, Experimental) `PDF viewer default zoom`: 113 | - Default: `auto` 114 | - Set the default zoom level for PDF viewers (PDF.js). 115 | - Supported values: `auto`, `page-fit`, `page-width`, or percentage values like `50`, `75`, `100`, `125`, `150`, `200`, `300`, `400`, or custom values. 116 | - This setting only applies when `Enable PDF redirection` is set to `true`. 117 | 118 | ## Privacy Policy 119 | 120 | We do not gather your personal data. If in doubt, please refer to the source code. 121 | 122 | ### Chrome / Edge Permissions 123 | 124 | - `tabs`: On extension button click, open a new tab and move it to the right of the old active tab. 125 | - `activeTab`: Read active tab's title and modify it using the tab's url. 126 | - `storage`: Save extension configurations. 127 | - `contextMenus`: When right-click the extension button, show a help menu item. 128 | - `scripting`: Inject content scripts to existing tabs. 129 | - `downloads`: Direct download PDF with paper's title as filename. 130 | - `*://arxiv.org/*`: Inject content scripts to existing tabs. 131 | - `*://export.arxiv.org/*`: Inject content scripts to existing tabs. 132 | - `*://browse.arxiv.org/*`: Inject content scripts to existing tabs. 133 | - `*://www.arxiv.org/*`: Inject content scripts to existing tabs. 134 | - `*://ar5iv.labs.arxiv.org/*`: Inject content scripts to existing tabs. 135 | 136 | ### Firefox Permissions 137 | 138 | - `tabs`: On extension button click, open a new tab and move it to the right of the old active tab. 139 | - `activeTab`: Read active tab's title and modify it using the tab's url. 140 | - `storage`: Save extension configurations. 141 | - `contextMenus`: When right-click the extension button, show a help menu item. 142 | - `webRequest`: Intercept ArXiv PDF request. 143 | - `webRequestBlocking`: Redirect the ArXiv PDF page to custom PDF container page. 144 | - `bookmarks`: When create a new bookmark of the PDF container page, bookmark the actual ArXiv PDF url instead. 145 | - `downloads`: Direct download PDF with paper's title as filename. 146 | - `*://arxiv.org/*pdf*`: Redirect PDF pages to custom PDF container. 147 | - `*://export.arxiv.org/*pdf*`: Redirect PDF pages to custom PDF container. 148 | - `*://browse.arxiv.org/*pdf*`: Redirect PDF pages to custom PDF container. 149 | - `*://www.arxiv.org/*pdf*`: Redirect PDF pages to custom PDF container. 150 | - `"content_security_policy": "script-src 'self'; object-src 'self' https://arxiv.org https://export.arxiv.org https://browse.arxiv.org https://www.arxiv.org;"`: For embedding PDF in container. 151 | - `"web_accessible_resources": [ "pdfviewer.html" ]`: To redirect from HTTPS to extension custom page requires them to be visible. 152 | 153 | ## Developer Notes 154 | 155 | ### Development 156 | 157 | - Chrome: [Debugging extensions](https://developer.chrome.com/docs/extensions/mv3/tut_debugging/) 158 | - Firefox: [Test and debug](https://extensionworkshop.com/documentation/develop/#test-and-debug) 159 | - Edge: [Sideload an extension](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/getting-started/extension-sideloading) 160 | 161 | For viewing the content script logs, open the Inspector of the arXiv webpage (as in normal web development). 162 | 163 | For viewing background script logs, open the Inspector of the plugin in the `Extensions` page. 164 | - Firefox: Go to `about:debugging#/runtime/this-firefox` and click `Inspect` on the temporarily loaded extension. 165 | - Chrome: Go to `chrome://extensions/` and click `Inspect views: background page` on the loaded (unpacked) extension. 166 | - Edge: Go to `edge://extensions/` and click `Inspect views: service worker` on the loaded (unpacked) extension. 167 | 168 | ### Tests 169 | 170 | The automated tests currently include the following: 171 | 172 | - **Default tests**: Test the default title name of arXiv abstract/PDF pages. 173 | - **Navigation tests**: Test the arxiv-utils button can switch between arXiv abstract/PDF pages, and the title is modified. 174 | 175 | The testcases along with their description is stored in [tests/testcases/testcases.yaml](tests/testcases/testcases.yaml). 176 | 177 | Other functions should still be tested manually: 178 | 179 | - **Bookmark tests**: Test the bookmarked URL. 180 | - Try to bookmark an abstract tab, the title should be the new title. 181 | - Try to bookmark a PDF tab, the title should be the new title. 182 | - (Firefox Only) Check the PDF bookmark's URL, it should be the original ArXiv PDF link. 183 | - **Download tests**: Test the downloaded file name. 184 | - Test PDF download (`Download PDF (arxiv-utils)`) in abstract. In firefox, only mouse left-click works, middle-click open up the original PDF page in a new tab. 185 | - Change filename format options, reload page, and download to verify the filename is changed. 186 | - Reset filename format option to default, reload page, and download to verify the filename format is default. 187 | - Test papers with long title. 188 | - Test papers with special characters in title. 189 | - The extension button should be disabled outside ArXiv's domain. 190 | - Clicking the extension button should open a new tab at the right of the current active tab (instead of open at the end of the tab list). 191 | - (Chrome Only) If [OneTab](https://www.one-tab.com/) is installed, click its extension button, the list should show the updated titles of both abstract and PDF page. 192 | - (Chrome Only) Clear the browser cache and reload the PDF page, the title should be the new title after PDF load. 193 | Test with: https://arxiv.org/abs/1512.03385 194 | - Verify there are no console errors in both the content script and background script logs. 195 | - Disable and re-enabling the extension should not cause any errors. 196 | - Installing or re-enabling the extension should immediately update the title of existing tabs. 197 | - The help menu item in the context menu should link to this GitHub page. 198 | - ar5iv tabs should have renamed title, and support navigation. 199 | 200 | ### Run Unit Tests Locally 201 | 202 | Launch the docker containers: 203 | 204 | ```sh 205 | cd tests/unit-test 206 | docker compose up -d 207 | ``` 208 | 209 | Then run the tests: 210 | 211 | ```sh 212 | docker exec -t unit-test-jest-tests-1 \ 213 | /app/tests/unit-test/install-and-run.sh 214 | ``` 215 | 216 | When done, stop the containers: 217 | 218 | ```sh 219 | cd tests/unit-test 220 | docker compose down 221 | ``` 222 | 223 | ### Run End-to-End Tests Locally 224 | 225 | Launch the docker containers: 226 | 227 | ```sh 228 | cd tests/end-to-end-test 229 | docker compose up -d 230 | ``` 231 | 232 | Then run the tests: 233 | 234 | ```sh 235 | docker exec -t end-to-end-test-selenium-tests-1 \ 236 | python "/app/tests/end-to-end-test/test_navigation.py" 237 | ``` 238 | 239 | When adding new test cases, it is often convenient to comment out existing test cases in [tests/testcases/testcases.yaml](tests/testcases/testcases.yaml); When testing specific browsers, you can modify the `for browser in [...]` part in [tests/test_navigation.py](tests/test_navigation.py) to only run tests for the desired browser. 240 | 241 | > If the test logs stuck at launching the webdriver, you may need to restart the containers. 242 | 243 | View the logs or open the following URLs for more details: 244 | - [Selenium Grid](http://localhost:4444/ui) 245 | - [noVNC for Chrome](http://localhost:7900) 246 | - [noVNC for Edge](http://localhost:7901) 247 | - [noVNC for Firefox](http://localhost:7902) 248 | 249 | > The default password for noVNC is `secret`. 250 | 251 | When done, stop the containers: 252 | 253 | ```sh 254 | cd tests/end-to-end-test 255 | docker compose down 256 | ``` 257 | 258 | ### Interactive End-to-End Testing 259 | 260 | Install VSCode and [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) plugin. 261 | 262 | ```sh 263 | tests/scripts/docker-compose.sh up -d 264 | ``` 265 | 266 | Press `Ctrl + P` and select `>Dev Container: Attach to Running Container...`, 267 | then select `/end-to-end-test-selenium-tests-1`. 268 | 269 | In the new VSCode window, click `Open Folders` and select `/app`. 270 | 271 | Install the [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) plugin inside the dev container. 272 | 273 | Launch a Terminal inside the dev container and run: 274 | 275 | ```sh 276 | apk add build-base linux-headers 277 | ``` 278 | 279 | Open `tests/end-to-end-test/test_interactive.py`, select the first cell and press `Shift + Enter` and click `Install` (Install the `ipykernel`). 280 | 281 | You can now begin interactive testing! 282 | 283 | Reference: [Developing inside a Container](https://code.visualstudio.com/docs/devcontainers/containers) 284 | 285 | ### arXiv API 286 | 287 | ```sh 288 | curl "https://export.arxiv.org/api/query?id_list=<ARXIV_ID>" 289 | ``` 290 | 291 | Reference: [arXiv API User's Manual](https://info.arxiv.org/help/api/user-manual.html#332-entry-metadata) 292 | 293 | ### Build and Publish 294 | 295 | Store dashboards: 296 | 297 | - Chrome: [Chrome Web Store Developer Dashboard](https://chrome.google.com/webstore/devconsole) 298 | - Firefox: [Add-on Developer Hub](https://addons.mozilla.org/en-US/developers/addons) 299 | - Edge: [Microsoft Partner Center](https://partner.microsoft.com/en-us/dashboard/microsoftedge) 300 | 301 | May need to update description, permission details, and screenshots for Firefox and Chrome store. 302 | 303 | Download the signed `.crx` or `.xpi` files: 304 | 305 | - Chrome: [How to download a CRX file from the Chrome web store for a given ID?](https://stackoverflow.com/a/14099762) 306 | - [Download CRX](https://clients2.google.com/service/update2/crx?response=redirect&prodversion=140.0.7339.128&acceptformat=crx2,crx3&x=id%3Dmnhdpeipjhhkmlhlcljdjpgmilbmehij%26uc) 307 | - Firefox: [How to download Firefox extensions from addons.mozilla.org without installing them?](https://superuser.com/a/441011) 308 | - [Download XPI](https://addons.mozilla.org/en-US/firefox/addon/arxiv-utils/) by right-clicking `Add to Firefox` button and select `Save Link As...`. 309 | - Edge: [How to get CRX file of a published in Microsoft Edge Web Store (Chromium Based)](https://stackoverflow.com/a/76016563) 310 | - [Download CRX](https://edge.microsoft.com/extensionwebstorebase/v1/crx?response=redirect&x=id%3Dngjpcfjabahdoadnajbhnikbemhmemdg%26installsource%3Dondemand%26uc) 311 | 312 | ## Frequently Asked Questions (FAQ) 313 | 314 | - Q: Why redirect PDFs to a custom viewer in Firefox? 315 | 316 | A: This is due to a bug in Firefox that disallows executing content scripts in the built-in pdf.js viewer, which disallows renaming for PDF tabs. See [Firefox Bug 1454760](https://bugzilla.mozilla.org/show_bug.cgi?id=1454760) for more details. 317 | 318 | - Q: Why do the custom PDF viewer in Firefox lacks many features? 319 | 320 | A: Since these features cannot be enabled easily. See [#4](https://github.com/j3soon/arxiv-utils/issues/4) and [#13](https://github.com/j3soon/arxiv-utils/pull/13) for further details. 321 | 322 | - Q: Selenium (or WebDriver) has no API to click addon/extension buttons, how do the automated tests click the arxiv-utils button? 323 | 324 | A: This can be achieved by any tool that can simulate mouse click. Since we use Selenium Grid, for simplicity, we apply a hacky workaround that use one meta browser to click the arxiv-utils button in another browser through VNC web viewer. I'm not sure if [other testing tools](https://learn.microsoft.com/en-us/microsoft-edge/test-and-automation/test-and-automation) can achieve this more easily. 325 | 326 | If you have further questions, please [open an issue](https://github.com/j3soon/arxiv-utils/issues). 327 | 328 | ## Related Extensions 329 | 330 | - [musically-ut/arXiv-title-fixer](https://github.com/musically-ut/arXiv-title-fixer) 331 | This requires a button click to change the pdf title, but will be considered less intrusive than running in the background. (Chrome Only) 332 | - [weakish/arxiv-url](https://github.com/weakish/arxiv-url) 333 | This claims to add a back button, but I can't get it working. 334 | - [imurray/redirectify](https://github.com/imurray/redirectify) 335 | Automatically redirect PDF links to HTML index page for many academic paper sites. (Compatible with arxiv-utils) 336 | - [vict0rsch/PaperMemory](https://github.com/vict0rsch/PaperMemory) 337 | If you're looking for an extension with a wider range of useful features, this extension offers just that. Although its UI modifications are slightly more intrusive than arxiv-utils, it is totally acceptable considering that it has so many extra features. It's also worth noting that arxiv-utils does not automatically invoke APIs from external services, which is a difference in design philosophy. 338 | - [AI/ML Papers with Code Everywhere - CatalyzeX](https://www.catalyzex.com/) 339 | [[chrome]](https://chrome.google.com/webstore/detail/aiml-papers-with-code-eve/aikkeehnlfpamidigaffhfmgbkdeheil?hl=en) 340 | [[firefox]](https://addons.mozilla.org/en-US/firefox/addon/code-finder-catalyzex/) 341 | Find code links and inject them to a variety of websites. (Compatible with arxiv-utils) 342 | -------------------------------------------------------------------------------- /tests/testcases/testcases.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | # URLs ending with `.pdf` will be skipped since it cannot be tested by Selenium. 3 | - url: https://arxiv.org/abs/2203.14206 4 | title: "[2203.14206] Denoising Likelihood Score Matching for Conditional Score-based Data Generation" 5 | description: basic testcase 6 | - url: https://arxiv.org/pdf/2203.14206 7 | title: 2203.14206.pdf 8 | description: basic testcase 9 | - url: https://arxiv.org/abs/1906.07413 10 | title: "[1906.07413] Learning Imbalanced Datasets with Label-Distribution-Aware Margin Loss" 11 | description: latex-defined pdf name 1 (for chrome) 12 | - url: https://arxiv.org/pdf/1906.07413 13 | title: Sharelatex Example 14 | description: latex-defined pdf name 1 (for chrome) 15 | - url: https://arxiv.org/abs/2003.01367 16 | title: "[2003.01367] Curriculum By Smoothing" 17 | description: latex-defined pdf name 2 (for chrome) 18 | - url: https://arxiv.org/pdf/2003.01367 19 | title: 2003.01367.pdf 20 | description: latex-defined pdf name 2 (for chrome) 21 | navigation: 22 | - url: https://arxiv.org/abs/2203.14206 23 | title: Denoising Likelihood Score Matching for Conditional Score-based Data Generation | Abstract 24 | pdf_url: https://arxiv.org/pdf/2203.14206 25 | pdf_title: Denoising Likelihood Score Matching for Conditional Score-based Data Generation | PDF 26 | description: basic testcase 27 | - url: https://arxiv.org/abs/1110.2832 28 | title: Can apparent superluminal neutrino speeds be explained as a quantum weak measurement? | Abstract 29 | pdf_url: https://arxiv.org/ftp/arxiv/papers/1110/1110.2832.pdf 30 | pdf_title: Can apparent superluminal neutrino speeds be explained as a quantum weak measurement? | PDF 31 | abs2pdf: False 32 | description: special ftp format 33 | - url: https://arxiv.org/abs/1110.2832 34 | title: Can apparent superluminal neutrino speeds be explained as a quantum weak measurement? | Abstract 35 | pdf_url: https://arxiv.org/pdf/1110.2832 36 | pdf_title: Can apparent superluminal neutrino speeds be explained as a quantum weak measurement? | PDF 37 | description: special ftp format 38 | - url: https://arxiv.org/abs/2003.13678 39 | title: Designing Network Design Spaces | Abstract 40 | pdf_url: https://export.arxiv.org/pdf/2003.13678 41 | pdf_title: Designing Network Design Spaces | PDF 42 | abs2pdf: False 43 | description: pdf with export.arxiv.org subdomain 44 | - url: https://export.arxiv.org/abs/2003.13678 45 | title: Designing Network Design Spaces | Abstract 46 | pdf_url: https://arxiv.org/pdf/2003.13678 47 | pdf_title: Designing Network Design Spaces | PDF 48 | pdf2abs: False 49 | description: abs with export.arxiv.org subdomain 50 | # browse.arxiv.org currently has `SSL_ERROR_BAD_CERT_DOMAIN` error, disabling for now 51 | # TODO: Add this back when the issue is fixed 52 | # - url: https://arxiv.org/abs/2003.13678 53 | # title: Designing Network Design Spaces | Abstract 54 | # pdf_url: https://browse.arxiv.org/pdf/2003.13678 55 | # pdf_title: Designing Network Design Spaces | PDF 56 | # abs2pdf: False 57 | # description: pdf with browse.arxiv.org subdomain 58 | # - url: https://browse.arxiv.org/abs/2003.13678 59 | # title: Designing Network Design Spaces | Abstract 60 | # pdf_url: https://arxiv.org/pdf/2003.13678 61 | # pdf_title: Designing Network Design Spaces | PDF 62 | # pdf2abs: False 63 | # description: abs with browse.arxiv.org subdomain 64 | - url: https://arxiv.org/abs/2003.13678 65 | title: Designing Network Design Spaces | Abstract 66 | pdf_url: https://www.arxiv.org/pdf/2003.13678 67 | pdf_title: Designing Network Design Spaces | PDF 68 | abs2pdf: False 69 | description: pdf with www.arxiv.org subdomain 70 | - url: https://www.arxiv.org/abs/2003.13678 71 | title: Designing Network Design Spaces | Abstract 72 | pdf_url: https://arxiv.org/pdf/2003.13678 73 | pdf_title: Designing Network Design Spaces | PDF 74 | pdf2abs: False 75 | description: abs with www.arxiv.org subdomain 76 | - url: https://arxiv.org/abs/2003.13678 77 | title: Designing Network Design Spaces | Abstract 78 | pdf_url: https://arxiv.org/pdf/2003.13678.pdf 79 | pdf_title: Designing Network Design Spaces | PDF 80 | abs2pdf: False 81 | description: pdf ending with `.pdf` 82 | - url: https://arxiv.org/abs/2003.13678 83 | title: Designing Network Design Spaces | Abstract 84 | pdf_url: https://arxiv.org/pdf/2003.13678/ 85 | pdf_title: Designing Network Design Spaces | PDF 86 | abs2pdf: False 87 | description: pdf with trailing slash 88 | - url: https://arxiv.org/abs/2003.13678/ 89 | title: Designing Network Design Spaces | Abstract 90 | pdf_url: https://arxiv.org/pdf/2003.13678 91 | pdf_title: Designing Network Design Spaces | PDF 92 | pdf2abs: False 93 | description: abs with trailing slash 94 | - url: https://arxiv.org/abs/2003.13678 95 | title: Designing Network Design Spaces | Abstract 96 | pdf_url: https://arxiv.org/pdf/2003.13678// 97 | pdf_title: Designing Network Design Spaces | PDF 98 | abs2pdf: False 99 | description: pdf with trailing double slash 100 | - url: https://arxiv.org/abs/2003.13678// 101 | title: Designing Network Design Spaces | Abstract 102 | pdf_url: https://arxiv.org/pdf/2003.13678 103 | pdf_title: Designing Network Design Spaces | PDF 104 | pdf2abs: False 105 | description: abs with trailing double slash 106 | - url: https://arxiv.org/abs/cs/9605103 107 | title: "Reinforcement Learning: A Survey | Abstract" 108 | pdf_url: https://arxiv.org/pdf/cs/9605103 109 | pdf_title: "Reinforcement Learning: A Survey | PDF" 110 | description: old url basic testcase 111 | - url: https://arxiv.org/abs/cs/9605103 112 | title: "Reinforcement Learning: A Survey | Abstract" 113 | pdf_url: https://arxiv.org/pdf/cs/9605103.pdf 114 | pdf_title: "Reinforcement Learning: A Survey | PDF" 115 | abs2pdf: False 116 | description: old url pdf ending with `.pdf` 117 | - url: https://arxiv.org/abs/cs/9605103 118 | title: "Reinforcement Learning: A Survey | Abstract" 119 | pdf_url: https://arxiv.org/pdf/cs/9605103/ 120 | pdf_title: "Reinforcement Learning: A Survey | PDF" 121 | abs2pdf: False 122 | description: old url pdf with trailing slash 123 | - url: https://arxiv.org/abs/cs/9605103/ 124 | title: "Reinforcement Learning: A Survey | Abstract" 125 | pdf_url: https://arxiv.org/pdf/cs/9605103 126 | pdf_title: "Reinforcement Learning: A Survey | PDF" 127 | pdf2abs: False 128 | description: old url abs with trailing slash 129 | - url: https://arxiv.org/abs/cond-mat/0408502 130 | title: Pressure Raman Effects and Internal Stress in Network Glasses | Abstract 131 | pdf_url: https://arxiv.org/ftp/cond-mat/papers/0408/0408502.pdf 132 | pdf_title: Pressure Raman Effects and Internal Stress in Network Glasses | PDF 133 | abs2pdf: False 134 | description: old url with special ftp format 135 | - url: https://arxiv.org/abs/1512.03385?query#anchor 136 | title: Deep Residual Learning for Image Recognition | Abstract 137 | pdf_url: https://arxiv.org/pdf/1512.03385 138 | pdf_title: Deep Residual Learning for Image Recognition | PDF 139 | pdf2abs: False 140 | description: abs with query strings and fragments 141 | - url: https://arxiv.org/abs/1512.03385 142 | title: Deep Residual Learning for Image Recognition | Abstract 143 | pdf_url: https://arxiv.org/pdf/1512.03385?query#anchor 144 | pdf_title: Deep Residual Learning for Image Recognition | PDF 145 | abs2pdf: False 146 | description: pdf with query strings and fragments 147 | - url: https://arxiv.org/abs/1906.07413 148 | title: Learning Imbalanced Datasets with Label-Distribution-Aware Margin Loss | Abstract 149 | pdf_url: https://arxiv.org/pdf/1906.07413 150 | pdf_title: Learning Imbalanced Datasets with Label-Distribution-Aware Margin Loss | PDF 151 | description: latex-defined pdf name 1 (for chrome) 152 | - url: https://arxiv.org/abs/2003.01367 153 | title: Curriculum By Smoothing | Abstract 154 | pdf_url: https://arxiv.org/pdf/2003.01367 155 | pdf_title: Curriculum By Smoothing | PDF 156 | description: latex-defined pdf name 2 (for chrome) 157 | - url: https://arxiv.org/abs/2003.13678 158 | title: Designing Network Design Spaces | Abstract 159 | pdf_url: http://arxiv.org/pdf/2003.13678 160 | pdf_title: Designing Network Design Spaces | PDF 161 | abs2pdf: False 162 | description: with HTTP (for firefox) 163 | - url: http://arxiv.org/abs/2003.13678 164 | title: Designing Network Design Spaces | Abstract 165 | pdf_url: https://arxiv.org/pdf/2003.13678 166 | pdf_title: Designing Network Design Spaces | PDF 167 | pdf2abs: False 168 | description: with HTTP (for firefox) 169 | - url: https://arxiv.org/abs/2407.10603 170 | title: "Leave No Knowledge Behind During Knowledge Distillation: Towards Practical and Effective Knowledge Distillation for Code-Switching ASR Using Realistic Data | Abstract" 171 | pdf_url: https://arxiv.org/pdf/2407.10603 172 | pdf_title: "Leave No Knowledge Behind During Knowledge Distillation: Towards Practical and Effective Knowledge Distillation for Code-Switching ASR Using Realistic Data | PDF" 173 | description: long paper title 174 | # Below are testcases for external URLs 175 | - url: https://ar5iv.labs.arxiv.org/html/1512.03385 176 | title: Deep Residual Learning for Image Recognition | HTML5 177 | url2: https://arxiv.org/abs/1512.03385 178 | title2: Deep Residual Learning for Image Recognition | Abstract 179 | description: ar5iv -> arXiv 180 | - url: https://huggingface.co/papers/2408.00714 181 | title: "Paper page - SAM 2: Segment Anything in Images and Videos" 182 | url2: https://arxiv.org/abs/2408.00714 183 | title2: "SAM 2: Segment Anything in Images and Videos | Abstract" 184 | description: ar5iv -> arXiv 185 | - url: https://openreview.net/pdf?id=LcF-EEt8cCC 186 | url2: https://openreview.net/forum?id=LcF-EEt8cCC 187 | title2: Denoising Likelihood Score Matching for Conditional Score-based Data Generation | OpenReview 188 | description: OpenReview pdf -> abs 189 | - url: https://openreview.net/forum?id=LcF-EEt8cCC 190 | title: Denoising Likelihood Score Matching for Conditional Score-based Data Generation | OpenReview 191 | url2: https://openreview.net/pdf?id=LcF-EEt8cCC 192 | description: OpenReview abs -> pdf 193 | - url: https://openreview.net/forum?id=LcF-EEt8cCC&referrer=[Author Console] 194 | title: Denoising Likelihood Score Matching for Conditional Score-based Data Generation | OpenReview 195 | url2: https://openreview.net/pdf?id=LcF-EEt8cCC 196 | description: OpenReview abs (with extra query strings) -> pdf 197 | - url: https://papers.nips.cc/paper_files/paper/1987/file/523f87e9d08e6071a3bbd150e6da40fb-Paper.pdf 198 | url2: https://papers.nips.cc/paper_files/paper/1987/hash/523f87e9d08e6071a3bbd150e6da40fb-Abstract.html 199 | title2: Bit-Serial Neural Networks 200 | description: NIPS pdf -> abs (1987) 201 | - url: https://papers.nips.cc/paper_files/paper/1987/hash/523f87e9d08e6071a3bbd150e6da40fb-Abstract.html 202 | title: Bit-Serial Neural Networks 203 | url2: https://papers.nips.cc/paper_files/paper/1987/file/523f87e9d08e6071a3bbd150e6da40fb-Paper.pdf 204 | description: NIPS abs -> pdf (1987) 205 | - url: https://papers.neurips.cc/paper_files/paper/1987/file/523f87e9d08e6071a3bbd150e6da40fb-Paper.pdf 206 | url2: https://papers.neurips.cc/paper_files/paper/1987/hash/523f87e9d08e6071a3bbd150e6da40fb-Abstract.html 207 | title2: Bit-Serial Neural Networks 208 | description: NIPS (papers.neurips.cc) pdf -> abs (1987) 209 | - url: https://papers.neurips.cc/paper_files/paper/1987/hash/523f87e9d08e6071a3bbd150e6da40fb-Abstract.html 210 | title: Bit-Serial Neural Networks 211 | url2: https://papers.neurips.cc/paper_files/paper/1987/file/523f87e9d08e6071a3bbd150e6da40fb-Paper.pdf 212 | description: NIPS (papers.neurips.cc) abs -> pdf (1987) 213 | - url: https://proceedings.nips.cc/paper_files/paper/1987/file/523f87e9d08e6071a3bbd150e6da40fb-Paper.pdf 214 | url2: https://proceedings.nips.cc/paper_files/paper/1987/hash/523f87e9d08e6071a3bbd150e6da40fb-Abstract.html 215 | title2: Bit-Serial Neural Networks 216 | description: NIPS (proceedings.nips.cc) pdf -> abs (1987) 217 | - url: https://proceedings.nips.cc/paper_files/paper/1987/hash/523f87e9d08e6071a3bbd150e6da40fb-Abstract.html 218 | title: Bit-Serial Neural Networks 219 | url2: https://proceedings.nips.cc/paper_files/paper/1987/file/523f87e9d08e6071a3bbd150e6da40fb-Paper.pdf 220 | description: NIPS (proceedings.nips.cc) abs -> pdf (1987) 221 | - url: https://proceedings.neurips.cc/paper_files/paper/1987/file/523f87e9d08e6071a3bbd150e6da40fb-Paper.pdf 222 | url2: https://proceedings.neurips.cc/paper_files/paper/1987/hash/523f87e9d08e6071a3bbd150e6da40fb-Abstract.html 223 | title2: Bit-Serial Neural Networks 224 | description: NIPS (proceedings.neurips.cc) pdf -> abs (1987) 225 | - url: https://proceedings.neurips.cc/paper_files/paper/1987/hash/523f87e9d08e6071a3bbd150e6da40fb-Abstract.html 226 | title: Bit-Serial Neural Networks 227 | url2: https://proceedings.neurips.cc/paper_files/paper/1987/file/523f87e9d08e6071a3bbd150e6da40fb-Paper.pdf 228 | description: NIPS (proceedings.neurips.cc) abs -> pdf (1987) 229 | - url: https://papers.nips.cc/paper_files/paper/2019/file/0234c510bc6d908b28c70ff313743079-Paper.pdf 230 | url2: https://papers.nips.cc/paper_files/paper/2019/hash/0234c510bc6d908b28c70ff313743079-Abstract.html 231 | title2: Improved Precision and Recall Metric for Assessing Generative Models 232 | description: NIPS pdf -> abs (2019) 233 | - url: https://papers.nips.cc/paper_files/paper/2019/file/0234c510bc6d908b28c70ff313743079-Reviews.html 234 | url2: https://papers.nips.cc/paper_files/paper/2019/hash/0234c510bc6d908b28c70ff313743079-Abstract.html 235 | title2: Improved Precision and Recall Metric for Assessing Generative Models 236 | description: NIPS reviews -> abs (2019) 237 | - url: https://papers.nips.cc/paper_files/paper/2019/file/0234c510bc6d908b28c70ff313743079-Metadata.json 238 | url2: https://papers.nips.cc/paper_files/paper/2019/hash/0234c510bc6d908b28c70ff313743079-Abstract.html 239 | title2: Improved Precision and Recall Metric for Assessing Generative Models 240 | description: NIPS metadata -> abs (2019) 241 | - url: https://papers.nips.cc/paper_files/paper/2019/file/0234c510bc6d908b28c70ff313743079-MetaReview.html 242 | url2: https://papers.nips.cc/paper_files/paper/2019/hash/0234c510bc6d908b28c70ff313743079-Abstract.html 243 | title2: Improved Precision and Recall Metric for Assessing Generative Models 244 | description: NIPS meta-review -> abs (2019) 245 | - url: https://papers.nips.cc/paper_files/paper/2019/file/0234c510bc6d908b28c70ff313743079-AuthorFeedback.pdf 246 | url2: https://papers.nips.cc/paper_files/paper/2019/hash/0234c510bc6d908b28c70ff313743079-Abstract.html 247 | title2: Improved Precision and Recall Metric for Assessing Generative Models 248 | description: NIPS author-feedback -> abs (2019) 249 | - url: https://papers.nips.cc/paper_files/paper/2019/hash/0234c510bc6d908b28c70ff313743079-Abstract.html 250 | title: Improved Precision and Recall Metric for Assessing Generative Models 251 | url2: https://papers.nips.cc/paper_files/paper/2019/file/0234c510bc6d908b28c70ff313743079-Paper.pdf 252 | description: NIPS abs -> pdf (2019) 253 | - url: https://papers.nips.cc/paper_files/paper/2021/file/cba0a4ee5ccd02fda0fe3f9a3e7b89fe-Paper.pdf 254 | url2: https://papers.nips.cc/paper_files/paper/2021/hash/cba0a4ee5ccd02fda0fe3f9a3e7b89fe-Abstract.html 255 | title2: "MLP-Mixer: An all-MLP Architecture for Vision" 256 | description: NIPS pdf -> abs (2021) 257 | - url: https://papers.nips.cc/paper_files/paper/2021/file/cba0a4ee5ccd02fda0fe3f9a3e7b89fe-Supplemental.pdf 258 | url2: https://papers.nips.cc/paper_files/paper/2021/hash/cba0a4ee5ccd02fda0fe3f9a3e7b89fe-Abstract.html 259 | title2: "MLP-Mixer: An all-MLP Architecture for Vision" 260 | description: NIPS sup -> abs (2021) 261 | - url: https://papers.nips.cc/paper_files/paper/2021/hash/cba0a4ee5ccd02fda0fe3f9a3e7b89fe-Abstract.html 262 | title: "MLP-Mixer: An all-MLP Architecture for Vision" 263 | url2: https://papers.nips.cc/paper_files/paper/2021/file/cba0a4ee5ccd02fda0fe3f9a3e7b89fe-Paper.pdf 264 | description: NIPS abs -> pdf (2021) 265 | - url: https://papers.nips.cc/paper_files/paper/2022/file/93f250215e4889119807b6fac3a57aec-Paper-Conference.pdf 266 | url2: https://papers.nips.cc/paper_files/paper/2022/hash/93f250215e4889119807b6fac3a57aec-Abstract-Conference.html 267 | title2: Decomposing NeRF for Editing via Feature Field Distillation 268 | description: NIPS pdf -> abs (2022) with `Conference` suffix 269 | - url: https://papers.nips.cc/paper_files/paper/2022/hash/93f250215e4889119807b6fac3a57aec-Abstract-Conference.html 270 | title: Decomposing NeRF for Editing via Feature Field Distillation 271 | url2: https://papers.nips.cc/paper_files/paper/2022/file/93f250215e4889119807b6fac3a57aec-Paper-Conference.pdf 272 | description: NIPS abs -> pdf (2022) with `Conference` suffix 273 | - url: https://proceedings.mlr.press/v139/sun21c/sun21c.pdf 274 | url2: https://proceedings.mlr.press/v139/sun21c.html 275 | title2: "DFAC Framework: Factorizing the Value Function via Quantile Mixture for Multi-Agent Distributional Q-Learning" 276 | description: PMLR pdf -> abs (2021) 277 | - url: https://proceedings.mlr.press/v139/sun21c/sun21c-supp.pdf 278 | url2: https://proceedings.mlr.press/v139/sun21c.html 279 | title2: "DFAC Framework: Factorizing the Value Function via Quantile Mixture for Multi-Agent Distributional Q-Learning" 280 | description: PMLR sup -> abs (2021) 281 | - url: https://proceedings.mlr.press/v139/sun21c.html 282 | title: "DFAC Framework: Factorizing the Value Function via Quantile Mixture for Multi-Agent Distributional Q-Learning" 283 | url2: https://proceedings.mlr.press/v139/sun21c/sun21c.pdf 284 | description: PMLR abs -> pdf (2021) 285 | - url: https://proceedings.mlr.press/v139/sun21c 286 | title: "DFAC Framework: Factorizing the Value Function via Quantile Mixture for Multi-Agent Distributional Q-Learning" 287 | url2: https://proceedings.mlr.press/v139/sun21c/sun21c.pdf 288 | description: PMLR abs (no trailing html) -> pdf (2021) 289 | - url: https://openaccess.thecvf.com/content_CVPR_2019/papers/Karras_A_Style-Based_Generator_Architecture_for_Generative_Adversarial_Networks_CVPR_2019_paper.pdf 290 | url2: https://openaccess.thecvf.com/content_CVPR_2019/html/Karras_A_Style-Based_Generator_Architecture_for_Generative_Adversarial_Networks_CVPR_2019_paper.html 291 | title2: "CVPR 2019 Open Access Repository" 292 | description: CVF pdf -> abs (Conference) 293 | - url: https://openaccess.thecvf.com/content_CVPR_2019/html/Karras_A_Style-Based_Generator_Architecture_for_Generative_Adversarial_Networks_CVPR_2019_paper.html 294 | title: "CVPR 2019 Open Access Repository" 295 | url2: https://openaccess.thecvf.com/content_CVPR_2019/papers/Karras_A_Style-Based_Generator_Architecture_for_Generative_Adversarial_Networks_CVPR_2019_paper.pdf 296 | description: CVF abs -> pdf (Conference) 297 | - url: https://openaccess.thecvf.com/content_CVPRW_2020/papers/w28/Wang_CSPNet_A_New_Backbone_That_Can_Enhance_Learning_Capability_of_CVPRW_2020_paper.pdf 298 | url2: https://openaccess.thecvf.com/content_CVPRW_2020/html/w28/Wang_CSPNet_A_New_Backbone_That_Can_Enhance_Learning_Capability_of_CVPRW_2020_paper.html 299 | title2: "CVPR 2020 Open Access Repository" 300 | description: CVF pdf -> abs (Workshop with underscore) 301 | - url: https://openaccess.thecvf.com/content_CVPRW_2020/html/w28/Wang_CSPNet_A_New_Backbone_That_Can_Enhance_Learning_Capability_of_CVPRW_2020_paper.html 302 | title: "CVPR 2020 Open Access Repository" 303 | url2: https://openaccess.thecvf.com/content_CVPRW_2020/papers/w28/Wang_CSPNet_A_New_Backbone_That_Can_Enhance_Learning_Capability_of_CVPRW_2020_paper.pdf 304 | description: CVF abs -> pdf (Workshop with underscore) 305 | - url: https://openaccess.thecvf.com/content/CVPR2022W/VOCVALC/papers/Fu_Coupling_Vision_and_Proprioception_for_Navigation_of_Legged_Robots_CVPRW_2022_paper.pdf 306 | url2: https://openaccess.thecvf.com/content/CVPR2022W/VOCVALC/html/Fu_Coupling_Vision_and_Proprioception_for_Navigation_of_Legged_Robots_CVPRW_2022_paper.html 307 | title2: "CVPR 2022 Open Access Repository" 308 | description: CVF pdf -> abs (Workshop with slash) 309 | - url: https://openaccess.thecvf.com/content/CVPR2022W/VOCVALC/html/Fu_Coupling_Vision_and_Proprioception_for_Navigation_of_Legged_Robots_CVPRW_2022_paper.html 310 | title: "CVPR 2022 Open Access Repository" 311 | url2: https://openaccess.thecvf.com/content/CVPR2022W/VOCVALC/papers/Fu_Coupling_Vision_and_Proprioception_for_Navigation_of_Legged_Robots_CVPRW_2022_paper.pdf 312 | description: CVF abs -> pdf (Workshop with slash) 313 | - url: https://www.jmlr.org/papers/v12/pedregosa11a.html 314 | title: "Scikit-learn: Machine Learning in Python" 315 | url2: https://www.jmlr.org/papers/volume12/pedregosa11a/pedregosa11a.pdf 316 | description: JMLR abs -> pdf 317 | - url: https://www.jmlr.org/papers/volume12/pedregosa11a/pedregosa11a.pdf 318 | url2: https://www.jmlr.org/papers/v12/pedregosa11a.html 319 | title2: "Scikit-learn: Machine Learning in Python" 320 | description: JMLR pdf -> abs 321 | - url: https://arxiv.org/html/2403.17537v1 322 | title: "NeRF-HuGS: Improved Neural Radiance Fields in Non-static Scenes Using Heuristics-Guided Segmentation" 323 | url2: https://arxiv.org/abs/2403.17537v1 324 | title2: "NeRF-HuGS: Improved Neural Radiance Fields in Non-static Scenes Using Heuristics-Guided Segmentation | Abstract" 325 | description: arXiv HTML -> arXiv 326 | # Below are testcases for Jest only 327 | - url: https://ieeexplore.ieee.org/document/10412086 328 | pdf_url: https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=10412086 329 | skip_selenium: True # skipped due to paywall 330 | description: IEEE 331 | - url: https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=10412086&tag=1 332 | url2: https://ieeexplore.ieee.org/document/10412086 333 | skip_selenium: True # skipped due to paywall 334 | description: IEEE pdf -> abs (with extra query strings) 335 | - url: https://aclanthology.org/N19-1423.pdf 336 | url2: https://aclanthology.org/N19-1423/ 337 | title2: "BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding - ACL Anthology" 338 | description: ACL pdf -> abs 339 | - url: https://aclanthology.org/N19-1423/ 340 | title: "BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding - ACL Anthology" 341 | url2: https://aclanthology.org/N19-1423.pdf 342 | description: ACL abs -> pdf 343 | - url: https://aclanthology.org/2024.acl-long.800.pdf 344 | url2: https://aclanthology.org/2024.acl-long.800/ 345 | title2: Why are Sensitive Functions Hard for Transformers? - ACL Anthology 346 | description: ACL pdf -> abs 347 | - url: https://aclanthology.org/2024.acl-long.800/ 348 | title: Why are Sensitive Functions Hard for Transformers? - ACL Anthology 349 | url2: https://aclanthology.org/2024.acl-long.800.pdf 350 | description: ACL abs -> pdf 351 | 352 | # TODO: Test download filenames: 353 | # - Title with colon: https://arxiv.org/abs/2102.07936 354 | # - Title with hash symbol: https://arxiv.org/abs/1611.04717 355 | # - Title with question mark: https://arxiv.org/abs/1811.02553v3 356 | # - Title with dollar sign and carat sign: https://arxiv.org/abs/1611.02779 357 | # TODO: using `${title}, ${firstAuthor} et al., ${authors}, ${publishedYear}, ${updatedYear}, v${version}.pdf` 358 | --------------------------------------------------------------------------------