Repository files navigation

GCCH — Google Chat Code Highlighter

Syntax highlighting and a copy button for code blocks in Google Chat.

Chat renders fenced code blocks as flat monospace text with no coloring and no way to copy them cleanly. GCCH fixes both, without touching the composer and without altering message data.

Quick start

Requires Node 20.11+ and pnpm 9+ (corepack enable is enough — the version is pinned via packageManager in package.json).

pnpm install
pnpm build

Then load the unpacked extension: chrome://extensions → enable Developer mode → Load unpacked → select dist/.

pnpm test# unit tests
pnpm check # typecheck + lint + format + tests
pnpm dev # rebuild the content script on change
pnpm package # zip dist/ into releases/ for the Web Store

After pnpm dev rebuilds, hit reload on the extension card and refresh Chat.

pnpm is used deliberately rather than npm: its non-flat node_modules makes phantom dependencies a hard error, so a package we never declared cannot quietly get bundled into the shipped extension.

How it works

A content script watches the message list, finds article[role="code"] blocks, and renders a highlighted copy into a shadow root beside the original — the original <article> is never modified, only hidden by our own stylesheet.

That one decision buys three things: Chat's native "copy message" stays byte-exact, Google's Wiz renderer never sees mutated nodes it might choke on, and disabling the extension restores the page perfectly without a reload.

Highlighting uses highlight.js with 15 explicitly registered languages. Blocks with an explicit language tag use it; blocks without one go through auto-detection that is gated on a relevance score, so logs and stack traces render as clean monospace rather than being confidently mislabeled.

Layout

src/
content/
index.ts lifecycle: settings, enable/disable, teardown
observer.ts MutationObserver, rAF-batched and throttled
detect.ts finds unprocessed blocks, rejects the composer
extract.ts article -> source text [pure]
language.ts language-tag parsing and aliases [pure]
highlight.ts hljs subset + auto-detect confidence gate
render.ts shadow host, copy button, theme swapping
theme.ts luminance-based light/dark detection [pure]
shared/
constants.ts EVERY Chat selector lives here
settings.ts storage schema, validated on read
logger.ts the only sanctioned console user
themes/ hljs theme CSS bundled as strings
popup/ settings UI
tests/
fixtures/ verbatim DOM captures from real Chat
docs/DOM-NOTES.md ground truth for the selectors

The [pure] modules take plain values and return plain values — no chrome.*, no globals, no DOM mutation. They hold nearly all the logic that can actually be wrong, and they are fully covered by tests.

When Google changes Chat's markup

This extension sits on top of a DOM that can change without notice, so that case is designed for rather than hoped against:

  • Every selector is in src/shared/constants.ts, each tagged DURABLE (a11y role, semantic tag) or VOLATILE (obfuscated class). Nothing else in the codebase contains a Chat selector.
  • Failure is silent, not destructive. Detection returns empty rather than throwing, mounting bails when the container is missing, and a highlighter error falls back to plain monospace with a working copy button. The worst realistic outcome is that GCCH does nothing — never that Chat breaks.
  • A health check names the problem. If a page has messages but no block matches after several scans, GCCH logs a one-time warning pointing at constants.ts.
  • Turn on Debug logging in the popup to see blocks found per scan, scan duration, language decisions with relevance scores, and any selector-tier fallback taken.

Fixing a break: capture the new markup per the recipe in docs/DOM-NOTES.md, add it as a new fixture beside the old one, update the selector, and get both fixtures passing.

Privacy and permissions

  • storage — preferences only.
  • scripting — registers the Gmail content script at runtime, if you opt in.
  • chat.google.com — where the extension does its work.
  • mail.google.comoptional, off by default, requested only when you enable "Chat in Gmail" in the popup. It is not in host_permissions because that install warning reads as "this extension can read your email".

Everything runs locally. No network requests, no analytics, no message content leaves the page. highlight.js and all theme CSS are bundled — nothing is fetched at runtime, and there is no eval anywhere, so the MV3 CSP story is trivial.

Releasing

  1. Bump version in bothpackage.json and manifest.json (they must match).
  2. Commit, then tag: git tag v1.2.3 && git push origin v1.2.3.
  3. The release workflow runs pnpm check, builds, packages dist/ into releases/gcch-1.2.3.zip, and attaches it to a GitHub Release — it fails the build if the tag and manifest version disagree.
  4. Download that zip from the release and upload it in the Chrome Web Store Developer Dashboard under your item → PackageUpload new package, then submit for review.

Web Store publishing itself isn't automated here — it needs a Google Cloud OAuth client tied to your developer account. If that becomes worth automating later, the chrome-webstore-upload-cli package is the standard tool.

Adding a language

Two places, both required:

  1. Import and register it in src/content/highlight.ts (LANGUAGES).
  2. Add its aliases to ALIASES in src/content/language.ts.

Auto-detection can only ever return a registered language, so step 1 also widens what untagged blocks can be detected as. Update the list assertion in tests/highlight.test.ts.

About

Google Chat Code Highlighter

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

