48 Commits

Author SHA1 Message Date
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
semantic-release-bot
1ef21d0975 chore(release): 1.7.1 [skip ci]
## [1.7.1](https://github.com/obsqrbtz/goose-highlighter/compare/v1.7.0...v1.7.1) (2025-06-27)

### Bug Fixes

* unicode support in regex ([ae1cf48](ae1cf48c53))
2025-06-27 14:05:56 +03:00
ae1cf48c53 fix: unicode support in regex 2025-06-27 14:05:12 +03:00
bca37e690f Trigger Build 2025-06-27 00:51:41 +03:00
semantic-release-bot
50c3facfae chore(release): 1.7.0 [skip ci]
# [1.7.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.6.0...v1.7.0) (2025-06-26)

### Bug Fixes

* colorbox styling ([1e704b5](1e704b51a8))
* colorbox styling ([08ad7c4](08ad7c4325))
* moved import/export to options section ([fe15965](fe15965e89))
* wordlist scrollbar styling ([b30fac5](b30fac5ded))

### Features

* add word search ([80d4bff](80d4bff0b4))
* added matching flags ([759307f](759307f983))
2025-06-27 00:49:54 +03:00
fe15965e89 fix: moved import/export to options section 2025-06-27 00:46:09 +03:00
759307f983 feat: added matching flags 2025-06-27 00:43:31 +03:00
b766f61b0c ci: publish on push to master 2025-06-27 00:00:24 +03:00
1e704b51a8 fix: colorbox styling 2025-06-26 23:59:12 +03:00
b30fac5ded fix: wordlist scrollbar styling 2025-06-26 23:40:15 +03:00
08ad7c4325 fix: colorbox styling 2025-06-26 23:35:49 +03:00
80d4bff0b4 feat: add word search 2025-06-25 16:25:52 +03:00
dbb6806a78 corrected russian summary, chore(release): 1.6.0 2025-06-25 16:00:50 +03:00
e9c7d58273 corrected portuguese and korean locales, chore(release): 1.6.0 2025-06-25 15:57:40 +03:00
19ab6eec0b trigger release, chore(release): 1.6.0 2025-06-25 15:51:30 +03:00
semantic-release-bot
dff29d447d chore(release): 1.6.0 [skip ci]
# [1.6.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.5.0...v1.6.0) (2025-06-25)

### Features

* add more locales ([d97becf](d97becfaae))
2025-06-25 15:49:26 +03:00
d97becfaae feat: add more locales 2025-06-25 15:49:04 +03:00
9926e7978f trigger release, chore(release): 1.5.0 2025-06-24 15:18:05 +03:00
23e0c5f772 chore: upload placeholder privacy policy (enforced by google) 2025-06-24 15:15:20 +03:00
2aa60d5553 ci: updated workflow, chore(release): 1.5.0 2025-06-24 15:11:33 +03:00
5eca0b395b Update publish-extension.yml 2025-06-24 15:09:51 +03:00
51de84bcb7 ci: updated workflow, chore(release): 1.5.0 2025-06-24 15:09:28 +03:00
19f54f1c2f ci: updated workflow, chore(release): 1.5.0 2025-06-24 15:08:45 +03:00
semantic-release-bot
408080345b chore(release): 1.5.0 [skip ci]
# [1.5.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.4.0...v1.5.0) (2025-06-24)

### Features

* updated readme (wrong commit type is intentional to trigger ci) ([aac8749](aac87493f2))
2025-06-24 15:04:33 +03:00
aac87493f2 feat: updated readme (wrong commit type is intentional to trigger ci) 2025-06-24 15:04:22 +03:00
semantic-release-bot
cdb432ba7a chore(release): 1.4.0 [skip ci]
# [1.4.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.3.0...v1.4.0) (2025-06-24)

### Features

* set up github action for publishing extension ([9d639b6](9d639b65a9))
2025-06-24 14:53:30 +03:00
9d639b65a9 feat: set up github action for publishing extension 2025-06-24 14:53:06 +03:00
b19ac34df0 chore: add versioning.md 2025-06-23 16:15:11 +03:00
31af44a93f chore: cleaned changelog 2025-06-23 16:12:21 +03:00
46 changed files with 4671 additions and 668 deletions

58
.github/workflows/publish-extension.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Publish Chrome Extension
on:
push:
branches: [ main ]
jobs:
publish:
name: Pack and Upload Extension
runs-on: ubuntu-latest
env:
EXTENSION_ID: ${{ secrets.GOOGLE_EXTENSION_ID }}
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: Set version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Create zip package
run: |
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
- name: Upload to Chrome Web Store
run: |
chrome-webstore-upload upload \
--source goose-highlighter.zip \
--extension-id $EXTENSION_ID \
--client-id ${{ secrets.GOOGLE_CLIENT_ID }} \
--client-secret ${{ secrets.GOOGLE_CLIENT_SECRET }} \
--refresh-token ${{ secrets.GOOGLE_REFRESH_TOKEN }}
- name: Publish extension
run: |
chrome-webstore-upload publish \
--extension-id $EXTENSION_ID \
--client-id ${{ secrets.GOOGLE_CLIENT_ID }} \
--client-secret ${{ secrets.GOOGLE_CLIENT_SECRET }} \
--refresh-token ${{ secrets.GOOGLE_REFRESH_TOKEN }}

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

@@ -7,16 +7,19 @@
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
[
[
"./scripts/semantic-prepare.js"
],
"@semantic-release/exec",
{
"prepareCmd": "node scripts/update-manifest-version.js ${nextRelease.version}"
}
],
[
"@semantic-release/git",
{
"assets": [
"manifest.json",
"CHANGELOG.md"
],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
"message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
}
]
]

View File

