48 Commits

Author SHA1 Message Date
semantic-release-bot
14f1b4b935 chore(release): 1.10.0
# [1.10.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.3...v1.10.0) (2025-11-20)

### Bug Fixes

* lighten debounce() usage, do not do full re-render on every change ([348d64f](348d64f356))
* made placeholder for textarea, added outline offset ([4f32be0](4f32be0b93))

### Features

* new tabbed layout ([18e167c](18e167cb7f))
* show all found words and allow jump to them (beta) ([1a4c91f](1a4c91fd5e))
2025-11-20 15:48:45 +03:00
1a4c91fd5e feat: show all found words and allow jump to them (beta) 2025-11-20 15:48:28 +03:00
1c58357418 chore(i18n): rephrased hint 2025-11-20 14:25:36 +03:00
521b3295e6 nit: show spinner while user settings are loaded 2025-11-20 14:13:49 +03:00
348d64f356 fix: lighten debounce() usage, do not do full re-render on every change 2025-11-20 14:09:43 +03:00
dfdc1742ec nit: add logo to header 2025-11-20 13:51:06 +03:00
18e167cb7f feat: new tabbed layout 2025-11-20 13:45:42 +03:00
0990543aa9 chore(i18n): update missing translations 2025-11-19 16:15:21 +03:00
4f32be0b93 fix: made placeholder for textarea, added outline offset 2025-11-19 16:12:55 +03:00
1ba701737e chore: add license 2025-11-19 10:02:59 +03:00
semantic-release-bot
d275a6fd0d chore(release): 1.9.3
## [1.9.3](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.2...v1.9.3) (2025-11-18)

### Bug Fixes

* use CSS Custom Highlight API to avoid dom modifications (fixes [#1](https://github.com/obsqrbtz/goose-highlighter/issues/1)) ([3f2bb60](3f2bb6080b))
2025-11-19 01:26:16 +03:00
3f2bb6080b fix: use CSS Custom Highlight API to avoid dom modifications (fixes #1) 2025-11-19 01:25:55 +03:00
3da28a2ad7 chore: update deps 2025-11-18 20:58:59 +03:00
semantic-release-bot
4f575d9534 chore(release): 1.9.2
## [1.9.2](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.1...v1.9.2) (2025-11-14)

### Bug Fixes

* **highlight:** prevent creating extra <span>'s ([#1](https://github.com/obsqrbtz/goose-highlighter/issues/1)) ([affddd3](affddd3dbc))
2025-11-14 13:40:56 +03:00
affddd3dbc fix(highlight): prevent creating extra <span>'s (#1) 2025-11-14 13:40:43 +03:00
semantic-release-bot
c8334f9e68 chore(release): 1.9.1
## [1.9.1](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.0...v1.9.1) (2025-11-05)

### Bug Fixes

* remove halowen styling ([172aa75](172aa7583b))
2025-11-05 14:59:42 +03:00
172aa7583b fix: remove halowen styling 2025-11-05 14:59:27 +03:00
semantic-release-bot
6d7d9ac151 chore(release): 1.9.0
# [1.9.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.5...v1.9.0) (2025-10-31)

### Features

* haloween styling ([5ef380e](5ef380e544))
2025-10-31 11:17:28 +03:00
5ef380e544 feat: haloween styling 2025-10-31 11:17:08 +03:00
semantic-release-bot
c634f6bc8b chore(release): 1.8.5
## [1.8.5](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.4...v1.8.5) (2025-10-29)

### Bug Fixes

* highlight colors when multiple list have different configurations ([67577c8](67577c89cf))
2025-10-29 12:30:37 +03:00
67577c89cf fix: highlight colors when multiple list have different configurations 2025-10-29 12:29:55 +03:00
semantic-release-bot
326e585021 chore(release): 1.8.4
## [1.8.4](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.3...v1.8.4) (2025-10-28)

### Bug Fixes

* do not re-highlight when already processing highlights ([8be53f3](8be53f3240))
2025-10-28 15:32:14 +03:00
8be53f3240 fix: do not re-highlight when already processing highlights 2025-10-28 15:31:46 +03:00
f07617fa55 Merge branch 'main' of https://github.com/obsqrbtz/goose-highlighter 2025-10-09 16:18:53 +03:00
e79874922a added logo 2025-10-09 16:18:36 +03:00
71216cbcd9 ci: update manifest in publish workflow 2025-10-08 17:54:06 +03:00
f292bd7149 nit: added footer with version and github. 2025-10-08 16:32:04 +03:00
584ced252f ci: publish on version tag push 2025-10-08 16:26:32 +03:00
semantic-release-bot
ff5752da84 chore(release): 1.8.3
## [1.8.3](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.2...v1.8.3) (2025-10-08)

### Bug Fixes

* stop observing when highlightting is disabled ([d7c8dbb](d7c8dbb5f0))
2025-10-08 16:11:52 +03:00
d7c8dbb5f0 fix: stop observing when highlightting is disabled 2025-10-08 16:11:25 +03:00
semantic-release-bot
bc02d0fb77 chore(release): 1.8.2
## [1.8.2](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.1...v1.8.2) (2025-10-08)

### Bug Fixes

* do not call save() on all keypresses in textboxes ([687d7c9](687d7c9e62))
* do not save anything in list settings section until presses the apply button ([0734bf3](0734bf3308))
2025-10-08 14:33:46 +03:00
0734bf3308 fix: do not save anything in list settings section until presses the apply button 2025-10-08 14:33:28 +03:00
687d7c9e62 fix: do not call save() on all keypresses in textboxes 2025-10-08 14:11:08 +03:00
58d48be6e4 chore: refactor 2025-10-08 13:53:47 +03:00
00d2cc592a ci: exclude scripts dir from release package 2025-10-08 11:42:37 +03:00
a6bc14ac76 docs: update readme 2025-10-08 11:34:33 +03:00
7d90f5d5bf ci: fix release script 2025-10-08 11:21:47 +03:00
a68f2ddbe8 ci: test release action 2025-10-08 11:18:54 +03:00
2a1034aef4 fix (ci): keep update-manifest-versions script in vanilla js 2025-10-08 11:16:31 +03:00
1ec17cd83e chore: migrated to typescript 2025-10-08 11:09:16 +03:00
b5386c706f fixed eslint config 2025-10-08 10:32:59 +03:00
5ca83fce0f fix npm install 2025-10-08 10:31:48 +03:00
semantic-release-bot
7e4f2b4ecf chore(release): 1.8.0
# [1.8.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.7.2...v1.8.0) (2025-10-07)

### Features

* add collapsible sections ([a158a30](a158a303b0))
* add websites to exception list ([915add3](915add3a4c))
2025-10-07 14:46:28 +03:00
a158a303b0 feat: add collapsible sections 2025-10-07 14:46:03 +03:00
915add3a4c feat: add websites to exception list 2025-10-07 14:18:23 +03:00
semantic-release-bot
a1701a3504 chore(release): 1.7.2
## [1.7.2](https://github.com/obsqrbtz/goose-highlighter/compare/v1.7.1...v1.7.2) (2025-10-06)

### Bug Fixes

* do not create <mark> elements, just wrap found words in <span> and add .css styling ([6ba0d2e](6ba0d2eb7c))
2025-10-06 14:53:36 +03:00
6ba0d2eb7c fix: do not create <mark> elements, just wrap found words in <span> and add .css styling 2025-10-06 14:53:24 +03:00
21a120e494 ci: corected auto commit message 2025-06-27 14:09:15 +03:00
48 changed files with 5305 additions and 2096 deletions

View File

@@ -2,7 +2,8 @@ name: Publish Chrome Extension
on:
push:
branches: [ main ]
tags:
- 'v*'
jobs:
publish:
@@ -16,13 +17,28 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build extension
run: npm run build
- name: Set version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Update manifest version
run: node scripts/update-manifest-version.js ${{ steps.version.outputs.VERSION }}
- name: Create zip package
run: |
zip -r goose-highlighter.zip . -x '*.git*' 'node_modules/*' 'versioning.md' '.releaserc.json' 'package.json' 'package-lock.json' 'README.md'
zip -r goose-highlighter.zip . -x '*.git*' 'node_modules/*' 'src/*' 'scripts/*' 'versioning.md' '.releaserc.json' 'package.json' 'package-lock.json' 'README.md' 'tsconfig.json' 'eslint.config.mjs'
- name: Install webstore upload CLI
run: npm install -g chrome-webstore-upload-cli

103
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,103 @@
name: Create GitHub Release
on:
push:
tags:
- 'v*'
jobs:
release:
name: Create Release with Extension Files
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build extension
run: npm run build
- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Update manifest version
run: node scripts/update-manifest-version.js ${{ steps.version.outputs.VERSION }}
- name: Create extension zip
run: |
zip -r goose-highlighter-${{ steps.version.outputs.VERSION }}.zip . \
-x '*.git*' 'node_modules/*' 'src/*' 'scripts/*' 'versioning.md' '.releaserc.json' \
'package.json' 'package-lock.json' 'README.md' 'tsconfig.json' \
'eslint.config.mjs' '.github/*' 'CHANGELOG.md' 'PRIVACY.MD'
- name: Install Chrome extension packaging tool
run: npm install -g crx3
- name: Generate private key for CRX
run: |
openssl genrsa -out key.pem 2048
- name: Create CRX file
run: |
# Create a temporary directory for the extension
mkdir temp_extension
# Copy all files except excluded ones to temp directory
rsync -av --exclude='.git*' --exclude='node_modules' --exclude='src' \
--exclude='scripts' --exclude='versioning.md' --exclude='.releaserc.json' \
--exclude='package.json' --exclude='package-lock.json' \
--exclude='README.md' --exclude='tsconfig.json' \
--exclude='eslint.config.mjs' --exclude='.github' \
--exclude='CHANGELOG.md' --exclude='PRIVACY.MD' \
--exclude='key.pem' --exclude='temp_extension' \
./ temp_extension/
# Create CRX file
crx3 temp_extension -o goose-highlighter-${{ steps.version.outputs.VERSION }}.crx -p key.pem
# Clean up
rm -rf temp_extension key.pem
- name: Generate release notes
id: release_notes
run: |
if [ -f CHANGELOG.md ]; then
# Extract the latest version's changes from CHANGELOG.md
awk '/^## \[${{ steps.version.outputs.VERSION }}\]/{flag=1; next} /^## \[/{flag=0} flag' CHANGELOG.md > release_notes.txt
if [ -s release_notes.txt ]; then
echo "RELEASE_NOTES<<EOF" >> $GITHUB_OUTPUT
cat release_notes.txt >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "RELEASE_NOTES=Release ${{ steps.version.outputs.VERSION }}" >> $GITHUB_OUTPUT
fi
else
echo "RELEASE_NOTES=Release ${{ steps.version.outputs.VERSION }}" >> $GITHUB_OUTPUT
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.ref_name }}
name: Release ${{ steps.version.outputs.VERSION }}
body: ${{ steps.release_notes.outputs.RELEASE_NOTES }}
files: |
goose-highlighter-${{ steps.version.outputs.VERSION }}.zip
goose-highlighter-${{ steps.version.outputs.VERSION }}.crx
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

7
.gitignore vendored
View File

@@ -1 +1,6 @@
node_modules
node_modules
dist
# Auto-generated files
src/content-standalone.ts

View File

@@ -19,7 +19,7 @@
"manifest.json",
"CHANGELOG.md"
],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
"message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
}
]
]

