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:
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));
|
||||
Reference in New Issue
Block a user