GCCH — Google Chat Code Highlighter

Syntax highlighting and a copy button for code blocks in Google Chat.

Chat renders fenced code blocks as flat monospace text with no coloring and no way to copy them cleanly. GCCH fixes both, without touching the composer and without altering message data.

Quick start

Requires Node 20.11+ and pnpm 9+ (corepack enable is enough — the version is pinned via packageManager in package.json).

pnpm install
pnpm build

Then load the unpacked extension: chrome://extensions → enable Developer mode → Load unpacked → select dist/.

pnpm test# unit tests
pnpm check # typecheck + lint + format + tests
pnpm dev # rebuild the content script on change
pnpm package # zip dist/ into releases/ for the Web Store

After pnpm dev rebuilds, hit reload on the extension card and refresh Chat.

pnpm is used deliberately rather than npm: its non-flat node_modules makes phantom dependencies a hard error, so a package we never declared cannot quietly get bundled into the shipped extension.

How it works

A content script watches the message list, finds article[role="code"] blocks, and renders a highlighted copy into a shadow root beside the original — the original <article> is never modified, only hidden by our own stylesheet.

That one decision buys three things: Chat's native "copy message" stays byte-exact, Google's Wiz renderer never sees mutated nodes it might choke on, and disabling the extension restores the page perfectly without a reload.

Highlighting uses highlight.js with 15 explicitly registered languages. Blocks with an explicit language tag use it; blocks without one go through auto-detection that is gated on a relevance score, so logs and stack traces render as clean monospace rather than being confidently mislabeled.

Layout

src/
content/
index.ts lifecycle: settings, enable/disable, teardown
observer.ts MutationObserver, rAF-batched and throttled
detect.ts finds unprocessed blocks, rejects the composer
extract.ts article -> source text [pure]
language.ts language-tag parsing and aliases [pure]
highlight.ts hljs subset + auto-detect confidence gate
render.ts shadow host, copy button, theme swapping
theme.ts luminance-based light/dark detection [pure]
shared/
constants.ts EVERY Chat selector lives here
settings.ts storage schema, validated on read
logger.ts the only sanctioned console user
themes/ hljs theme CSS bundled as strings
popup/ settings UI
tests/
fixtures/ verbatim DOM captures from real Chat
docs/DOM-NOTES.md ground truth for the selectors

The [pure] modules take plain values and return plain values — no chrome.*, no globals, no DOM mutation. They hold nearly all the logic that can actually be wrong, and they are fully covered by tests.

When Google changes Chat's markup

This extension sits on top of a DOM that can change without notice, so that case is designed for rather than hoped against:

  • Every selector is in src/shared/constants.ts, each tagged DURABLE (a11y role, semantic tag) or VOLATILE (obfuscated class). Nothing else in the codebase contains a Chat selector.
  • Failure is silent, not destructive. Detection returns empty rather than throwing, mounting bails when the container is missing, and a highlighter error falls back to plain monospace with a working copy button. The worst realistic outcome is that GCCH does nothing — never that Chat breaks.
  • A health check names the problem. If a page has messages but no block matches after several scans, GCCH logs a one-time warning pointing at constants.ts.
  • Turn on Debug logging in the popup to see blocks found per scan, scan duration, language decisions with relevance scores, and any selector-tier fallback taken.

Fixing a break: capture the new markup per the recipe in docs/DOM-NOTES.md, add it as a new fixture beside the old one, update the selector, and get both fixtures passing.

Privacy and permissions

  • storage — preferences only.
  • scripting — registers the Gmail content script at runtime, if you opt in.
  • chat.google.com — where the extension does its work.
  • mail.google.comoptional, off by default, requested only when you enable "Chat in Gmail" in the popup. It is not in host_permissions because that install warning reads as "this extension can read your email".

Everything runs locally. No network requests, no analytics, no message content leaves the page. highlight.js and all theme CSS are bundled — nothing is fetched at runtime, and there is no eval anywhere, so the MV3 CSP story is trivial.

Releasing

  1. Bump version in bothpackage.json and manifest.json (they must match).
  2. Commit, then tag: git tag v1.2.3 && git push origin v1.2.3.
  3. The release workflow runs pnpm check, builds, packages dist/ into releases/gcch-1.2.3.zip, and attaches it to a GitHub Release — it fails the build if the tag and manifest version disagree.
  4. Download that zip from the release and upload it in the Chrome Web Store Developer Dashboard under your item → PackageUpload new package, then submit for review.

Web Store publishing itself isn't automated here — it needs a Google Cloud OAuth client tied to your developer account. If that becomes worth automating later, the chrome-webstore-upload-cli package is the standard tool.

Adding a language

Two places, both required:

  1. Import and register it in src/content/highlight.ts (LANGUAGES).
  2. Add its aliases to ALIASES in src/content/language.ts.

Auto-detection can only ever return a registered language, so step 1 also widens what untagged blocks can be detected as. Update the list assertion in tests/highlight.test.ts.

About

Google Chat Code Highlighter

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