2
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -1,3 +1,89 @@
# [1.10.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.3...v1.10.0) (2025-11-20)
### Bug Fixes
* lighten debounce() usage, do not do full re-render on every change ([348d64f](https://github.com/obsqrbtz/goose-highlighter/commit/348d64f35693c11e7b14edcbe59b910195974950))
* made placeholder for textarea, added outline offset ([4f32be0](https://github.com/obsqrbtz/goose-highlighter/commit/4f32be0b93b5a39dcb034b4a15bbeca05add0a1f))
### Features
* new tabbed layout ([18e167c](https://github.com/obsqrbtz/goose-highlighter/commit/18e167cb7f2e758e09b201f7eff4cdbad080774e))
* show all found words and allow jump to them (beta) ([1a4c91f](https://github.com/obsqrbtz/goose-highlighter/commit/1a4c91fd5e35cc2227a580465ba9078200200623))
## [1.9.3](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.2...v1.9.3) (2025-11-18)
### Bug Fixes
* use CSS Custom Highlight API to avoid dom modifications (fixes [#1](https://github.com/obsqrbtz/goose-highlighter/issues/1)) ([3f2bb60](https://github.com/obsqrbtz/goose-highlighter/commit/3f2bb6080ba3a9ac0599ad6594f0d877c12bb62f))
## [1.9.2](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.1...v1.9.2) (2025-11-14)
### Bug Fixes
* **highlight:** prevent creating extra <span>'s ([#1](https://github.com/obsqrbtz/goose-highlighter/issues/1)) ([affddd3](https://github.com/obsqrbtz/goose-highlighter/commit/affddd3dbc7de30100ca134ec65f4dc090275ca5))
## [1.9.1](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.0...v1.9.1) (2025-11-05)
### Bug Fixes
* remove halowen styling ([172aa75](https://github.com/obsqrbtz/goose-highlighter/commit/172aa7583b325761af43c780db4ac61dc4bda99b))
# [1.9.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.5...v1.9.0) (2025-10-31)
### Features
* haloween styling ([5ef380e](https://github.com/obsqrbtz/goose-highlighter/commit/5ef380e54447f45f7360dd4b7b84456aae55bfee))
## [1.8.5](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.4...v1.8.5) (2025-10-29)
### Bug Fixes
* highlight colors when multiple list have different configurations ([67577c8](https://github.com/obsqrbtz/goose-highlighter/commit/67577c89cffca1ab6d40a8913e51b7c3c6f91c85))
## [1.8.4](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.3...v1.8.4) (2025-10-28)
### Bug Fixes
* do not re-highlight when already processing highlights ([8be53f3](https://github.com/obsqrbtz/goose-highlighter/commit/8be53f32402c2f0f228ca003ef3805c5ff0b6e88))
## [1.8.3](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.2...v1.8.3) (2025-10-08)
### Bug Fixes
* stop observing when highlightting is disabled ([d7c8dbb](https://github.com/obsqrbtz/goose-highlighter/commit/d7c8dbb5f0011afe83739841218aa737794074e3))
## [1.8.2](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.1...v1.8.2) (2025-10-08)
### Bug Fixes
* do not call save() on all keypresses in textboxes ([687d7c9](https://github.com/obsqrbtz/goose-highlighter/commit/687d7c9e62f0f282ce73e86cdc62aaf275c9dafe))
* do not save anything in list settings section until presses the apply button ([0734bf3](https://github.com/obsqrbtz/goose-highlighter/commit/0734bf330824c60f0d5c4784e99660b9e652efd6))
# [1.8.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.7.2...v1.8.0) (2025-10-07)
### Features
* add collapsible sections ([a158a30](https://github.com/obsqrbtz/goose-highlighter/commit/a158a303b01416f81e69bb137b71d3369904b044))
* add websites to exception list ([915add3](https://github.com/obsqrbtz/goose-highlighter/commit/915add3a4cdbff390a4d0f7d227a4ece5fa31072))
## [1.7.2](https://github.com/obsqrbtz/goose-highlighter/compare/v1.7.1...v1.7.2) (2025-10-06)
### Bug Fixes
* do not create <mark> elements, just wrap found words in <span> and add .css styling ([6ba0d2e](https://github.com/obsqrbtz/goose-highlighter/commit/6ba0d2eb7c7346cdca3921a12d300a714439efa5))
## [1.7.1](https://github.com/obsqrbtz/goose-highlighter/compare/v1.7.0...v1.7.1) (2025-06-27)

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Daniel Dada
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,15 +1,59 @@
# Goose Highlighter
# <img src="img/logo.png" alt="Goose Highlighter Logo" width="32" style="vertical-align: middle;"> Goose Highlighter
Goose Highlighter is a browser extension that allows you to highlight custom words and phrases on any webpage. Organize your highlights into lists, customize their appearance, and toggle highlighting or theme modes with ease.
Goose Highlighter is a browser extension that allows you to highlight words on any webpage.
## Features
- **Multiple Highlight Lists:** Organize words into separate lists.
- **Custom Colors:** Set background and foreground for each list and individual word.
- **Custom Colors:** Set background and foreground for each list or individual word.
- **Bulk Add:** Paste multiple words at once.
- **Enable/Disable:** Toggle highlighting globally, per list, or per word.
- **Import/Export:** Backup or share your highlight lists as JSON files.
- **Page Navigation:** View all highlights on the current page and jump to any occurrence with a single click.
- **Site Exceptions:** Add specific websites to an exceptions list to disable highlighting there.
- **Import/Export:** Backup or share your highlight lists and exceptions as JSON files.
## Install
- go to [Chrome Web Store page](https://chromewebstore.google.com/detail/goose-highlighter/kdoehicejfnccbmecpkfjlbljpfogoep) and choose `Add to chrome`.
### From Chrome Web Store (Recommended)
- Go to [Chrome Web Store page](https://chromewebstore.google.com/detail/goose-highlighter/kdoehicejfnccbmecpkfjlbljpfogoep) and choose `Add to chrome`.
### Manual Installation
#### Option 1: Install from CRX File (Releases)
1. **Download:** Get the latest `.crx` file from the [Releases section](https://github.com/obsqrbtz/goose-highlighter/releases)
2. **Install in Chrome:**
- Open Chrome and go to `chrome://extensions/`
- Enable "Developer mode" (toggle in top-right corner)
- Drag and drop the `.crx` file onto the extensions page
- Click "Add extension" when prompted
#### Option 2: Install from ZIP File (Releases)
1. **Download:** Get the latest `.zip` file from the [Releases section](https://github.com/obsqrbtz/goose-highlighter/releases)
2. **Extract:** Unzip the downloaded file to a folder of your choice
3. **Load in Chrome:**
- Open Chrome and go to `chrome://extensions/`
- Enable "Developer mode" (toggle in top-right corner)
- Click "Load unpacked" button
- Select the extracted folder containing the extension files
#### Option 3: Build from Source
1. **Prerequisites:** Node.js 20+ and npm
2. **Clone the repository:**
```bash
git clone https://github.com/obsqrbtz/goose-highlighter.git
cd goose-highlighter
```
3. **Install dependencies:**
```bash
npm install
```
4. **Build the extension:**
```bash
npm run build
```
5. **Load in Chrome:**
- Open Chrome and go to `chrome://extensions/`
- Enable "Developer mode" (toggle in top-right corner)
- Click "Load unpacked" button
- Select the entire `goose-highlighter` folder (not the `dist` folder)

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "Hervorhebung aktivieren"
},
"apply": {
"message": "Anwenden"
},
"paste_hint": {
"message": "Wörter hier einfügen"
"message": "Fügen Sie hier Wörter oder Phrasen ein. Jedes neue Wort/jede neue Phrase sollte in der nächsten Zeile beginnen."
},
"apply_paste": {
"message": "Wörter hinzufügen"
@@ -94,5 +97,80 @@
},
"options": { "message": "Optionen" },
"match_case": { "message": "Groß-/Kleinschreibung beachten" },
"match_whole": { "message": "Ganzes Wort übereinstimmen" }
"match_whole": { "message": "Ganzes Wort übereinstimmen" },
"site_exceptions": {
"message": "Website-Ausnahmen"
},
"add_exception": {
"message": "Zu Ausnahmen hinzufügen"
},
"remove_exception": {
"message": "Aktuelle entfernen"
},
"manage_exceptions": {
"message": "Verwalten"
},
"exceptions_list": {
"message": "Ausnahme-Websites:"
},
"clear_all": {
"message": "Alle löschen"
},
"confirm_clear_exceptions": {
"message": "Möchten Sie wirklich alle Ausnahmen löschen?"
},
"remove": {
"message": "Entfernen"
},
"tab_lists": {
"message": "Listen"
},
"tab_words": {
"message": "Wörter"
},
"tab_exceptions": {
"message": "Ausnahmen"
},
"no_exceptions": {
"message": "Keine Ausnahmen"
},
"toggle_highlighting_title": {
"message": "Hervorhebung umschalten"
},
"toggle_dark_mode_title": {
"message": "Dunkelmodus umschalten"
},
"select_title": {
"message": "Auswählen"
},
"word_placeholder": {
"message": "Wort oder Phrase"
},
"background_color_title": {
"message": "Hintergrundfarbe"
},
"text_color_title": {
"message": "Textfarbe"
},
"tab_page_highlights": {
"message": "Auf Seite"
},
"highlights_on_page": {
"message": "Hervorhebungen auf dieser Seite"
},
"total_highlights": {
"message": "Gesamt"
},
"refresh": {
"message": "Aktualisieren"
},
"no_highlights_on_page": {
"message": "Keine Hervorhebungen auf dieser Seite gefunden"
},
"previous": {
"message": "Vorherige"
},
"next": {
"message": "Nächste"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "Enable Highlight"
},
"apply": {
"message": "Apply"
},
"paste_hint": {
"message": "Paste words here"
"message": "Paste words or phrases here. Each new word/phrase should start from next line."
},
"apply_paste": {
"message": "Add Words"
@@ -100,5 +103,80 @@
},
"match_whole": {
"message": "Match Whole Word"
},
"site_exceptions": {
"message": "Site Exceptions"
},
"add_exception": {
"message": "Add to Exceptions"
},
"remove_exception": {
"message": "Remove current"
},
"manage_exceptions": {
"message": "Manage"
},
"exceptions_list": {
"message": "Exception Sites:"
},
"clear_all": {
"message": "Clear All"
},
"confirm_clear_exceptions": {
"message": "Are you sure you want to clear all exceptions?"
},
"remove": {
"message": "Remove"
},
"tab_lists": {
"message": "Lists"
},
"tab_words": {
"message": "Words"
},
"tab_exceptions": {
"message": "Exceptions"
},
"no_exceptions": {
"message": "No exceptions"
},
"toggle_highlighting_title": {
"message": "Toggle highlighting"
},
"toggle_dark_mode_title": {
"message": "Toggle dark mode"
},
"select_title": {
"message": "Select"
},
"word_placeholder": {
"message": "Word or phrase"
},
"background_color_title": {
"message": "Background color"
},
"text_color_title": {
"message": "Text color"
},
"tab_page_highlights": {
"message": "On Page"
},
"highlights_on_page": {
"message": "Highlights on This Page"
},
"total_highlights": {
"message": "Total"
},
"refresh": {
"message": "Refresh"
},
"no_highlights_on_page": {
"message": "No highlights found on this page"
},
"previous": {
"message": "Previous"
},
"next": {
"message": "Next"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "Activar resaltado"
},
"apply": {
"message": "Aplicar"
},
"paste_hint": {
"message": "Pega las palabras aquí"
"message": "Pegue palabras o frases aquí. Cada nueva palabra/frase debe comenzar en la siguiente línea."
},
"apply_paste": {
"message": "Agregar palabras"
@@ -100,5 +103,80 @@
},
"match_whole": {
"message": "Coincidir palabra completa"
},
"site_exceptions": {
"message": "Excepciones de sitios"
},
"add_exception": {
"message": "Agregar a excepciones"
},
"remove_exception": {
"message": "Eliminar actual"
},
"manage_exceptions": {
"message": "Gestionar"
},
"exceptions_list": {
"message": "Sitios de excepción:"
},
"clear_all": {
"message": "Limpiar todo"
},
"confirm_clear_exceptions": {
"message": "¿Estás seguro de que deseas limpiar todas las excepciones?"
},
"remove": {
"message": "Eliminar"
},
"tab_lists": {
"message": "Listas"
},
"tab_words": {
"message": "Palabras"
},
"tab_exceptions": {
"message": "Excepciones"
},
"no_exceptions": {
"message": "Sin excepciones"
},
"toggle_highlighting_title": {
"message": "Alternar resaltado"
},
"toggle_dark_mode_title": {
"message": "Alternar modo oscuro"
},
"select_title": {
"message": "Seleccionar"
},
"word_placeholder": {
"message": "Palabra o frase"
},
"background_color_title": {
"message": "Color de fondo"
},
"text_color_title": {
"message": "Color de texto"
},
"tab_page_highlights": {
"message": "En página"
},
"highlights_on_page": {
"message": "Resaltados en esta página"
},
"total_highlights": {
"message": "Total"
},
"refresh": {
"message": "Actualizar"
},
"no_highlights_on_page": {
"message": "No se encontraron resaltados en esta página"
},
"previous": {
"message": "Anterior"
},
"next": {
"message": "Siguiente"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "Activer la surbrillance"
},
"apply": {
"message": "Appliquer"
},
"paste_hint": {
"message": "Collez les mots ici"
"message": "Collez des mots ou des phrases ici. Chaque nouveau mot/phrase doit commencer sur la ligne suivante."
},
"apply_paste": {
"message": "Ajouter des mots"
@@ -100,5 +103,80 @@
},
"match_whole": {
"message": "Mot entier seulement"
},
"site_exceptions": {
"message": "Exceptions de sites"
},
"add_exception": {
"message": "Ajouter aux exceptions"
},
"remove_exception": {
"message": "Supprimer l'actuel"
},
"manage_exceptions": {
"message": "Gérer"
},
"exceptions_list": {
"message": "Sites d'exception :"
},
"clear_all": {
"message": "Tout effacer"
},
"confirm_clear_exceptions": {
"message": "Êtes-vous sûr de vouloir effacer toutes les exceptions ?"
},
"remove": {
"message": "Supprimer"
},
"tab_lists": {
"message": "Listes"
},
"tab_words": {
"message": "Mots"
},
"tab_exceptions": {
"message": "Exceptions"
},
"no_exceptions": {
"message": "Aucune exception"
},
"toggle_highlighting_title": {
"message": "Activer/désactiver la surbrillance"
},
"toggle_dark_mode_title": {
"message": "Activer/désactiver le mode sombre"
},
"select_title": {
"message": "Sélectionner"
},
"word_placeholder": {
"message": "Mot ou phrase"
},
"background_color_title": {
"message": "Couleur d'arrière-plan"
},
"text_color_title": {
"message": "Couleur du texte"
},
"tab_page_highlights": {
"message": "Sur la page"
},
"highlights_on_page": {
"message": "Surlignages sur cette page"
},
"total_highlights": {
"message": "Total"
},
"refresh": {
"message": "Actualiser"
},
"no_highlights_on_page": {
"message": "Aucun surlignage trouvé sur cette page"
},
"previous": {
"message": "Précédent"
},
"next": {
"message": "Suivant"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "हाइलाइट सक्षम करें"
},
"apply": {
"message": "लागू करें"
},
"paste_hint": {
"message": "यहाँ शब्द चिपकाएँ"
"message": "यहाँ शब्द या वाक्यांश चिपकाएँ। प्रत्येक नया शब्द/वाक्यांश अगली पंक्ति से शुरू होना चाहिए।"
},
"apply_paste": {
"message": "शब्द जोड़ें"
@@ -100,5 +103,80 @@
},
"match_whole": {
"message": "पूरा शब्द मिलाएं"
},
"site_exceptions": {
"message": "साइट अपवाद"
},
"add_exception": {
"message": "अपवादों में जोड़ें"
},
"remove_exception": {
"message": "वर्तमान हटाएं"
},
"manage_exceptions": {
"message": "प्रबंधित करें"
},
"exceptions_list": {
"message": "अपवाद साइटें:"
},
"clear_all": {
"message": "सभी साफ करें"
},
"confirm_clear_exceptions": {
"message": "क्या आप वाकई सभी अपवादों को साफ करना चाहते हैं?"
},
"remove": {
"message": "हटाएं"
},
"tab_lists": {
"message": "सूचियाँ"
},
"tab_words": {
"message": "शब्द"
},
"tab_exceptions": {
"message": "अपवाद"
},
"no_exceptions": {
"message": "कोई अपवाद नहीं"
},
"toggle_highlighting_title": {
"message": "हाइलाइटिंग टॉगल करें"
},
"toggle_dark_mode_title": {
"message": "डार्क मोड टॉगल करें"
},
"select_title": {
"message": "चुनें"
},
"word_placeholder": {
"message": "शब्द या वाक्यांश"
},
"background_color_title": {
"message": "पृष्ठभूमि रंग"
},
"text_color_title": {
"message": "पाठ रंग"
},
"tab_page_highlights": {
"message": "पृष्ठ पर"
},
"highlights_on_page": {
"message": "इस पृष्ठ पर हाइलाइट"
},
"total_highlights": {
"message": "कुल"
},
"refresh": {
"message": "रीफ्रेश करें"
},
"no_highlights_on_page": {
"message": "इस पृष्ठ पर कोई हाइलाइट नहीं मिला"
},
"previous": {
"message": "पिछला"
},
"next": {
"message": "अगला"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "Abilita evidenziazione"
},
"apply": {
"message": "Applica"
},
"paste_hint": {
"message": "Incolla le parole qui"
"message": "Incolla parole o frasi qui. Ogni nuova parola/frase deve iniziare dalla riga successiva."
},
"apply_paste": {
"message": "Aggiungi parole"
@@ -100,5 +103,80 @@
},
"match_whole": {
"message": "Solo parola intera"
},
"site_exceptions": {
"message": "Eccezioni siti"
},
"add_exception": {
"message": "Aggiungi alle eccezioni"
},
"remove_exception": {
"message": "Rimuovi corrente"
},
"manage_exceptions": {
"message": "Gestisci"
},
"exceptions_list": {
"message": "Siti di eccezione:"
},
"clear_all": {
"message": "Cancella tutto"
},
"confirm_clear_exceptions": {
"message": "Sei sicuro di voler cancellare tutte le eccezioni?"
},
"remove": {
"message": "Rimuovi"
},
"tab_lists": {
"message": "Elenchi"
},
"tab_words": {
"message": "Parole"
},
"tab_exceptions": {
"message": "Eccezioni"
},
"no_exceptions": {
"message": "Nessuna eccezione"
},
"toggle_highlighting_title": {
"message": "Attiva/disattiva evidenziazione"
},
"toggle_dark_mode_title": {
"message": "Attiva/disattiva modalità scura"
},
"select_title": {
"message": "Seleziona"
},
"word_placeholder": {
"message": "Parola o frase"
},
"background_color_title": {
"message": "Colore di sfondo"
},
"text_color_title": {
"message": "Colore del testo"
},
"tab_page_highlights": {
"message": "Sulla pagina"
},
"highlights_on_page": {
"message": "Evidenziazioni su questa pagina"
},
"total_highlights": {
"message": "Totale"
},
"refresh": {
"message": "Aggiorna"
},
"no_highlights_on_page": {
"message": "Nessuna evidenziazione trovata su questa pagina"
},
"previous": {
"message": "Precedente"
},
"next": {
"message": "Successivo"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "ハイライトを有効にする"
},
"apply": {
"message": "適用"
},
"paste_hint": {
"message": "ここに単語を貼り付けてください"
"message": "ここに単語またはフレーズを貼り付けてください。各新しい単語/フレーズは次の行から始める必要があります。"
},
"apply_paste": {
"message": "単語を追加"
@@ -100,5 +103,80 @@
},
"match_whole": {
"message": "完全一致"
},
"site_exceptions": {
"message": "サイト例外"
},
"add_exception": {
"message": "例外に追加"
},
"remove_exception": {
"message": "現在を削除"
},
"manage_exceptions": {
"message": "管理"
},
"exceptions_list": {
"message": "例外サイト:"
},
"clear_all": {
"message": "すべてクリア"
},
"confirm_clear_exceptions": {
"message": "すべての例外をクリアしてもよろしいですか?"
},
"remove": {
"message": "削除"
},
"tab_lists": {
"message": "リスト"
},
"tab_words": {
"message": "単語"
},
"tab_exceptions": {
"message": "例外"
},
"no_exceptions": {
"message": "例外なし"
},
"toggle_highlighting_title": {
"message": "ハイライトの切り替え"
},
"toggle_dark_mode_title": {
"message": "ダークモードの切り替え"
},
"select_title": {
"message": "選択"
},
"word_placeholder": {
"message": "単語またはフレーズ"
},
"background_color_title": {
"message": "背景色"
},
"text_color_title": {
"message": "文字色"
},
"tab_page_highlights": {
"message": "ページ上"
},
"highlights_on_page": {
"message": "このページのハイライト"
},
"total_highlights": {
"message": "合計"
},
"refresh": {
"message": "更新"
},
"no_highlights_on_page": {
"message": "このページにハイライトが見つかりません"
},
"previous": {
"message": "前へ"
},
"next": {
"message": "次へ"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "하이라이트 활성화"
},
"apply": {
"message": "적용"
},
"paste_hint": {
"message": "여기에 단어를 붙여넣기"
"message": "여기에 단어나 구문을 붙여넣으세요. 각 새 단어/구문은 다음 줄에서 시작해야 합니다."
},
"apply_paste": {
"message": "단어 추가"
@@ -100,5 +103,80 @@
},
"match_whole": {
"message": "전체 단어 일치"
},
"site_exceptions": {
"message": "사이트 예외"
},
"add_exception": {
"message": "예외에 추가"
},
"remove_exception": {
"message": "현재 제거"
},
"manage_exceptions": {
"message": "관리"
},
"exceptions_list": {
"message": "예외 사이트:"
},
"clear_all": {
"message": "모두 지우기"
},
"confirm_clear_exceptions": {
"message": "모든 예외를 지우시겠습니까?"
},
"remove": {
"message": "제거"
},
"tab_lists": {
"message": "리스트"
},
"tab_words": {
"message": "단어"
},
"tab_exceptions": {
"message": "예외"
},
"no_exceptions": {
"message": "예외 없음"
},
"toggle_highlighting_title": {
"message": "하이라이트 전환"
},
"toggle_dark_mode_title": {
"message": "다크 모드 전환"
},
"select_title": {
"message": "선택"
},
"word_placeholder": {
"message": "단어 또는 구문"
},
"background_color_title": {
"message": "배경색"
},
"text_color_title": {
"message": "글자색"
},
"tab_page_highlights": {
"message": "페이지에서"
},
"highlights_on_page": {
"message": "이 페이지의 하이라이트"
},
"total_highlights": {
"message": "전체"
},
"refresh": {
"message": "새로고침"
},
"no_highlights_on_page": {
"message": "이 페이지에서 하이라이트를 찾을 수 없습니다"
},
"previous": {
"message": "이전"
},
"next": {
"message": "다음"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "Markeren inschakelen"
},
"apply": {
"message": "Toepassen"
},
"paste_hint": {
"message": "Plak hier de woorden"
"message": "Plak hier woorden of zinnen. Elk nieuw woord/zin moet op de volgende regel beginnen."
},
"apply_paste": {
"message": "Woorden toevoegen"
@@ -100,5 +103,80 @@
},
"match_whole": {
"message": "Alleen volledig woord"
},
"site_exceptions": {
"message": "Site-uitzonderingen"
},
"add_exception": {
"message": "Toevoegen aan uitzonderingen"
},
"remove_exception": {
"message": "Huidige verwijderen"
},
"manage_exceptions": {
"message": "Beheren"
},
"exceptions_list": {
"message": "Uitzondering sites:"
},
"clear_all": {
"message": "Alles wissen"
},
"confirm_clear_exceptions": {
"message": "Weet je zeker dat je alle uitzonderingen wilt wissen?"
},
"remove": {
"message": "Verwijderen"
},
"tab_lists": {
"message": "Lijsten"
},
"tab_words": {
"message": "Woorden"
},
"tab_exceptions": {
"message": "Uitzonderingen"
},
"no_exceptions": {
"message": "Geen uitzonderingen"
},
"toggle_highlighting_title": {
"message": "Markeren in-/uitschakelen"
},
"toggle_dark_mode_title": {
"message": "Donkere modus in-/uitschakelen"
},
"select_title": {
"message": "Selecteren"
},
"word_placeholder": {
"message": "Woord of zin"
},
"background_color_title": {
"message": "Achtergrondkleur"
},
"text_color_title": {
"message": "Tekstkleur"
},
"tab_page_highlights": {
"message": "Op pagina"
},
"highlights_on_page": {
"message": "Markeringen op deze pagina"
},
"total_highlights": {
"message": "Totaal"
},
"refresh": {
"message": "Vernieuwen"
},
"no_highlights_on_page": {
"message": "Geen markeringen gevonden op deze pagina"
},
"previous": {
"message": "Vorige"
},
"next": {
"message": "Volgende"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "Włącz podświetlanie"
},
"apply": {
"message": "Zastosuj"
},
"paste_hint": {
"message": "Wklej tutaj słowa"
"message": "Wklej tutaj słowa lub frazy. Każde nowe słowo/fraza powinno zaczynać się od następnej linii."
},
"apply_paste": {
"message": "Dodaj słowa"
@@ -100,5 +103,80 @@
},
"match_whole": {
"message": "Tylko całe słowo"
},
"site_exceptions": {
"message": "Wyjątki stron"
},
"add_exception": {
"message": "Dodaj do wyjątków"
},
"remove_exception": {
"message": "Usuń bieżący"
},
"manage_exceptions": {
"message": "Zarządzaj"
},
"exceptions_list": {
"message": "Strony wyjątków:"
},
"clear_all": {
"message": "Wyczyść wszystko"
},
"confirm_clear_exceptions": {
"message": "Czy na pewno chcesz wyczyścić wszystkie wyjątki?"
},
"remove": {
"message": "Usuń"
},
"tab_lists": {
"message": "Listy"
},
"tab_words": {
"message": "Słowa"
},
"tab_exceptions": {
"message": "Wyjątki"
},
"no_exceptions": {
"message": "Brak wyjątków"
},
"toggle_highlighting_title": {
"message": "Przełącz podświetlanie"
},
"toggle_dark_mode_title": {
"message": "Przełącz tryb ciemny"
},
"select_title": {
"message": "Zaznacz"
},
"word_placeholder": {
"message": "Słowo lub fraza"
},
"background_color_title": {
"message": "Kolor tła"
},
"text_color_title": {
"message": "Kolor tekstu"
},
"tab_page_highlights": {
"message": "Na stronie"
},
"highlights_on_page": {
"message": "Podświetlenia na tej stronie"
},
"total_highlights": {
"message": "Łącznie"
},
"refresh": {
"message": "Odśwież"
},
"no_highlights_on_page": {
"message": "Nie znaleziono podświetleń na tej stronie"
},
"previous": {
"message": "Poprzedni"
},
"next": {
"message": "Następny"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "Ativar destaque"
},
"apply": {
"message": "Aplicar"
},
"paste_hint": {
"message": "Cole as palavras aqui"
"message": "Cole palavras ou frases aqui. Cada nova palavra/frase deve começar na próxima linha."
},
"apply_paste": {
"message": "Adicionar palavras"
@@ -94,5 +97,80 @@
},
"options": { "message": "Opções" },
"match_case": { "message": "Diferenciar maiúsculas/minúsculas" },
"match_whole": { "message": "Palavra inteira" }
"match_whole": { "message": "Palavra inteira" },
"site_exceptions": {
"message": "Exceções de sites"
},
"add_exception": {
"message": "Adicionar às exceções"
},
"remove_exception": {
"message": "Remover atual"
},
"manage_exceptions": {
"message": "Gerenciar"
},
"exceptions_list": {
"message": "Sites de exceção:"
},
"clear_all": {
"message": "Limpar tudo"
},
"confirm_clear_exceptions": {
"message": "Tem certeza de que deseja limpar todas as exceções?"
},
"remove": {
"message": "Remover"
},
"tab_lists": {
"message": "Listas"
},
"tab_words": {
"message": "Palavras"
},
"tab_exceptions": {
"message": "Exceções"
},
"no_exceptions": {
"message": "Sem exceções"
},
"toggle_highlighting_title": {
"message": "Alternar destaque"
},
"toggle_dark_mode_title": {
"message": "Alternar modo escuro"
},
"select_title": {
"message": "Selecionar"
},
"word_placeholder": {
"message": "Palavra ou frase"
},
"background_color_title": {
"message": "Cor de fundo"
},
"text_color_title": {
"message": "Cor do texto"
},
"tab_page_highlights": {
"message": "Na página"
},
"highlights_on_page": {
"message": "Destaques nesta página"
},
"total_highlights": {
"message": "Total"
},
"refresh": {
"message": "Atualizar"
},
"no_highlights_on_page": {
"message": "Nenhum destaque encontrado nesta página"
},
"previous": {
"message": "Anterior"
},
"next": {
"message": "Próximo"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "Включить выделение"
},
"apply": {
"message": "Применить"
},
"paste_hint": {
"message": "Вставьте список слов здесь"
"message": "Вставьте сюда слова или фразы. Каждое новое слово/фраза должны начинаться с новой строки."
},
"apply_paste": {
"message": "Добавить слова"
@@ -100,5 +103,80 @@
},
"match_whole": {
"message": "Слово целиком"
},
"site_exceptions": {
"message": "Сайты-исключения"
},
"add_exception": {
"message": "Добавить в исключения"
},
"remove_exception": {
"message": "Удалить из исключений"
},
"manage_exceptions": {
"message": "Управление"
},
"exceptions_list": {
"message": "Сайты-исключения:"
},
"clear_all": {
"message": "Очистить все"
},
"confirm_clear_exceptions": {
"message": "Вы уверены, что хотите очистить все исключения?"
},
"remove": {
"message": "Удалить"
},
"tab_lists": {
"message": "Списки"
},
"tab_words": {
"message": "Слова"
},
"tab_exceptions": {
"message": "Исключения"
},
"no_exceptions": {
"message": "Нет исключений"
},
"toggle_highlighting_title": {
"message": "Выделение(вкл/выкл)"
},
"toggle_dark_mode_title": {
"message": "Темная/светлая тема"
},
"select_title": {
"message": "Выбрать"
},
"word_placeholder": {
"message": "Слово или фраза"
},
"background_color_title": {
"message": "Цвет фона"
},
"text_color_title": {
"message": "Цвет текста"
},
"tab_page_highlights": {
"message": "На странице"
},
"highlights_on_page": {
"message": "Найдено на этой странице"
},
"total_highlights": {
"message": "Всего"
},
"refresh": {
"message": "Обновить"
},
"no_highlights_on_page": {
"message": "На этой странице не найдены слова из списка"
},
"previous": {
"message": "Предыдущее"
},
"next": {
"message": "Следующее"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "Vurgulamayı Etkinleştir"
},
"apply": {
"message": "Uygula"
},
"paste_hint": {
"message": "Kelimeleri buraya yapıştırın"
"message": "Kelimeleri veya ifadeleri buraya yapıştırın. Her yeni kelime/ifade bir sonraki satırdan başlamalıdır."
},
"apply_paste": {
"message": "Kelimeleri Ekle"
@@ -100,5 +103,80 @@
},
"match_whole": {
"message": "Tüm kelimeyle eşleş"
},
"site_exceptions": {
"message": "Site İstisnaları"
},
"add_exception": {
"message": "İstisnalara Ekle"
},
"remove_exception": {
"message": "Mevcut olanı kaldır"
},
"manage_exceptions": {
"message": "Yönet"
},
"exceptions_list": {
"message": "İstisna Siteleri:"
},
"clear_all": {
"message": "Hepsini Temizle"
},
"confirm_clear_exceptions": {
"message": "Tüm istisnaları temizlemek istediğinizden emin misiniz?"
},
"remove": {
"message": "Kaldır"
},
"tab_lists": {
"message": "Listeler"
},
"tab_words": {
"message": "Kelimeler"
},
"tab_exceptions": {
"message": "İstisnalar"
},
"no_exceptions": {
"message": "İstisna yok"
},
"toggle_highlighting_title": {
"message": "Vurgulamayı aç/kapat"
},
"toggle_dark_mode_title": {
"message": "Karanlık modu aç/kapat"
},
"select_title": {
"message": "Seç"
},
"word_placeholder": {
"message": "Kelime veya ifade"
},
"background_color_title": {
"message": "Arka plan rengi"
},
"text_color_title": {
"message": "Metin rengi"
},
"tab_page_highlights": {
"message": "Sayfada"
},
"highlights_on_page": {
"message": "Bu sayfadaki vurgular"
},
"total_highlights": {
"message": "Toplam"
},
"refresh": {
"message": "Yenile"
},
"no_highlights_on_page": {
"message": "Bu sayfada vurgu bulunamadı"
},
"previous": {
"message": "Önceki"
},
"next": {
"message": "Sonraki"
}
}

View File

@@ -26,8 +26,11 @@
"enable_highlight": {
"message": "启用高亮"
},
"apply": {
"message": "应用"
},
"paste_hint": {
"message": "在此粘贴单词"
"message": "在此粘贴单词或短语。每个新单词/短语应从下一行开始。"
},
"apply_paste": {
"message": "添加单词"
@@ -100,5 +103,80 @@
},
"match_whole": {
"message": "全词匹配"
},
"site_exceptions": {
"message": "网站例外"
},
"add_exception": {
"message": "添加到例外"
},
"remove_exception": {
"message": "移除当前"
},
"manage_exceptions": {
"message": "管理"
},
"exceptions_list": {
"message": "例外网站:"
},
"clear_all": {
"message": "清除全部"
},
"confirm_clear_exceptions": {
"message": "您确定要清除所有例外吗?"
},
"remove": {
"message": "移除"
},
"tab_lists": {
"message": "列表"
},
"tab_words": {
"message": "单词"
},
"tab_exceptions": {
"message": "例外"
},
"no_exceptions": {
"message": "无例外"
},
"toggle_highlighting_title": {
"message": "切换高亮"
},
"toggle_dark_mode_title": {
"message": "切换暗黑模式"
},
"select_title": {
"message": "选择"
},
"word_placeholder": {
"message": "单词或短语"
},
"background_color_title": {
"message": "背景颜色"
},
"text_color_title": {
"message": "文字颜色"
},
"tab_page_highlights": {
"message": "页面上"
},
"highlights_on_page": {
"message": "此页面上的高亮"
},
"total_highlights": {
"message": "总计"
},
"refresh": {
"message": "刷新"
},
"no_highlights_on_page": {
"message": "此页面上未找到高亮"
},
"previous": {
"message": "上一个"
},
"next": {
"message": "下一个"
}
}

View File

@@ -1,10 +0,0 @@
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === "complete" && /^https?:/.test(tab.url)) {
chrome.scripting.executeScript({
target: { tabId },
files: ["main.js"]
}).catch(err => {
console.warn("Injection failed:", err);
});
}
});

40
eslint.config.mjs Normal file
View File

@@ -0,0 +1,40 @@
import globals from 'globals';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import js from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default [...compat.extends('eslint:recommended'), {
languageOptions: {
globals: {
...globals.browser,
...globals.webextensions,
chrome: 'readonly',
},
ecmaVersion: 12,
sourceType: 'module',
},
rules: {
semi: ['error', 'always'],
quotes: ['error', 'single'],
},
}, {
files: ['scripts/**/*.js'],
languageOptions: {
globals: {
...globals.node,
},
ecmaVersion: 12,
sourceType: 'module',
},
}];

BIN
img/logo-outlined.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

BIN
img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

155
main.js
View File

@@ -1,155 +0,0 @@
let currentLists = [];
let isGlobalHighlightEnabled = true;
let matchCase = false;
let matchWhole = false;
function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function clearHighlights() {
// Remove all <mark> elements added by the highlighter
const marks = document.querySelectorAll('mark[data-gh]');
for (const mark of marks) {
// Replace the <mark> with its text content
const parent = mark.parentNode;
if (parent) {
parent.replaceChild(document.createTextNode(mark.textContent), mark);
parent.normalize(); // Merge adjacent text nodes
}
}
}
function processNodes() {
observer.disconnect();
clearHighlights();
// If global highlighting is disabled, skip processing
if (!isGlobalHighlightEnabled) {
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
return;
}
const textNodes = [];
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode: node => {
if (node.parentNode && node.parentNode.nodeName === 'MARK') return NodeFilter.FILTER_REJECT;
if (node.parentNode && ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME'].includes(node.parentNode.nodeName)) return NodeFilter.FILTER_REJECT;
if (!node.nodeValue.trim()) return NodeFilter.FILTER_SKIP;
return NodeFilter.FILTER_ACCEPT;
}
});
while (walker.nextNode()) textNodes.push(walker.currentNode);
const activeWords = [];
for (const list of currentLists) {
if (!list.active) continue;
for (const word of list.words) {
if (!word.active) continue;
activeWords.push({
text: word.wordStr,
background: word.background || list.background,
foreground: word.foreground || list.foreground
});
}
}
if (activeWords.length > 0) {
const wordMap = new Map();
for (const word of activeWords) {
wordMap.set(matchCase ? word.text : word.text.toLowerCase(), word);
}
let flags = matchCase ? 'gu' : 'giu';
let wordsPattern = Array.from(wordMap.keys()).map(escapeRegex).join('|');
if (matchWhole) {
wordsPattern = `(?:(?<!\\p{L})|^)(${wordsPattern})(?:(?!\\p{L})|$)`;
}
try {
const pattern = new RegExp(`(${wordsPattern})`, flags);
for (const node of textNodes) {
if (!node.nodeValue || !pattern.test(node.nodeValue)) continue;
const span = document.createElement('span');
span.innerHTML = node.nodeValue.replace(pattern, match => {
const lookup = matchCase ? match : match.toLowerCase();
const word = wordMap.get(lookup) || { background: '#ffff00', foreground: '#000000' };
return `<mark data-gh style="background:${word.background};color:${word.foreground};padding:0 2px;">${match}</mark>`;
});
node.parentNode.replaceChild(span, node);
}
} catch (e) {
console.error("Regex error:", e);
}
}
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
}
const debouncedProcessNodes = debounce(processNodes, 300);
function setListsAndUpdate(lists) {
currentLists = lists;
debouncedProcessNodes();
}
// Debounce helper function
function debounce(func, wait) {
let timeout;
return function () {
const context = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
// Initial highlight on load
chrome.storage.local.get(["lists", "globalHighlightEnabled", "matchCaseEnabled", "matchWholeEnabled"], ({ lists, globalHighlightEnabled, matchCaseEnabled, matchWholeEnabled }) => {
if (Array.isArray(lists)) setListsAndUpdate(lists);
if (globalHighlightEnabled !== undefined) {
isGlobalHighlightEnabled = globalHighlightEnabled;
}
matchCase = !!matchCaseEnabled;
matchWhole = !!matchWholeEnabled;
processNodes();
});
// Listen for updates from the popup and re-apply highlights
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "WORD_LIST_UPDATED") {
chrome.storage.local.get("lists", ({ lists }) => {
if (Array.isArray(lists)) setListsAndUpdate(lists);
});
} else if (message.type === "GLOBAL_TOGGLE_UPDATED") {
isGlobalHighlightEnabled = message.enabled;
processNodes();
} else if (message.type === "MATCH_OPTIONS_UPDATED") {
matchCase = !!message.matchCase;
matchWhole = !!message.matchWhole;
processNodes();
}
});
// Set up observer and scroll handler
const observer = new MutationObserver(debouncedProcessNodes);
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
window.addEventListener('scroll', debouncedProcessNodes);

View File

@@ -2,11 +2,12 @@
"manifest_version": 3,
"name": "__MSG_extension_name__",
"description": "__MSG_extension_description__",
"version": "1.7.1",
"version": "1.10.0",
"default_locale": "en",
"permissions": [
"scripting",
"storage"
"storage",
"tabs"
],
"host_permissions": [
"<all_urls>"
@@ -16,7 +17,8 @@
"default_icon": "icons/icon128.png"
},
"background": {
"service_worker": "background.js"
"service_worker": "dist/background.js",
"type": "module"
},
"icons": {
"48": "icons/icon48.png",

2714
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,21 @@
{
"devDependencies": {
"@eslint/css": "^0.9.0",
"@eslint/js": "^9.30.0",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^7.1.0",
"@semantic-release/git": "^10.0.1",
"semantic-release": "^24.2.5"
"@types/chrome": "^0.0.270",
"eslint": "^9.30.0",
"globals": "^16.2.0",
"semantic-release": "^24.2.9",
"typescript": "^5.6.0"
},
"scripts": {
"prepare": "node scripts/update-manifest-version.js"
"build": "node scripts/build-content-standalone.js && tsc",
"watch": "tsc --watch",
"clean": "rimraf dist",
"rebuild": "npm run clean && npm run build",
"prepare": "npm run build && node scripts/update-manifest-version.js"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,92 +11,138 @@
</head>
<body class="dark">
<div class="loading-overlay">
<div class="loading-spinner"></div>
</div>
<div class="container">
<div class="header-bar">
<span class="title">
<i class="fa-solid fa-highlighter"></i> Goose Highlighter
<img src="../img/logo-outlined.png" alt="Goose Highlighter" style="height: 28px; vertical-align: middle; margin-right: 8px;"> Goose Highlighter
</span>
<div class="icon-toggles">
<label class="icon-toggle" title="Toggle highlighting">
<label class="icon-toggle" data-i18n-title="toggle_highlighting_title" title="Toggle highlighting">
<input type="checkbox" class="hidden-toggle" id="globalHighlightToggle" />
<i class="toggle-icon global-icon fa-solid"></i>
</label>
<label class="icon-toggle" title="Toggle dark mode">
<label class="icon-toggle" data-i18n-title="toggle_dark_mode_title" title="Toggle dark mode">
<input type="checkbox" class="hidden-toggle" id="themeToggle" />
<i class="toggle-icon theme-icon fa-solid"></i>
</label>
</div>
</div>
<div class="section">
<div class="section-header">
<h2><i class="fa-solid fa-list"></i> <span data-i18n="highlight_lists">Highlight Lists</span></h2>
</div>
<label for="listSelect" data-i18n="select_list">Select List:</label>
<select id="listSelect"></select>
<div class="button-row">
<button id="newListBtn"><i class="fa-solid fa-plus"></i> <span data-i18n="new_list">New</span></button>
<button id="deleteListBtn" class="danger"><i class="fa-solid fa-trash"></i> <span
data-i18n="delete_list">Delete</span></button>
</div>
<div class="tabs">
<button class="tab-button active" data-tab="lists"><i class="fa-solid fa-list"></i> <span data-i18n="tab_lists">Lists</span></button>
<button class="tab-button" data-tab="words"><i class="fa-solid fa-tags"></i> <span data-i18n="tab_words">Words</span></button>
<button class="tab-button" data-tab="page-highlights"><i class="fa-solid fa-location-dot"></i> <span data-i18n="tab_page_highlights">On Page</span></button>
<button class="tab-button" data-tab="exceptions"><i class="fa-solid fa-ban"></i> <span data-i18n="tab_exceptions">Exceptions</span></button>
<button class="tab-button" data-tab="options"><i class="fa-solid fa-sliders"></i> <span data-i18n="options">Options</span></button>
</div>
<div class="section">
<h2><i class="fa-solid fa-gear"></i> <span data-i18n="list_settings">List Settings</span></h2>
<label><span data-i18n="list_name">List Name:</span> <input type="text" id="listName" /></label>
<div class="color-row">
<div class="color-label">
<span data-i18n="background">Background:</span>
<input type="color" id="listBg" />
</div>
<div class="color-label">
<span data-i18n="foreground">Foreground:</span>
<input type="color" id="listFg" />
<div class="tab-content active" data-tab-content="lists">
<div class="section" data-section="lists">
<label for="listSelect" data-i18n="select_list">Select List:</label>
<select id="listSelect"></select>
<div class="button-row">
<button id="newListBtn"><i class="fa-solid fa-plus"></i> <span data-i18n="new_list">New</span></button>
<button id="deleteListBtn" class="danger"><i class="fa-solid fa-trash"></i> <span
data-i18n="delete_list">Delete</span></button>
</div>
</div>
<label>
<span data-i18n="enable_highlight">Enable Highlighting</span>
<input type="checkbox" class="switch" id="listActive" />
</label>
</div>
<div class="section">
<h2><i class="fa-solid fa-pen"></i> <span data-i18n="add_words">Add Words</span></h2>
<textarea id="bulkPaste" data-i18n="paste_hint" placeholder="Paste words here..."></textarea>
<button id="addWordsBtn"><span data-i18n="apply_paste">Add Words</span></button>
</div>
<div class="section">
<h2><i class="fa-solid fa-tags"></i> <span data-i18n="word_list">Word List</span>(<span id="wordCount">0</span>)
</h2>
<div class="button-row wrap">
<button id="selectAllBtn"><span data-i18n="select_all">Select All</span></button>
<button id="deselectAllBtn"><span data-i18n="deselect_all">Clear</span></button>
<button id="enableSelectedBtn"><span data-i18n="enable_selected">Enable</span></button>
<button id="disableSelectedBtn"><span data-i18n="disable_selected">Disable</span></button>
<button id="deleteSelectedBtn" class="danger"><span data-i18n="delete_selected">Delete</span></button>
<div class="section" data-section="settings">
<h3><i class="fa-solid fa-gear"></i> <span data-i18n="list_settings">List Settings</span></h3>
<label><span data-i18n="list_name">List Name:</span> <input type="text" id="listName" /></label>
<div class="color-row">
<div class="color-label">
<span data-i18n="background">Background:</span>
<input type="color" id="listBg" />
</div>
<div class="color-label">
<span data-i18n="foreground">Foreground:</span>
<input type="color" id="listFg" />
</div>
</div>
<label>
<span data-i18n="enable_highlight">Enable Highlighting</span>
<input type="checkbox" class="switch" id="listActive" />
</label>
<button id="applyListSettingsBtn"><i class="fa-solid fa-check"></i> <span data-i18n="apply">Apply</span></button>
</div>
<input type="text" id="wordSearch" data-i18n="search_placeholder" placeholder="Search..." />
<div id="wordList"></div>
</div>
<div class="section">
<h2><i class="fa-solid fa-sliders"></i> <span data-i18n="options">Options</span></h2>
<div class="button-row" style="margin-bottom:8px;">
<label><input type="checkbox" id="matchCase" /> <span data-i18n="match_case">Match Case</span></label>
<label><input type="checkbox" id="matchWhole" /> <span data-i18n="match_whole">Match Whole Word</span></label>
<div class="tab-content" data-tab-content="words">
<div class="section" data-section="addwords">
<h3><i class="fa-solid fa-pen"></i> <span data-i18n="add_words">Add Words</span></h3>
<textarea id="bulkPaste" data-i18n="paste_hint" placeholder="Paste words here..."></textarea>
<button id="addWordsBtn"><span data-i18n="apply_paste">Add Words</span></button>
</div>
<div class="button-row">
<button id="importBtn"><i class="fa-solid fa-upload"></i> <span data-i18n="import_list">Import</span></button>
<input type="file" id="importInput" accept="application/json" hidden />
<button id="exportBtn"><i class="fa-solid fa-download"></i> <span data-i18n="export_list">Export</span></button>
<div class="section" data-section="wordlist">
<h3><i class="fa-solid fa-tags"></i> <span data-i18n="word_list">Word List</span> (<span id="wordCount">0</span>)</h3>
<div class="button-row wrap">
<button id="selectAllBtn"><span data-i18n="select_all">Select All</span></button>
<button id="deselectAllBtn"><span data-i18n="deselect_all">Clear</span></button>
<button id="enableSelectedBtn"><span data-i18n="enable_selected">Enable</span></button>
<button id="disableSelectedBtn"><span data-i18n="disable_selected">Disable</span></button>
<button id="deleteSelectedBtn" class="danger"><span data-i18n="delete_selected">Delete</span></button>
</div>
<input type="text" id="wordSearch" data-i18n="search_placeholder" placeholder="Search..." />
<div id="wordList"></div>
</div>
</div>
<div class="tab-content" data-tab-content="page-highlights">
<div class="section" data-section="page-highlights">
<h3><i class="fa-solid fa-location-dot"></i> <span data-i18n="highlights_on_page">Highlights on This Page</span></h3>
<div class="page-highlights-info">
<span data-i18n="total_highlights">Total:</span> <strong id="totalHighlightsCount">0</strong>
</div>
<button id="refreshHighlightsBtn"><i class="fa-solid fa-rotate"></i> <span data-i18n="refresh">Refresh</span></button>
<div id="pageHighlightsList" class="page-highlights-list"></div>
</div>
</div>
<div class="tab-content" data-tab-content="exceptions">
<div class="section" data-section="exceptions">
<h3><i class="fa-solid fa-ban"></i> <span data-i18n="site_exceptions">Site Exceptions</span></h3>
<div class="button-row">
<button id="toggleExceptionBtn"><i class="fa-solid fa-plus"></i> <span id="exceptionBtnText" data-i18n="add_exception">Add to Exceptions</span></button>
</div>
<h3 data-i18n="exceptions_list">Exception Sites:</h3>
<div id="exceptionsList" class="exceptions-list"></div>
<button id="clearExceptionsBtn" class="danger"><i class="fa-solid fa-trash"></i> <span data-i18n="clear_all">Clear All</span></button>
</div>
</div>
<div class="tab-content" data-tab-content="options">
<div class="section" data-section="options">
<h3><i class="fa-solid fa-sliders"></i> <span data-i18n="options">Options</span></h3>
<div class="button-row" style="margin-bottom:8px;">
<label><input type="checkbox" id="matchCase" /> <span data-i18n="match_case">Match Case</span></label>
<label><input type="checkbox" id="matchWhole" /> <span data-i18n="match_whole">Match Whole Word</span></label>
</div>
<div class="button-row">
<button id="importBtn"><i class="fa-solid fa-upload"></i> <span data-i18n="import_list">Import</span></button>
<input type="file" id="importInput" accept="application/json" hidden />
<button id="exportBtn"><i class="fa-solid fa-download"></i> <span data-i18n="export_list">Export</span></button>
</div>
</div>
</div>
</div>
<script src="../storage.js"></script>
<script src="popup.js"></script>
<footer class="footer">
<div class="footer-content">
<span class="version">v<span id="version-number">...</span></span>
<a href="https://github.com/obsqrbtz/goose-highlighter" target="_blank" class="github-link">
<i class="fa-brands fa-github"></i>
<span>GitHub</span>
</a>
</div>
</footer>
<script type="module" src="../dist/popup/popup.js"></script>
</body>
</html>

View File

@@ -1,459 +0,0 @@
const listSelect = document.getElementById("listSelect");
const listName = document.getElementById("listName");
const listBg = document.getElementById("listBg");
const listFg = document.getElementById("listFg");
const listActive = document.getElementById("listActive");
const bulkPaste = document.getElementById("bulkPaste");
const wordList = document.getElementById("wordList");
const importInput = document.getElementById("importInput");
const matchCase = document.getElementById("matchCase");
const matchWhole = document.getElementById("matchWhole");
let lists = [];
let currentListIndex = 0;
let saveTimeout;
let selectedCheckboxes = new Set();
let globalHighlightEnabled = true;
let wordSearchQuery = "";
let matchCaseEnabled = false;
let matchWholeEnabled = false;
function escapeHtml(str) {
return str.replace(/[&<>"']/g, function (m) {
return ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
})[m];
});
}
async function debouncedSave() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(async () => {
await chrome.storage.local.set({ lists });
}, 500);
}
async function save() {
await chrome.storage.local.set({
lists: lists,
globalHighlightEnabled: globalHighlightEnabled,
matchCaseEnabled,
matchWholeEnabled
});
renderLists();
renderWords();
chrome.tabs.query({}, function (tabs) {
for (let tab of tabs) {
if (tab.id) {
chrome.tabs.sendMessage(tab.id, { type: "WORD_LIST_UPDATED" });
chrome.tabs.sendMessage(tab.id, {
type: "GLOBAL_TOGGLE_UPDATED",
enabled: globalHighlightEnabled
});
chrome.tabs.sendMessage(tab.id, {
type: "MATCH_OPTIONS_UPDATED",
matchCase: matchCaseEnabled,
matchWhole: matchWholeEnabled
});
}
}
});
}
async function updateGlobalToggleState() {
await chrome.storage.local.set({ globalHighlightEnabled: globalHighlightEnabled });
chrome.tabs.query({}, function (tabs) {
for (let tab of tabs) {
if (tab.id) {
chrome.tabs.sendMessage(tab.id, {
type: "GLOBAL_TOGGLE_UPDATED",
enabled: globalHighlightEnabled
});
}
}
});
}
async function load() {
const res = await chrome.storage.local.get({
lists: [],
globalHighlightEnabled: true,
matchCaseEnabled: false,
matchWholeEnabled: false
});
lists = res.lists;
globalHighlightEnabled = res.globalHighlightEnabled !== false;
matchCaseEnabled = !!res.matchCaseEnabled;
matchWholeEnabled = !!res.matchWholeEnabled;
matchCase.checked = matchCaseEnabled;
matchWhole.checked = matchWholeEnabled;
if (!lists.length) {
lists.push({
id: Date.now(),
name: chrome.i18n.getMessage("default_list_name"),
background: "#ffff00",
foreground: "#000000",
active: true,
words: []
});
}
renderLists();
renderWords();
document.getElementById("globalHighlightToggle").checked = globalHighlightEnabled;
}
function renderLists() {
listSelect.innerHTML = lists.map((list, index) =>
`<option value="${index}">${escapeHtml(list.name)}</option>`
).join("");
listSelect.value = currentListIndex;
updateListForm();
}
function updateListForm() {
const list = lists[currentListIndex];
listName.value = list.name;
listBg.value = list.background;
listFg.value = list.foreground;
listActive.checked = list.active;
}
function renderWords() {
const list = lists[currentListIndex];
let filteredWords = list.words;
if (wordSearchQuery.trim()) {
const q = wordSearchQuery.trim().toLowerCase();
filteredWords = list.words.filter(w => w.wordStr.toLowerCase().includes(q));
}
const itemHeight = 32;
const containerHeight = wordList.clientHeight;
const scrollTop = wordList.scrollTop;
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 2,
filteredWords.length
);
wordList.innerHTML = '';
const spacer = document.createElement('div');
spacer.style.position = 'relative';
spacer.style.height = `${filteredWords.length * itemHeight}px`;
spacer.style.width = '100%';
for (let i = startIndex; i < endIndex; i++) {
const w = filteredWords[i];
if (!w) continue;
const container = document.createElement("div");
container.style.height = `${itemHeight}px`;
container.style.position = 'absolute';
container.style.top = `${i * itemHeight}px`;
container.style.width = 'calc(100% - 8px)';
container.style.left = '4px';
container.style.right = '4px';
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.gap = '6px';
container.style.padding = '0 4px';
container.style.boxSizing = 'border-box';
container.style.background = 'var(--highlight-tag)';
container.style.border = '1px solid var(--highlight-tag-border)';
const realIndex = list.words.indexOf(w);
const cbSelect = document.createElement("input");
cbSelect.type = "checkbox";
cbSelect.className = "word-checkbox";
cbSelect.dataset.index = realIndex;
if (selectedCheckboxes.has(realIndex)) {
cbSelect.checked = true;
}
const inputWord = document.createElement("input");
inputWord.type = "text";
inputWord.value = w.wordStr;
inputWord.dataset.wordEdit = realIndex;
inputWord.style.flexGrow = '1';
inputWord.style.minWidth = '0';
inputWord.style.padding = '4px 8px';
inputWord.style.borderRadius = '4px';
inputWord.style.border = '1px solid var(--input-border)';
inputWord.style.backgroundColor = 'var(--input-bg)';
inputWord.style.color = 'var(--text-color)';
const inputBg = document.createElement("input");
inputBg.type = "color";
inputBg.value = w.background || list.background;
inputBg.dataset.bgEdit = realIndex;
inputBg.style.width = '24px';
inputBg.style.height = '24px';
inputBg.style.flexShrink = '0';
const inputFg = document.createElement("input");
inputFg.type = "color";
inputFg.value = w.foreground || list.foreground;
inputFg.dataset.fgEdit = realIndex;
inputFg.style.width = '24px';
inputFg.style.height = '24px';
inputFg.style.flexShrink = '0';
const activeContainer = document.createElement("label");
activeContainer.className = "word-active";
activeContainer.style.display = 'flex';
activeContainer.style.alignItems = 'center';
activeContainer.style.gap = '4px';
activeContainer.style.flexShrink = '0';
const cbActive = document.createElement("input");
cbActive.type = "checkbox";
cbActive.checked = w.active !== false;
cbActive.dataset.activeEdit = realIndex;
cbActive.className = "switch";
activeContainer.appendChild(cbActive);
container.appendChild(cbSelect);
container.appendChild(inputWord);
container.appendChild(inputBg);
container.appendChild(inputFg);
container.appendChild(activeContainer);
spacer.appendChild(container);
}
wordList.appendChild(spacer);
const wordCount = document.getElementById('wordCount');
if (wordCount) {
wordCount.textContent = filteredWords.length;
}
}
document.addEventListener('DOMContentLoaded', () => {
localizePage();
document.getElementById("selectAllBtn").onclick = () => {
const list = lists[currentListIndex];
list.words.forEach((_, index) => {
selectedCheckboxes.add(index);
});
renderWords();
};
document.getElementById("globalHighlightToggle").addEventListener('change', function () {
globalHighlightEnabled = this.checked;
updateGlobalToggleState();
});
wordList.addEventListener("change", e => {
if (e.target.type === "checkbox") {
if (e.target.dataset.index != null) {
if (e.target.checked) {
selectedCheckboxes.add(+e.target.dataset.index);
} else {
selectedCheckboxes.delete(+e.target.dataset.index);
}
renderWords();
} else if (e.target.dataset.activeEdit != null) {
lists[currentListIndex].words[e.target.dataset.activeEdit].active = e.target.checked;
save();
}
}
});
let scrollTimeout;
wordList.addEventListener('scroll', () => {
if (scrollTimeout) {
return;
}
scrollTimeout = setTimeout(() => {
requestAnimationFrame(renderWords);
scrollTimeout = null;
}, 16); // ~60fps
});
listSelect.onchange = () => {
selectedCheckboxes.clear();
currentListIndex = +listSelect.value;
renderWords();
updateListForm();
};
document.getElementById("newListBtn").onclick = () => {
lists.push({
id: Date.now(),
name: chrome.i18n.getMessage("new_list_name"),
background: "#ffff00",
foreground: "#000000",
active: true,
words: []
});
currentListIndex = lists.length - 1;
save();
};
document.getElementById("deleteListBtn").onclick = () => {
if (confirm(chrome.i18n.getMessage("confirm_delete_list"))) {
lists.splice(currentListIndex, 1);
currentListIndex = Math.max(0, currentListIndex - 1);
save();
}
};
listName.oninput = () => { lists[currentListIndex].name = listName.value; save(); };
listBg.oninput = () => { lists[currentListIndex].background = listBg.value; save(); };
listFg.oninput = () => { lists[currentListIndex].foreground = listFg.value; save(); };
listActive.onchange = () => { lists[currentListIndex].active = listActive.checked; save(); };
document.getElementById("addWordsBtn").onclick = () => {
const words = bulkPaste.value.split(/\n+/).map(w => w.trim()).filter(Boolean);
const list = lists[currentListIndex];
for (const w of words) list.words.push({ wordStr: w, background: "", foreground: "", active: true });
bulkPaste.value = "";
save();
};
document.getElementById("deleteSelectedBtn").onclick = () => {
if (confirm(chrome.i18n.getMessage("confirm_delete_words"))) {
const list = lists[currentListIndex];
const toDelete = Array.from(selectedCheckboxes);
lists[currentListIndex].words = list.words.filter((_, i) => !toDelete.includes(i));
selectedCheckboxes.clear();
save();
renderWords();
}
};
document.getElementById("disableSelectedBtn").onclick = () => {
const list = lists[currentListIndex];
selectedCheckboxes.forEach(index => {
list.words[index].active = false;
});
save();
renderWords();
};
document.getElementById("enableSelectedBtn").onclick = () => {
const list = lists[currentListIndex];
selectedCheckboxes.forEach(index => {
list.words[index].active = true;
});
save();
renderWords();
};
wordList.addEventListener("input", e => {
const index = e.target.dataset.wordEdit ?? e.target.dataset.bgEdit ?? e.target.dataset.fgEdit;
if (index == null) return;
const word = lists[currentListIndex].words[index];
if (e.target.dataset.wordEdit != null) word.wordStr = e.target.value;
if (e.target.dataset.bgEdit != null) word.background = e.target.value;
if (e.target.dataset.fgEdit != null) word.foreground = e.target.value;
save();
});
const exportBtn = document.getElementById("exportBtn");
exportBtn.onclick = () => {
const blob = new Blob([JSON.stringify(lists, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "highlight-lists.json";
a.click();
URL.revokeObjectURL(url);
};
const importBtn = document.getElementById("importBtn");
importBtn.onclick = () => importInput.click();
importInput.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
try {
const data = JSON.parse(e.target.result);
if (Array.isArray(data)) {
lists = data;
currentListIndex = 0;
save();
}
} catch (err) {
alert(chrome.i18n.getMessage("invalid_json_error"));
}
};
reader.readAsText(file);
};
function localizePage() {
const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(element => {
const message = element.dataset.i18n;
const localizedText = chrome.i18n.getMessage(message);
if (localizedText) {
if (element.tagName === 'INPUT' && element.hasAttribute('placeholder')) {
element.placeholder = localizedText;
} else {
element.textContent = localizedText;
}
}
});
}
const toggle = document.getElementById('themeToggle');
const body = document.body;
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
body.classList.remove('dark');
body.classList.add('light');
toggle.checked = false;
} else {
body.classList.add('dark');
body.classList.remove('light');
toggle.checked = true;
}
toggle.addEventListener('change', () => {
if (toggle.checked) {
body.classList.add('dark');
body.classList.remove('light');
localStorage.setItem('theme', 'dark');
} else {
body.classList.remove('dark');
body.classList.add('light');
localStorage.setItem('theme', 'light');
}
});
document.getElementById("deselectAllBtn").onclick = () => {
selectedCheckboxes.clear();
renderWords();
};
const wordSearch = document.getElementById("wordSearch");
wordSearch.addEventListener("input", (e) => {
wordSearchQuery = e.target.value;
renderWords();
});
matchCase.addEventListener('change', () => {
matchCaseEnabled = matchCase.checked;
save();
});
matchWhole.addEventListener('change', () => {
matchWholeEnabled = matchWhole.checked;
save();
});
load();
});

View File

@@ -0,0 +1,39 @@
const fs = require('fs');
const typesContent = fs.readFileSync('src/types.ts', 'utf8');
const domUtilsContent = fs.readFileSync('src/utils/DOMUtils.ts', 'utf8');
const storageServiceContent = fs.readFileSync('src/services/StorageService.ts', 'utf8');
const messageServiceContent = fs.readFileSync('src/services/MessageService.ts', 'utf8');
const highlightEngineContent = fs.readFileSync('src/content/HighlightEngine.ts', 'utf8');
const contentScriptContent = fs.readFileSync('src/content/ContentScript.ts', 'utf8');
const mainContent = fs.readFileSync('src/main.ts', 'utf8');
function extractDefinitions(content, filename) {
// Remove import statements
let cleaned = content.replace(/^import\s+.*?;?\s*$/gm, '');
// Remove export keywords but keep the definitions
cleaned = cleaned.replace(/^export\s+/gm, '');
// Add a comment header
cleaned = `// === ${filename} ===\n${cleaned}\n`;
return cleaned;
}
// Extract and combine all definitions
const combinedContent = `// Auto-generated standalone content script
// Do not edit this file directly - edit the source files and rebuild
${extractDefinitions(typesContent, 'types.ts')}
${extractDefinitions(domUtilsContent, 'utils/DOMUtils.ts')}
${extractDefinitions(storageServiceContent, 'services/StorageService.ts')}
${extractDefinitions(messageServiceContent, 'services/MessageService.ts')}
${extractDefinitions(highlightEngineContent, 'content/HighlightEngine.ts')}
${extractDefinitions(contentScriptContent, 'content/ContentScript.ts')}
${extractDefinitions(mainContent, 'main.ts')}
`;
fs.writeFileSync('src/content-standalone.ts', combinedContent);
console.log('Generated standalone content script');

View File

@@ -1,13 +1,13 @@
const fs = require("fs");
const fs = require('fs');
const version = process.argv[2];
if (!version) {
console.error("❌ No version passed");
process.exit(1);
console.log('No version passed, skipping manifest update');
process.exit(0);
}
const manifest = JSON.parse(fs.readFileSync("manifest.json", "utf-8"));
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf-8'));
manifest.version = version;
fs.writeFileSync("manifest.json", JSON.stringify(manifest, null, 2));
fs.writeFileSync('manifest.json', JSON.stringify(manifest, null, 2));
console.log(`Updated manifest.json to version ${version}`);
console.log(`Updated manifest.json to version ${version}`);

36
src/background.ts Normal file
View File

@@ -0,0 +1,36 @@
import { StorageService } from './services/StorageService.js';
class BackgroundService {
constructor() {
this.initialize();
}
private initialize(): void {
this.setupTabUpdateListener();
this.setupInstallListener();
}
private setupTabUpdateListener(): void {
chrome.tabs.onUpdated.addListener((tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab): void => {
if (changeInfo.status === 'complete' && tab.url && /^https?:/.test(tab.url)) {
chrome.scripting.executeScript({
target: { tabId },
files: ['dist/content-standalone.js']
}).catch((err: unknown) => {
console.warn('Injection failed:', err);
});
}
});
}
private setupInstallListener(): void {
chrome.runtime.onInstalled.addListener(async (): Promise<void> => {
const data = await StorageService.get(['exceptionsList']);
if (!data.exceptionsList) {
await StorageService.update('exceptionsList', []);
}
});
}
}
new BackgroundService();

View File

@@ -0,0 +1,139 @@
import { HighlightList, MessageData } from '../types.js';
import { StorageService } from '../services/StorageService.js';
import { MessageService } from '../services/MessageService.js';
import { HighlightEngine } from './HighlightEngine.js';
export class ContentScript {
private lists: HighlightList[] = [];
private isGlobalHighlightEnabled = true;
private exceptionsList: string[] = [];
private isCurrentSiteException = false;
private matchCase = false;
private matchWhole = false;
private highlightEngine: HighlightEngine;
private isProcessing = false;
constructor() {
this.highlightEngine = new HighlightEngine(() => this.processHighlights());
this.initialize();
}
private async initialize(): Promise<void> {
await this.loadSettings();
this.setupMessageListener();
this.processHighlights();
}
private async loadSettings(): Promise<void> {
const data = await StorageService.get([
'lists',
'globalHighlightEnabled',
'matchCaseEnabled',
'matchWholeEnabled',
'exceptionsList'
]);
this.lists = data.lists || [];
this.isGlobalHighlightEnabled = data.globalHighlightEnabled ?? true;
this.matchCase = data.matchCaseEnabled ?? false;
this.matchWhole = data.matchWholeEnabled ?? false;
this.exceptionsList = data.exceptionsList || [];
this.isCurrentSiteException = this.checkCurrentSiteException();
}
private checkCurrentSiteException(): boolean {
const currentHostname = window.location.hostname;
return this.exceptionsList.includes(currentHostname);
}
private setupMessageListener(): void {
MessageService.onMessage((message: MessageData, sender: any, sendResponse: (response?: any) => void) => {
switch (message.type) {
case 'WORD_LIST_UPDATED':
this.handleWordListUpdate();
return false;
case 'GLOBAL_TOGGLE_UPDATED':
this.handleGlobalToggleUpdate(message.enabled!);
return false;
case 'MATCH_OPTIONS_UPDATED':
this.handleMatchOptionsUpdate(message.matchCase!, message.matchWhole!);
return false;
case 'EXCEPTIONS_LIST_UPDATED':
this.handleExceptionsUpdate();
return false;
case 'GET_PAGE_HIGHLIGHTS':
this.handleGetPageHighlights(sendResponse);
return true;
case 'SCROLL_TO_HIGHLIGHT':
this.handleScrollToHighlight(message.word!, message.index!);
return false;
}
return false;
});
}
private async handleWordListUpdate(): Promise<void> {
const data = await StorageService.get(['lists']);
this.lists = data.lists || [];
this.processHighlights();
}
private handleGlobalToggleUpdate(enabled: boolean): void {
this.isGlobalHighlightEnabled = enabled;
this.processHighlights();
}
private handleMatchOptionsUpdate(matchCase: boolean, matchWhole: boolean): void {
this.matchCase = matchCase;
this.matchWhole = matchWhole;
this.processHighlights();
}
private async handleExceptionsUpdate(): Promise<void> {
const data = await StorageService.get(['exceptionsList']);
this.exceptionsList = data.exceptionsList || [];
this.isCurrentSiteException = this.checkCurrentSiteException();
this.processHighlights();
}
private processHighlights(): void {
if (this.isProcessing) return;
this.isProcessing = true;
try {
if (!this.isGlobalHighlightEnabled || this.isCurrentSiteException) {
this.highlightEngine.clearHighlights();
this.highlightEngine.stopObserving();
return;
}
this.highlightEngine.highlight(this.lists, this.matchCase, this.matchWhole);
} finally {
this.isProcessing = false;
}
}
private handleGetPageHighlights(sendResponse: (response: any) => void): void {
const activeWords: ActiveWord[] = [];
for (const list of this.lists) {
if (!list.active) continue;
for (const word of list.words) {
if (!word.active) continue;
activeWords.push({
text: word.wordStr,
background: word.background || list.background,
foreground: word.foreground || list.foreground
});
}
}
const highlights = this.highlightEngine.getPageHighlights(activeWords);
sendResponse({ highlights });
}
private handleScrollToHighlight(word: string, index: number): void {
this.highlightEngine.scrollToHighlight(word, index);
}
}

View File

@@ -0,0 +1,286 @@
import { HighlightList, ActiveWord, CONSTANTS } from '../types.js';
import { DOMUtils } from '../utils/DOMUtils.js';
export class HighlightEngine {
private styleSheet: CSSStyleSheet | null = null;
private highlights = new Map<string, Highlight>();
private highlightsByWord = new Map<string, Range[]>();
private observer: MutationObserver;
private isHighlighting = false;
private currentMatchCase = false;
constructor(private onUpdate: () => void) {
this.observer = new MutationObserver(DOMUtils.debounce((mutations: MutationRecord[]) => {
if (this.isHighlighting) return;
const hasContentChanges = mutations.some((mutation: MutationRecord) => {
if (mutation.type !== 'childList') return false;
const allNodes = [...Array.from(mutation.addedNodes), ...Array.from(mutation.removedNodes)];
return allNodes.some(node => {
if (node.nodeType === Node.TEXT_NODE) return true;
if (node instanceof Element) return true;
return false;
});
});
if (hasContentChanges) {
this.onUpdate();
}
}, CONSTANTS.DEBOUNCE_DELAY));
}
private initializeStyleSheet(): void {
if (!this.styleSheet) {
const style = document.createElement('style');
style.id = 'goose-highlighter-styles';
document.head.appendChild(style);
this.styleSheet = style.sheet!;
}
}
private updateHighlightStyles(activeWords: ActiveWord[]): void {
this.initializeStyleSheet();
while (this.styleSheet!.cssRules.length > 0) {
this.styleSheet!.deleteRule(0);
}
const uniqueStyles = new Map<string, number>();
let styleIndex = 0;
for (const word of activeWords) {
const styleKey = `${word.background}-${word.foreground}`;
if (!uniqueStyles.has(styleKey)) {
uniqueStyles.set(styleKey, styleIndex);
const rule = `::highlight(gh-${styleIndex}) { background-color: ${word.background}; color: ${word.foreground}; }`;
this.styleSheet!.insertRule(rule, this.styleSheet!.cssRules.length);
styleIndex++;
}
}
}
clearHighlights(): void {
this.observer.disconnect();
this.clearHighlightsInternal();
}
private getTextNodes(): Text[] {
const textNodes: Text[] = [];
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node: Text) => {
if (node.parentNode && ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME'].includes(node.parentNode.nodeName)) {
return NodeFilter.FILTER_REJECT;
}
if (!node.nodeValue?.trim()) {
return NodeFilter.FILTER_SKIP;
}
return NodeFilter.FILTER_ACCEPT;
}
}
);
while (walker.nextNode()) {
textNodes.push(walker.currentNode as Text);
}
return textNodes;
}
private extractActiveWords(lists: HighlightList[]): ActiveWord[] {
const activeWords: ActiveWord[] = [];
for (const list of lists) {
if (!list.active) continue;
for (const word of list.words) {
if (!word.active) continue;
activeWords.push({
text: word.wordStr,
background: word.background || list.background,
foreground: word.foreground || list.foreground
});
}
}
return activeWords;
}
highlight(lists: HighlightList[], matchCase: boolean, matchWhole: boolean): void {
if (this.isHighlighting) return;
this.isHighlighting = true;
this.currentMatchCase = matchCase;
this.observer.disconnect();
this.clearHighlightsInternal();
const activeWords = this.extractActiveWords(lists);
if (activeWords.length === 0) {
this.startObserving();
this.isHighlighting = false;
return;
}
this.updateHighlightStyles(activeWords);
const styleMap = new Map<string, number>();
const uniqueStyles = new Map<string, number>();
let styleIndex = 0;
for (const word of activeWords) {
const styleKey = `${word.background}-${word.foreground}`;
if (!uniqueStyles.has(styleKey)) {
uniqueStyles.set(styleKey, styleIndex++);
}
const lookup = matchCase ? word.text : word.text.toLowerCase();
styleMap.set(lookup, uniqueStyles.get(styleKey)!);
}
const flags = matchCase ? 'gu' : 'giu';
let wordsPattern = Array.from(styleMap.keys()).map(DOMUtils.escapeRegex).join('|');
if (matchWhole) {
wordsPattern = `(?:(?<!\\p{L})|^)(${wordsPattern})(?:(?!\\p{L})|$)`;
}
try {
const pattern = new RegExp(`(${wordsPattern})`, flags);
const textNodes = this.getTextNodes();
const rangesByStyle = new Map<number, Range[]>();
this.highlightsByWord.clear();
for (const node of textNodes) {
if (!node.nodeValue) continue;
const text = node.nodeValue;
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(text)) !== null) {
const lookup = matchCase ? match[0] : match[0].toLowerCase();
const styleIdx = styleMap.get(lookup);
if (styleIdx !== undefined) {
const range = new Range();
range.setStart(node, match.index);
range.setEnd(node, match.index + match[0].length);
if (!rangesByStyle.has(styleIdx)) {
rangesByStyle.set(styleIdx, []);
}
rangesByStyle.get(styleIdx)!.push(range);
if (!this.highlightsByWord.has(lookup)) {
this.highlightsByWord.set(lookup, []);
}
this.highlightsByWord.get(lookup)!.push(range);
}
}
}
for (const [styleIdx, ranges] of rangesByStyle) {
const highlight = new Highlight(...ranges);
const highlightName = `gh-${styleIdx}`;
this.highlights.set(highlightName, highlight);
CSS.highlights.set(highlightName, highlight);
}
} catch (e) {
console.error('Regex error:', e);
}
this.startObserving();
this.isHighlighting = false;
}
private clearHighlightsInternal(): void {
for (const name of this.highlights.keys()) {
CSS.highlights.delete(name);
}
this.highlights.clear();
this.highlightsByWord.clear();
if (this.styleSheet && this.styleSheet.cssRules.length > 0) {
while (this.styleSheet.cssRules.length > 0) {
this.styleSheet.deleteRule(0);
}
}
}
getPageHighlights(activeWords: ActiveWord[]): Array<{ word: string; count: number; background: string; foreground: string }> {
const seen = new Map<string, { word: string; count: number; background: string; foreground: string }>();
for (const activeWord of activeWords) {
const lookup = this.currentMatchCase ? activeWord.text : activeWord.text.toLowerCase();
const ranges = this.highlightsByWord.get(lookup);
if (ranges && ranges.length > 0 && !seen.has(lookup)) {
seen.set(lookup, {
word: activeWord.text,
count: ranges.length,
background: activeWord.background,
foreground: activeWord.foreground
});
}
}
return Array.from(seen.values());
}
scrollToHighlight(word: string, index: number): void {
const lookup = this.currentMatchCase ? word : word.toLowerCase();
const ranges = this.highlightsByWord.get(lookup);
if (!ranges || ranges.length === 0) return;
const targetIndex = Math.min(index, ranges.length - 1);
const range = ranges[targetIndex];
if (!range) return;
try {
const rect = range.getBoundingClientRect();
const absoluteTop = window.pageYOffset + rect.top;
const middle = absoluteTop - (window.innerHeight / 2) + (rect.height / 2);
window.scrollTo({
top: middle,
behavior: 'smooth'
});
const flashHighlight = new Highlight(range);
CSS.highlights.set('gh-flash', flashHighlight);
if (this.styleSheet) {
const flashRule = '::highlight(gh-flash) { background-color: rgba(255, 165, 0, 0.8); box-shadow: 0 0 10px 3px rgba(255, 165, 0, 0.8); }';
const ruleIndex = this.styleSheet.insertRule(flashRule, this.styleSheet.cssRules.length);
setTimeout(() => {
CSS.highlights.delete('gh-flash');
if (this.styleSheet && ruleIndex < this.styleSheet.cssRules.length) {
this.styleSheet.deleteRule(ruleIndex);
}
}, 600);
}
} catch (e) {
console.error('Error scrolling to highlight:', e);
}
}
stopObserving(): void {
this.observer.disconnect();
}
private startObserving(): void {
this.observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false
});
}
destroy(): void {
this.observer.disconnect();
this.clearHighlights();
}
}

3
src/main.ts Normal file
View File

@@ -0,0 +1,3 @@
import { ContentScript } from './content/ContentScript.js';
new ContentScript();

View File

@@ -0,0 +1,719 @@
import { HighlightList, HighlightWord, ExportData, HighlightInfo } from '../types.js';
import { StorageService } from '../services/StorageService.js';
import { MessageService } from '../services/MessageService.js';
import { DOMUtils } from '../utils/DOMUtils.js';
export class PopupController {
private lists: HighlightList[] = [];
private currentListIndex = 0;
private selectedCheckboxes = new Set<number>();
private globalHighlightEnabled = true;
private wordSearchQuery = '';
private matchCaseEnabled = false;
private matchWholeEnabled = false;
private exceptionsList: string[] = [];
private currentTabHost = '';
private activeTab = 'lists';
private pageHighlights: Array<{ word: string; count: number; background: string; foreground: string }> = [];
private highlightIndices = new Map<string, number>();
async initialize(): Promise<void> {
await this.loadData();
await this.getCurrentTab();
this.loadActiveTab();
this.translateTitles();
this.setupEventListeners();
this.render();
this.hideLoadingOverlay();
}
private hideLoadingOverlay(): void {
const overlay = document.querySelector('.loading-overlay');
if (overlay) {
overlay.classList.add('hidden');
setTimeout(() => overlay.remove(), 200);
}
}
private async loadData(): Promise<void> {
const data = await StorageService.get();
this.lists = data.lists || [];
this.globalHighlightEnabled = data.globalHighlightEnabled ?? true;
this.matchCaseEnabled = data.matchCaseEnabled ?? false;
this.matchWholeEnabled = data.matchWholeEnabled ?? false;
this.exceptionsList = data.exceptionsList || [];
if (this.lists.length === 0) {
this.lists.push({
id: Date.now(),
name: chrome.i18n.getMessage('default_list_name') || 'Default List',
background: '#ffff00',
foreground: '#000000',
active: true,
words: []
});
}
}
private async getCurrentTab(): Promise<void> {
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab?.url) {
const url = new URL(tab.url);
this.currentTabHost = url.hostname;
}
} catch (e) {
console.warn('Could not get current tab:', e);
}
}
private loadActiveTab(): void {
const saved = localStorage.getItem('goose-highlighter-active-tab');
if (saved) {
this.activeTab = saved;
}
}
private translateTitles(): void {
document.querySelectorAll('[data-i18n-title]').forEach(element => {
const key = element.getAttribute('data-i18n-title');
if (key) {
const translation = chrome.i18n.getMessage(key);
if (translation) {
element.setAttribute('title', translation);
}
}
});
}
private saveActiveTab(): void {
localStorage.setItem('goose-highlighter-active-tab', this.activeTab);
}
private switchTab(tabName: string): void {
this.activeTab = tabName;
this.saveActiveTab();
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-tab') === tabName);
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.getAttribute('data-tab-content') === tabName);
});
if (tabName === 'page-highlights') {
this.loadPageHighlights();
}
}
private setupEventListeners(): void {
this.setupTabs();
this.setupListManagement();
this.setupWordManagement();
this.setupSettings();
this.setupPageHighlights();
this.setupExceptions();
this.setupImportExport();
this.setupTheme();
}
private setupTabs(): void {
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', () => {
const tabName = (button as HTMLElement).getAttribute('data-tab');
if (tabName) this.switchTab(tabName);
});
});
this.switchTab(this.activeTab);
}
private setupListManagement(): void {
const listSelect = document.getElementById('listSelect') as HTMLSelectElement;
listSelect.addEventListener('change', () => {
this.selectedCheckboxes.clear();
this.currentListIndex = +listSelect.value;
this.renderWords();
this.updateListForm();
});
// Apply button for list settings
document.getElementById('applyListSettingsBtn')?.addEventListener('click', () => {
this.applyListSettings();
});
document.getElementById('newListBtn')?.addEventListener('click', () => {
this.lists.push({
id: Date.now(),
name: chrome.i18n.getMessage('new_list_name') || 'New List',
background: '#ffff00',
foreground: '#000000',
active: true,
words: []
});
this.currentListIndex = this.lists.length - 1;
this.save();
});
document.getElementById('deleteListBtn')?.addEventListener('click', () => {
if (confirm(chrome.i18n.getMessage('confirm_delete_list') || 'Delete this list?')) {
this.lists.splice(this.currentListIndex, 1);
this.currentListIndex = Math.max(0, this.currentListIndex - 1);
this.save();
}
});
}
private setupWordManagement(): void {
const bulkPaste = document.getElementById('bulkPaste') as HTMLTextAreaElement;
const wordList = document.getElementById('wordList') as HTMLDivElement;
const wordSearch = document.getElementById('wordSearch') as HTMLInputElement;
document.getElementById('addWordsBtn')?.addEventListener('click', () => {
const words = bulkPaste.value.split(/\n+/).map(w => w.trim()).filter(Boolean);
const list = this.lists[this.currentListIndex];
for (const w of words) {
list.words.push({
wordStr: w,
background: '',
foreground: '',
active: true
});
}
bulkPaste.value = '';
this.save();
});
this.setupWordListEvents(wordList);
this.setupWordSelection();
wordSearch.addEventListener('input', (e) => {
this.wordSearchQuery = (e.target as HTMLInputElement).value;
this.renderWords();
});
}
private setupWordListEvents(wordList: HTMLDivElement): void {
wordList.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement;
if (target.type === 'checkbox' && target.dataset.index != null) {
const index = +target.dataset.index;
if (target.checked) {
this.selectedCheckboxes.add(index);
} else {
this.selectedCheckboxes.delete(index);
}
this.renderWords();
}
});
wordList.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement;
const index = +(target.dataset.bgEdit ?? target.dataset.fgEdit ?? -1);
if (index === -1) return;
const word = this.lists[this.currentListIndex].words[index];
if (target.dataset.bgEdit != null) word.background = target.value;
if (target.dataset.fgEdit != null) word.foreground = target.value;
this.save();
});
wordList.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const target = e.target as HTMLInputElement;
const index = +(target.dataset.wordEdit ?? -1);
if (index === -1) return;
const word = this.lists[this.currentListIndex].words[index];
if (target.dataset.wordEdit != null) {
word.wordStr = target.value;
this.save();
}
}
});
let scrolling = false;
wordList.addEventListener('scroll', () => {
if (scrolling) return;
scrolling = true;
requestAnimationFrame(() => {
this.renderWords();
scrolling = false;
});
});
}
private setupWordSelection(): void {
document.getElementById('selectAllBtn')?.addEventListener('click', () => {
const list = this.lists[this.currentListIndex];
list.words.forEach((_, index) => {
this.selectedCheckboxes.add(index);
});
this.renderWords();
});
document.getElementById('deselectAllBtn')?.addEventListener('click', () => {
this.selectedCheckboxes.clear();
this.renderWords();
});
document.getElementById('deleteSelectedBtn')?.addEventListener('click', () => {
if (confirm(chrome.i18n.getMessage('confirm_delete_words') || 'Delete selected words?')) {
const list = this.lists[this.currentListIndex];
const toDelete = Array.from(this.selectedCheckboxes);
this.lists[this.currentListIndex].words = list.words.filter((_, i) => !toDelete.includes(i));
this.selectedCheckboxes.clear();
this.save();
this.renderWords();
}
});
document.getElementById('enableSelectedBtn')?.addEventListener('click', () => {
const list = this.lists[this.currentListIndex];
this.selectedCheckboxes.forEach(index => {
list.words[index].active = true;
});
this.save();
this.renderWords();
});
document.getElementById('disableSelectedBtn')?.addEventListener('click', () => {
const list = this.lists[this.currentListIndex];
this.selectedCheckboxes.forEach(index => {
list.words[index].active = false;
});
this.save();
this.renderWords();
});
}
private setupSettings(): void {
const globalToggle = document.getElementById('globalHighlightToggle') as HTMLInputElement;
const matchCase = document.getElementById('matchCase') as HTMLInputElement;
const matchWhole = document.getElementById('matchWhole') as HTMLInputElement;
globalToggle.addEventListener('change', async () => {
this.globalHighlightEnabled = globalToggle.checked;
await StorageService.update('globalHighlightEnabled', this.globalHighlightEnabled);
MessageService.sendToAllTabs({
type: 'GLOBAL_TOGGLE_UPDATED',
enabled: this.globalHighlightEnabled
});
});
matchCase.addEventListener('change', async () => {
this.matchCaseEnabled = matchCase.checked;
await StorageService.update('matchCaseEnabled', this.matchCaseEnabled);
MessageService.sendToAllTabs({
type: 'MATCH_OPTIONS_UPDATED',
matchCase: this.matchCaseEnabled,
matchWhole: this.matchWholeEnabled
});
});
matchWhole.addEventListener('change', async () => {
this.matchWholeEnabled = matchWhole.checked;
await StorageService.update('matchWholeEnabled', this.matchWholeEnabled);
MessageService.sendToAllTabs({
type: 'MATCH_OPTIONS_UPDATED',
matchCase: this.matchCaseEnabled,
matchWhole: this.matchWholeEnabled
});
});
}
private setupPageHighlights(): void {
document.getElementById('refreshHighlightsBtn')?.addEventListener('click', async () => {
await this.loadPageHighlights();
});
document.getElementById('pageHighlightsList')?.addEventListener('click', async (e) => {
const target = e.target as HTMLElement;
const item = target.closest('.page-highlight-item') as HTMLElement;
if (!item) return;
const word = item.dataset.word;
if (!word) return;
if (target.classList.contains('highlight-prev')) {
await this.navigateHighlight(word, -1);
} else if (target.classList.contains('highlight-next')) {
await this.navigateHighlight(word, 1);
} else {
await this.jumpToHighlight(word, 0);
}
});
}
private async loadPageHighlights(): Promise<void> {
try {
const response = await MessageService.sendToActiveTab({ type: 'GET_PAGE_HIGHLIGHTS' });
if (response && response.highlights) {
this.pageHighlights = response.highlights;
this.highlightIndices.clear();
this.pageHighlights.forEach(h => this.highlightIndices.set(h.word, 0));
this.renderPageHighlights();
}
} catch (e) {
console.error('Error loading page highlights:', e);
this.pageHighlights = [];
this.renderPageHighlights();
}
}
private async jumpToHighlight(word: string, index: number): Promise<void> {
this.highlightIndices.set(word, index);
await MessageService.sendToActiveTab({
type: 'SCROLL_TO_HIGHLIGHT',
word,
index
});
this.renderPageHighlights();
}
private async navigateHighlight(word: string, direction: number): Promise<void> {
const highlight = this.pageHighlights.find(h => h.word === word);
if (!highlight) return;
const currentIndex = this.highlightIndices.get(word) || 0;
let newIndex = currentIndex + direction;
if (newIndex < 0) newIndex = highlight.count - 1;
if (newIndex >= highlight.count) newIndex = 0;
await this.jumpToHighlight(word, newIndex);
}
private renderPageHighlights(): void {
const container = document.getElementById('pageHighlightsList');
const countElement = document.getElementById('totalHighlightsCount');
if (!container || !countElement) return;
const totalCount = this.pageHighlights.reduce((sum, h) => sum + h.count, 0);
countElement.textContent = totalCount.toString();
if (this.pageHighlights.length === 0) {
container.innerHTML = `<div class="page-highlights-empty">${chrome.i18n.getMessage('no_highlights_on_page') || 'No highlights on this page'}</div>`;
return;
}
container.innerHTML = this.pageHighlights.map(highlight => {
const currentIndex = this.highlightIndices.get(highlight.word) || 0;
return `
<div class="page-highlight-item" data-word="${DOMUtils.escapeHtml(highlight.word)}">
<div class="page-highlight-word">
<span class="page-highlight-preview" style="background-color: ${highlight.background}; color: ${highlight.foreground};">
${DOMUtils.escapeHtml(highlight.word)}
</span>
${highlight.count > 1 ? `<span class="page-highlight-position">${currentIndex + 1}/${highlight.count}</span>` : ''}
</div>
<span class="page-highlight-count">${highlight.count}</span>
${highlight.count > 1 ? `
<div class="page-highlight-nav">
<button class="highlight-prev" title="${chrome.i18n.getMessage('previous') || 'Previous'}">
<i class="fa-solid fa-chevron-up"></i>
</button>
<button class="highlight-next" title="${chrome.i18n.getMessage('next') || 'Next'}">
<i class="fa-solid fa-chevron-down"></i>
</button>
</div>
` : ''}
</div>
`;
}).join('');
}
private setupExceptions(): void {
document.getElementById('toggleExceptionBtn')?.addEventListener('click', async () => {
if (!this.currentTabHost) return;
const isException = this.exceptionsList.includes(this.currentTabHost);
if (isException) {
this.exceptionsList = this.exceptionsList.filter(domain => domain !== this.currentTabHost);
} else {
this.exceptionsList.push(this.currentTabHost);
}
this.updateExceptionButton();
this.renderExceptions();
await StorageService.update('exceptionsList', this.exceptionsList);
MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' });
});
document.getElementById('clearExceptionsBtn')?.addEventListener('click', async () => {
if (confirm(chrome.i18n.getMessage('confirm_clear_exceptions') || 'Clear all exceptions?')) {
this.exceptionsList = [];
this.updateExceptionButton();
this.renderExceptions();
await StorageService.update('exceptionsList', this.exceptionsList);
MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' });
}
});
document.getElementById('exceptionsList')?.addEventListener('click', async (e) => {
const target = e.target as HTMLElement;
if (target.classList.contains('exception-remove')) {
const domain = target.dataset.domain!;
this.exceptionsList = this.exceptionsList.filter(d => d !== domain);
this.updateExceptionButton();
this.renderExceptions();
await StorageService.update('exceptionsList', this.exceptionsList);
MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' });
}
});
}
private setupImportExport(): void {
const importInput = document.getElementById('importInput') as HTMLInputElement;
document.getElementById('exportBtn')?.addEventListener('click', () => {
const exportData: ExportData = {
lists: this.lists,
exceptionsList: this.exceptionsList
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'highlight-lists.json';
a.click();
URL.revokeObjectURL(url);
});
document.getElementById('importBtn')?.addEventListener('click', () => {
importInput.click();
});
importInput.addEventListener('change', (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
if (Array.isArray(data)) {
this.lists = data;
} else if (data && typeof data === 'object') {
if (Array.isArray(data.lists)) {
this.lists = data.lists;
}
if (Array.isArray(data.exceptionsList)) {
this.exceptionsList = data.exceptionsList;
}
}
this.currentListIndex = 0;
this.updateExceptionButton();
this.renderExceptions();
this.save();
} catch (err) {
alert(chrome.i18n.getMessage('invalid_json_error') + ': ' + (err as Error).message);
}
};
reader.readAsText(file);
});
}
private setupTheme(): void {
const toggle = document.getElementById('themeToggle') as HTMLInputElement;
const body = document.body;
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
body.classList.remove('dark');
body.classList.add('light');
toggle.checked = false;
} else {
body.classList.add('dark');
body.classList.remove('light');
toggle.checked = true;
}
toggle.addEventListener('change', () => {
if (toggle.checked) {
body.classList.add('dark');
body.classList.remove('light');
localStorage.setItem('theme', 'dark');
} else {
body.classList.remove('dark');
body.classList.add('light');
localStorage.setItem('theme', 'light');
}
});
}
private applyListSettings(): void {
const listName = document.getElementById('listName') as HTMLInputElement;
const listBg = document.getElementById('listBg') as HTMLInputElement;
const listFg = document.getElementById('listFg') as HTMLInputElement;
const listActive = document.getElementById('listActive') as HTMLInputElement;
this.lists[this.currentListIndex].name = listName.value;
this.lists[this.currentListIndex].background = listBg.value;
this.lists[this.currentListIndex].foreground = listFg.value;
this.lists[this.currentListIndex].active = listActive.checked;
this.save();
}
private async save(): Promise<void> {
await StorageService.set({
lists: this.lists,
globalHighlightEnabled: this.globalHighlightEnabled,
matchCaseEnabled: this.matchCaseEnabled,
matchWholeEnabled: this.matchWholeEnabled,
exceptionsList: this.exceptionsList
});
this.renderLists();
MessageService.sendToAllTabs({ type: 'WORD_LIST_UPDATED' });
}
private render(): void {
this.renderLists();
this.renderWords();
this.renderExceptions();
this.updateExceptionButton();
this.updateFormValues();
}
private renderLists(): void {
const listSelect = document.getElementById('listSelect') as HTMLSelectElement;
listSelect.innerHTML = this.lists.map((list, index) =>
`<option value="${index}">${DOMUtils.escapeHtml(list.name)}</option>`
).join('');
listSelect.value = this.currentListIndex.toString();
this.updateListForm();
}
private updateListForm(): void {
const list = this.lists[this.currentListIndex];
(document.getElementById('listName') as HTMLInputElement).value = list.name;
(document.getElementById('listBg') as HTMLInputElement).value = list.background;
(document.getElementById('listFg') as HTMLInputElement).value = list.foreground;
(document.getElementById('listActive') as HTMLInputElement).checked = list.active;
}
private renderWords(): void {
const list = this.lists[this.currentListIndex];
const wordList = document.getElementById('wordList') as HTMLDivElement;
let filteredWords = list.words;
if (this.wordSearchQuery.trim()) {
const q = this.wordSearchQuery.trim().toLowerCase();
filteredWords = list.words.filter(w => w.wordStr.toLowerCase().includes(q));
}
const itemHeight = 32;
const itemSpacing = 2;
const totalItemHeight = itemHeight + itemSpacing;
const containerHeight = wordList.clientHeight || 250;
const scrollTop = wordList.scrollTop;
const startIndex = Math.max(0, Math.floor(scrollTop / totalItemHeight) - 1);
const visibleCount = Math.ceil(containerHeight / totalItemHeight);
const endIndex = Math.min(startIndex + visibleCount + 2, filteredWords.length);
wordList.innerHTML = '';
const spacer = document.createElement('div');
spacer.style.position = 'relative';
spacer.style.height = `${filteredWords.length * totalItemHeight}px`;
spacer.style.width = '100%';
for (let i = startIndex; i < endIndex; i++) {
const w = filteredWords[i];
if (!w) continue;
const realIndex = list.words.indexOf(w);
const container = this.createWordItem(w, realIndex, i, itemHeight);
spacer.appendChild(container);
}
wordList.appendChild(spacer);
const wordCount = document.getElementById('wordCount');
if (wordCount) {
wordCount.textContent = filteredWords.length.toString();
}
}
private createWordItem(word: HighlightWord, realIndex: number, displayIndex: number, itemHeight: number): HTMLDivElement {
const container = document.createElement('div');
container.className = 'word-item';
if (word.active === false) {
container.classList.add('disabled');
}
container.style.cssText = `
position: absolute;
top: ${displayIndex * (itemHeight + 2)}px;
`;
const list = this.lists[this.currentListIndex];
container.innerHTML = `
<input type="checkbox" class="word-checkbox" data-index="${realIndex}" ${this.selectedCheckboxes.has(realIndex) ? 'checked' : ''} title="${chrome.i18n.getMessage('select_title') || 'Select'}">
<input type="text" value="${DOMUtils.escapeHtml(word.wordStr)}" data-word-edit="${realIndex}" placeholder="${chrome.i18n.getMessage('word_placeholder') || 'Word or phrase'}">
<input type="color" value="${word.background || list.background}" data-bg-edit="${realIndex}" title="${chrome.i18n.getMessage('background_color_title') || 'Background color'}">
<input type="color" value="${word.foreground || list.foreground}" data-fg-edit="${realIndex}" title="${chrome.i18n.getMessage('text_color_title') || 'Text color'}">
`;
return container;
}
private updateExceptionButton(): void {
const toggleBtn = document.getElementById('toggleExceptionBtn');
const btnText = document.getElementById('exceptionBtnText');
if (!toggleBtn || !btnText || !this.currentTabHost) return;
const isException = this.exceptionsList.includes(this.currentTabHost);
if (isException) {
btnText.textContent = chrome.i18n.getMessage('remove_exception') || 'Remove from Exceptions';
toggleBtn.className = 'danger';
const icon = toggleBtn.querySelector('i');
if (icon) icon.className = 'fa-solid fa-trash';
} else {
btnText.textContent = chrome.i18n.getMessage('add_exception') || 'Add to Exceptions';
toggleBtn.className = '';
const icon = toggleBtn.querySelector('i');
if (icon) icon.className = 'fa-solid fa-plus';
}
}
private renderExceptions(): void {
const container = document.getElementById('exceptionsList');
if (!container) return;
if (this.exceptionsList.length === 0) {
container.innerHTML = `<div class="exception-item">${chrome.i18n.getMessage('no_exceptions') || 'No exceptions'}</div>`;
return;
}
container.innerHTML = this.exceptionsList.map(domain =>
`<div class="exception-item">
<span class="exception-domain">${DOMUtils.escapeHtml(domain)}</span>
<button class="exception-remove" data-domain="${DOMUtils.escapeHtml(domain)}">${chrome.i18n.getMessage('remove') || 'Remove'}</button>
</div>`
).join('');
}
private updateFormValues(): void {
(document.getElementById('globalHighlightToggle') as HTMLInputElement).checked = this.globalHighlightEnabled;
(document.getElementById('matchCase') as HTMLInputElement).checked = this.matchCaseEnabled;
(document.getElementById('matchWhole') as HTMLInputElement).checked = this.matchWholeEnabled;
}
}