@@ -1,37 +1,73 @@
# [1.3.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.2.0...v1.3.0) (2025-06-23)
## [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
* force version bump test ([fc710fc](https://github.com/obsqrbtz/goose-highlighter/commit/fc710fce5d463cb2b6dd2675ed5bf295a7ea8bb8))
* semantic-prepare.js ([726e7f0](https://github.com/obsqrbtz/goose-highlighter/commit/726e7f082659545f9b37d021dd6f4cbbd43ce1ad))
* trigger release ([e35863d](https://github.com/obsqrbtz/goose-highlighter/commit/e35863d10ea51f55449ac41d68f931d3f906e7d1))
* 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.3.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.2.0...v1.3.0) (2025-06-23)
## [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)
### Bug Fixes
* unicode support in regex ([ae1cf48](https://github.com/obsqrbtz/goose-highlighter/commit/ae1cf48c53cd42e65279cf2acde1a2860d8a31ee))
# [1.7.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.6.0...v1.7.0) (2025-06-26)
### Bug Fixes
* colorbox styling ([1e704b5](https://github.com/obsqrbtz/goose-highlighter/commit/1e704b51a859845e539224aeb389a4e493d64520))
* colorbox styling ([08ad7c4](https://github.com/obsqrbtz/goose-highlighter/commit/08ad7c432541ea4240dec05a340ad0b3279ce82f))
* moved import/export to options section ([fe15965](https://github.com/obsqrbtz/goose-highlighter/commit/fe15965e89e8483f6b96eb779617053664c9d5b1))
* wordlist scrollbar styling ([b30fac5](https://github.com/obsqrbtz/goose-highlighter/commit/b30fac5deda7941035d8ae23001c998c2584c03e))
### Features
* force version bump test ([fc710fc](https://github.com/obsqrbtz/goose-highlighter/commit/fc710fce5d463cb2b6dd2675ed5bf295a7ea8bb8))
* semantic-prepare.js ([726e7f0](https://github.com/obsqrbtz/goose-highlighter/commit/726e7f082659545f9b37d021dd6f4cbbd43ce1ad))
* add word search ([80d4bff](https://github.com/obsqrbtz/goose-highlighter/commit/80d4bff0b4ef7c9e97506d1fe43a827bcc4b28fd))
* added matching flags ([759307f](https://github.com/obsqrbtz/goose-highlighter/commit/759307f9834a2bbb23e963e2042b7d41d5cfda44))
# [1.3.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.2.0...v1.3.0) (2025-06-23)
# [1.6.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.5.0...v1.6.0) (2025-06-25)
### Features
* semantic-prepare.js ([726e7f0](https://github.com/obsqrbtz/goose-highlighter/commit/726e7f082659545f9b37d021dd6f4cbbd43ce1ad))
* add more locales ([d97becf](https://github.com/obsqrbtz/goose-highlighter/commit/d97becfaae696e33247840090e8a752b5ed4ed72))
# [1.3.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.2.0...v1.3.0) (2025-06-23)
# [1.5.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.4.0...v1.5.0) (2025-06-24)
### Features
* semantic-prepare.js ([726e7f0](https://github.com/obsqrbtz/goose-highlighter/commit/726e7f082659545f9b37d021dd6f4cbbd43ce1ad))
* updated readme (wrong commit type is intentional to trigger ci) ([aac8749](https://github.com/obsqrbtz/goose-highlighter/commit/aac87493f29293e3d3291ba899032cf62504c14c))
# [1.2.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.1.1...v1.2.0) (2025-06-23)
# [1.4.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.3.0...v1.4.0) (2025-06-24)
### Features
* some test feature ([c388f90](https://github.com/obsqrbtz/goose-highlighter/commit/c388f907a50b9b9dfbc45022015fcd6f52894a7c))
* set up github action for publishing extension ([9d639b6](https://github.com/obsqrbtz/goose-highlighter/commit/9d639b65a9a1bc8b926f58fa7135aac7736aca7e))

1
PRIVACY.MD Normal file
View File

@@ -0,0 +1 @@
Goose Highlighter does not collect, store, or transmit any personal or sensitive user data.

View File

@@ -1,11 +1,58 @@
# 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.
- **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
### 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)

122
_locales/de/messages.json Normal file
View File

@@ -0,0 +1,122 @@
{
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "Markiere Wörter und Phrasen auf jeder Webseite. Erstelle benutzerdefinierte Listen, verwende Farben, importiere/exportiere und mehr."
},
"select_list": {
"message": "Liste auswählen:"
},
"new_list": {
"message": "Neue Liste"
},
"delete_list": {
"message": "Liste löschen"
},
"list_name": {
"message": "Listenname:"
},
"background": {
"message": "Hintergrund:"
},
"foreground": {
"message": "Vordergrund:"
},
"enable_highlight": {
"message": "Hervorhebung aktivieren"
},
"paste_hint": {
"message": "Wörter hier einfügen"
},
"apply_paste": {
"message": "Wörter hinzufügen"
},
"select_all": {
"message": "Auswählen"
},
"delete_selected": {
"message": "Löschen"
},
"disable_selected": {
"message": "Deaktivieren"
},
"enable_selected": {
"message": "Aktivieren"
},
"import_list": {
"message": "JSON importieren"
},
"export_list": {
"message": "JSON exportieren"
},
"default_list_name": {
"message": "Standardliste"
},
"new_list_name": {
"message": "Neue Liste"
},
"word_active_label": {
"message": "aktiv"
},
"invalid_json_error": {
"message": "Ungültige JSON-Datei"
},
"confirm_delete_list": {
"message": "Möchten Sie diese Liste wirklich löschen?"
},
"confirm_delete_words": {
"message": "Möchten Sie die ausgewählten Wörter wirklich löschen?"
},
"deselect_all": {
"message": "Abwählen"
},
"highlight_lists": {
"message": "Hervorhebungslisten"
},
"dark_mode": {
"message": "Dunkelmodus"
},
"list_settings": {
"message": "Listeneinstellungen"
},
"word_list": {
"message": "Wortliste"
},
"add_words": {
"message": "Wörter hinzufügen"
},
"global_highlight_toggle": {
"message": "Aktivieren"
},
"search_placeholder": {
"message": "Suchen..."
},
"options": { "message": "Optionen" },
"match_case": { "message": "Groß-/Kleinschreibung beachten" },
"match_whole": { "message": "Ganzes Wort übereinstimmen" },
"site_exceptions": {
"message": "Website-Ausnahmen"
},
"add_exception": {
"message": "Zu Ausnahmen hinzufügen"
},
"remove_exception": {
"message": "Aus Ausnahmen 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"
}
}

View File

@@ -2,6 +2,9 @@
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "Highlight words and phrases on any website. Create custom lists, use colors, import/export, and more."
},
"select_list": {
"message": "Select List:"
},
@@ -85,5 +88,41 @@
},
"global_highlight_toggle": {
"message": "Enable"
},
"search_placeholder": {
"message": "Search..."
},
"options": {
"message": "Options"
},
"match_case": {
"message": "Match Case"
},
"match_whole": {
"message": "Match Whole Word"
},
"site_exceptions": {
"message": "Site Exceptions"
},
"add_exception": {
"message": "Add to Exceptions"
},
"remove_exception": {
"message": "Remove from Exceptions"
},
"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"
}
}

128
_locales/es/messages.json Normal file
View File

@@ -0,0 +1,128 @@
{
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "Resalta palabras y frases en cualquier sitio web. Crea listas personalizadas, usa colores, importa/exporta y más."
},
"select_list": {
"message": "Seleccionar lista:"
},
"new_list": {
"message": "Nueva lista"
},
"delete_list": {
"message": "Eliminar lista"
},
"list_name": {
"message": "Nombre de la lista:"
},
"background": {
"message": "Fondo:"
},
"foreground": {
"message": "Texto:"
},
"enable_highlight": {
"message": "Activar resaltado"
},
"paste_hint": {
"message": "Pega las palabras aquí"
},
"apply_paste": {
"message": "Agregar palabras"
},
"select_all": {
"message": "Seleccionar"
},
"delete_selected": {
"message": "Eliminar"
},
"disable_selected": {
"message": "Desactivar"
},
"enable_selected": {
"message": "Activar"
},
"import_list": {
"message": "Importar JSON"
},
"export_list": {
"message": "Exportar JSON"
},
"default_list_name": {
"message": "Lista predeterminada"
},
"new_list_name": {
"message": "Nueva lista"
},
"word_active_label": {
"message": "activo"
},
"invalid_json_error": {
"message": "Archivo JSON no válido"
},
"confirm_delete_list": {
"message": "¿Estás seguro de que deseas eliminar esta lista?"
},
"confirm_delete_words": {
"message": "¿Estás seguro de que deseas eliminar las palabras seleccionadas?"
},
"deselect_all": {
"message": "Deseleccionar"
},
"highlight_lists": {
"message": "Listas de resaltado"
},
"dark_mode": {
"message": "Modo oscuro"
},
"list_settings": {
"message": "Configuración de la lista"
},
"word_list": {
"message": "Lista de palabras"
},
"add_words": {
"message": "Agregar palabras"
},
"global_highlight_toggle": {
"message": "Activar"
},
"search_placeholder": {
"message": "Buscar..."
},
"options": {
"message": "Opciones"
},
"match_case": {
"message": "Coincidir mayúsculas/minúsculas"
},
"match_whole": {
"message": "Coincidir palabra completa"
},
"site_exceptions": {
"message": "Excepciones de sitios"
},
"add_exception": {
"message": "Agregar a excepciones"
},
"remove_exception": {
"message": "Quitar de excepciones"
},
"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"
}
}

128
_locales/fr/messages.json Normal file
View File

@@ -0,0 +1,128 @@
{
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "Surlignez des mots et des phrases sur n'importe quel site web. Créez des listes personnalisées, utilisez des couleurs, importez/exportez, et plus encore."
},
"select_list": {
"message": "Sélectionner une liste :"
},
"new_list": {
"message": "Nouvelle liste"
},
"delete_list": {
"message": "Supprimer la liste"
},
"list_name": {
"message": "Nom de la liste :"
},
"background": {
"message": "Arrière-plan :"
},
"foreground": {
"message": "Texte :"
},
"enable_highlight": {
"message": "Activer la surbrillance"
},
"paste_hint": {
"message": "Collez les mots ici"
},
"apply_paste": {
"message": "Ajouter des mots"
},
"select_all": {
"message": "Sélectionner"
},
"delete_selected": {
"message": "Supprimer"
},
"disable_selected": {
"message": "Désactiver"
},
"enable_selected": {
"message": "Activer"
},
"import_list": {
"message": "Importer JSON"
},
"export_list": {
"message": "Exporter JSON"
},
"default_list_name": {
"message": "Liste par défaut"
},
"new_list_name": {
"message": "Nouvelle liste"
},
"word_active_label": {
"message": "actif"
},
"invalid_json_error": {
"message": "Fichier JSON invalide"
},
"confirm_delete_list": {
"message": "Êtes-vous sûr de vouloir supprimer cette liste ?"
},
"confirm_delete_words": {
"message": "Êtes-vous sûr de vouloir supprimer les mots sélectionnés ?"
},
"deselect_all": {
"message": "Désélectionner"
},
"highlight_lists": {
"message": "Listes de surbrillance"
},
"dark_mode": {
"message": "Mode sombre"
},
"list_settings": {
"message": "Paramètres de la liste"
},
"word_list": {
"message": "Liste de mots"
},
"add_words": {
"message": "Ajouter des mots"
},
"global_highlight_toggle": {
"message": "Activer"
},
"search_placeholder": {
"message": "Rechercher..."
},
"options": {
"message": "Options"
},
"match_case": {
"message": "Respecter la casse"
},
"match_whole": {
"message": "Mot entier seulement"
},
"site_exceptions": {
"message": "Exceptions de sites"
},
"add_exception": {
"message": "Ajouter aux exceptions"
},
"remove_exception": {
"message": "Retirer des exceptions"
},
"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"
}
}

128
_locales/hi/messages.json Normal file
View File

@@ -0,0 +1,128 @@
{
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "किसी भी वेबसाइट पर शब्द और वाक्यांश हाइलाइट करें। कस्टम सूचियाँ बनाएं, रंगों का उपयोग करें, आयात/निर्यात करें और बहुत कुछ।"
},
"select_list": {
"message": "सूची चुनें:"
},
"new_list": {
"message": "नई सूची"
},
"delete_list": {
"message": "सूची हटाएं"
},
"list_name": {
"message": "सूची का नाम:"
},
"background": {
"message": "पृष्ठभूमि:"
},
"foreground": {
"message": "फॉरग्राउंड:"
},
"enable_highlight": {
"message": "हाइलाइट सक्षम करें"
},
"paste_hint": {
"message": "यहाँ शब्द चिपकाएँ"
},
"apply_paste": {
"message": "शब्द जोड़ें"
},
"select_all": {
"message": "सभी चुनें"
},
"delete_selected": {
"message": "हटाएं"
},
"disable_selected": {
"message": "अक्षम करें"
},
"enable_selected": {
"message": "सक्षम करें"
},
"import_list": {
"message": "JSON आयात करें"
},
"export_list": {
"message": "JSON निर्यात करें"
},
"default_list_name": {
"message": "डिफ़ॉल्ट सूची"
},
"new_list_name": {
"message": "नई सूची"
},
"word_active_label": {
"message": "सक्रिय"
},
"invalid_json_error": {
"message": "अमान्य JSON फ़ाइल"
},
"confirm_delete_list": {
"message": "क्या आप वाकई इस सूची को हटाना चाहते हैं?"
},
"confirm_delete_words": {
"message": "क्या आप वाकई चयनित शब्दों को हटाना चाहते हैं?"
},
"deselect_all": {
"message": "चयन हटाएँ"
},
"highlight_lists": {
"message": "हाइलाइट सूचियाँ"
},
"dark_mode": {
"message": "डार्क मोड"
},
"list_settings": {
"message": "सूची सेटिंग्स"
},
"word_list": {
"message": "शब्द सूची"
},
"add_words": {
"message": "शब्द जोड़ें"
},
"global_highlight_toggle": {
"message": "सक्षम करें"
},
"search_placeholder": {
"message": "खोजें..."
},
"options": {
"message": "विकल्प"
},
"match_case": {
"message": "केस मिलाएं"
},
"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": "हटाएं"
}
}

128
_locales/it/messages.json Normal file
View File

@@ -0,0 +1,128 @@
{
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "Evidenzia parole e frasi su qualsiasi sito web. Crea liste personalizzate, usa colori, importa/esporta e altro ancora."
},
"select_list": {
"message": "Seleziona elenco:"
},
"new_list": {
"message": "Nuovo elenco"
},
"delete_list": {
"message": "Elimina elenco"
},
"list_name": {
"message": "Nome elenco:"
},
"background": {
"message": "Sfondo:"
},
"foreground": {
"message": "Colore del testo:"
},
"enable_highlight": {
"message": "Abilita evidenziazione"
},
"paste_hint": {
"message": "Incolla le parole qui"
},
"apply_paste": {
"message": "Aggiungi parole"
},
"select_all": {
"message": "Seleziona"
},
"delete_selected": {
"message": "Elimina"
},
"disable_selected": {
"message": "Disattiva"
},
"enable_selected": {
"message": "Attiva"
},
"import_list": {
"message": "Importa JSON"
},
"export_list": {
"message": "Esporta JSON"
},
"default_list_name": {
"message": "Elenco predefinito"
},
"new_list_name": {
"message": "Nuovo elenco"
},
"word_active_label": {
"message": "attivo"
},
"invalid_json_error": {
"message": "File JSON non valido"
},
"confirm_delete_list": {
"message": "Sei sicuro di voler eliminare questo elenco?"
},
"confirm_delete_words": {
"message": "Sei sicuro di voler eliminare le parole selezionate?"
},
"deselect_all": {
"message": "Deseleziona"
},
"highlight_lists": {
"message": "Elenchi di evidenziazione"
},
"dark_mode": {
"message": "Modalità scura"
},
"list_settings": {
"message": "Impostazioni elenco"
},
"word_list": {
"message": "Elenco parole"
},
"add_words": {
"message": "Aggiungi parole"
},
"global_highlight_toggle": {
"message": "Attiva"
},
"search_placeholder": {
"message": "Cerca..."
},
"options": {
"message": "Opzioni"
},
"match_case": {
"message": "Maiuscole/minuscole"
},
"match_whole": {
"message": "Solo parola intera"
},
"site_exceptions": {
"message": "Eccezioni siti"
},
"add_exception": {
"message": "Aggiungi alle eccezioni"
},
"remove_exception": {
"message": "Rimuovi dalle eccezioni"
},
"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"
}
}

128
_locales/ja/messages.json Normal file
View File

@@ -0,0 +1,128 @@
{
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "任意のウェブサイトで単語やフレーズをハイライトします。カスタムリストの作成、色の使用、インポート/エクスポートなどが可能です。"
},
"select_list": {
"message": "リストを選択:"
},
"new_list": {
"message": "新しいリスト"
},
"delete_list": {
"message": "リストを削除"
},
"list_name": {
"message": "リスト名:"
},
"background": {
"message": "背景色:"
},
"foreground": {
"message": "文字色:"
},
"enable_highlight": {
"message": "ハイライトを有効にする"
},
"paste_hint": {
"message": "ここに単語を貼り付けてください"
},
"apply_paste": {
"message": "単語を追加"
},
"select_all": {
"message": "すべて選択"
},
"delete_selected": {
"message": "削除"
},
"disable_selected": {
"message": "無効にする"
},
"enable_selected": {
"message": "有効にする"
},
"import_list": {
"message": "JSONをインポート"
},
"export_list": {
"message": "JSONをエクスポート"
},
"default_list_name": {
"message": "デフォルトリスト"
},
"new_list_name": {
"message": "新しいリスト"
},
"word_active_label": {
"message": "有効"
},
"invalid_json_error": {
"message": "無効なJSONファイル"
},
"confirm_delete_list": {
"message": "このリストを削除してもよろしいですか?"
},
"confirm_delete_words": {
"message": "選択した単語を削除してもよろしいですか?"
},
"deselect_all": {
"message": "すべて解除"
},
"highlight_lists": {
"message": "ハイライトリスト"
},
"dark_mode": {
"message": "ダークモード"
},
"list_settings": {
"message": "リスト設定"
},
"word_list": {
"message": "単語リスト"
},
"add_words": {
"message": "単語を追加"
},
"global_highlight_toggle": {
"message": "有効にする"
},
"search_placeholder": {
"message": "検索..."
},
"options": {
"message": "オプション"
},
"match_case": {
"message": "大文字と小文字を区別"
},
"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": "削除"
}
}

128
_locales/ko/messages.json Normal file
View File

@@ -0,0 +1,128 @@
{
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "어떤 웹사이트에서든 단어와 구문을 하이라이트하세요. 맞춤 목록 생성, 색상 사용, 가져오기/내보내기 등 다양한 기능을 제공합니다."
},
"select_list": {
"message": "리스트 선택:"
},
"new_list": {
"message": "새 리스트"
},
"delete_list": {
"message": "리스트 삭제"
},
"list_name": {
"message": "리스트 이름:"
},
"background": {
"message": "배경색:"
},
"foreground": {
"message": "글자색:"
},
"enable_highlight": {
"message": "하이라이트 활성화"
},
"paste_hint": {
"message": "여기에 단어를 붙여넣기"
},
"apply_paste": {
"message": "단어 추가"
},
"select_all": {
"message": "선택"
},
"delete_selected": {
"message": "삭제"
},
"disable_selected": {
"message": "비활성화"
},
"enable_selected": {
"message": "활성화"
},
"import_list": {
"message": "JSON 가져오기"
},
"export_list": {
"message": "JSON 내보내기"
},
"default_list_name": {
"message": "기본 리스트"
},
"new_list_name": {
"message": "새 리스트"
},
"word_active_label": {
"message": "활성"
},
"invalid_json_error": {
"message": "잘못된 JSON 파일"
},
"confirm_delete_list": {
"message": "이 리스트를 삭제하시겠습니까?"
},
"confirm_delete_words": {
"message": "선택한 단어를 삭제하시겠습니까?"
},
"deselect_all": {
"message": "선택 해제"
},
"highlight_lists": {
"message": "하이라이트 리스트"
},
"dark_mode": {
"message": "다크 모드"
},
"list_settings": {
"message": "리스트 설정"
},
"word_list": {
"message": "단어 리스트"
},
"add_words": {
"message": "단어 추가"
},
"global_highlight_toggle": {
"message": "활성화"
},
"search_placeholder": {
"message": "검색..."
},
"options": {
"message": "옵션"
},
"match_case": {
"message": "대소문자 구분"
},
"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": "제거"
}
}

128
_locales/nl/messages.json Normal file
View File

@@ -0,0 +1,128 @@
{
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "Markeer woorden en zinnen op elke website. Maak aangepaste lijsten, gebruik kleuren, importeer/exporteer en meer."
},
"select_list": {
"message": "Selecteer lijst:"
},
"new_list": {
"message": "Nieuwe lijst"
},
"delete_list": {
"message": "Lijst verwijderen"
},
"list_name": {
"message": "Lijstnaam:"
},
"background": {
"message": "Achtergrond:"
},
"foreground": {
"message": "Tekstkleur:"
},
"enable_highlight": {
"message": "Markeren inschakelen"
},
"paste_hint": {
"message": "Plak hier de woorden"
},
"apply_paste": {
"message": "Woorden toevoegen"
},
"select_all": {
"message": "Selecteren"
},
"delete_selected": {
"message": "Verwijderen"
},
"disable_selected": {
"message": "Uitschakelen"
},
"enable_selected": {
"message": "Inschakelen"
},
"import_list": {
"message": "JSON importeren"
},
"export_list": {
"message": "JSON exporteren"
},
"default_list_name": {
"message": "Standaardlijst"
},
"new_list_name": {
"message": "Nieuwe lijst"
},
"word_active_label": {
"message": "actief"
},
"invalid_json_error": {
"message": "Ongeldig JSON-bestand"
},
"confirm_delete_list": {
"message": "Weet je zeker dat je deze lijst wilt verwijderen?"
},
"confirm_delete_words": {
"message": "Weet je zeker dat je de geselecteerde woorden wilt verwijderen?"
},
"deselect_all": {
"message": "Deselecteren"
},
"highlight_lists": {
"message": "Markeer lijsten"
},
"dark_mode": {
"message": "Donkere modus"
},
"list_settings": {
"message": "Lijst instellingen"
},
"word_list": {
"message": "Woordenlijst"
},
"add_words": {
"message": "Woorden toevoegen"
},
"global_highlight_toggle": {
"message": "Inschakelen"
},
"search_placeholder": {
"message": "Zoeken..."
},
"options": {
"message": "Opties"
},
"match_case": {
"message": "Hoofdlettergevoelig"
},
"match_whole": {
"message": "Alleen volledig woord"
},
"site_exceptions": {
"message": "Site-uitzonderingen"
},
"add_exception": {
"message": "Toevoegen aan uitzonderingen"
},
"remove_exception": {
"message": "Verwijderen uit uitzonderingen"
},
"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"
}
}

128
_locales/pl/messages.json Normal file
View File

@@ -0,0 +1,128 @@
{
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "Podświetlaj słowa i frazy na dowolnej stronie internetowej. Twórz niestandardowe listy, używaj kolorów, importuj/eksportuj i więcej."
},
"select_list": {
"message": "Wybierz listę:"
},
"new_list": {
"message": "Nowa lista"
},
"delete_list": {
"message": "Usuń listę"
},
"list_name": {
"message": "Nazwa listy:"
},
"background": {
"message": "Tło:"
},
"foreground": {
"message": "Kolor tekstu:"
},
"enable_highlight": {
"message": "Włącz podświetlanie"
},
"paste_hint": {
"message": "Wklej tutaj słowa"
},
"apply_paste": {
"message": "Dodaj słowa"
},
"select_all": {
"message": "Zaznacz"
},
"delete_selected": {
"message": "Usuń"
},
"disable_selected": {
"message": "Wyłącz"
},
"enable_selected": {
"message": "Włącz"
},
"import_list": {
"message": "Importuj JSON"
},
"export_list": {
"message": "Eksportuj JSON"
},
"default_list_name": {
"message": "Lista domyślna"
},
"new_list_name": {
"message": "Nowa lista"
},
"word_active_label": {
"message": "aktywny"
},
"invalid_json_error": {
"message": "Nieprawidłowy plik JSON"
},
"confirm_delete_list": {
"message": "Czy na pewno chcesz usunąć tę listę?"
},
"confirm_delete_words": {
"message": "Czy na pewno chcesz usunąć wybrane słowa?"
},
"deselect_all": {
"message": "Odznacz"
},
"highlight_lists": {
"message": "Listy podświetleń"
},
"dark_mode": {
"message": "Tryb ciemny"
},
"list_settings": {
"message": "Ustawienia listy"
},
"word_list": {
"message": "Lista słów"
},
"add_words": {
"message": "Dodaj słowa"
},
"global_highlight_toggle": {
"message": "Włącz"
},
"search_placeholder": {
"message": "Szukaj..."
},
"options": {
"message": "Opcje"
},
"match_case": {
"message": "Uwzględnij wielkość liter"
},
"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ń z wyjątków"
},
"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ń"
}
}

View File

@@ -0,0 +1,122 @@
{
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "Destaque palavras e frases em qualquer site. Crie listas personalizadas, use cores, importe/exporte e muito mais."
},
"select_list": {
"message": "Selecionar lista:"
},
"new_list": {
"message": "Nova lista"
},
"delete_list": {
"message": "Excluir lista"
},
"list_name": {
"message": "Nome da lista:"
},
"background": {
"message": "Fundo:"
},
"foreground": {
"message": "Texto:"
},
"enable_highlight": {
"message": "Ativar destaque"
},
"paste_hint": {
"message": "Cole as palavras aqui"
},
"apply_paste": {
"message": "Adicionar palavras"
},
"select_all": {
"message": "Selecionar"
},
"delete_selected": {
"message": "Excluir"
},
"disable_selected": {
"message": "Desativar"
},
"enable_selected": {
"message": "Ativar"
},
"import_list": {
"message": "Importar JSON"
},
"export_list": {
"message": "Exportar JSON"
},
"default_list_name": {
"message": "Lista padrão"
},
"new_list_name": {
"message": "Nova lista"
},
"word_active_label": {
"message": "ativo"
},
"invalid_json_error": {
"message": "Arquivo JSON inválido"
},
"confirm_delete_list": {
"message": "Tem certeza de que deseja excluir esta lista?"
},
"confirm_delete_words": {
"message": "Tem certeza de que deseja excluir as palavras selecionadas?"
},
"deselect_all": {
"message": "Desmarcar"
},
"highlight_lists": {
"message": "Listas de destaque"
},
"dark_mode": {
"message": "Modo escuro"
},
"list_settings": {
"message": "Configurações da lista"
},
"word_list": {
"message": "Lista de palavras"
},
"add_words": {
"message": "Adicionar palavras"
},
"global_highlight_toggle": {
"message": "Ativar"
},
"search_placeholder": {
"message": "Pesquisar..."
},
"options": { "message": "Opções" },
"match_case": { "message": "Diferenciar maiúsculas/minúsculas" },
"match_whole": { "message": "Palavra inteira" },
"site_exceptions": {
"message": "Exceções de sites"
},
"add_exception": {
"message": "Adicionar às exceções"
},
"remove_exception": {
"message": "Remover das exceções"
},
"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"
}
}

View File

@@ -2,6 +2,9 @@
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "Выделяйте слова и фразы на любом сайте. Создавайте собственные списки, используйте цвета, импортируйте и экспортируйте данные."
},
"select_list": {
"message": "Выберите список:"
},
@@ -85,5 +88,41 @@
},
"global_highlight_toggle": {
"message": "Вкл"
},
"search_placeholder": {
"message": "Поиск..."
},
"options": {
"message": "Опции"
},
"match_case": {
"message": "С учетом регистра"
},
"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": "Удалить"
}
}

128
_locales/tr/messages.json Normal file
View File

@@ -0,0 +1,128 @@
{
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "Herhangi bir web sitesinde kelimeleri ve ifadeleri vurgulayın. Özel listeler oluşturun, renkler kullanın, içe/dışa aktarın ve daha fazlası."
},
"select_list": {
"message": "Liste Seç:"
},
"new_list": {
"message": "Yeni Liste"
},
"delete_list": {
"message": "Listeyi Sil"
},
"list_name": {
"message": "Liste Adı:"
},
"background": {
"message": "Arka Plan:"
},
"foreground": {
"message": "Metin Rengi:"
},
"enable_highlight": {
"message": "Vurgulamayı Etkinleştir"
},
"paste_hint": {
"message": "Kelimeleri buraya yapıştırın"
},
"apply_paste": {
"message": "Kelimeleri Ekle"
},
"select_all": {
"message": "Seç"
},
"delete_selected": {
"message": "Sil"
},
"disable_selected": {
"message": "Devre Dışı Bırak"
},
"enable_selected": {
"message": "Etkinleştir"
},
"import_list": {
"message": "JSON İçe Aktar"
},
"export_list": {
"message": "JSON Dışa Aktar"
},
"default_list_name": {
"message": "Varsayılan Liste"
},
"new_list_name": {
"message": "Yeni Liste"
},
"word_active_label": {
"message": "aktif"
},
"invalid_json_error": {
"message": "Geçersiz JSON dosyası"
},
"confirm_delete_list": {
"message": "Bu listeyi silmek istediğinizden emin misiniz?"
},
"confirm_delete_words": {
"message": "Seçili kelimeleri silmek istediğinizden emin misiniz?"
},
"deselect_all": {
"message": "Seçimi Kaldır"
},
"highlight_lists": {
"message": "Vurgulama Listeleri"
},
"dark_mode": {
"message": "Karanlık Mod"
},
"list_settings": {
"message": "Liste Ayarları"
},
"word_list": {
"message": "Kelime Listesi"
},
"add_words": {
"message": "Kelimeleri Ekle"
},
"global_highlight_toggle": {
"message": "Etkinleştir"
},
"search_placeholder": {
"message": "Ara..."
},
"options": {
"message": "Seçenekler"
},
"match_case": {
"message": "Büyük/küçük harf duyarlı"
},
"match_whole": {
"message": "Tüm kelimeyle eşleş"
},
"site_exceptions": {
"message": "Site İstisnaları"
},
"add_exception": {
"message": "İstisnalara Ekle"
},
"remove_exception": {
"message": "İstisnalardan Çıkar"
},
"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"
}
}

View File

@@ -0,0 +1,128 @@
{
"extension_name": {
"message": "Goose Highlighter"
},
"extension_description": {
"message": "在任何网站上高亮显示单词和短语。创建自定义列表,使用颜色,导入/导出等功能。"
},
"select_list": {
"message": "选择列表:"
},
"new_list": {
"message": "新建列表"
},
"delete_list": {
"message": "删除列表"
},
"list_name": {
"message": "列表名称:"
},
"background": {
"message": "背景:"
},
"foreground": {
"message": "文字颜色:"
},
"enable_highlight": {
"message": "启用高亮"
},
"paste_hint": {
"message": "在此粘贴单词"
},
"apply_paste": {
"message": "添加单词"
},
"select_all": {
"message": "选择"
},
"delete_selected": {
"message": "删除"
},
"disable_selected": {
"message": "禁用"
},
"enable_selected": {
"message": "启用"
},
"import_list": {
"message": "导入 JSON"
},
"export_list": {
"message": "导出 JSON"
},
"default_list_name": {
"message": "默认列表"
},
"new_list_name": {
"message": "新建列表"
},
"word_active_label": {
"message": "激活"
},
"invalid_json_error": {
"message": "无效的 JSON 文件"
},
"confirm_delete_list": {
"message": "您确定要删除此列表吗?"
},
"confirm_delete_words": {
"message": "您确定要删除选中的单词吗?"
},
"deselect_all": {
"message": "取消选择"
},
"highlight_lists": {
"message": "高亮列表"
},
"dark_mode": {
"message": "暗黑模式"
},
"list_settings": {
"message": "列表设置"
},
"word_list": {
"message": "单词列表"
},
"add_words": {
"message": "添加单词"
},
"global_highlight_toggle": {
"message": "启用"
},
"search_placeholder": {
"message": "搜索..."
},
"options": {
"message": "选项"
},
"match_case": {
"message": "区分大小写"
},
"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": "移除"
}
}

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',
},
}];

133
main.js
View File

@@ -1,133 +0,0 @@
let currentLists = [];
let isGlobalHighlightEnabled = true;
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(word.text.toLowerCase(), word);
const pattern = new RegExp(`(${Array.from(wordMap.keys()).map(escapeRegex).join('|')})`, 'gi');
for (const node of textNodes) {
if (!pattern.test(node.nodeValue)) continue;
const span = document.createElement('span');
span.innerHTML = node.nodeValue.replace(pattern, match => {
const word = wordMap.get(match.toLowerCase()) || { 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);
}
}
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"], ({ lists, globalHighlightEnabled }) => {
if (Array.isArray(lists)) setListsAndUpdate(lists);
if (globalHighlightEnabled !== undefined) {
isGlobalHighlightEnabled = globalHighlightEnabled;
}
processNodes(); // Initial processing
});
// 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();
}
});
// 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