GCCH — Google Chat Code Highlighter

Syntax highlighting and a copy button for code blocks in Google Chat.

Chat renders fenced code blocks as flat monospace text with no coloring and no way to copy them cleanly. GCCH fixes both, without touching the composer and without altering message data.

Quick start

Requires Node 20.11+ and pnpm 9+ (corepack enable is enough — the version is pinned via packageManager in package.json).

pnpm install
pnpm build

Then load the unpacked extension: chrome://extensions → enable Developer mode → Load unpacked → select dist/.

pnpm test# unit tests
pnpm check # typecheck + lint + format + tests
pnpm dev # rebuild the content script on change
pnpm package # zip dist/ into releases/ for the Web Store

After pnpm dev rebuilds, hit reload on the extension card and refresh Chat.

pnpm is used deliberately rather than npm: its non-flat node_modules makes phantom dependencies a hard error, so a package we never declared cannot quietly get bundled into the shipped extension.

How it works

A content script watches the message list, finds article[role="code"] blocks, and renders a highlighted copy into a shadow root beside the original — the original <article> is never modified, only hidden by our own stylesheet.

That one decision buys three things: Chat's native "copy message" stays byte-exact, Google's Wiz renderer never sees mutated nodes it might choke on, and disabling the extension restores the page perfectly without a reload.

Highlighting uses highlight.js with 15 explicitly registered languages. Blocks with an explicit language tag use it; blocks without one go through auto-detection that is gated on a relevance score, so logs and stack traces render as clean monospace rather than being confidently mislabeled.

Layout

src/
content/
index.ts lifecycle: settings, enable/disable, teardown
observer.ts MutationObserver, rAF-batched and throttled
detect.ts finds unprocessed blocks, rejects the composer
extract.ts article -> source text [pure]
language.ts language-tag parsing and aliases [pure]
highlight.ts hljs subset + auto-detect confidence gate
render.ts shadow host, copy button, theme swapping
theme.ts luminance-based light/dark detection [pure]
shared/
constants.ts EVERY Chat selector lives here
settings.ts storage schema, validated on read
logger.ts the only sanctioned console user
themes/ hljs theme CSS bundled as strings
popup/ settings UI
tests/
fixtures/ verbatim DOM captures from real Chat
docs/DOM-NOTES.md ground truth for the selectors

The [pure] modules take plain values and return plain values — no chrome.*, no globals, no DOM mutation. They hold nearly all the logic that can actually be wrong, and they are fully covered by tests.

When Google changes Chat's markup

This extension sits on top of a DOM that can change without notice, so that case is designed for rather than hoped against:

  • Every selector is in src/shared/constants.ts, each tagged DURABLE (a11y role, semantic tag) or VOLATILE (obfuscated class). Nothing else in the codebase contains a Chat selector.
  • Failure is silent, not destructive. Detection returns empty rather than throwing, mounting bails when the container is missing, and a highlighter error falls back to plain monospace with a working copy button. The worst realistic outcome is that GCCH does nothing — never that Chat breaks.
  • A health check names the problem. If a page has messages but no block matches after several scans, GCCH logs a one-time warning pointing at constants.ts.
  • Turn on Debug logging in the popup to see blocks found per scan, scan duration, language decisions with relevance scores, and any selector-tier fallback taken.

Fixing a break: capture the new markup per the recipe in docs/DOM-NOTES.md, add it as a new fixture beside the old one, update the selector, and get both fixtures passing.

Privacy and permissions

  • storage — preferences only.
  • scripting — registers the Gmail content script at runtime, if you opt in.
  • chat.google.com — where the extension does its work.
  • mail.google.comoptional, off by default, requested only when you enable "Chat in Gmail" in the popup. It is not in host_permissions because that install warning reads as "this extension can read your email".

Everything runs locally. No network requests, no analytics, no message content leaves the page. highlight.js and all theme CSS are bundled — nothing is fetched at runtime, and there is no eval anywhere, so the MV3 CSP story is trivial.

Releasing

  1. Bump version in bothpackage.json and manifest.json (they must match).
  2. Commit, then tag: git tag v1.2.3 && git push origin v1.2.3.
  3. The release workflow runs pnpm check, builds, packages dist/ into releases/gcch-1.2.3.zip, and attaches it to a GitHub Release — it fails the build if the tag and manifest version disagree.
  4. Download that zip from the release and upload it in the Chrome Web Store Developer Dashboard under your item → PackageUpload new package, then submit for review.

Web Store publishing itself isn't automated here — it needs a Google Cloud OAuth client tied to your developer account. If that becomes worth automating later, the chrome-webstore-upload-cli package is the standard tool.

Adding a language

Two places, both required:

  1. Import and register it in src/content/highlight.ts (LANGUAGES).
  2. Add its aliases to ALIASES in src/content/language.ts.

Auto-detection can only ever return a registered language, so step 1 also widens what untagged blocks can be detected as. Update the list assertion in tests/highlight.test.ts.

About

Google Chat Code Highlighter

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