31
src/popup/popup.ts Normal file
View File

@@ -0,0 +1,31 @@
import { PopupController } from './PopupController.js';
function localizePage(): void {
const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(element => {
const message = (element as HTMLElement).dataset.i18n!;
const localizedText = chrome.i18n.getMessage(message);
if (localizedText) {
if ((element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') && (element as HTMLInputElement).hasAttribute('placeholder')) {
(element as HTMLInputElement).placeholder = localizedText;
} else {
element.textContent = localizedText;
}
}
});
}
function displayVersion(): void {
const manifest = chrome.runtime.getManifest();
const versionElement = document.getElementById('version-number');
if (versionElement && manifest.version) {
versionElement.textContent = manifest.version;
}
}
document.addEventListener('DOMContentLoaded', async () => {
localizePage();
displayVersion();
const controller = new PopupController();
await controller.initialize();
});

View File

@@ -0,0 +1,33 @@
import { MessageData } from '../types.js';
export class MessageService {
static sendToAllTabs(message: MessageData): void {
chrome.tabs.query({}, (tabs) => {
tabs.forEach(tab => {
if (tab.id) {
chrome.tabs.sendMessage(tab.id, message).catch(() => {
// Ignore errors for tabs that can't receive messages
});
}
});
});
}
static sendToTab(tabId: number, message: MessageData): void {
chrome.tabs.sendMessage(tabId, message).catch(() => {
// Ignore errors for tabs that can't receive messages
});
}
static onMessage(callback: (message: MessageData, sender: any, sendResponse: (response?: any) => void) => void | boolean): void {
chrome.runtime.onMessage.addListener(callback);
}
static async sendToActiveTab(message: MessageData): Promise<any> {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab.id) {
return chrome.tabs.sendMessage(tab.id, message);
}
return null;
}
}