@@ -1,12 +1,13 @@
{
"manifest_version": 3,
"name": "Goose Highlighter",
"version": "1.3.0",
"description": "Highlight text on web pages",
"name": "__MSG_extension_name__",
"description": "__MSG_extension_description__",
"version": "1.8.3",
"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",

1148
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +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.5",
"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"
}
}

View File

@@ -7,6 +7,7 @@
--button-hover: #444;
--button-text: white;
--accent: #ec9c23;
--accent-hover: #ffb84d;
--accent-text: #000;
--highlight-tag: #292929;
--highlight-tag-border: #444;
@@ -18,6 +19,10 @@
--switch-bg: #444;
--checkbox-accent: #ec9c23;
--checkbox-border: #666;
--scrollbar-bg: var(--section-bg);
--scrollbar-thumb: var(--accent);
--scrollbar-thumb-hover: var(--accent-hover);
--scrollbar-thumb-border: var(--section-bg);
}
body {
@@ -75,6 +80,7 @@ h1 {
border-radius: var(--border-radius);
padding: 14px;
box-shadow: var(--shadow);
transition: all 0.2s ease;
}
.section-header {
@@ -82,6 +88,60 @@ h1 {
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
cursor: pointer;
padding-left: 8px;
padding-right: 8px;
}
.section-header:hover {
background: var(--highlight-tag);
border-radius: 6px;
transition: background 0.2s ease;
}
.collapse-toggle {
background: none !important;
border: none !important;
color: var(--text-color) !important;
padding: 4px !important;
margin: 0 !important;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
.collapse-toggle:hover {
background: var(--button-hover) !important;
color: var(--accent) !important;
}
.collapse-toggle i {
transition: transform 0.2s ease;
font-size: 0.8em;
}
.section.collapsed .collapse-toggle i {
transform: rotate(180deg);
}
.section-content {
overflow: hidden;
max-height: 2000px;
}
.section.collapsed .section-content {
max-height: 0;
margin-bottom: 0;
padding-bottom: 0;
}
.section.collapsed .section-header {
margin-bottom: 0;
}
.section h2 {
@@ -91,6 +151,7 @@ h1 {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
input[type="text"],
@@ -115,11 +176,40 @@ textarea {
input[type="color"] {
background: none;
border: none;
cursor: pointer;
border: 2px solid var(--input-border);
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0,0,0,0.10);
width: 36px;
height: 36px;
margin-left: 6px;
cursor: pointer;
padding: 0;
transition: border 0.2s, box-shadow 0.2s;
appearance: none;
-webkit-appearance: none;
overflow: hidden;
}
input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
border-radius: 0;
}
input[type="color"]::-webkit-color-swatch {
border-radius: 0;
border: none;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
input[type="color"]::-moz-color-swatch {
border-radius: 0;
border: none;
padding: 0;
width: 100%;
height: 100%;
}
input[type="checkbox"] {
@@ -128,9 +218,9 @@ input[type="checkbox"] {
background-color: var(--input-bg);
border: 1px solid var(--checkbox-border);
border-radius: 4px;
width: 16px;
height: 16px;
transform: scale(1.2);
width: 12px;
height: 12px;
transform: scale(1);
cursor: pointer;
position: relative;
}
@@ -138,10 +228,10 @@ input[type="checkbox"] {
input[type="checkbox"]:checked::after {
content: "";
position: absolute;
top: 2px;
left: 5px;
width: 4px;
height: 9px;
top: 1px;
left: 3px;
width: 3px;
height: 7px;
border: solid var(--checkbox-accent);
border-width: 0 2px 2px 0;
transform: rotate(45deg);
@@ -223,6 +313,19 @@ button.danger:hover {
flex-wrap: wrap;
}
.button-row label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-size: 1em;
}
.button-row input[type="checkbox"] {
margin: 0;
vertical-align: middle;
}
.color-row {
display: flex;
gap: 24px;
@@ -236,6 +339,12 @@ button.danger:hover {
gap: 6px;
}
#wordSearch{
width:100%;
margin-bottom:8px;
margin-top: 8px;
}
#wordList {
margin-top: 8px;
position: relative;
@@ -380,4 +489,127 @@ label:has(input.switch) {
display: flex;
align-items: center;
gap: 8px;
}
#wordList,
body {
scrollbar-width: thin;
scrollbar-color: var(--accent) var(--section-bg);
}
#wordList::-webkit-scrollbar,
body::-webkit-scrollbar {
width: 8px;
background: var(--section-bg);
border-radius: 8px;
}
#wordList::-webkit-scrollbar-thumb,
body::-webkit-scrollbar-thumb {
background: var(--accent);
border-radius: 8px;
min-height: 24px;
border: 2px solid var(--section-bg);
}
#wordList::-webkit-scrollbar-thumb:hover,
body::-webkit-scrollbar-thumb:hover {
background: var(--accent-hover);
}
#wordList::-webkit-scrollbar-corner,
body::-webkit-scrollbar-corner {
background: var(--section-bg);
}
:root {
--scrollbar-bg: var(--section-bg);
--scrollbar-thumb: var(--accent);
--scrollbar-thumb-hover: var(--accent-hover);
--scrollbar-thumb-border: var(--section-bg);
}
html, body, #wordList {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-bg);
}
html::-webkit-scrollbar,
body::-webkit-scrollbar,
#wordList::-webkit-scrollbar {
width: 8px;
background: var(--scrollbar-bg);
border-radius: 8px;
}
html::-webkit-scrollbar-thumb,
body::-webkit-scrollbar-thumb,
#wordList::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 8px;
min-height: 24px;
border: 2px solid var(--scrollbar-thumb-border);
}
html::-webkit-scrollbar-thumb:hover,
body::-webkit-scrollbar-thumb:hover,
#wordList::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover);
}
html::-webkit-scrollbar-corner,
body::-webkit-scrollbar-corner,
#wordList::-webkit-scrollbar-corner {
background: var(--scrollbar-bg);
}
/* Exception Panel Styles */
.exceptions-panel {
margin-top: 10px;
padding: 12px;
border: 1px solid var(--input-border);
border-radius: var(--border-radius);
background: var(--input-bg);
}
.exceptions-list {
max-height: 120px;
overflow-y: auto;
margin: 8px 0;
border: 1px solid var(--input-border);
border-radius: 6px;
background: var(--section-bg);
}
.exception-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid var(--highlight-tag-border);
font-size: 12px;
}
.exception-item:last-child {
border-bottom: none;
}
.exception-domain {
flex: 1;
word-break: break-all;
margin-right: 8px;
}
.exception-remove {
background: var(--danger);
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 10px;
}
.exception-remove:hover {
background: #d00030;
}

View File

@@ -29,68 +29,127 @@
</div>
</div>
<div class="section">
<div class="section" data-section="exceptions">
<div class="section-header">
<h2><i class="fa-solid fa-ban"></i> <span data-i18n="site_exceptions">Site Exceptions</span></h2>
<button class="collapse-toggle" data-target="exceptions">
<i class="fa-solid fa-chevron-up"></i>
</button>
</div>
<div class="section-content" id="exceptions-content">
<div class="button-row">
<button id="toggleExceptionBtn"><i class="fa-solid fa-ban"></i> <span id="exceptionBtnText" data-i18n="add_exception">Add to Exceptions</span></button>
<button id="manageExceptionsBtn"><i class="fa-solid fa-list"></i> <span data-i18n="manage_exceptions">Manage</span></button>
</div>
<div id="exceptionsPanel" class="exceptions-panel" style="display: none;">
<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>
<div class="section" data-section="lists">
<div class="section-header">
<h2><i class="fa-solid fa-list"></i> <span data-i18n="highlight_lists">Highlight Lists</span></h2>
<button class="collapse-toggle" data-target="lists">
<i class="fa-solid fa-chevron-up"></i>
</button>
</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>
<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="section-content" id="lists-content">
<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">
<div class="section-header">
<h2><i class="fa-solid fa-gear"></i> <span data-i18n="list_settings">List Settings</span></h2>
<button class="collapse-toggle" data-target="settings">
<i class="fa-solid fa-chevron-up"></i>
</button>
</div>
<div class="section-content" id="settings-content">
<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>
<div id="wordList"></div>
</div>
<div class="section">
<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="addwords">
<div class="section-header">
<h2><i class="fa-solid fa-pen"></i> <span data-i18n="add_words">Add Words</span></h2>
<button class="collapse-toggle" data-target="addwords">
<i class="fa-solid fa-chevron-up"></i>
</button>
</div>
<div class="section-content" id="addwords-content">
<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>
<div class="section" data-section="wordlist">
<div class="section-header">
<h2><i class="fa-solid fa-tags"></i> <span data-i18n="word_list">Word List</span>(<span id="wordCount">0</span>)
</h2>
<button class="collapse-toggle" data-target="wordlist">
<i class="fa-solid fa-chevron-up"></i>
</button>
</div>
<div class="section-content" id="wordlist-content">
<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="section" data-section="options">
<div class="section-header">
<h2><i class="fa-solid fa-sliders"></i> <span data-i18n="options">Options</span></h2>
<button class="collapse-toggle" data-target="options">
<i class="fa-solid fa-chevron-up"></i>
</button>
</div>
<div class="section-content" id="options-content">
<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>
<script type="module" src="../dist/popup/popup.js"></script>
</body>
</html>

View File

@@ -1,419 +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");
let lists = [];
let currentListIndex = 0;
let saveTimeout;
let selectedCheckboxes = new Set();
let globalHighlightEnabled = true;
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
});
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
});
}
}
});
}
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
});
lists = res.lists;
globalHighlightEnabled = res.globalHighlightEnabled !== false; // Default to true if undefined
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];
const fragment = document.createDocumentFragment();
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,
list.words.length
);
wordList.innerHTML = '';
const spacer = document.createElement('div');
spacer.style.position = 'relative';
spacer.style.height = `${list.words.length * itemHeight}px`;
spacer.style.width = '100%';
for (let i = startIndex; i < endIndex; i++) {
const w = list.words[i];
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 cbSelect = document.createElement("input");
cbSelect.type = "checkbox";
cbSelect.className = "word-checkbox";
cbSelect.dataset.index = i;
if (selectedCheckboxes.has(i)) {
cbSelect.checked = true;
}
const inputWord = document.createElement("input");
inputWord.type = "text";
inputWord.value = w.wordStr;
inputWord.dataset.wordEdit = i;
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 = i;
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 = i;
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 = i;
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 = list.words.length;
}
}
document.addEventListener('DOMContentLoaded', () => {
localizePage();
document.getElementById("selectAllBtn").onclick = () => {
const list = lists[currentListIndex];
list.words.forEach((_, index) => {
selectedCheckboxes.add(index);
});
renderWords();
};
// Add event listener for the global toggle
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();
};
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,12 +0,0 @@
const fs = require("fs");
const path = require("path");
module.exports = async ({ nextRelease }) => {
const manifestPath = path.resolve(__dirname, "../manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
manifest.version = nextRelease.version;
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
console.log(`✅ Updated manifest.json to version ${nextRelease.version}`);
};

View File

@@ -0,0 +1,13 @@
const fs = require('fs');
const version = process.argv[2];
if (!version) {
console.log('No version passed, skipping manifest update');
process.exit(0);
}
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf-8'));
manifest.version = version;
fs.writeFileSync('manifest.json', JSON.stringify(manifest, null, 2));
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,115 @@
import { HighlightList, MessageData } from '../types.js';
import { StorageService } from '../services/StorageService.js';
import { MessageService } from '../services/MessageService.js';
import { HighlightEngine } from './HighlightEngine.js';
import { DOMUtils } from '../utils/DOMUtils.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.setupScrollHandler();
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) => {
switch (message.type) {
case 'WORD_LIST_UPDATED':
this.handleWordListUpdate();
break;
case 'GLOBAL_TOGGLE_UPDATED':
this.handleGlobalToggleUpdate(message.enabled!);
break;
case 'MATCH_OPTIONS_UPDATED':
this.handleMatchOptionsUpdate(message.matchCase!, message.matchWhole!);
break;
case 'EXCEPTIONS_LIST_UPDATED':
this.handleExceptionsUpdate();
break;
}
});
}
private setupScrollHandler(): void {
const debouncedProcess = DOMUtils.debounce(() => this.processHighlights(), 300);
window.addEventListener('scroll', debouncedProcess);
}
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;
}
}
}