GCCH — Google Chat Code Highlighter

Syntax highlighting and a copy button for code blocks in Google Chat.

Chat renders fenced code blocks as flat monospace text with no coloring and no way to copy them cleanly. GCCH fixes both, without touching the composer and without altering message data.

Quick start

Requires Node 20.11+ and pnpm 9+ (corepack enable is enough — the version is pinned via packageManager in package.json).

pnpm install
pnpm build

Then load the unpacked extension: chrome://extensions → enable Developer mode → Load unpacked → select dist/.

pnpm test# unit tests
pnpm check # typecheck + lint + format + tests
pnpm dev # rebuild the content script on change
pnpm package # zip dist/ into releases/ for the Web Store

After pnpm dev rebuilds, hit reload on the extension card and refresh Chat.

pnpm is used deliberately rather than npm: its non-flat node_modules makes phantom dependencies a hard error, so a package we never declared cannot quietly get bundled into the shipped extension.

How it works

A content script watches the message list, finds article[role="code"] blocks, and renders a highlighted copy into a shadow root beside the original — the original <article> is never modified, only hidden by our own stylesheet.

That one decision buys three things: Chat's native "copy message" stays byte-exact, Google's Wiz renderer never sees mutated nodes it might choke on, and disabling the extension restores the page perfectly without a reload.

Highlighting uses highlight.js with 15 explicitly registered languages. Blocks with an explicit language tag use it; blocks without one go through auto-detection that is gated on a relevance score, so logs and stack traces render as clean monospace rather than being confidently mislabeled.

Layout

src/
content/
index.ts lifecycle: settings, enable/disable, teardown
observer.ts MutationObserver, rAF-batched and throttled
detect.ts finds unprocessed blocks, rejects the composer
extract.ts article -> source text [pure]
language.ts language-tag parsing and aliases [pure]
highlight.ts hljs subset + auto-detect confidence gate
render.ts shadow host, copy button, theme swapping
theme.ts luminance-based light/dark detection [pure]
shared/
constants.ts EVERY Chat selector lives here
settings.ts storage schema, validated on read
logger.ts the only sanctioned console user
themes/ hljs theme CSS bundled as strings
popup/ settings UI
tests/
fixtures/ verbatim DOM captures from real Chat
docs/DOM-NOTES.md ground truth for the selectors

The [pure] modules take plain values and return plain values — no chrome.*, no globals, no DOM mutation. They hold nearly all the logic that can actually be wrong, and they are fully covered by tests.

When Google changes Chat's markup

This extension sits on top of a DOM that can change without notice, so that case is designed for rather than hoped against:

  • Every selector is in src/shared/constants.ts, each tagged DURABLE (a11y role, semantic tag) or VOLATILE (obfuscated class). Nothing else in the codebase contains a Chat selector.
  • Failure is silent, not destructive. Detection returns empty rather than throwing, mounting bails when the container is missing, and a highlighter error falls back to plain monospace with a working copy button. The worst realistic outcome is that GCCH does nothing — never that Chat breaks.
  • A health check names the problem. If a page has messages but no block matches after several scans, GCCH logs a one-time warning pointing at constants.ts.
  • Turn on Debug logging in the popup to see blocks found per scan, scan duration, language decisions with relevance scores, and any selector-tier fallback taken.

Fixing a break: capture the new markup per the recipe in docs/DOM-NOTES.md, add it as a new fixture beside the old one, update the selector, and get both fixtures passing.

Privacy and permissions

  • storage — preferences only.
  • scripting — registers the Gmail content script at runtime, if you opt in.
  • chat.google.com — where the extension does its work.
  • mail.google.comoptional, off by default, requested only when you enable "Chat in Gmail" in the popup. It is not in host_permissions because that install warning reads as "this extension can read your email".

Everything runs locally. No network requests, no analytics, no message content leaves the page. highlight.js and all theme CSS are bundled — nothing is fetched at runtime, and there is no eval anywhere, so the MV3 CSP story is trivial.

Releasing

  1. Bump version in bothpackage.json and manifest.json (they must match).
  2. Commit, then tag: git tag v1.2.3 && git push origin v1.2.3.
  3. The release workflow runs pnpm check, builds, packages dist/ into releases/gcch-1.2.3.zip, and attaches it to a GitHub Release — it fails the build if the tag and manifest version disagree.
  4. Download that zip from the release and upload it in the Chrome Web Store Developer Dashboard under your item → PackageUpload new package, then submit for review.

Web Store publishing itself isn't automated here — it needs a Google Cloud OAuth client tied to your developer account. If that becomes worth automating later, the chrome-webstore-upload-cli package is the standard tool.

Adding a language

Two places, both required:

  1. Import and register it in src/content/highlight.ts (LANGUAGES).
  2. Add its aliases to ALIASES in src/content/language.ts.

Auto-detection can only ever return a registered language, so step 1 also widens what untagged blocks can be detected as. Update the list assertion in tests/highlight.test.ts.

About

Google Chat Code Highlighter

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