View File

@@ -0,0 +1,25 @@
import { StorageData, DEFAULT_STORAGE } from '../types.js';
export class StorageService {
static async get<K extends keyof StorageData>(keys: K[]): Promise<Pick<StorageData, K>>;
static async get(): Promise<StorageData>;
static async get(keys?: (keyof StorageData)[]): Promise<any> {
const defaults = DEFAULT_STORAGE;
if (keys) {
const keyDefaults: any = {};
keys.forEach(key => {
keyDefaults[key] = defaults[key];
});
return chrome.storage.local.get(keyDefaults);
}
return chrome.storage.local.get(defaults);
}
static async set(data: Partial<StorageData>): Promise<void> {
return chrome.storage.local.set(data);
}
static async update<K extends keyof StorageData>(key: K, value: StorageData[K]): Promise<void> {
return this.set({ [key]: value } as Partial<StorageData>);
}
}

65
src/types.ts Normal file
View File

@@ -0,0 +1,65 @@
export interface HighlightWord {
wordStr: string;
background: string;
foreground: string;
active: boolean;
}
export interface HighlightList {
id: number;
name: string;
background: string;
foreground: string;
active: boolean;
words: HighlightWord[];
}
export interface StorageData {
lists: HighlightList[];
globalHighlightEnabled: boolean;
matchCaseEnabled: boolean;
matchWholeEnabled: boolean;
exceptionsList: string[];
}
export interface ActiveWord {
text: string;
background: string;
foreground: string;
}
export interface HighlightInfo {
word: string;
count: number;
background: string;
foreground: string;
}
export interface MessageData {
type: 'WORD_LIST_UPDATED' | 'GLOBAL_TOGGLE_UPDATED' | 'MATCH_OPTIONS_UPDATED' | 'EXCEPTIONS_LIST_UPDATED' | 'GET_PAGE_HIGHLIGHTS' | 'PAGE_HIGHLIGHTS_RESPONSE' | 'SCROLL_TO_HIGHLIGHT';
enabled?: boolean;
matchCase?: boolean;
matchWhole?: boolean;
highlights?: HighlightInfo[];
word?: string;
index?: number;
}
export interface ExportData {
lists: HighlightList[];
exceptionsList: string[];
}
export const DEFAULT_STORAGE: StorageData = {
lists: [],
globalHighlightEnabled: true,
matchCaseEnabled: false,
matchWholeEnabled: false,
exceptionsList: []
};
export const CONSTANTS = {
WORD_ITEM_HEIGHT: 32,
DEBOUNCE_DELAY: 150,
SCROLL_THROTTLE: 16
} as const;

