├── 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 |
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 |
24 | Experimental Features:
25 |
33 |
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 |
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 | [](https://github.com/j3soon/arxiv-utils/actions/workflows/test-with-jest.yaml)
4 | [](https://github.com/j3soon/arxiv-utils/actions/workflows/test-with-selenium.yaml)
5 | [](https://github.com/j3soon/arxiv-utils/actions/workflows/build-and-publish.yaml)
6 |
7 | [](https://chrome.google.com/webstore/detail/arxiv-utils/mnhdpeipjhhkmlhlcljdjpgmilbmehij)
8 | [](https://chrome.google.com/webstore/detail/arxiv-utils/mnhdpeipjhhkmlhlcljdjpgmilbmehij)
9 | [](https://chrome.google.com/webstore/detail/arxiv-utils/mnhdpeipjhhkmlhlcljdjpgmilbmehij)
10 |
11 | [](https://addons.mozilla.org/en-US/firefox/addon/arxiv-utils/)
12 | [](https://addons.mozilla.org/en-US/firefox/addon/arxiv-utils/)
13 | [](https://addons.mozilla.org/en-US/firefox/addon/arxiv-utils/)
14 |
15 | [](https://microsoftedge.microsoft.com/addons/detail/arxivutils/ngjpcfjabahdoadnajbhnikbemhmemdg)
16 | [](https://microsoftedge.microsoft.com/addons/detail/arxivutils/ngjpcfjabahdoadnajbhnikbemhmemdg)
17 | [](https://microsoftedge.microsoft.com/addons/detail/arxivutils/ngjpcfjabahdoadnajbhnikbemhmemdg)
18 |
19 | 
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 | 
48 | Finally... Meaningful paper title instead of paper id! (For Firefox, this is achieved through a custom PDF container.)
49 | 
50 | Difficult to get back to abstract page...
51 | Click to get back to abstract page!
52 | 
53 | TADA~ The abstract page is shown at the right of the PDF page! Both with meaningful title!
54 | 
55 | The button is disabled if not in ArXiv's domain.
56 | Meaningful bookmark titles.
57 | 
58 | Meaningful OneTab entries! (Chrome & Edge only)
59 | 
60 | Opened too many tabs? Search in terms of the paper title!
61 | 
62 | Works well with vertical tabs.
63 | 
64 | Right-click the extension icon and select `Options` to set your preference. (Chrome & Edge)
65 | 
66 | Go to add-ons page, click the extension select `Options` to set your preference. (Firefox)
67 | 
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="
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 |
--------------------------------------------------------------------------------