GCCH — Google Chat Code Highlighter

Syntax highlighting and a copy button for code blocks in Google Chat.

Chat renders fenced code blocks as flat monospace text with no coloring and no way to copy them cleanly. GCCH fixes both, without touching the composer and without altering message data.

Quick start

Requires Node 20.11+ and pnpm 9+ (corepack enable is enough — the version is pinned via packageManager in package.json).

pnpm install
pnpm build

Then load the unpacked extension: chrome://extensions → enable Developer mode → Load unpacked → select dist/.

pnpm test# unit tests
pnpm check # typecheck + lint + format + tests
pnpm dev # rebuild the content script on change
pnpm package # zip dist/ into releases/ for the Web Store

After pnpm dev rebuilds, hit reload on the extension card and refresh Chat.

pnpm is used deliberately rather than npm: its non-flat node_modules makes phantom dependencies a hard error, so a package we never declared cannot quietly get bundled into the shipped extension.

How it works

A content script watches the message list, finds article[role="code"] blocks, and renders a highlighted copy into a shadow root beside the original — the original <article> is never modified, only hidden by our own stylesheet.

That one decision buys three things: Chat's native "copy message" stays byte-exact, Google's Wiz renderer never sees mutated nodes it might choke on, and disabling the extension restores the page perfectly without a reload.

Highlighting uses highlight.js with 15 explicitly registered languages. Blocks with an explicit language tag use it; blocks without one go through auto-detection that is gated on a relevance score, so logs and stack traces render as clean monospace rather than being confidently mislabeled.

Layout

src/
content/
index.ts lifecycle: settings, enable/disable, teardown
observer.ts MutationObserver, rAF-batched and throttled
detect.ts finds unprocessed blocks, rejects the composer
extract.ts article -> source text [pure]
language.ts language-tag parsing and aliases [pure]
highlight.ts hljs subset + auto-detect confidence gate
render.ts shadow host, copy button, theme swapping
theme.ts luminance-based light/dark detection [pure]
shared/
constants.ts EVERY Chat selector lives here
settings.ts storage schema, validated on read
logger.ts the only sanctioned console user
themes/ hljs theme CSS bundled as strings
popup/ settings UI
tests/
fixtures/ verbatim DOM captures from real Chat
docs/DOM-NOTES.md ground truth for the selectors

The [pure] modules take plain values and return plain values — no chrome.*, no globals, no DOM mutation. They hold nearly all the logic that can actually be wrong, and they are fully covered by tests.

When Google changes Chat's markup

This extension sits on top of a DOM that can change without notice, so that case is designed for rather than hoped against:

  • Every selector is in src/shared/constants.ts, each tagged DURABLE (a11y role, semantic tag) or VOLATILE (obfuscated class). Nothing else in the codebase contains a Chat selector.
  • Failure is silent, not destructive. Detection returns empty rather than throwing, mounting bails when the container is missing, and a highlighter error falls back to plain monospace with a working copy button. The worst realistic outcome is that GCCH does nothing — never that Chat breaks.
  • A health check names the problem. If a page has messages but no block matches after several scans, GCCH logs a one-time warning pointing at constants.ts.
  • Turn on Debug logging in the popup to see blocks found per scan, scan duration, language decisions with relevance scores, and any selector-tier fallback taken.

Fixing a break: capture the new markup per the recipe in docs/DOM-NOTES.md, add it as a new fixture beside the old one, update the selector, and get both fixtures passing.

Privacy and permissions

  • storage — preferences only.
  • scripting — registers the Gmail content script at runtime, if you opt in.
  • chat.google.com — where the extension does its work.
  • mail.google.comoptional, off by default, requested only when you enable "Chat in Gmail" in the popup. It is not in host_permissions because that install warning reads as "this extension can read your email".

Everything runs locally. No network requests, no analytics, no message content leaves the page. highlight.js and all theme CSS are bundled — nothing is fetched at runtime, and there is no eval anywhere, so the MV3 CSP story is trivial.

Releasing

  1. Bump version in bothpackage.json and manifest.json (they must match).
  2. Commit, then tag: git tag v1.2.3 && git push origin v1.2.3.
  3. The release workflow runs pnpm check, builds, packages dist/ into releases/gcch-1.2.3.zip, and attaches it to a GitHub Release — it fails the build if the tag and manifest version disagree.
  4. Download that zip from the release and upload it in the Chrome Web Store Developer Dashboard under your item → PackageUpload new package, then submit for review.

Web Store publishing itself isn't automated here — it needs a Google Cloud OAuth client tied to your developer account. If that becomes worth automating later, the chrome-webstore-upload-cli package is the standard tool.

Adding a language

Two places, both required:

  1. Import and register it in src/content/highlight.ts (LANGUAGES).
  2. Add its aliases to ALIASES in src/content/language.ts.

Auto-detection can only ever return a registered language, so step 1 also widens what untagged blocks can be detected as. Update the list assertion in tests/highlight.test.ts.

About

Google Chat Code Highlighter

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