27
src/types/css-highlights.d.ts vendored Normal file
View File

@@ -0,0 +1,27 @@
// CSS Highlights API type declarations
interface Highlight {
new(...ranges: Range[]): Highlight;
add(range: Range): void;
clear(): void;
delete(range: Range): boolean;
has(range: Range): boolean;
readonly size: number;
}
interface HighlightRegistry {
set(name: string, highlight: Highlight): void;
get(name: string): Highlight | undefined;
delete(name: string): boolean;
clear(): void;
has(name: string): boolean;
readonly size: number;
}
interface CSS {
highlights: HighlightRegistry;
}
declare var Highlight: {
prototype: Highlight;
new(...ranges: Range[]): Highlight;
};

41
src/utils/DOMUtils.ts Normal file
View File

@@ -0,0 +1,41 @@
export class DOMUtils {
static escapeHtml(str: string): string {
const escapeMap: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
return str.replace(/[&<>"']/g, (match) => escapeMap[match]);
}
static escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
static debounce<T extends (...args: any[]) => void>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: number;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = window.setTimeout(() => func(...args), wait);
};
}
static throttle<T extends (...args: any[]) => void>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
}

View File

@@ -1,8 +0,0 @@
async function getLists() {
const { lists } = await chrome.storage.local.get("lists");
return lists || [];
}
async function saveLists(lists) {
await chrome.storage.local.set({ lists });
}

30
tsconfig.json Normal file
View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022", "DOM"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"noEmitOnError": false,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"exactOptionalPropertyTypes": false,
"types": ["chrome"]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}