View File

@@ -0,0 +1,195 @@
import { HighlightList, ActiveWord } from '../types.js';
import { DOMUtils } from '../utils/DOMUtils.js';
export class HighlightEngine {
private styleSheet: CSSStyleSheet | null = null;
private wordStyleMap = new Map<string, string>();
private observer: MutationObserver;
constructor(private onUpdate: () => void) {
this.observer = new MutationObserver(DOMUtils.debounce((mutations: MutationRecord[]) => {
const hasRelevantChanges = mutations.some((mutation: MutationRecord) => {
if (mutation.target instanceof Element && mutation.target.hasAttribute('data-gh')) {
return false;
}
const addedNodes = Array.from(mutation.addedNodes);
const removedNodes = Array.from(mutation.removedNodes);
const isOurChange = [...addedNodes, ...removedNodes].some(node =>
node instanceof Element && (node.hasAttribute('data-gh') || node.querySelector('[data-gh]'))
);
return !isOurChange;
});
if (hasRelevantChanges) {
onUpdate();
}
}, 300));
}
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 updateWordStyles(activeWords: ActiveWord[]): void {
this.initializeStyleSheet();
while (this.styleSheet!.cssRules.length > 0) {
this.styleSheet!.deleteRule(0);
}
this.wordStyleMap.clear();
const uniqueStyles = new Map<string, string>();
for (const word of activeWords) {
const styleKey = `${word.background}-${word.foreground}`;
if (!uniqueStyles.has(styleKey)) {
const className = `highlighted-word-${uniqueStyles.size}`;
uniqueStyles.set(styleKey, className);
const rule = `.${className} { background: ${word.background}; color: ${word.foreground}; padding: 0 2px; }`;
this.styleSheet!.insertRule(rule, this.styleSheet!.cssRules.length);
}
const lookup = word.text;
this.wordStyleMap.set(lookup, uniqueStyles.get(styleKey)!);
}
}
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 && (node.parentNode as Element).hasAttribute('data-gh')) {
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 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 {
this.observer.disconnect();
this.clearHighlightsInternal();
const activeWords = this.extractActiveWords(lists);
if (activeWords.length === 0) {
this.startObserving();
return;
}
this.updateWordStyles(activeWords);
const wordMap = new Map<string, ActiveWord>();
for (const word of activeWords) {
const key = matchCase ? word.text : word.text.toLowerCase();
wordMap.set(key, word);
}
const flags = matchCase ? 'gu' : 'giu';
let wordsPattern = Array.from(wordMap.keys()).map(DOMUtils.escapeRegex).join('|');
if (matchWhole) {
wordsPattern = `(?:(?<!\\p{L})|^)(${wordsPattern})(?:(?!\\p{L})|$)`;
}
try {
const pattern = new RegExp(`(${wordsPattern})`, flags);
const textNodes = this.getTextNodes();
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 className = this.wordStyleMap.get(lookup) || 'highlighted-word-0';
return `<span data-gh class="${className}">${match}</span>`;
});
node.parentNode?.replaceChild(span, node);
}
} catch (e) {
console.error('Regex error:', e);
}
this.startObserving();
}
private clearHighlightsInternal(): void {
const highlightedElements = document.querySelectorAll('[data-gh]');
highlightedElements.forEach(element => {
const parent = element.parentNode;
if (parent) {
parent.replaceChild(document.createTextNode(element.textContent || ''), element);
parent.normalize();
}
});
if (this.styleSheet && this.styleSheet.cssRules.length > 0) {
while (this.styleSheet.cssRules.length > 0) {
this.styleSheet.deleteRule(0);
}
}
}
stopObserving(): void {
this.observer.disconnect();
}
private startObserving(): void {
this.observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
// Don't observe attribute changes to avoid triggering on our own style changes
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,638 @@
import { HighlightList, HighlightWord, ExportData } 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 sectionStates: Record<string, boolean> = {};
async initialize(): Promise<void> {
await this.loadData();
await this.getCurrentTab();
this.loadSectionStates();
this.initializeSectionStates();
this.setupEventListeners();
this.render();
}
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 loadSectionStates(): void {
const saved = localStorage.getItem('goose-highlighter-section-states');
if (saved) {
try {
this.sectionStates = JSON.parse(saved);
} catch {
this.sectionStates = {};
}
}
}
private saveSectionStates(): void {
localStorage.setItem('goose-highlighter-section-states', JSON.stringify(this.sectionStates));
}
private initializeSectionStates(): void {
Object.keys(this.sectionStates).forEach(sectionName => {
const section = document.querySelector(`[data-section="${sectionName}"]`);
if (section && this.sectionStates[sectionName]) {
section.classList.add('collapsed');
}
});
}
private setupEventListeners(): void {
this.setupSectionToggles();
this.setupListManagement();
this.setupWordManagement();
this.setupSettings();
this.setupExceptions();
this.setupImportExport();
this.setupTheme();
}
private setupSectionToggles(): void {
document.querySelectorAll('.collapse-toggle').forEach(button => {
button.addEventListener('click', (e) => {
e.stopPropagation();
const targetSection = (button as HTMLElement).getAttribute('data-target');
if (targetSection) this.toggleSection(targetSection);
});
});
document.querySelectorAll('.section-header').forEach(header => {
header.addEventListener('click', (e) => {
if ((e.target as HTMLElement).tagName === 'BUTTON' ||
(e.target as HTMLElement).tagName === 'INPUT' ||
(e.target as HTMLElement).closest('button')) {
return;
}
const section = (header as HTMLElement).closest('.section');
const sectionName = section?.getAttribute('data-section');
if (sectionName) this.toggleSection(sectionName);
});
});
}
private toggleSection(sectionName: string): void {
const section = document.querySelector(`[data-section="${sectionName}"]`);
if (!section) return;
const isCollapsed = section.classList.contains('collapsed');
if (isCollapsed) {
section.classList.remove('collapsed');
this.sectionStates[sectionName] = false;
} else {
section.classList.add('collapsed');
this.sectionStates[sectionName] = true;
}
this.saveSectionStates();
}
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') {
if (target.dataset.index != null) {
const index = +target.dataset.index;
if (target.checked) {
this.selectedCheckboxes.add(index);
} else {
this.selectedCheckboxes.delete(index);
}
this.renderWords();
} else if (target.dataset.activeEdit != null) {
const index = +target.dataset.activeEdit;
this.lists[this.currentListIndex].words[index].active = target.checked;
this.save();
}
}
});
wordList.addEventListener('input', (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 scrollTimeout: number;
wordList.addEventListener('scroll', () => {
if (scrollTimeout) return;
scrollTimeout = window.setTimeout(() => {
requestAnimationFrame(() => this.renderWords());
scrollTimeout = 0;
}, 16);
});
}
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', () => {
this.globalHighlightEnabled = globalToggle.checked;
this.updateGlobalToggleState();
});
matchCase.addEventListener('change', () => {
this.matchCaseEnabled = matchCase.checked;
this.save();
});
matchWhole.addEventListener('change', () => {
this.matchWholeEnabled = matchWhole.checked;
this.save();
});
}
private setupExceptions(): void {
document.getElementById('toggleExceptionBtn')?.addEventListener('click', () => {
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();
this.save();
});
document.getElementById('manageExceptionsBtn')?.addEventListener('click', () => {
const panel = document.getElementById('exceptionsPanel');
if (panel) {
const isVisible = panel.style.display !== 'none';
panel.style.display = isVisible ? 'none' : 'block';
}
});
document.getElementById('clearExceptionsBtn')?.addEventListener('click', () => {
if (confirm(chrome.i18n.getMessage('confirm_clear_exceptions') || 'Clear all exceptions?')) {
this.exceptionsList = [];
this.updateExceptionButton();
this.renderExceptions();
this.save();
}
});
document.getElementById('exceptionsList')?.addEventListener('click', (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();
this.save();
}
});
}
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.render();
MessageService.sendToAllTabs({ type: 'WORD_LIST_UPDATED' });
MessageService.sendToAllTabs({
type: 'GLOBAL_TOGGLE_UPDATED',
enabled: this.globalHighlightEnabled
});
MessageService.sendToAllTabs({
type: 'MATCH_OPTIONS_UPDATED',
matchCase: this.matchCaseEnabled,
matchWhole: this.matchWholeEnabled
});
MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' });
}
private async updateGlobalToggleState(): Promise<void> {
await StorageService.update('globalHighlightEnabled', this.globalHighlightEnabled);
MessageService.sendToAllTabs({
type: 'GLOBAL_TOGGLE_UPDATED',
enabled: this.globalHighlightEnabled
});
}
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 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 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.style.cssText = `
height: ${itemHeight}px;
position: absolute;
top: ${displayIndex * itemHeight}px;
width: calc(100% - 8px);
left: 4px;
right: 4px;
display: flex;
align-items: center;
gap: 6px;
padding: 0 4px;
box-sizing: border-box;
background: var(--highlight-tag);
border: 1px solid var(--highlight-tag-border);
`;
const list = this.lists[this.currentListIndex];
container.innerHTML = `
<input type="checkbox" class="word-checkbox" data-index="${realIndex}" ${this.selectedCheckboxes.has(realIndex) ? 'checked' : ''}>
<input type="text" value="${DOMUtils.escapeHtml(word.wordStr)}" data-word-edit="${realIndex}" style="flex-grow: 1; min-width: 0; padding: 4px 8px; border-radius: 4px; border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--text-color);">
<input type="color" value="${word.background || list.background}" data-bg-edit="${realIndex}" style="width: 24px; height: 24px; flex-shrink: 0;">
<input type="color" value="${word.foreground || list.foreground}" data-fg-edit="${realIndex}" style="width: 24px; height: 24px; flex-shrink: 0;">
<label class="word-active" style="display: flex; align-items: center; gap: 4px; flex-shrink: 0;">
<input type="checkbox" ${word.active !== false ? 'checked' : ''} data-active-edit="${realIndex}" class="switch">
</label>
`;
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-check';
} 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-ban';
}
}
private renderExceptions(): void {
const container = document.getElementById('exceptionsList');
if (!container) return;
if (this.exceptionsList.length === 0) {
container.innerHTML = '<div class="exception-item">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;
}
}

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

@@ -0,0 +1,22 @@
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 as HTMLInputElement).hasAttribute('placeholder')) {
(element as HTMLInputElement).placeholder = localizedText;
} else {
element.textContent = localizedText;
}
}
});
}
document.addEventListener('DOMContentLoaded', async () => {
localizePage();
const controller = new PopupController();
await controller.initialize();
});

View File

@@ -0,0 +1,25 @@
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) => void): void {
chrome.runtime.onMessage.addListener(callback);
}
}

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>);
}
}

55
src/types.ts Normal file
View File

@@ -0,0 +1,55 @@
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 MessageData {
type: 'WORD_LIST_UPDATED' | 'GLOBAL_TOGGLE_UPDATED' | 'MATCH_OPTIONS_UPDATED' | 'EXCEPTIONS_LIST_UPDATED';
enabled?: boolean;
matchCase?: boolean;
matchWhole?: boolean;
}
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: 300,
SCROLL_THROTTLE: 16
} as const;

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"
]
}

3
versioning.md Normal file
View File

@@ -0,0 +1,3 @@
```
npx semantic-release --no-ci
```