mirror of
https://codeberg.org/Tilo-K/diff-highlighter.git
synced 2026-07-03 16:23:02 +00:00
Initial commit: Diff Highlighter Chrome extension (MV3, TypeScript)
Highlights .diff/.patch files inline — additions green, deletions red, hunk/file headers styled, with toggleable highlight.js syntax highlighting. - MV3, content script gated on text/plain + URL ending in .diff/.patch - Two-phase render: instant diff colors, then chunked idle-time syntax - Popup switches + Alt+D shortcut, persisted in chrome.storage.sync, live re-render - Light/dark via prefers-color-scheme; only the `storage` permission - TypeScript + esbuild build; Forgejo Actions CI (typecheck + build) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
15
.forgejo/workflows/ci.yml
Normal file
15
.forgejo/workflows/ci.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name: build
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
# On Codeberg's hosted runner use a label such as `codeberg-tiny`.
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: node:22-bookworm
|
||||||
|
steps:
|
||||||
|
# Forgejo resolves actions from data.forgejo.org, hence the full URL.
|
||||||
|
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run typecheck
|
||||||
|
- run: npm run build
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
85
README.md
Normal file
85
README.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Diff Highlighter
|
||||||
|
|
||||||
|
A Chrome (Manifest V3) extension that highlights `.diff` and `.patch` files in the
|
||||||
|
browser: **additions green, deletions red**, with hunk/file headers styled and the
|
||||||
|
underlying code **syntax-highlighted** by language. Works on GitHub/Bitbucket PR
|
||||||
|
`.diff`/`.patch` URLs, raw files, and local `file://` diffs. Light/dark aware, fast,
|
||||||
|
and fully toggleable.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- A content script runs only on `text/plain` pages whose URL ends in `.diff`/`.patch`
|
||||||
|
(the `include_globs` matcher covers query strings too, e.g. `…/123.diff?token=…`).
|
||||||
|
- It reads the browser-rendered `<pre>`, parses the unified-diff structure, and:
|
||||||
|
1. **Instantly** repaints every line with its diff color (green/red/context/headers).
|
||||||
|
2. **Progressively** layers syntax highlighting via [highlight.js](https://highlightjs.org/),
|
||||||
|
processed in idle-time chunks so large diffs stay responsive. Above ~50k highlightable
|
||||||
|
lines, syntax is skipped to preserve speed (diff colors remain).
|
||||||
|
- Toggling is instant and reload-free: the popup writes `chrome.storage.sync`, and the
|
||||||
|
content script re-renders on change. `Alt+D` flips the master toggle from anywhere.
|
||||||
|
- Only the `storage` permission is requested — declarative content scripts need no host
|
||||||
|
permissions.
|
||||||
|
|
||||||
|
## Toggles
|
||||||
|
|
||||||
|
| Control | Effect |
|
||||||
|
| --- | --- |
|
||||||
|
| **Highlighting** (popup) | Master switch for the green/red diff layer. |
|
||||||
|
| **Syntax highlighting** (popup) | Layer language-aware token colors on top. |
|
||||||
|
| `Alt+D` | Flip the master switch (rebind at `chrome://extensions/shortcuts`). |
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Requires Node 20+.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm ci
|
||||||
|
npm run build # type-checks, then bundles to dist/
|
||||||
|
npm run watch # rebuild on change (dev)
|
||||||
|
```
|
||||||
|
|
||||||
|
`dist/` is the unpacked extension (git-ignored).
|
||||||
|
|
||||||
|
## Load in Chrome
|
||||||
|
|
||||||
|
1. `chrome://extensions` → enable **Developer mode**.
|
||||||
|
2. **Load unpacked** → select the `dist/` folder.
|
||||||
|
3. For local diffs, open the extension's **Details** page and enable
|
||||||
|
**“Allow access to file URLs.”** Then try `samples/example.diff`.
|
||||||
|
|
||||||
|
## Continuous integration (Codeberg / Forgejo Actions)
|
||||||
|
|
||||||
|
CI lives in [`.forgejo/workflows/ci.yml`](.forgejo/workflows/ci.yml) and runs
|
||||||
|
`npm ci → typecheck → build` in a `node:22-bookworm` container — the same steps as a
|
||||||
|
local build, so a green local build means green CI.
|
||||||
|
|
||||||
|
To wire it up on Codeberg:
|
||||||
|
|
||||||
|
1. Push this repo to Codeberg:
|
||||||
|
```sh
|
||||||
|
git remote add origin https://codeberg.org/<you>/diff-highlighter.git
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
2. Connect a runner: repo **Settings → Actions → Runners** (or use Codeberg's hosted
|
||||||
|
runners). If using a hosted runner, change `runs-on: docker` to its label
|
||||||
|
(e.g. `codeberg-tiny`).
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Triggers on URLs/files whose path ends in `.diff`/`.patch` and that render as plain text.
|
||||||
|
In-app **HTML** diff views (GitHub's "Files changed", Bitbucket's `/diff` endpoint) are out
|
||||||
|
of scope — those are full HTML pages, not raw text.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
manifest.json MV3 manifest
|
||||||
|
content.ts detect + render + live toggle
|
||||||
|
diff-parser.ts pure diff parsing / language detection
|
||||||
|
background.ts defaults + Alt+D command
|
||||||
|
popup/ popup UI
|
||||||
|
styles/ diff.css, hljs-theme.css, popup.css (all light/dark)
|
||||||
|
scripts/ build.mjs (esbuild), make-icons.mjs
|
||||||
|
.forgejo/workflows/ CI
|
||||||
|
```
|
||||||
BIN
icons/icon128.png
Normal file
BIN
icons/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 391 B |
BIN
icons/icon16.png
Normal file
BIN
icons/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 B |
BIN
icons/icon32.png
Normal file
BIN
icons/icon32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 149 B |
BIN
icons/icon48.png
Normal file
BIN
icons/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 183 B |
580
package-lock.json
generated
Normal file
580
package-lock.json
generated
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
{
|
||||||
|
"name": "diff-highlighter",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "diff-highlighter",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"highlight.js": "^11.10.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chrome": "^0.0.287",
|
||||||
|
"@types/node": "^22.10.0",
|
||||||
|
"esbuild": "^0.28.1",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/chrome": {
|
||||||
|
"version": "0.0.287",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.287.tgz",
|
||||||
|
"integrity": "sha512-wWhBNPNXZHwycHKNYnexUcpSbrihVZu++0rdp6GEk5ZgAglenLx+RwdEouh6FrHS0XQiOxSd62yaujM1OoQlZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/filesystem": "*",
|
||||||
|
"@types/har-format": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/filesystem": {
|
||||||
|
"version": "0.0.36",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz",
|
||||||
|
"integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/filewriter": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/filewriter": {
|
||||||
|
"version": "0.0.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz",
|
||||||
|
"integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/har-format": {
|
||||||
|
"version": "1.2.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz",
|
||||||
|
"integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.19.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz",
|
||||||
|
"integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.28.1",
|
||||||
|
"@esbuild/android-arm": "0.28.1",
|
||||||
|
"@esbuild/android-arm64": "0.28.1",
|
||||||
|
"@esbuild/android-x64": "0.28.1",
|
||||||
|
"@esbuild/darwin-arm64": "0.28.1",
|
||||||
|
"@esbuild/darwin-x64": "0.28.1",
|
||||||
|
"@esbuild/freebsd-arm64": "0.28.1",
|
||||||
|
"@esbuild/freebsd-x64": "0.28.1",
|
||||||
|
"@esbuild/linux-arm": "0.28.1",
|
||||||
|
"@esbuild/linux-arm64": "0.28.1",
|
||||||
|
"@esbuild/linux-ia32": "0.28.1",
|
||||||
|
"@esbuild/linux-loong64": "0.28.1",
|
||||||
|
"@esbuild/linux-mips64el": "0.28.1",
|
||||||
|
"@esbuild/linux-ppc64": "0.28.1",
|
||||||
|
"@esbuild/linux-riscv64": "0.28.1",
|
||||||
|
"@esbuild/linux-s390x": "0.28.1",
|
||||||
|
"@esbuild/linux-x64": "0.28.1",
|
||||||
|
"@esbuild/netbsd-arm64": "0.28.1",
|
||||||
|
"@esbuild/netbsd-x64": "0.28.1",
|
||||||
|
"@esbuild/openbsd-arm64": "0.28.1",
|
||||||
|
"@esbuild/openbsd-x64": "0.28.1",
|
||||||
|
"@esbuild/openharmony-arm64": "0.28.1",
|
||||||
|
"@esbuild/sunos-x64": "0.28.1",
|
||||||
|
"@esbuild/win32-arm64": "0.28.1",
|
||||||
|
"@esbuild/win32-ia32": "0.28.1",
|
||||||
|
"@esbuild/win32-x64": "0.28.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/highlight.js": {
|
||||||
|
"version": "11.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||||
|
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "diff-highlighter",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Chrome extension that highlights .diff and .patch files — additions green, deletions red, with toggleable syntax highlighting.",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"build": "tsc --noEmit && node scripts/build.mjs",
|
||||||
|
"watch": "node scripts/build.mjs --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"highlight.js": "^11.10.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chrome": "^0.0.287",
|
||||||
|
"@types/node": "^22.10.0",
|
||||||
|
"esbuild": "^0.28.1",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
samples/example.diff
Normal file
35
samples/example.diff
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
diff --git a/src/greet.ts b/src/greet.ts
|
||||||
|
index 1a2b3c4..5d6e7f8 100644
|
||||||
|
--- a/src/greet.ts
|
||||||
|
+++ b/src/greet.ts
|
||||||
|
@@ -1,8 +1,9 @@
|
||||||
|
// Greeting helpers
|
||||||
|
-export function greet(name) {
|
||||||
|
- return "Hello " + name;
|
||||||
|
+export function greet(name: string): string {
|
||||||
|
+ const trimmed = name.trim();
|
||||||
|
+ return `Hello, ${trimmed || "world"}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
-console.log(greet("there"));
|
||||||
|
+console.log(greet(" there "));
|
||||||
|
diff --git a/scripts/run.py b/scripts/run.py
|
||||||
|
index aaaaaaa..bbbbbbb 100644
|
||||||
|
--- a/scripts/run.py
|
||||||
|
+++ b/scripts/run.py
|
||||||
|
@@ -1,4 +1,5 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
-def main():
|
||||||
|
- print("hi")
|
||||||
|
+def main() -> int:
|
||||||
|
+ print("hello from python")
|
||||||
|
+ return 0
|
||||||
|
diff --git a/README.md b/README.md
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..ccccccc
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/README.md
|
||||||
|
@@ -0,0 +1,2 @@
|
||||||
|
+# Demo
|
||||||
|
+A tiny sample used to exercise the highlighter.
|
||||||
57
scripts/build.mjs
Normal file
57
scripts/build.mjs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Builds the extension into dist/: bundles the TypeScript entry points with
|
||||||
|
// esbuild and copies the static assets (manifest, popup HTML, CSS, icons).
|
||||||
|
// Run `node scripts/build.mjs` for a one-shot build, or with `--watch` for dev.
|
||||||
|
import * as esbuild from 'esbuild';
|
||||||
|
import { cp, mkdir, rm } from 'node:fs/promises';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const dist = path.join(root, 'dist');
|
||||||
|
const watch = process.argv.includes('--watch');
|
||||||
|
|
||||||
|
/** Copy a file or directory from a repo-relative path into dist/ (flattened). */
|
||||||
|
async function copy(from, to) {
|
||||||
|
const src = path.join(root, from);
|
||||||
|
if (!existsSync(src)) return;
|
||||||
|
await cp(src, path.join(dist, to), { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyStatic() {
|
||||||
|
await copy('src/manifest.json', 'manifest.json');
|
||||||
|
await copy('src/popup/popup.html', 'popup.html');
|
||||||
|
await copy('src/styles/diff.css', 'diff.css');
|
||||||
|
await copy('src/styles/hljs-theme.css', 'hljs-theme.css');
|
||||||
|
await copy('src/styles/popup.css', 'popup.css');
|
||||||
|
await copy('icons', 'icons');
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
entryPoints: {
|
||||||
|
content: path.join(root, 'src/content.ts'),
|
||||||
|
background: path.join(root, 'src/background.ts'),
|
||||||
|
popup: path.join(root, 'src/popup/popup.ts'),
|
||||||
|
},
|
||||||
|
outdir: dist,
|
||||||
|
entryNames: '[name]',
|
||||||
|
bundle: true,
|
||||||
|
format: 'iife',
|
||||||
|
target: ['chrome120'],
|
||||||
|
minify: !watch,
|
||||||
|
sourcemap: watch ? 'inline' : false,
|
||||||
|
logLevel: 'info',
|
||||||
|
};
|
||||||
|
|
||||||
|
await rm(dist, { recursive: true, force: true });
|
||||||
|
await mkdir(dist, { recursive: true });
|
||||||
|
await copyStatic();
|
||||||
|
|
||||||
|
if (watch) {
|
||||||
|
const ctx = await esbuild.context(options);
|
||||||
|
await ctx.watch();
|
||||||
|
console.log('watching for changes…');
|
||||||
|
} else {
|
||||||
|
await esbuild.build(options);
|
||||||
|
console.log('built ->', path.relative(root, dist));
|
||||||
|
}
|
||||||
91
scripts/make-icons.mjs
Normal file
91
scripts/make-icons.mjs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// Generates the extension icons (green "added" band over a red "removed" band)
|
||||||
|
// as PNGs, with zero dependencies — just Node's zlib. Run: node scripts/make-icons.mjs
|
||||||
|
import zlib from 'node:zlib';
|
||||||
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const outDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../icons');
|
||||||
|
|
||||||
|
const CRC_TABLE = (() => {
|
||||||
|
const t = new Uint32Array(256);
|
||||||
|
for (let n = 0; n < 256; n++) {
|
||||||
|
let c = n;
|
||||||
|
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||||
|
t[n] = c >>> 0;
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
})();
|
||||||
|
|
||||||
|
function crc32(buf) {
|
||||||
|
let c = 0xffffffff;
|
||||||
|
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
||||||
|
return (c ^ 0xffffffff) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function chunk(type, data) {
|
||||||
|
const len = Buffer.alloc(4);
|
||||||
|
len.writeUInt32BE(data.length, 0);
|
||||||
|
const typeBuf = Buffer.from(type, 'ascii');
|
||||||
|
const crc = Buffer.alloc(4);
|
||||||
|
crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
|
||||||
|
return Buffer.concat([len, typeBuf, data, crc]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function png(size, rgba) {
|
||||||
|
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||||
|
const ihdr = Buffer.alloc(13);
|
||||||
|
ihdr.writeUInt32BE(size, 0);
|
||||||
|
ihdr.writeUInt32BE(size, 4);
|
||||||
|
ihdr[8] = 8; // bit depth
|
||||||
|
ihdr[9] = 6; // color type RGBA
|
||||||
|
const stride = size * 4;
|
||||||
|
const raw = Buffer.alloc((stride + 1) * size);
|
||||||
|
for (let y = 0; y < size; y++) {
|
||||||
|
raw[y * (stride + 1)] = 0; // filter: none
|
||||||
|
rgba.copy(raw, y * (stride + 1) + 1, y * stride, y * stride + stride);
|
||||||
|
}
|
||||||
|
const idat = zlib.deflateSync(raw, { level: 9 });
|
||||||
|
return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', Buffer.alloc(0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insideRounded(x, y, x0, y0, x1, y1, r) {
|
||||||
|
const cxL = x0 + r, cxR = x1 - r, cyT = y0 + r, cyB = y1 - r;
|
||||||
|
let cx = null, cy = null;
|
||||||
|
if (x < cxL && y < cyT) { cx = cxL; cy = cyT; }
|
||||||
|
else if (x > cxR && y < cyT) { cx = cxR; cy = cyT; }
|
||||||
|
else if (x < cxL && y > cyB) { cx = cxL; cy = cyB; }
|
||||||
|
else if (x > cxR && y > cyB) { cx = cxR; cy = cyB; }
|
||||||
|
if (cx === null) return true;
|
||||||
|
const dx = x - cx, dy = y - cy;
|
||||||
|
return dx * dx + dy * dy <= r * r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeIcon(size) {
|
||||||
|
const px = Buffer.alloc(size * size * 4); // transparent
|
||||||
|
const green = [46, 160, 67, 255];
|
||||||
|
const red = [215, 58, 73, 255];
|
||||||
|
const inset = Math.max(1, Math.round(size * 0.06));
|
||||||
|
const r = Math.round(size * 0.2);
|
||||||
|
const x0 = inset, y0 = inset, x1 = size - 1 - inset, y1 = size - 1 - inset;
|
||||||
|
const mid = Math.floor(size / 2);
|
||||||
|
const gap = Math.max(1, Math.round(size * 0.04));
|
||||||
|
const set = (x, y, c) => {
|
||||||
|
const i = (y * size + x) * 4;
|
||||||
|
px[i] = c[0]; px[i + 1] = c[1]; px[i + 2] = c[2]; px[i + 3] = c[3];
|
||||||
|
};
|
||||||
|
for (let y = y0; y <= y1; y++) {
|
||||||
|
for (let x = x0; x <= x1; x++) {
|
||||||
|
if (!insideRounded(x, y, x0, y0, x1, y1, r)) continue;
|
||||||
|
if (Math.abs(y - mid) < gap) continue; // divider between bands
|
||||||
|
set(x, y, y < mid ? green : red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return png(size, px);
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(outDir, { recursive: true });
|
||||||
|
for (const size of [16, 32, 48, 128]) {
|
||||||
|
writeFileSync(path.join(outDir, `icon${size}.png`), makeIcon(size));
|
||||||
|
}
|
||||||
|
console.log('icons written ->', path.relative(process.cwd(), outDir));
|
||||||
18
src/background.ts
Normal file
18
src/background.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Service worker: seeds default settings on install and handles the keyboard
|
||||||
|
// shortcut by flipping the master toggle. Content scripts react via
|
||||||
|
// chrome.storage.onChanged, so no direct messaging is needed.
|
||||||
|
import { DEFAULT_SETTINGS } from './settings';
|
||||||
|
import type { Settings } from './settings';
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
chrome.storage.sync.get(DEFAULT_SETTINGS, (current) => {
|
||||||
|
chrome.storage.sync.set(current as Settings); // persist resolved defaults
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.commands.onCommand.addListener((command) => {
|
||||||
|
if (command !== 'toggle-highlighting') return;
|
||||||
|
chrome.storage.sync.get(DEFAULT_SETTINGS, (s) => {
|
||||||
|
chrome.storage.sync.set({ enabled: !(s as Settings).enabled });
|
||||||
|
});
|
||||||
|
});
|
||||||
136
src/content.ts
Normal file
136
src/content.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// Runs on text/plain pages whose URL ends in .diff/.patch. Replaces the raw
|
||||||
|
// <pre> with a colored, optionally syntax-highlighted rendering, and reacts to
|
||||||
|
// the popup/keyboard toggles live (no reload).
|
||||||
|
import hljs from 'highlight.js/lib/common';
|
||||||
|
import { parseDiff } from './diff-parser';
|
||||||
|
import type { ParsedLine } from './diff-parser';
|
||||||
|
import { DEFAULT_SETTINGS } from './settings';
|
||||||
|
import type { Settings } from './settings';
|
||||||
|
|
||||||
|
const CHUNK = 300; // code lines highlighted per idle slice
|
||||||
|
const MAX_SYNTAX_LINES = 50_000; // above this, keep diff colors but skip syntax
|
||||||
|
|
||||||
|
// --- Guards: only touch browser-rendered plain-text diffs. -------------------
|
||||||
|
const pre =
|
||||||
|
document.contentType === 'text/plain'
|
||||||
|
? document.querySelector<HTMLPreElement>('body > pre') ??
|
||||||
|
document.querySelector<HTMLPreElement>('pre')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (pre) run(pre);
|
||||||
|
|
||||||
|
function run(pre: HTMLPreElement): void {
|
||||||
|
const raw = pre.textContent ?? '';
|
||||||
|
|
||||||
|
// Parse + build the base HTML once; reused on every (re)render.
|
||||||
|
let baseHtml: string | null = null;
|
||||||
|
const ensureBaseHtml = (): string => (baseHtml ??= buildHtml(parseDiff(raw)));
|
||||||
|
|
||||||
|
let idleHandle: number | null = null;
|
||||||
|
const cancelHighlight = (): void => {
|
||||||
|
if (idleHandle !== null) {
|
||||||
|
cancelIdle(idleHandle);
|
||||||
|
idleHandle = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const restorePlain = (): void => {
|
||||||
|
cancelHighlight();
|
||||||
|
if (pre.classList.contains('dh-on')) {
|
||||||
|
pre.textContent = raw;
|
||||||
|
pre.classList.remove('dh-on');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = (settings: Settings): void => {
|
||||||
|
cancelHighlight();
|
||||||
|
pre.innerHTML = ensureBaseHtml();
|
||||||
|
pre.classList.add('dh-on');
|
||||||
|
if (settings.syntax) scheduleSyntax();
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleSyntax = (): void => {
|
||||||
|
const nodes = Array.from(
|
||||||
|
pre.querySelectorAll<HTMLElement>('.dh-code[data-lang]'),
|
||||||
|
);
|
||||||
|
if (nodes.length > MAX_SYNTAX_LINES) {
|
||||||
|
console.info(
|
||||||
|
`[diff-highlighter] ${nodes.length} highlightable lines — skipping syntax for speed`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let i = 0;
|
||||||
|
const step = (): void => {
|
||||||
|
const end = Math.min(i + CHUNK, nodes.length);
|
||||||
|
for (; i < end; i++) {
|
||||||
|
const el = nodes[i]!;
|
||||||
|
const lang = el.dataset['lang']!;
|
||||||
|
try {
|
||||||
|
el.innerHTML = hljs.highlight(el.textContent ?? '', {
|
||||||
|
language: lang,
|
||||||
|
ignoreIllegals: true,
|
||||||
|
}).value;
|
||||||
|
} catch {
|
||||||
|
/* leave the escaped text in place */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idleHandle = i < nodes.length ? requestIdle(step) : null;
|
||||||
|
};
|
||||||
|
idleHandle = requestIdle(step);
|
||||||
|
};
|
||||||
|
|
||||||
|
const apply = (settings: Settings): void => {
|
||||||
|
if (settings.enabled) render(settings);
|
||||||
|
else restorePlain();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadAndApply = (): void => {
|
||||||
|
chrome.storage.sync.get(DEFAULT_SETTINGS, (s) => apply(s as Settings));
|
||||||
|
};
|
||||||
|
|
||||||
|
loadAndApply();
|
||||||
|
|
||||||
|
chrome.storage.onChanged.addListener((changes, area) => {
|
||||||
|
if (area !== 'sync') return;
|
||||||
|
if ('enabled' in changes || 'syntax' in changes) loadAndApply();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Rendering helpers -------------------------------------------------------
|
||||||
|
|
||||||
|
function buildHtml(lines: ParsedLine[]): string {
|
||||||
|
let html = '';
|
||||||
|
for (const line of lines) {
|
||||||
|
const hasGutter = line.kind === 'add' || line.kind === 'del' || line.kind === 'ctx';
|
||||||
|
const gutter = hasGutter
|
||||||
|
? `<span class="dh-gutter">${escapeHtml(line.marker)}</span>`
|
||||||
|
: '';
|
||||||
|
const langAttr =
|
||||||
|
line.lang && hljs.getLanguage(line.lang) ? ` data-lang="${line.lang}"` : '';
|
||||||
|
html +=
|
||||||
|
`<span class="dh-line dh-${line.kind}">${gutter}` +
|
||||||
|
`<span class="dh-code"${langAttr}>${escapeHtml(line.code)}</span></span>`;
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ESCAPE: Record<string, string> = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
};
|
||||||
|
function escapeHtml(s: string): string {
|
||||||
|
return s.replace(/[&<>]/g, (c) => ESCAPE[c]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Idle scheduling (with setTimeout fallback) ------------------------------
|
||||||
|
|
||||||
|
const requestIdle: (cb: () => void) => number =
|
||||||
|
typeof requestIdleCallback === 'function'
|
||||||
|
? (cb) => requestIdleCallback(cb)
|
||||||
|
: (cb) => setTimeout(cb, 0) as unknown as number;
|
||||||
|
|
||||||
|
const cancelIdle: (handle: number) => void =
|
||||||
|
typeof cancelIdleCallback === 'function'
|
||||||
|
? (handle) => cancelIdleCallback(handle)
|
||||||
|
: (handle) => clearTimeout(handle);
|
||||||
119
src/diff-parser.ts
Normal file
119
src/diff-parser.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// Pure, DOM-free parsing of unified diff / patch text. Splitting this out keeps
|
||||||
|
// the logic testable and the content script focused on rendering.
|
||||||
|
|
||||||
|
export type LineKind = 'add' | 'del' | 'hunk' | 'file' | 'meta' | 'ctx';
|
||||||
|
|
||||||
|
export interface ParsedLine {
|
||||||
|
kind: LineKind;
|
||||||
|
/** Gutter content: '+', '-', ' ', or '' for header lines. */
|
||||||
|
marker: string;
|
||||||
|
/** The remainder of the line to render (and maybe syntax-highlight). */
|
||||||
|
code: string;
|
||||||
|
/** Detected highlight.js language for this line's code, when known. */
|
||||||
|
lang: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** File-extension → highlight.js language id (limited to the common bundle). */
|
||||||
|
const EXT_TO_LANG: Record<string, string> = {
|
||||||
|
js: 'javascript', jsx: 'javascript', mjs: 'javascript', cjs: 'javascript',
|
||||||
|
ts: 'typescript', tsx: 'typescript', mts: 'typescript', cts: 'typescript',
|
||||||
|
py: 'python', pyw: 'python',
|
||||||
|
rb: 'ruby', go: 'go', rs: 'rust', java: 'java',
|
||||||
|
kt: 'kotlin', kts: 'kotlin',
|
||||||
|
c: 'c', h: 'c',
|
||||||
|
cc: 'cpp', cpp: 'cpp', cxx: 'cpp', hpp: 'cpp', hh: 'cpp', hxx: 'cpp',
|
||||||
|
cs: 'csharp', php: 'php', swift: 'swift',
|
||||||
|
sh: 'bash', bash: 'bash', zsh: 'bash',
|
||||||
|
json: 'json', yml: 'yaml', yaml: 'yaml',
|
||||||
|
xml: 'xml', html: 'xml', htm: 'xml', svg: 'xml', vue: 'xml', xhtml: 'xml',
|
||||||
|
css: 'css', scss: 'scss', sass: 'scss', less: 'less',
|
||||||
|
md: 'markdown', markdown: 'markdown',
|
||||||
|
sql: 'sql', lua: 'lua',
|
||||||
|
pl: 'perl', pm: 'perl',
|
||||||
|
r: 'r',
|
||||||
|
ini: 'ini', toml: 'ini', cfg: 'ini', conf: 'ini',
|
||||||
|
mk: 'makefile',
|
||||||
|
graphql: 'graphql', gql: 'graphql',
|
||||||
|
m: 'objectivec', mm: 'objectivec',
|
||||||
|
vb: 'vbnet',
|
||||||
|
};
|
||||||
|
|
||||||
|
const META_PREFIXES = [
|
||||||
|
'diff ', 'index ', 'new file', 'deleted file', 'old mode', 'new mode',
|
||||||
|
'rename ', 'copy ', 'similarity ', 'dissimilarity ', 'Binary files',
|
||||||
|
'GIT binary patch', '\\ No newline',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function classifyLine(line: string): LineKind {
|
||||||
|
if (line.startsWith('@@')) return 'hunk';
|
||||||
|
// Must precede the +/- checks below.
|
||||||
|
if (line.startsWith('+++') || line.startsWith('---')) return 'file';
|
||||||
|
if (line.startsWith('+')) return 'add';
|
||||||
|
if (line.startsWith('-')) return 'del';
|
||||||
|
for (const p of META_PREFIXES) if (line.startsWith(p)) return 'meta';
|
||||||
|
return 'ctx';
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMarker(line: string, kind: LineKind): { marker: string; code: string } {
|
||||||
|
if (kind === 'add' || kind === 'del') return { marker: line[0]!, code: line.slice(1) };
|
||||||
|
if (kind === 'ctx') {
|
||||||
|
if (line.startsWith(' ')) return { marker: ' ', code: line.slice(1) };
|
||||||
|
return { marker: '', code: line }; // blank / no-prefix line
|
||||||
|
}
|
||||||
|
return { marker: '', code: line }; // hunk / file / meta shown whole
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map a file path to a highlight.js language id, or null if unknown. */
|
||||||
|
export function languageForPath(p: string): string | null {
|
||||||
|
const base = (p.split('/').pop() ?? p).trim();
|
||||||
|
if (base.toLowerCase() === 'makefile') return 'makefile';
|
||||||
|
if (base.toLowerCase() === 'dockerfile') return null;
|
||||||
|
const dot = base.lastIndexOf('.');
|
||||||
|
if (dot <= 0) return null;
|
||||||
|
return EXT_TO_LANG[base.slice(dot + 1).toLowerCase()] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract the relevant path from a `diff --git`, `---`, or `+++` header line. */
|
||||||
|
function pathFromHeader(line: string): string | null {
|
||||||
|
if (line.startsWith('diff --git')) {
|
||||||
|
const m = line.match(/ b\/(.+)$/);
|
||||||
|
return m ? m[1]! : null;
|
||||||
|
}
|
||||||
|
if (line.startsWith('+++ ') || line.startsWith('--- ')) {
|
||||||
|
let p = line.slice(4).replace(/\t.*$/, '').trim(); // drop trailing tab+timestamp
|
||||||
|
if (p === '/dev/null') return null;
|
||||||
|
p = p.replace(/^[ab]\//, '');
|
||||||
|
return p || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse raw diff text into classified lines, threading the per-file language. */
|
||||||
|
export function parseDiff(raw: string): ParsedLine[] {
|
||||||
|
const out: ParsedLine[] = [];
|
||||||
|
let lang: string | null = null;
|
||||||
|
|
||||||
|
for (const line of raw.split('\n')) {
|
||||||
|
const kind = classifyLine(line);
|
||||||
|
|
||||||
|
if (kind === 'meta' && line.startsWith('diff --git')) {
|
||||||
|
// New file section in a git diff — reset the language (may be unknown).
|
||||||
|
const p = pathFromHeader(line);
|
||||||
|
lang = p ? languageForPath(p) : null;
|
||||||
|
} else if (kind === 'file') {
|
||||||
|
const p = pathFromHeader(line);
|
||||||
|
if (line.startsWith('--- ')) {
|
||||||
|
lang = p ? languageForPath(p) : null; // start of a plain unified-diff section
|
||||||
|
} else if (p) {
|
||||||
|
const refined = languageForPath(p); // +++ new path is most authoritative
|
||||||
|
if (refined) lang = refined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { marker, code } = splitMarker(line, kind);
|
||||||
|
const codeLang = kind === 'add' || kind === 'del' || kind === 'ctx' ? lang : null;
|
||||||
|
out.push({ kind, marker, code, lang: codeLang });
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
7
src/hljs-common.d.ts
vendored
Normal file
7
src/hljs-common.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// The `highlight.js/lib/common` subpath (core + ~37 common languages) ships JS
|
||||||
|
// but no dedicated typings, so reuse the main package's API type.
|
||||||
|
declare module 'highlight.js/lib/common' {
|
||||||
|
import type { HLJSApi } from 'highlight.js';
|
||||||
|
const hljs: HLJSApi;
|
||||||
|
export default hljs;
|
||||||
|
}
|
||||||
41
src/manifest.json
Normal file
41
src/manifest.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Diff Highlighter",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Highlights .diff and .patch files — additions green, deletions red, with toggleable syntax highlighting.",
|
||||||
|
"permissions": ["storage"],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_title": "Diff Highlighter",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"32": "icons/icon32.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"32": "icons/icon32.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["https://*/*", "http://*/*", "file:///*"],
|
||||||
|
"include_globs": ["*.diff", "*.diff?*", "*.patch", "*.patch?*"],
|
||||||
|
"js": ["content.js"],
|
||||||
|
"css": ["diff.css", "hljs-theme.css"],
|
||||||
|
"run_at": "document_end"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commands": {
|
||||||
|
"toggle-highlighting": {
|
||||||
|
"suggested_key": { "default": "Alt+D" },
|
||||||
|
"description": "Toggle diff highlighting"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/popup/popup.html
Normal file
32
src/popup/popup.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" href="popup.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Diff Highlighter</h1>
|
||||||
|
|
||||||
|
<label class="row">
|
||||||
|
<span class="label">Highlighting</span>
|
||||||
|
<input type="checkbox" id="enabled" class="switch" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="row">
|
||||||
|
<span class="label">Syntax highlighting</span>
|
||||||
|
<input type="checkbox" id="syntax" class="switch" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p class="hint">
|
||||||
|
Toggle anywhere with <kbd>Alt</kbd>+<kbd>D</kbd>
|
||||||
|
(rebind at <code>chrome://extensions/shortcuts</code>).
|
||||||
|
</p>
|
||||||
|
<p class="hint">
|
||||||
|
For local <code>file://</code> diffs, enable
|
||||||
|
<em>“Allow access to file URLs”</em> on the extension’s details page.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29
src/popup/popup.ts
Normal file
29
src/popup/popup.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Drives the popup switches and persists them. The content script reads the
|
||||||
|
// same chrome.storage.sync keys and re-renders the open diff live.
|
||||||
|
import { DEFAULT_SETTINGS } from '../settings';
|
||||||
|
import type { Settings } from '../settings';
|
||||||
|
|
||||||
|
const enabledEl = document.getElementById('enabled') as HTMLInputElement;
|
||||||
|
const syntaxEl = document.getElementById('syntax') as HTMLInputElement;
|
||||||
|
|
||||||
|
function reflectDisabled(enabled: boolean): void {
|
||||||
|
// Syntax highlighting only makes sense while the master toggle is on.
|
||||||
|
syntaxEl.disabled = !enabled;
|
||||||
|
syntaxEl.closest('.row')?.classList.toggle('muted', !enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.storage.sync.get(DEFAULT_SETTINGS, (s) => {
|
||||||
|
const settings = s as Settings;
|
||||||
|
enabledEl.checked = settings.enabled;
|
||||||
|
syntaxEl.checked = settings.syntax;
|
||||||
|
reflectDisabled(settings.enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
enabledEl.addEventListener('change', () => {
|
||||||
|
chrome.storage.sync.set({ enabled: enabledEl.checked });
|
||||||
|
reflectDisabled(enabledEl.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
syntaxEl.addEventListener('change', () => {
|
||||||
|
chrome.storage.sync.set({ syntax: syntaxEl.checked });
|
||||||
|
});
|
||||||
12
src/settings.ts
Normal file
12
src/settings.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/** User-facing toggles, persisted in chrome.storage.sync. */
|
||||||
|
export interface Settings {
|
||||||
|
/** Master switch for the green/red diff layer. */
|
||||||
|
enabled: boolean;
|
||||||
|
/** Layer language-aware syntax highlighting on top of the diff colors. */
|
||||||
|
syntax: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS: Settings = {
|
||||||
|
enabled: true,
|
||||||
|
syntax: true,
|
||||||
|
};
|
||||||
82
src/styles/diff.css
Normal file
82
src/styles/diff.css
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/* Diff layout + line colors. Everything is scoped under `pre.dh-on` so toggling
|
||||||
|
the extension off (which removes that class) fully restores the plain view. */
|
||||||
|
|
||||||
|
pre.dh-on {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
tab-size: 4;
|
||||||
|
font: 12px/1.5 ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
||||||
|
"Liberation Mono", monospace;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #1f2328;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.dh-on .dh-line {
|
||||||
|
display: block;
|
||||||
|
white-space: pre;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.dh-on .dh-gutter {
|
||||||
|
display: inline-block;
|
||||||
|
width: 2ch;
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.dh-on .dh-add {
|
||||||
|
background: #e6ffec;
|
||||||
|
}
|
||||||
|
pre.dh-on .dh-del {
|
||||||
|
background: #ffebe9;
|
||||||
|
}
|
||||||
|
pre.dh-on .dh-add .dh-gutter {
|
||||||
|
color: #1a7f37;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
pre.dh-on .dh-del .dh-gutter {
|
||||||
|
color: #cf222e;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.dh-on .dh-hunk {
|
||||||
|
color: #8250df;
|
||||||
|
background: #f1eeff;
|
||||||
|
}
|
||||||
|
pre.dh-on .dh-file {
|
||||||
|
color: #57606a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
pre.dh-on .dh-meta {
|
||||||
|
color: #57606a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
pre.dh-on {
|
||||||
|
background: #0d1117;
|
||||||
|
color: #e6edf3;
|
||||||
|
}
|
||||||
|
pre.dh-on .dh-add {
|
||||||
|
background: rgba(46, 160, 67, 0.15);
|
||||||
|
}
|
||||||
|
pre.dh-on .dh-del {
|
||||||
|
background: rgba(248, 81, 73, 0.15);
|
||||||
|
}
|
||||||
|
pre.dh-on .dh-add .dh-gutter {
|
||||||
|
color: #3fb950;
|
||||||
|
}
|
||||||
|
pre.dh-on .dh-del .dh-gutter {
|
||||||
|
color: #f85149;
|
||||||
|
}
|
||||||
|
pre.dh-on .dh-hunk {
|
||||||
|
color: #d2a8ff;
|
||||||
|
background: rgba(56, 139, 253, 0.1);
|
||||||
|
}
|
||||||
|
pre.dh-on .dh-file,
|
||||||
|
pre.dh-on .dh-meta {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/styles/hljs-theme.css
Normal file
103
src/styles/hljs-theme.css
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/* Compact highlight.js token theme (GitHub-like), light + dark. Scoped to the
|
||||||
|
diff view; the diff line background sits behind these foreground colors. */
|
||||||
|
|
||||||
|
pre.dh-on .hljs-comment,
|
||||||
|
pre.dh-on .hljs-quote {
|
||||||
|
color: #6e7781;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-keyword,
|
||||||
|
pre.dh-on .hljs-selector-tag,
|
||||||
|
pre.dh-on .hljs-literal,
|
||||||
|
pre.dh-on .hljs-doctag,
|
||||||
|
pre.dh-on .hljs-meta .hljs-keyword {
|
||||||
|
color: #cf222e;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-string,
|
||||||
|
pre.dh-on .hljs-regexp,
|
||||||
|
pre.dh-on .hljs-addition,
|
||||||
|
pre.dh-on .hljs-symbol,
|
||||||
|
pre.dh-on .hljs-bullet,
|
||||||
|
pre.dh-on .hljs-link {
|
||||||
|
color: #0a3069;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-number,
|
||||||
|
pre.dh-on .hljs-variable,
|
||||||
|
pre.dh-on .hljs-template-variable,
|
||||||
|
pre.dh-on .hljs-attr,
|
||||||
|
pre.dh-on .hljs-attribute,
|
||||||
|
pre.dh-on .hljs-property {
|
||||||
|
color: #0550ae;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-title,
|
||||||
|
pre.dh-on .hljs-title.function_,
|
||||||
|
pre.dh-on .hljs-section {
|
||||||
|
color: #8250df;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-type,
|
||||||
|
pre.dh-on .hljs-class .hljs-title,
|
||||||
|
pre.dh-on .hljs-title.class_,
|
||||||
|
pre.dh-on .hljs-built_in {
|
||||||
|
color: #953800;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-name,
|
||||||
|
pre.dh-on .hljs-tag {
|
||||||
|
color: #116329;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-meta {
|
||||||
|
color: #6e7781;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
pre.dh-on .hljs-comment,
|
||||||
|
pre.dh-on .hljs-quote {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-keyword,
|
||||||
|
pre.dh-on .hljs-selector-tag,
|
||||||
|
pre.dh-on .hljs-literal,
|
||||||
|
pre.dh-on .hljs-doctag,
|
||||||
|
pre.dh-on .hljs-meta .hljs-keyword {
|
||||||
|
color: #ff7b72;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-string,
|
||||||
|
pre.dh-on .hljs-regexp,
|
||||||
|
pre.dh-on .hljs-addition,
|
||||||
|
pre.dh-on .hljs-symbol,
|
||||||
|
pre.dh-on .hljs-bullet,
|
||||||
|
pre.dh-on .hljs-link {
|
||||||
|
color: #a5d6ff;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-number,
|
||||||
|
pre.dh-on .hljs-variable,
|
||||||
|
pre.dh-on .hljs-template-variable,
|
||||||
|
pre.dh-on .hljs-attr,
|
||||||
|
pre.dh-on .hljs-attribute,
|
||||||
|
pre.dh-on .hljs-property {
|
||||||
|
color: #79c0ff;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-title,
|
||||||
|
pre.dh-on .hljs-title.function_,
|
||||||
|
pre.dh-on .hljs-section {
|
||||||
|
color: #d2a8ff;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-type,
|
||||||
|
pre.dh-on .hljs-class .hljs-title,
|
||||||
|
pre.dh-on .hljs-title.class_,
|
||||||
|
pre.dh-on .hljs-built_in {
|
||||||
|
color: #ffa657;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-name,
|
||||||
|
pre.dh-on .hljs-tag {
|
||||||
|
color: #7ee787;
|
||||||
|
}
|
||||||
|
pre.dh-on .hljs-meta {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/styles/popup.css
Normal file
75
src/styles/popup.css
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 260px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 14px 16px;
|
||||||
|
font: 13px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #1f2328;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 7px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.muted {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: #1a7f37;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #57606a;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border: 1px solid #d0d7de;
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background: #1c2128;
|
||||||
|
color: #e6edf3;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
kbd {
|
||||||
|
background: #2d333b;
|
||||||
|
border-color: #444c56;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["chrome", "node"],
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user