Loading drafts from local storage...
diff --git a/browser-extension/src/lib/config.ts b/browser-extension/src/lib/config.ts
new file mode 100644
index 0000000..1f5e748
--- /dev/null
+++ b/browser-extension/src/lib/config.ts
@@ -0,0 +1,10 @@
+const MODES = ['PROD', 'PLAYGROUNDS_PR'] as const
+
+export type ModeType = (typeof MODES)[number]
+
+export const CONFIG = {
+ ADDED_OVERTYPE_CLASS: 'gitcasso-overtype',
+ DEBUG: true, // enabled debug logging
+ EXTENSION_NAME: 'gitcasso', // decorates logs
+ MODE: 'PLAYGROUNDS_PR' satisfies ModeType,
+} as const
diff --git a/browser-extension/src/lib/enhancer.ts b/browser-extension/src/lib/enhancer.ts
new file mode 100644
index 0000000..82ac2eb
--- /dev/null
+++ b/browser-extension/src/lib/enhancer.ts
@@ -0,0 +1,26 @@
+import type { OverType } from '../overtype/mock-overtype'
+
+/**
+ * stores enough info about the location of a draft to:
+ * - display it in a table
+ * - reopen the draft in-context
+ */
+export interface CommentSpot {
+ unique_key: string
+ type: string
+}
+
+/** wraps the textareas of a given platform with Gitcasso's enhancements */
+export interface CommentEnhancer
{
+ /** guarantees to only return a type within this list */
+ forSpotTypes(): string[]
+ /**
+ * whenever a new `textarea` is added to any webpage, this method is called.
+ * if we return non-null, then we become the handler for that text area.
+ */
+ tryToEnhance(textarea: HTMLTextAreaElement): [OverType, Spot] | null
+
+ tableIcon(spot: Spot): string
+ tableTitle(spot: Spot): string
+ buildUrl(spot: Spot): string
+}
diff --git a/browser-extension/src/lib/enhancers/github.ts b/browser-extension/src/lib/enhancers/github.ts
new file mode 100644
index 0000000..a055263
--- /dev/null
+++ b/browser-extension/src/lib/enhancers/github.ts
@@ -0,0 +1,109 @@
+import { OverType } from '../../overtype/mock-overtype'
+import type { CommentEnhancer, CommentSpot } from '../enhancer'
+
+const GITHUB_SPOT_TYPES = [
+ 'GH_ISSUE_NEW',
+ 'GH_PR_NEW',
+ 'GH_ISSUE_ADD_COMMENT',
+ 'GH_PR_ADD_COMMENT',
+ /* TODO
+ 'GH_ISSUE_EDIT_COMMENT',
+ 'GH_PR_EDIT_COMMENT',
+ 'GH_PR_CODE_COMMENT',
+ */
+] as const
+
+export type GitHubSpotType = (typeof GITHUB_SPOT_TYPES)[number]
+
+export interface GitHubSpot extends CommentSpot {
+ type: GitHubSpotType // Override to narrow from string to specific union
+ domain: string
+ slug: string // owner/repo
+ number?: number | undefined // issue/PR number, undefined for new issues and PRs
+}
+
+export class GitHubEnhancer implements CommentEnhancer {
+ forSpotTypes(): string[] {
+ return [...GITHUB_SPOT_TYPES]
+ }
+
+ tryToEnhance(textarea: HTMLTextAreaElement): [OverType, GitHubSpot] | null {
+ // Only handle GitHub domains
+ if (!window.location.hostname.includes('github')) {
+ return null
+ }
+
+ const pathname = window.location.pathname
+
+ // Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456
+ const match = pathname.match(/^\/([^/]+)\/([^/]+)(?:\/(issues|pull)\/(\d+))?/)
+ if (!match) return null
+
+ const [, owner, repo, urlType, numberStr] = match
+ const slug = `${owner}/${repo}`
+ const number = numberStr ? parseInt(numberStr, 10) : undefined
+
+ // Determine comment type
+ let type: GitHubSpotType
+
+ if (pathname.includes('/issues/new')) {
+ type = 'GH_ISSUE_NEW'
+ } else if (pathname.includes('/compare/') || pathname.endsWith('/compare')) {
+ type = 'GH_PR_NEW'
+ } else if (urlType && number) {
+ if (urlType === 'issues') {
+ type = 'GH_ISSUE_ADD_COMMENT'
+ } else {
+ type = 'GH_PR_ADD_COMMENT'
+ }
+ } else {
+ return null
+ }
+
+ // Generate unique key based on context
+ let unique_key = `github:${slug}`
+ if (number) {
+ unique_key += `:${urlType}:${number}`
+ } else {
+ unique_key += ':new'
+ }
+
+ const spot: GitHubSpot = {
+ domain: window.location.hostname,
+ number,
+ slug,
+ type,
+ unique_key,
+ }
+ const overtype = new OverType(textarea)
+ return [overtype, spot]
+ }
+
+ tableTitle(spot: GitHubSpot): string {
+ const { slug, number } = spot
+ if (number) {
+ return `Comment on ${slug} #${number}`
+ }
+ return `New ${window.location.pathname.includes('/issues/') ? 'issue' : 'PR'} in ${slug}`
+ }
+
+ tableIcon(spot: GitHubSpot): string {
+ switch (spot.type) {
+ case 'GH_ISSUE_NEW':
+ case 'GH_ISSUE_ADD_COMMENT':
+ return '🐛' // Issue icon
+ case 'GH_PR_NEW':
+ case 'GH_PR_ADD_COMMENT':
+ return '🔄' // PR icon
+ }
+ }
+
+ buildUrl(spot: GitHubSpot): string {
+ const baseUrl = `https://${spot.domain}/${spot.slug}`
+ if (spot.number) {
+ const type = spot.type.indexOf('ISSUE') ? 'issues' : 'pull'
+ return `${baseUrl}/${type}/${spot.number}`
+ }
+ return baseUrl
+ }
+}
diff --git a/browser-extension/src/entrypoints/content/logger.ts b/browser-extension/src/lib/logger.ts
similarity index 100%
rename from browser-extension/src/entrypoints/content/logger.ts
rename to browser-extension/src/lib/logger.ts
diff --git a/browser-extension/src/lib/registries.ts b/browser-extension/src/lib/registries.ts
new file mode 100644
index 0000000..467043a
--- /dev/null
+++ b/browser-extension/src/lib/registries.ts
@@ -0,0 +1,62 @@
+import type { OverType } from '../overtype/mock-overtype'
+import type { CommentEnhancer, CommentSpot } from './enhancer'
+import { GitHubEnhancer } from './enhancers/github'
+
+export interface EnhancedTextarea {
+ textarea: HTMLTextAreaElement
+ spot: T
+ handler: CommentEnhancer
+ overtype: OverType
+}
+
+export class EnhancerRegistry {
+ private enhancers = new Set>()
+
+ constructor() {
+ // Register all available handlers
+ this.register(new GitHubEnhancer())
+ }
+
+ private register(handler: CommentEnhancer): void {
+ this.enhancers.add(handler)
+ }
+
+ tryToEnhance(textarea: HTMLTextAreaElement): EnhancedTextarea | null {
+ for (const handler of this.enhancers) {
+ try {
+ const result = handler.tryToEnhance(textarea)
+ if (result) {
+ const [overtype, spot] = result
+ return { handler, overtype, spot, textarea }
+ }
+ } catch (error) {
+ console.warn('Handler failed to identify textarea:', error)
+ }
+ }
+ return null
+ }
+
+ getEnhancerCount(): number {
+ return this.enhancers.size
+ }
+}
+
+export class TextareaRegistry {
+ private textareas = new Map>()
+
+ register(textareaInfo: EnhancedTextarea): void {
+ this.textareas.set(textareaInfo.textarea, textareaInfo)
+ // TODO: register as a draft in progress with the global list
+ }
+
+ unregisterDueToModification(textarea: HTMLTextAreaElement): void {
+ if (this.textareas.has(textarea)) {
+ // TODO: register as abandoned or maybe submitted with the global list
+ this.textareas.delete(textarea)
+ }
+ }
+
+ get(textarea: HTMLTextAreaElement): EnhancedTextarea | undefined {
+ return this.textareas.get(textarea)
+ }
+}
diff --git a/browser-extension/src/overtype/mock-overtype.ts b/browser-extension/src/overtype/mock-overtype.ts
new file mode 100644
index 0000000..fec648c
--- /dev/null
+++ b/browser-extension/src/overtype/mock-overtype.ts
@@ -0,0 +1,44 @@
+/**
+ * Mock implementation of Overtype for development
+ * This wraps a textarea and provides a minimal interface
+ */
+export class OverType {
+ public element: HTMLTextAreaElement
+ public instanceId: number
+ public initialized: boolean = true
+
+ private static instanceCount = 0
+
+ constructor(target: HTMLTextAreaElement) {
+ this.element = target
+ this.instanceId = ++OverType.instanceCount
+
+ // Store reference on the element
+ ;(target as any).overTypeInstance = this
+
+ // Apply basic styling or enhancement
+ this.enhance()
+ }
+
+ private enhance(): void {
+ // Mock enhancement - could add basic styling, event handlers, etc.
+ this.element.style.fontFamily =
+ 'Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace'
+ this.element.style.fontSize = '14px'
+ this.element.style.lineHeight = '1.5'
+ }
+
+ getValue(): string {
+ return this.element.value
+ }
+
+ setValue(value: string): void {
+ this.element.value = value
+ }
+
+ destroy(): void {
+ // Clean up any enhancements
+ delete (this.element as any).overTypeInstance
+ this.initialized = false
+ }
+}
diff --git a/browser-extension/src/playgrounds/github-playground.ts b/browser-extension/src/playgrounds/github-playground.ts
new file mode 100644
index 0000000..dae988f
--- /dev/null
+++ b/browser-extension/src/playgrounds/github-playground.ts
@@ -0,0 +1,49 @@
+import hljs from "highlight.js";
+import OverType from "../overtype/overtype";
+
+export function githubPrNewCommentContentScript() {
+ if (window.location.hostname !== "github.com") {
+ return;
+ }
+ OverType.setCodeHighlighter(hljsHighlighter);
+ const ghCommentBox = document.getElementById(
+ "new_comment_field"
+ ) as HTMLTextAreaElement | null;
+ if (ghCommentBox) {
+ const overtypeContainer = modifyDOM(ghCommentBox);
+ new OverType(overtypeContainer, {
+ placeholder: "Add your comment here...",
+ autoResize: true,
+ minHeight: "102px",
+ padding: "var(--base-size-8)",
+ });
+ }
+}
+
+function modifyDOM(overtypeInput: HTMLTextAreaElement): HTMLElement {
+ overtypeInput.classList.add("overtype-input");
+ const overtypePreview = document.createElement("div");
+ overtypePreview.classList.add("overtype-preview");
+ overtypeInput.insertAdjacentElement("afterend", overtypePreview);
+ const overtypeWrapper = overtypeInput.parentElement!.closest("div")!;
+ overtypeWrapper.classList.add("overtype-wrapper");
+ overtypeInput.placeholder = "Add your comment here...";
+ const overtypeContainer = overtypeWrapper.parentElement!.closest("div")!;
+ overtypeContainer.classList.add("overtype-container");
+ return overtypeContainer.parentElement!.closest("div")!;
+}
+
+function hljsHighlighter(code: string, language: string) {
+ try {
+ if (language && hljs.getLanguage(language)) {
+ const result = hljs.highlight(code, { language });
+ return result.value;
+ } else {
+ const result = hljs.highlightAuto(code);
+ return result.value;
+ }
+ } catch (error) {
+ console.warn("highlight.js highlighting failed:", error);
+ return code;
+ }
+}
diff --git a/browser-extension/tests/lib/enhancers/__snapshots__/github.test.ts.snap b/browser-extension/tests/lib/enhancers/__snapshots__/github.test.ts.snap
new file mode 100644
index 0000000..fa2f132
--- /dev/null
+++ b/browser-extension/tests/lib/enhancers/__snapshots__/github.test.ts.snap
@@ -0,0 +1,11 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`GitHubHandler > should create correct GitHubContext spot for PR comment > github-pr-517-spot 1`] = `
+{
+ "domain": "github.com",
+ "number": 517,
+ "slug": "diffplug/selfie",
+ "type": "GH_PR_ADD_COMMENT",
+ "unique_key": "github:diffplug/selfie:pull:517",
+}
+`;
diff --git a/browser-extension/tests/lib/enhancers/github.test.ts b/browser-extension/tests/lib/enhancers/github.test.ts
new file mode 100644
index 0000000..2ec59ef
--- /dev/null
+++ b/browser-extension/tests/lib/enhancers/github.test.ts
@@ -0,0 +1,92 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { EnhancerRegistry, TextareaRegistry } from '../../../src/lib/registries'
+
+// Mock WXT's defineContentScript global
+vi.stubGlobal('defineContentScript', vi.fn())
+
+describe('GitHubHandler', () => {
+ let enhancers: EnhancerRegistry
+ let enhancedTextareas: TextareaRegistry
+ let mockTextarea: HTMLTextAreaElement
+
+ beforeEach(() => {
+ // Reset DOM and registries for each test
+ document.body.innerHTML = ''
+ enhancers = new EnhancerRegistry()
+ enhancedTextareas = new TextareaRegistry()
+
+ // Mock window.location for GitHub PR page
+ Object.defineProperty(window, 'location', {
+ value: {
+ hostname: 'github.com',
+ href: 'https://github.com/diffplug/selfie/pull/517',
+ pathname: '/diffplug/selfie/pull/517',
+ },
+ writable: true,
+ })
+
+ // Create a mock textarea element that mimics GitHub's PR comment box
+ mockTextarea = document.createElement('textarea')
+ mockTextarea.name = 'comment[body]'
+ mockTextarea.placeholder = 'Leave a comment'
+ mockTextarea.className = 'form-control markdown-body'
+
+ // Add it to a typical GitHub comment form structure
+ const commentForm = document.createElement('div')
+ commentForm.className = 'js-new-comment-form'
+ commentForm.appendChild(mockTextarea)
+ document.body.appendChild(commentForm)
+ })
+
+ it('should identify GitHub PR textarea and register it in TextareaRegistry', () => {
+ // Simulate the content script's enhanceMaybe function
+ const enhancedTextarea = enhancers.tryToEnhance(mockTextarea)
+
+ expect(enhancedTextarea).toBeTruthy()
+ expect(enhancedTextarea?.textarea).toBe(mockTextarea)
+ expect(enhancedTextarea?.spot.type).toBe('GH_PR_ADD_COMMENT')
+
+ // Register the enhanced textarea
+ if (enhancedTextarea) {
+ enhancedTextareas.register(enhancedTextarea)
+ }
+
+ // Verify it's in the registry
+ const registeredTextarea = enhancedTextareas.get(mockTextarea)
+ expect(registeredTextarea).toBeTruthy()
+ expect(registeredTextarea?.textarea).toBe(mockTextarea)
+ })
+
+ it('should create correct GitHubContext spot for PR comment', () => {
+ const enhancedTextarea = enhancers.tryToEnhance(mockTextarea)
+
+ expect(enhancedTextarea).toBeTruthy()
+
+ // Snapshot test on the spot value
+ expect(enhancedTextarea?.spot).toMatchSnapshot('github-pr-517-spot')
+
+ // Also verify specific expected values
+ expect(enhancedTextarea?.spot).toMatchObject({
+ domain: 'github.com',
+ number: 517,
+ slug: 'diffplug/selfie',
+ type: 'GH_PR_ADD_COMMENT',
+ unique_key: 'github:diffplug/selfie:pull:517',
+ })
+ })
+
+ it('should not enhance textarea on non-GitHub pages', () => {
+ // Change location to non-GitHub site
+ Object.defineProperty(window, 'location', {
+ value: {
+ hostname: 'example.com',
+ href: 'https://example.com/some/page',
+ pathname: '/some/page',
+ },
+ writable: true,
+ })
+
+ const enhancedTextarea = enhancers.tryToEnhance(mockTextarea)
+ expect(enhancedTextarea).toBeNull()
+ })
+})
diff --git a/browser-extension/tests/setup.ts b/browser-extension/tests/setup.ts
index 1171022..a9d0dd3 100644
--- a/browser-extension/tests/setup.ts
+++ b/browser-extension/tests/setup.ts
@@ -1,38 +1 @@
import '@testing-library/jest-dom/vitest'
-import { vi } from 'vitest'
-
-// Mock browser APIs
-interface GlobalWithBrowser {
- browser: {
- runtime: {
- sendMessage: ReturnType
- onMessage: {
- addListener: ReturnType
- }
- }
- }
- chrome: GlobalWithBrowser['browser']
-}
-
-const globalWithBrowser = global as unknown as GlobalWithBrowser
-
-globalWithBrowser.browser = {
- runtime: {
- onMessage: {
- addListener: vi.fn(),
- },
- sendMessage: vi.fn(),
- },
-}
-
-globalWithBrowser.chrome = globalWithBrowser.browser
-
-// Mock console methods to reduce noise in tests
-global.console = {
- ...console,
- debug: vi.fn(),
- error: vi.fn(),
- info: vi.fn(),
- log: vi.fn(),
- warn: vi.fn(),
-}
diff --git a/browser-extension/vitest.config.ts b/browser-extension/vitest.config.ts
index d247fc3..af7d365 100644
--- a/browser-extension/vitest.config.ts
+++ b/browser-extension/vitest.config.ts
@@ -1,14 +1,8 @@
-import path from 'node:path'
import { defineConfig } from 'vitest/config'
+import { WxtVitest } from 'wxt/testing'
export default defineConfig({
- resolve: {
- alias: {
- '@': path.resolve(__dirname, './entrypoints'),
- '@content': path.resolve(__dirname, './entrypoints/content'),
- '@utils': path.resolve(__dirname, './entrypoints/content/utils'),
- },
- },
+ plugins: [WxtVitest()],
test: {
coverage: {
exclude: [
diff --git a/browser-extension/wxt.config.ts b/browser-extension/wxt.config.ts
index 0514d1a..edc2589 100644
--- a/browser-extension/wxt.config.ts
+++ b/browser-extension/wxt.config.ts
@@ -3,7 +3,7 @@ import { defineConfig } from 'wxt'
export default defineConfig({
manifest: {
description:
- 'Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly places).',
+ 'Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).',
host_permissions: ['https://*/*', 'http://*/*'],
icons: {
16: '/icons/icon-16.png',