GCCH — Google Chat Code Highlighter

Syntax highlighting and a copy button for code blocks in Google Chat.

Chat renders fenced code blocks as flat monospace text with no coloring and no way to copy them cleanly. GCCH fixes both, without touching the composer and without altering message data.

Quick start

Requires Node 20.11+ and pnpm 9+ (corepack enable is enough — the version is pinned via packageManager in package.json).

pnpm install
pnpm build

Then load the unpacked extension: chrome://extensions → enable Developer mode → Load unpacked → select dist/.

pnpm test# unit tests
pnpm check # typecheck + lint + format + tests
pnpm dev # rebuild the content script on change
pnpm package # zip dist/ into releases/ for the Web Store

After pnpm dev rebuilds, hit reload on the extension card and refresh Chat.

pnpm is used deliberately rather than npm: its non-flat node_modules makes phantom dependencies a hard error, so a package we never declared cannot quietly get bundled into the shipped extension.

How it works

A content script watches the message list, finds article[role="code"] blocks, and renders a highlighted copy into a shadow root beside the original — the original <article> is never modified, only hidden by our own stylesheet.

That one decision buys three things: Chat's native "copy message" stays byte-exact, Google's Wiz renderer never sees mutated nodes it might choke on, and disabling the extension restores the page perfectly without a reload.

Highlighting uses highlight.js with 15 explicitly registered languages. Blocks with an explicit language tag use it; blocks without one go through auto-detection that is gated on a relevance score, so logs and stack traces render as clean monospace rather than being confidently mislabeled.

Layout

src/
content/
index.ts lifecycle: settings, enable/disable, teardown
observer.ts MutationObserver, rAF-batched and throttled
detect.ts finds unprocessed blocks, rejects the composer
extract.ts article -> source text [pure]
language.ts language-tag parsing and aliases [pure]
highlight.ts hljs subset + auto-detect confidence gate
render.ts shadow host, copy button, theme swapping
theme.ts luminance-based light/dark detection [pure]
shared/
constants.ts EVERY Chat selector lives here
settings.ts storage schema, validated on read
logger.ts the only sanctioned console user
themes/ hljs theme CSS bundled as strings
popup/ settings UI
tests/
fixtures/ verbatim DOM captures from real Chat
docs/DOM-NOTES.md ground truth for the selectors

The [pure] modules take plain values and return plain values — no chrome.*, no globals, no DOM mutation. They hold nearly all the logic that can actually be wrong, and they are fully covered by tests.

When Google changes Chat's markup

This extension sits on top of a DOM that can change without notice, so that case is designed for rather than hoped against:

  • Every selector is in src/shared/constants.ts, each tagged DURABLE (a11y role, semantic tag) or VOLATILE (obfuscated class). Nothing else in the codebase contains a Chat selector.
  • Failure is silent, not destructive. Detection returns empty rather than throwing, mounting bails when the container is missing, and a highlighter error falls back to plain monospace with a working copy button. The worst realistic outcome is that GCCH does nothing — never that Chat breaks.
  • A health check names the problem. If a page has messages but no block matches after several scans, GCCH logs a one-time warning pointing at constants.ts.
  • Turn on Debug logging in the popup to see blocks found per scan, scan duration, language decisions with relevance scores, and any selector-tier fallback taken.

Fixing a break: capture the new markup per the recipe in docs/DOM-NOTES.md, add it as a new fixture beside the old one, update the selector, and get both fixtures passing.

Privacy and permissions

  • storage — preferences only.
  • scripting — registers the Gmail content script at runtime, if you opt in.
  • chat.google.com — where the extension does its work.
  • mail.google.comoptional, off by default, requested only when you enable "Chat in Gmail" in the popup. It is not in host_permissions because that install warning reads as "this extension can read your email".

Everything runs locally. No network requests, no analytics, no message content leaves the page. highlight.js and all theme CSS are bundled — nothing is fetched at runtime, and there is no eval anywhere, so the MV3 CSP story is trivial.

Releasing

  1. Bump version in bothpackage.json and manifest.json (they must match).
  2. Commit, then tag: git tag v1.2.3 && git push origin v1.2.3.
  3. The release workflow runs pnpm check, builds, packages dist/ into releases/gcch-1.2.3.zip, and attaches it to a GitHub Release — it fails the build if the tag and manifest version disagree.
  4. Download that zip from the release and upload it in the Chrome Web Store Developer Dashboard under your item → PackageUpload new package, then submit for review.

Web Store publishing itself isn't automated here — it needs a Google Cloud OAuth client tied to your developer account. If that becomes worth automating later, the chrome-webstore-upload-cli package is the standard tool.

Adding a language

Two places, both required:

  1. Import and register it in src/content/highlight.ts (LANGUAGES).
  2. Add its aliases to ALIASES in src/content/language.ts.

Auto-detection can only ever return a registered language, so step 1 also widens what untagged blocks can be detected as. Update the list assertion in tests/highlight.test.ts.

About

Google Chat Code Highlighter

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

GCCH — Google Chat Code Highlighter

Syntax highlighting and a copy button for code blocks in Google Chat.

Chat renders fenced code blocks as flat monospace text with no coloring and no way to copy them cleanly. GCCH fixes both, without touching the composer and without altering message data.

Quick start

Requires Node 20.11+ and pnpm 9+ (corepack enable is enough — the version is pinned via packageManager in package.json).

pnpm install
pnpm build

Then load the unpacked extension: chrome://extensions → enable Developer mode → Load unpacked → select dist/.

pnpm test# unit tests
pnpm check # typecheck + lint + format + tests
pnpm dev # rebuild the content script on change
pnpm package # zip dist/ into releases/ for the Web Store

After pnpm dev rebuilds, hit reload on the extension card and refresh Chat.

pnpm is used deliberately rather than npm: its non-flat node_modules makes phantom dependencies a hard error, so a package we never declared cannot quietly get bundled into the shipped extension.

How it works

A content script watches the message list, finds article[role="code"] blocks, and renders a highlighted copy into a shadow root beside the original — the original <article> is never modified, only hidden by our own stylesheet.

That one decision buys three things: Chat's native "copy message" stays byte-exact, Google's Wiz renderer never sees mutated nodes it might choke on, and disabling the extension restores the page perfectly without a reload.

Highlighting uses highlight.js with 15 explicitly registered languages. Blocks with an explicit language tag use it; blocks without one go through auto-detection that is gated on a relevance score, so logs and stack traces render as clean monospace rather than being confidently mislabeled.

Layout

src/
content/
index.ts lifecycle: settings, enable/disable, teardown
observer.ts MutationObserver, rAF-batched and throttled
detect.ts finds unprocessed blocks, rejects the composer
extract.ts article -> source text [pure]
language.ts language-tag parsing and aliases [pure]
highlight.ts hljs subset + auto-detect confidence gate
render.ts shadow host, copy button, theme swapping
theme.ts luminance-based light/dark detection [pure]
shared/
constants.ts EVERY Chat selector lives here
settings.ts storage schema, validated on read
logger.ts the only sanctioned console user
themes/ hljs theme CSS bundled as strings
popup/ settings UI
tests/
fixtures/ verbatim DOM captures from real Chat
docs/DOM-NOTES.md ground truth for the selectors

The [pure] modules take plain values and return plain values — no chrome.*, no globals, no DOM mutation. They hold nearly all the logic that can actually be wrong, and they are fully covered by tests.

When Google changes Chat's markup

This extension sits on top of a DOM that can change without notice, so that case is designed for rather than hoped against:

  • Every selector is in src/shared/constants.ts, each tagged DURABLE (a11y role, semantic tag) or VOLATILE (obfuscated class). Nothing else in the codebase contains a Chat selector.
  • Failure is silent, not destructive. Detection returns empty rather than throwing, mounting bails when the container is missing, and a highlighter error falls back to plain monospace with a working copy button. The worst realistic outcome is that GCCH does nothing — never that Chat breaks.
  • A health check names the problem. If a page has messages but no block matches after several scans, GCCH logs a one-time warning pointing at constants.ts.
  • Turn on Debug logging in the popup to see blocks found per scan, scan duration, language decisions with relevance scores, and any selector-tier fallback taken.

Fixing a break: capture the new markup per the recipe in docs/DOM-NOTES.md, add it as a new fixture beside the old one, update the selector, and get both fixtures passing.

Privacy and permissions

  • storage — preferences only.
  • scripting — registers the Gmail content script at runtime, if you opt in.
  • chat.google.com — where the extension does its work.
  • mail.google.comoptional, off by default, requested only when you enable "Chat in Gmail" in the popup. It is not in host_permissions because that install warning reads as "this extension can read your email".

Everything runs locally. No network requests, no analytics, no message content leaves the page. highlight.js and all theme CSS are bundled — nothing is fetched at runtime, and there is no eval anywhere, so the MV3 CSP story is trivial.

Releasing

  1. Bump version in bothpackage.json and manifest.json (they must match).
  2. Commit, then tag: git tag v1.2.3 && git push origin v1.2.3.
  3. The release workflow runs pnpm check, builds, packages dist/ into releases/gcch-1.2.3.zip, and attaches it to a GitHub Release — it fails the build if the tag and manifest version disagree.
  4. Download that zip from the release and upload it in the Chrome Web Store Developer Dashboard under your item → PackageUpload new package, then submit for review.

Web Store publishing itself isn't automated here — it needs a Google Cloud OAuth client tied to your developer account. If that becomes worth automating later, the chrome-webstore-upload-cli package is the standard tool.

Adding a language

Two places, both required:

  1. Import and register it in src/content/highlight.ts (LANGUAGES).
  2. Add its aliases to ALIASES in src/content/language.ts.

Auto-detection can only ever return a registered language, so step 1 also widens what untagged blocks can be detected as. Update the list assertion in tests/highlight.test.ts.

About

Google Chat Code Highlighter

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

GCCH — Google Chat Code Highlighter

Syntax highlighting and a copy button for code blocks in Google Chat.

Chat renders fenced code blocks as flat monospace text with no coloring and no way to copy them cleanly. GCCH fixes both, without touching the composer and without altering message data.

Quick start

Requires Node 20.11+ and pnpm 9+ (corepack enable is enough — the version is pinned via packageManager in package.json).

pnpm install
pnpm build

Then load the unpacked extension: chrome://extensions → enable Developer mode → Load unpacked → select dist/.

pnpm test# unit tests
pnpm check # typecheck + lint + format + tests
pnpm dev # rebuild the content script on change
pnpm package # zip dist/ into releases/ for the Web Store

After pnpm dev rebuilds, hit reload on the extension card and refresh Chat.

pnpm is used deliberately rather than npm: its non-flat node_modules makes phantom dependencies a hard error, so a package we never declared cannot quietly get bundled into the shipped extension.

How it works

A content script watches the message list, finds article[role="code"] blocks, and renders a highlighted copy into a shadow root beside the original — the original <article> is never modified, only hidden by our own stylesheet.

That one decision buys three things: Chat's native "copy message" stays byte-exact, Google's Wiz renderer never sees mutated nodes it might choke on, and disabling the extension restores the page perfectly without a reload.

Highlighting uses highlight.js with 15 explicitly registered languages. Blocks with an explicit language tag use it; blocks without one go through auto-detection that is gated on a relevance score, so logs and stack traces render as clean monospace rather than being confidently mislabeled.

Layout

src/
content/
index.ts lifecycle: settings, enable/disable, teardown
observer.ts MutationObserver, rAF-batched and throttled
detect.ts finds unprocessed blocks, rejects the composer
extract.ts article -> source text [pure]
language.ts language-tag parsing and aliases [pure]
highlight.ts hljs subset + auto-detect confidence gate
render.ts shadow host, copy button, theme swapping
theme.ts luminance-based light/dark detection [pure]
shared/
constants.ts EVERY Chat selector lives here
settings.ts storage schema, validated on read
logger.ts the only sanctioned console user
themes/ hljs theme CSS bundled as strings
popup/ settings UI
tests/
fixtures/ verbatim DOM captures from real Chat
docs/DOM-NOTES.md ground truth for the selectors

The [pure] modules take plain values and return plain values — no chrome.*, no globals, no DOM mutation. They hold nearly all the logic that can actually be wrong, and they are fully covered by tests.

When Google changes Chat's markup

This extension sits on top of a DOM that can change without notice, so that case is designed for rather than hoped against:

  • Every selector is in src/shared/constants.ts, each tagged DURABLE (a11y role, semantic tag) or VOLATILE (obfuscated class). Nothing else in the codebase contains a Chat selector.
  • Failure is silent, not destructive. Detection returns empty rather than throwing, mounting bails when the container is missing, and a highlighter error falls back to plain monospace with a working copy button. The worst realistic outcome is that GCCH does nothing — never that Chat breaks.
  • A health check names the problem. If a page has messages but no block matches after several scans, GCCH logs a one-time warning pointing at constants.ts.
  • Turn on Debug logging in the popup to see blocks found per scan, scan duration, language decisions with relevance scores, and any selector-tier fallback taken.

Fixing a break: capture the new markup per the recipe in docs/DOM-NOTES.md, add it as a new fixture beside the old one, update the selector, and get both fixtures passing.

Privacy and permissions

  • storage — preferences only.
  • scripting — registers the Gmail content script at runtime, if you opt in.
  • chat.google.com — where the extension does its work.
  • mail.google.comoptional, off by default, requested only when you enable "Chat in Gmail" in the popup. It is not in host_permissions because that install warning reads as "this extension can read your email".

Everything runs locally. No network requests, no analytics, no message content leaves the page. highlight.js and all theme CSS are bundled — nothing is fetched at runtime, and there is no eval anywhere, so the MV3 CSP story is trivial.

Releasing

  1. Bump version in bothpackage.json and manifest.json (they must match).
  2. Commit, then tag: git tag v1.2.3 && git push origin v1.2.3.
  3. The release workflow runs pnpm check, builds, packages dist/ into releases/gcch-1.2.3.zip, and attaches it to a GitHub Release — it fails the build if the tag and manifest version disagree.
  4. Download that zip from the release and upload it in the Chrome Web Store Developer Dashboard under your item → PackageUpload new package, then submit for review.

Web Store publishing itself isn't automated here — it needs a Google Cloud OAuth client tied to your developer account. If that becomes worth automating later, the chrome-webstore-upload-cli package is the standard tool.

Adding a language

Two places, both required:

  1. Import and register it in src/content/highlight.ts (LANGUAGES).
  2. Add its aliases to ALIASES in src/content/language.ts.

Auto-detection can only ever return a registered language, so step 1 also widens what untagged blocks can be detected as. Update the list assertion in tests/highlight.test.ts.

About

Google Chat Code Highlighter

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages