├── .prettierrc.json ├── .gitignore ├── .eslintignore ├── commitlint.config.cjs ├── docs ├── icon.psd ├── icon-large.png ├── icon-chatgpt.png ├── demo-recording.gif ├── demo-screenshot.png ├── demo-show-diagram.png ├── screenshot-debugging-xpath.png ├── screenshots │ ├── 2-class-diagram.png │ ├── 3-graph-diagram.png │ ├── 4-sequence-diagram.png │ └── 1-flowchart-diagram.png ├── chrome-web-store-assets │ └── extension-details.md └── icon-mermaidjs.svg ├── .husky ├── pre-commit └── commit-msg ├── src ├── images │ ├── icon16.png │ ├── icon32.png │ ├── icon48.png │ └── icon128.png ├── lib │ ├── configuration.ts │ ├── __test_files__ │ │ └── code-blocks.html │ ├── test-utils.ts │ ├── extension.ts │ ├── render-diagram.ts │ ├── extension.test.ts │ ├── chatgpt-dom.ts │ ├── prepare-code-block.ts │ ├── chatgpt-dom.test.ts │ ├── prepare-code-block.test.ts │ └── render-diagram.test.ts ├── setup-jest.js ├── options.html ├── manifest.json ├── content.ts └── options.ts ├── .prettierignore ├── .eslintrc.yaml ├── tsconfig.json ├── makefile ├── LICENSE ├── .github └── workflows │ ├── pull-request.yaml │ └── main.yaml ├── jest.config.mjs ├── package.json ├── webpack.config.js ├── CHANGELOG.md └── README.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | release/ 4 | node_modules/ 5 | coverage/ 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts. 2 | build 3 | coverage 4 | dist 5 | CHANGELOG.md 6 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /docs/icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/docs/icon.psd -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /docs/icon-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/docs/icon-large.png -------------------------------------------------------------------------------- /docs/icon-chatgpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/docs/icon-chatgpt.png -------------------------------------------------------------------------------- /src/images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/src/images/icon16.png -------------------------------------------------------------------------------- /src/images/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/src/images/icon32.png -------------------------------------------------------------------------------- /src/images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/src/images/icon48.png -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /docs/demo-recording.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/docs/demo-recording.gif -------------------------------------------------------------------------------- /docs/demo-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/docs/demo-screenshot.png -------------------------------------------------------------------------------- /src/images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/src/images/icon128.png -------------------------------------------------------------------------------- /docs/demo-show-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/docs/demo-show-diagram.png -------------------------------------------------------------------------------- /docs/screenshot-debugging-xpath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/docs/screenshot-debugging-xpath.png -------------------------------------------------------------------------------- /docs/screenshots/2-class-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/docs/screenshots/2-class-diagram.png -------------------------------------------------------------------------------- /docs/screenshots/3-graph-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/docs/screenshots/3-graph-diagram.png -------------------------------------------------------------------------------- /docs/screenshots/4-sequence-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/docs/screenshots/4-sequence-diagram.png -------------------------------------------------------------------------------- /docs/screenshots/1-flowchart-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmkerr/chatgpt-diagrams-extension/HEAD/docs/screenshots/1-flowchart-diagram.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts. 2 | build 3 | coverage 4 | dist 5 | CHANGELOG.md 6 | makefile 7 | .eslintignore 8 | 9 | # We need to keep the samples as close to the origin ChatGPT page as possible. 10 | samples/ 11 | src/lib/__test_files__/ 12 | -------------------------------------------------------------------------------- /src/lib/configuration.ts: -------------------------------------------------------------------------------- 1 | export enum DisplayMode { 2 | BelowDiagram = "BelowDiagram", 3 | AsTabs = "AsTabs", 4 | } 5 | 6 | export class Configuration { 7 | displayMode: DisplayMode; 8 | 9 | constructor(displayMode: DisplayMode) { 10 | this.displayMode = displayMode; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/setup-jest.js: -------------------------------------------------------------------------------- 1 | // Polyfill the TextEncoder/TextDecoder as this is no longer done by jsdom. 2 | // https://stackoverflow.com/questions/68468203/why-am-i-getting-textencoder-is-not-defined-in-jest 3 | import { TextEncoder, TextDecoder } from "util"; 4 | Object.assign(global, { TextDecoder, TextEncoder }); 5 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "prettier" 3 | - "plugin:@typescript-eslint/recommended" 4 | plugins: 5 | - "prettier" 6 | - "@typescript-eslint" 7 | env: 8 | browser: true 9 | node: true 10 | parser: "@typescript-eslint/parser" 11 | parserOptions: 12 | ecmaVersion: 2019 13 | sourceType: module 14 | rules: 15 | prettier/prettier: ["error"] 16 | -------------------------------------------------------------------------------- /src/lib/__test_files__/code-blocks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | Sample One 6 |
7 |
8 | Sample Two 9 |
10 |
11 | Sample Three 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/test-utils.ts: -------------------------------------------------------------------------------- 1 | export function elementByTestId( 2 | document: Document, 3 | testId: string 4 | ): HTMLElement { 5 | const element = document.querySelector( 6 | `[data-test-id=${testId}]` 7 | ) as HTMLElement; 8 | if (!element) { 9 | throw new Error(`Unable to find element with test id '${testId}'`); 10 | } 11 | 12 | return element; 13 | } 14 | -------------------------------------------------------------------------------- /docs/chrome-web-store-assets/extension-details.md: -------------------------------------------------------------------------------- 1 | ## Category 2 | 3 | Productivity 4 | 5 | ## Detailed Description 6 | 7 | The ChatGPT Diagrams extension adds a new button to the ChatGPT website - "Show Diagram". This button appears above code samples. If your code sample is in Mermaid format (a popular text based format for diagrams) then when you press the button the diagram will be shown in-line beneath the code sample. 8 | 9 | This is a great way to improve your interactions with ChatGPT and build and refine diagrams in real time! 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "jsx": "react", 6 | "lib": ["dom", "dom.iterable", "es2019"], 7 | "module": "es6", 8 | "moduleResolution": "node", 9 | "noImplicitReturns": true, 10 | "noUnusedLocals": true, 11 | "outDir": "dist", 12 | "strict": true, 13 | "sourceMap": true, 14 | "target": "es2019", 15 | "types": ["node", "chrome"] 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ChatGPT Diagrams Options 5 | 6 | 7 | 11 | 12 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "ChatGPT Diagrams", 4 | "description": "Render diagrams directly in ChatGPT", 5 | "options_ui": { 6 | "page": "options.html", 7 | "open_in_tab": true 8 | }, 9 | "permissions": ["storage"], 10 | "content_scripts": [ 11 | { 12 | "js": ["content.js"], 13 | "matches": ["https://chat.openai.com/*", "http://localhost/*"] 14 | } 15 | ], 16 | "icons": { 17 | "16": "images/icon16.png", 18 | "32": "images/icon32.png", 19 | "48": "images/icon48.png", 20 | "128": "images/icon128.png" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/extension.ts: -------------------------------------------------------------------------------- 1 | import { DisplayMode, Configuration } from "./configuration"; 2 | 3 | export abstract class Extension { 4 | public static async getConfiguration(): Promise { 5 | const options = await chrome.storage.sync.get({ 6 | displayMode: DisplayMode.BelowDiagram, 7 | likesColor: true, 8 | }); 9 | 10 | // Set the options. 11 | const config = new Configuration(options.displayMode); 12 | 13 | return config; 14 | } 15 | 16 | public static async setConfiguration( 17 | configuration: Configuration 18 | ): Promise { 19 | return await chrome.storage.sync.set(configuration); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | help: # Show help for each of the Makefile recipes. 3 | @grep -E '^[a-zA-Z0-9 -]+:.*#' Makefile | sort | while read -r l; do printf "\033[1;32m$$(echo $$l | cut -f 1 -d':')\033[00m:$$(echo $$l | cut -f 2- -d'#')\n"; done 4 | 5 | .PHONY: build 6 | build: # build the extension bundle 7 | rm -rf ./dist 8 | npm run build 9 | 10 | .PHONY: test 11 | test: # test the code 12 | npm run prettier # check formatting 13 | npm run lint # check lintin 14 | npm run tsc # validate that we can compile 15 | npm run test # run the unit tests 16 | 17 | .PHONY: release 18 | release: # build the release package 19 | npm run build 20 | rm -rf ./release && mkdir -p ./release 21 | cd ./dist && zip ../release/chatgpt-diagrams-extension.zip . -r 22 | ls ./release 23 | 24 | .PHONY: serve-samples 25 | serve-samples: # serve the sample ChatGPT pages. 26 | (cd samples/ && python -m http.server 3000) 27 | -------------------------------------------------------------------------------- /docs/icon-mermaidjs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dave Kerr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/render-diagram.ts: -------------------------------------------------------------------------------- 1 | import mermaid from "mermaid"; 2 | 3 | export async function renderDiagram( 4 | container: HTMLElement, 5 | id: string, 6 | code: string 7 | ) { 8 | try { 9 | // Render the diagram using the Mermaid.js library, then insert into our 10 | // container. 11 | // Hack Part 1: Rather than giving mermaid our container element as the 12 | // third parameter, we have to let it put error content in the document 13 | // body, then remove it ourselves. This is because I cannot get it to 14 | // sucessfully use the JSDOM mocked document in this case - even through 15 | // when _successfully_ rendering diagrams it works. 16 | const { svg } = await mermaid.render(`chatgpt-diagram-${id}`, code); 17 | container.innerHTML = svg; 18 | } catch (err) { 19 | // In the future we will return an error, but for now we will let the 20 | // mermaid error UI content sit in the container, as this is fairly clear 21 | // for the user. Later we can add more of our own branding and content. 22 | console.warn("an error occurred rendering the diagram", err); 23 | 24 | // Hack Part 2: grab the error content added to the global document, move it 25 | // into our container. Note the extra 'd' in the id below. 26 | const errorContent = global.document.body.querySelector( 27 | `#dchatgpt-diagram-${id}` 28 | ); 29 | container.insertAdjacentHTML("beforeend", errorContent?.outerHTML || ""); 30 | errorContent?.remove(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: Validate Pull Request 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | validate-pull-request: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | 12 | # Fixup Git URLs, see: 13 | # https://stackoverflow.com/questions/70663523/the-unauthenticated-git-protocol-on-port-9418-is-no-longer-supported 14 | - name: Fix up git URLs 15 | run: echo -e '[url "https://github.com/"]\n insteadOf = "git://github.com/"' >> ~/.gitconfig 16 | 17 | # Install dependencies. 18 | - name: Install Dependencies 19 | run: npm install 20 | 21 | # Ensure that we can build the extension. 22 | - name: Build 23 | run: make build 24 | 25 | # Run all tests. 26 | - name: Test 27 | run: make test 28 | 29 | # Upload coverage. 30 | - name: Upload coverage reports to Codecov 31 | uses: codecov/codecov-action@v3 32 | 33 | # Verify that we can create the release package. 34 | - name: Verify Release 35 | run: | 36 | make release 37 | if [ -f ./release/chatgpt-diagrams-extension.zip ]; then 38 | echo "release exists..." 39 | else 40 | echo "cannot find release..." 41 | exit 1 42 | fi 43 | 44 | # Upload extension artifact. 45 | - name: Upload Extension Artifact 46 | uses: actions/upload-artifact@v3 47 | with: 48 | name: chatgpt-diagrams-browser-extension 49 | path: ./release/chatgpt-diagrams-extension.zip 50 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 7 | export default { 8 | // We're using ts-jest for typescript support. Treat *.ts files as ESM 9 | // modules so that we can use 'import'. 10 | // See: 11 | // https://kulshekhar.github.io/ts-jest/docs/next/guides/esm-support/ 12 | preset: "ts-jest", 13 | extensionsToTreatAsEsm: [".ts"], 14 | moduleNameMapper: { 15 | "^(\\.{1,2}/.*)\\.js$": "$1", 16 | }, 17 | transform: { 18 | // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` 19 | // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` 20 | "^.+\\.tsx?$": [ 21 | "ts-jest", 22 | { 23 | useESM: true, 24 | }, 25 | ], 26 | }, 27 | 28 | // Automatically clear mock calls, instances, contexts and results before every test 29 | clearMocks: true, 30 | 31 | // Indicates whether the coverage information should be collected while executing the test 32 | collectCoverage: true, 33 | collectCoverageFrom: ["src/**/*.{js,ts}"], 34 | 35 | // The directory where Jest should output its coverage files 36 | coverageDirectory: "coverage", 37 | 38 | // Indicates which provider should be used to instrument code for coverage 39 | coverageProvider: "v8", 40 | 41 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 42 | setupFilesAfterEnv: ["./src/setup-jest.js"], 43 | 44 | // The test environment that will be used for testing 45 | testEnvironment: "jsdom", 46 | }; 47 | -------------------------------------------------------------------------------- /src/content.ts: -------------------------------------------------------------------------------- 1 | import mermaid from "mermaid"; 2 | import { findCodeBlocks } from "./lib/chatgpt-dom"; 3 | import { prepareCodeBlock } from "./lib/prepare-code-block"; 4 | 5 | const config = { 6 | scanForDiagramsIntervalMS: 1000, 7 | }; 8 | 9 | mermaid.initialize({ 10 | startOnLoad: false, 11 | theme: "forest", 12 | }); 13 | 14 | // First, we set up the triggers that we will use to identify when there are 15 | // new code blocks to check. If the page was static, we could just traverse 16 | // the DOM, however as it is (currently) rendered with React, the elements 17 | // are regularly re-created. To simplify the process of finding the code 18 | // elements, we just scan for them on a timer. 19 | setInterval(() => updateDiagrams(), config.scanForDiagramsIntervalMS); 20 | 21 | function updateDiagrams() { 22 | // We search for any code blocks because at the moment ChatGPT rarely 23 | // correctly classifies the code as mermaid (it is often rust/lus/scss, etc). 24 | const codeBlocks = findCodeBlocks(window.document); 25 | const unprocessedCodeBlocks = codeBlocks.filter((e) => !e.isProcessed); 26 | 27 | // TODO we should find a way to include this kind of console output only when 28 | // running locally (a bit like the developer title). Until then, disable it 29 | // as it is noisy. 30 | // console.log( 31 | // `Found ${unprocessedCodeBlocks.length}/${codeBlocks.length} unprocessed code blocks...` 32 | // ); 33 | 34 | // Loop through each unprocessed code block, then prepare each one, adding 35 | // the diagram buttons and DOM elements. 36 | unprocessedCodeBlocks.forEach((codeBlock) => { 37 | prepareCodeBlock(window.document, codeBlock); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { DisplayMode, Configuration } from "./lib/configuration"; 2 | import { Extension } from "./lib/extension"; 3 | 4 | // Saves options to chrome.storage 5 | const saveOptions = async () => { 6 | // Create a new configuration object, then populate it from the html. 7 | const displayMode = (( 8 | document.getElementById("display_mode") 9 | )).value; 10 | const likesColor = (document.getElementById("like")) 11 | .checked; 12 | console.log( 13 | `UI State: Display Mode - ${displayMode}, Likes Color - ${likesColor}` 14 | ); 15 | const configuration = new Configuration(displayMode); 16 | 17 | await Extension.setConfiguration(configuration); 18 | 19 | // Update status to let user know options were saved. 20 | const status = document.getElementById("status"); 21 | status.textContent = "Options saved."; 22 | setTimeout(() => { 23 | status.textContent = ""; 24 | }, 750); 25 | }; 26 | 27 | // Restores select box and checkbox state using the preferences 28 | // stored in chrome.storage. 29 | const restoreOptions = async () => { 30 | console.log("test version 1"); 31 | const options = await Extension.getConfiguration(); 32 | 33 | // Check what we get from Chrome, update the UI. 34 | console.log( 35 | `Loaded options from storge: ${JSON.stringify(options, null, 2)}` 36 | ); 37 | (document.getElementById("display_mode")).value = 38 | options.displayMode; 39 | (document.getElementById("like")).checked = true; //options.likesColor; 40 | }; 41 | 42 | document.addEventListener("DOMContentLoaded", restoreOptions); 43 | (document.getElementById("save")).addEventListener( 44 | "click", 45 | saveOptions 46 | ); 47 | -------------------------------------------------------------------------------- /src/lib/extension.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, afterEach, jest, test } from "@jest/globals"; 2 | import { Extension } from "./extension"; 3 | import { DisplayMode } from "./configuration"; 4 | 5 | function chromeObject() { 6 | return { 7 | storage: { 8 | sync: { 9 | get: jest.fn(), 10 | set: jest.fn(), 11 | }, 12 | }, 13 | }; 14 | } 15 | 16 | describe("extension", () => { 17 | describe("Extension.getConfiguration() ", () => { 18 | afterEach(() => { 19 | jest.restoreAllMocks(); 20 | }); 21 | 22 | test("correctly maps configuration from Chrome storage", async () => { 23 | // Mock the chrome storage, note that it is key/value and not typed... 24 | const mockChrome = chromeObject(); 25 | mockChrome.storage.sync.get.mockImplementation(() => 26 | Promise.resolve({ 27 | displayMode: "AsTabs", 28 | }) 29 | ); 30 | // @ts-expect-error - TS knows the global 'window' doesn't have a 'chrome' object. 31 | global.chrome = mockChrome; 32 | 33 | // Get the mapped config, note that it is typed. 34 | const config = await Extension.getConfiguration(); 35 | expect(config.displayMode).toEqual(DisplayMode.AsTabs); 36 | }); 37 | 38 | test("sets the correct defaults if there is no configuration in Chrome storage", async () => { 39 | // Mock the chrome storage, note that it is empty... 40 | const mockChrome = chromeObject(); 41 | mockChrome.storage.sync.get.mockImplementation((defaults) => 42 | Promise.resolve(Object.assign({}, defaults)) 43 | ); 44 | // @ts-expect-error - TS knows the global 'window' doesn't have a 'chrome' object. 45 | global.chrome = mockChrome; 46 | 47 | // Get the mapped config, note that it is and the default values have been set. 48 | const config = await Extension.getConfiguration(); 49 | expect(config.displayMode).toEqual(DisplayMode.BelowDiagram); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-diagrams", 3 | "version": "0.1.2", 4 | "description": "Enhance the ChatGPT website to allow diagrams to be presented inline.", 5 | "type": "module", 6 | "scripts": { 7 | "start": "webpack --mode=development --watch", 8 | "build": "webpack --mode=production", 9 | "lint": "eslint .", 10 | "lint:fix": "eslint --fix .", 11 | "prettier": "prettier --check .", 12 | "prettier:fix": "prettier --ignore-unknown --write .", 13 | "test": "NODE_OPTIONS=--experimental-vm-modules jest", 14 | "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", 15 | "test:debug": "NODE_OPTIONS=--experimental-vm-modules node --inspect-brk ./node_modules/.bin/jest --runInBand --watch", 16 | "tsc": "tsc", 17 | "prepare": "husky install" 18 | }, 19 | "license": "MIT", 20 | "lint-staged": { 21 | "**/*": "npm run prettier:fix", 22 | "**/*.{js,ts}": "npm run lint:fix" 23 | }, 24 | "devDependencies": { 25 | "@commitlint/cli": "^17.6.3", 26 | "@commitlint/config-conventional": "^17.6.3", 27 | "@parcel/config-webextension": "^2.8.3", 28 | "@rollup/plugin-commonjs": "^24.0.1", 29 | "@rollup/plugin-node-resolve": "^15.0.2", 30 | "@rollup/plugin-typescript": "^11.1.1", 31 | "@types/chrome": "^0.0.236", 32 | "@types/d3": "^7.4.0", 33 | "@types/dompurify": "^3.0.2", 34 | "@types/jsdom": "^21.1.1", 35 | "@typescript-eslint/eslint-plugin": "^5.59.6", 36 | "@typescript-eslint/parser": "^5.59.6", 37 | "chrome-extension-manifest-webpack-plugin": "^1.0.8", 38 | "copy-webpack-plugin": "^11.0.0", 39 | "eslint": "^8.40.0", 40 | "eslint-config-prettier": "^8.8.0", 41 | "eslint-plugin-prettier": "^4.2.1", 42 | "html-webpack-plugin": "^5.5.1", 43 | "husky": "^8.0.3", 44 | "jest": "^29.5.0", 45 | "jest-environment-jsdom": "^29.2.0", 46 | "jsdom": "^22.0.0", 47 | "lint-staged": "^13.2.2", 48 | "parcel": "^2.8.3", 49 | "prettier": "2.8.8", 50 | "rollup": "^3.20.2", 51 | "rollup-plugin-chrome-extension": "^3.6.12", 52 | "ts-jest": "^29.1.0", 53 | "ts-loader": "^9.4.2", 54 | "tslib": "^2.5.0", 55 | "typescript": "^5.0.4", 56 | "webpack": "^5.82.1", 57 | "webpack-cli": "^5.1.1" 58 | }, 59 | "dependencies": { 60 | "mermaid": "^10.1.0" 61 | }, 62 | "browserslist": "> 0.5%, last 2 versions, not dead" 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | main: 10 | runs-on: ubuntu-20.04 11 | # Write all permissions needed for 'release please' PR creation. 12 | permissions: write-all 13 | steps: 14 | # If the 'release please' action has been performed, we can actually 15 | # deploy the website. 16 | # Note: *every* step here needs to check the 'release_created' flag. 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | 20 | # Fixup Git URLs, see: 21 | # https://stackoverflow.com/questions/70663523/the-unauthenticated-git-protocol-on-port-9418-is-no-longer-supported 22 | - name: Fix up git URLs 23 | run: echo -e '[url "https://github.com/"]\n insteadOf = "git://github.com/"' >> ~/.gitconfig 24 | 25 | # Install dependencies. 26 | - name: Install Dependencies 27 | run: npm install 28 | 29 | # Ensure that we can build the extension. 30 | - name: Build 31 | run: make build 32 | 33 | # Run all tests. 34 | - name: Test 35 | run: make test 36 | 37 | # Upload coverage. 38 | - name: Upload coverage reports to Codecov 39 | uses: codecov/codecov-action@v3 40 | 41 | # Create and verify the release. 42 | - name: Create and Verify Release 43 | run: | 44 | make release 45 | if [ -f ./release/chatgpt-diagrams-extension.zip ]; then 46 | echo "release exists..." 47 | else 48 | echo "cannot find release..." 49 | exit 1 50 | fi 51 | 52 | # Upload extension artifact. 53 | - name: Upload Extension Artifact 54 | uses: actions/upload-artifact@v3 55 | with: 56 | name: chatgpt-diagrams-browser-extension 57 | path: ./release/chatgpt-diagrams-extension.zip 58 | 59 | # Run Release Please to create release pull requests if we have merged to 60 | # the main branch. 61 | - uses: google-github-actions/release-please-action@v3 62 | id: release 63 | with: 64 | token: ${{ secrets.GITHUB_TOKEN }} 65 | release-type: node 66 | package-name: chatgpt-diagrams-extension 67 | 68 | # If we have created a release, attach the artifacts to it. 69 | - name: Upload Release Artifact 70 | if: ${{ steps.release.outputs.release_created }} 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | run: gh release upload ${{ steps.release.outputs.tag_name }} ./release/chatgpt-diagrams-extension.zip 74 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import CopyPlugin from "copy-webpack-plugin"; 4 | import HtmlWebpackPlugin from "html-webpack-plugin"; 5 | 6 | function transformManifest(buffer, mode) { 7 | // Load the manifest object, update the versions, from the package.json, 8 | // send back to webpack. 9 | const pkg = JSON.parse(fs.readFileSync("./package.json")); 10 | const manifest = JSON.parse(buffer.toString()); 11 | 12 | // If we are in development mode, update the name of the extension to make 13 | // it more obvious when we are debugging. 14 | if (mode === "development") { 15 | // TODO; add '10s ago' or whatever so that we can see how recent, 16 | manifest.name = `${manifest.name} - Local`; 17 | } 18 | 19 | return JSON.stringify({ ...manifest, version: pkg.version }, null, 2); 20 | } 21 | 22 | export default (_, argv) => ({ 23 | // Use cheap and fast inline source maps in development mode. 24 | // For prodution, standalone source maps. 25 | // Note that the 'eval' or 'sourcemap' options don't seem to load in Chrome 26 | // for some reason. So using inline for now. 27 | devtool: 28 | argv.mode === "development" 29 | ? "inline-cheap-module-source-map" 30 | : "source-map", 31 | cache: { 32 | type: "filesystem", 33 | }, 34 | entry: { 35 | options: "./src/options.ts", 36 | content: "./src/content.ts", 37 | }, 38 | output: { 39 | publicPath: "", 40 | path: path.join(process.cwd(), "dist"), 41 | filename: "[name].js", 42 | clean: true, 43 | asyncChunks: false, 44 | }, 45 | resolve: { 46 | extensions: [".ts", ".js"], 47 | }, 48 | module: { 49 | rules: [ 50 | { 51 | test: /\.ts$/, 52 | exclude: /node_modules/, 53 | use: { 54 | loader: "ts-loader", 55 | }, 56 | }, 57 | ], 58 | }, 59 | plugins: [ 60 | new HtmlWebpackPlugin({ 61 | template: "src/options.html", 62 | filename: "options.html", 63 | chunks: ["options"], 64 | hash: true, 65 | inject: true, 66 | }), 67 | new CopyPlugin({ 68 | patterns: [ 69 | // Copy the images, icons etc, as is. 70 | { from: "src/images", to: "images" }, 71 | // Copy the manifest - but update its version using the transform fn. 72 | { 73 | from: "src/manifest.json", 74 | to: "manifest.json", 75 | transform(content) { 76 | return transformManifest(content, argv.mode); 77 | }, 78 | }, 79 | ], 80 | }), 81 | ], 82 | // These hints tell webpack that we can expect much larger than usual assets 83 | // and entry points (as we compile things down to a few small files, this is 84 | // ok as we load the extension from disk not the web). 85 | performance: { 86 | maxEntrypointSize: 5 * 1024 * 1024, 87 | maxAssetSize: 5 * 1024 * 1024, 88 | }, 89 | }); 90 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.2](https://github.com/dwmkerr/chatgpt-diagrams-extension/compare/v0.1.1...v0.1.2) (2023-12-06) 4 | 5 | 6 | ### Miscellaneous Chores 7 | 8 | * release 0.1.2 ([e3e809b](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/e3e809bc20eaf9a61032b1ab5a43e73c0d9520c2)) 9 | 10 | ## [0.1.1](https://github.com/dwmkerr/chatgpt-diagrams-extension/compare/v0.1.0...v0.1.1) (2023-12-06) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * remove noisy debug logging ([a051cef](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/a051cef0383d44deff53c73523902760dd268662)) 16 | 17 | ## [0.1.0](https://github.com/dwmkerr/chatgpt-diagrams-extension/compare/v0.0.6...v0.1.0) (2023-07-05) 18 | 19 | 20 | ### Features 21 | 22 | * tab display mode ([#22](https://github.com/dwmkerr/chatgpt-diagrams-extension/issues/22)) ([a91451e](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/a91451efc9f61abfefd6c46c54b0b57663613cb5)) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * validate mermaid content is correct in tests ([#23](https://github.com/dwmkerr/chatgpt-diagrams-extension/issues/23)) ([80e83ac](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/80e83ac1d254b172b9417424e6562beba41e2f10)) 28 | 29 | ## [0.0.6](https://github.com/dwmkerr/chatgpt-diagrams-extension/compare/v0.0.5...v0.0.6) (2023-05-23) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * correctly capture mermaidjs errors ([#19](https://github.com/dwmkerr/chatgpt-diagrams-extension/issues/19)) ([5efc5f6](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/5efc5f67c9923b6c81e8f12cce39f57ec75927f5)) 35 | 36 | ## [0.0.5](https://github.com/dwmkerr/chatgpt-diagrams-extension/compare/v0.0.4...v0.0.5) (2023-05-15) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * correct zip release package format ([#8](https://github.com/dwmkerr/chatgpt-diagrams-extension/issues/8)) ([60ee805](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/60ee8059ab5c8acce65788f91139b027c81cb0c3)) 42 | 43 | ## [0.0.4](https://github.com/dwmkerr/chatgpt-diagrams-extension/compare/v0.0.2...v0.0.4) (2023-05-04) 44 | 45 | 46 | ### Miscellaneous Chores 47 | 48 | * release 0.0.3 ([c199009](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/c199009d8e9628915e55d12efb3bf0652d64f879)) 49 | * release 0.0.4 ([5c65693](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/5c65693b0110e2c2eee5d5c111699007a7e05c62)) 50 | 51 | ## 0.0.2 (2023-05-04) 52 | 53 | 54 | ### Features 55 | 56 | * basic extension to render chatgpt diagrams ([5ad5948](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/5ad5948f620d61501963846cad7274ec39fccb13)) 57 | * release-please for build ([8f3f424](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/8f3f4246d270a21cb3999d60c0442161fdd09301)) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * install dependencies in PR pipeline ([bfed13e](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/bfed13e72315b4984a2f4424981addd6e97d886a)) 63 | * update release please permissions ([640f496](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/640f49687e55df75d37aef69d7d4df0c5b0727fe)) 64 | 65 | 66 | ### Miscellaneous Chores 67 | 68 | * release 0.0.2 ([b5ae6a7](https://github.com/dwmkerr/chatgpt-diagrams-extension/commit/b5ae6a7fae1e79e8b46619606f6b4ebf7ae8c04e)) 69 | -------------------------------------------------------------------------------- /src/lib/chatgpt-dom.ts: -------------------------------------------------------------------------------- 1 | export type ChatGPTCodeDOM = { 2 | // If 'true' indicates that we have already created the diagram. 3 | isProcessed: boolean; 4 | 5 | // The index of the block, i.e. the zero based order of the block in the 6 | // document. This is used to create unique IDs. 7 | index: number; 8 | 9 | // The actual code in the sample. 10 | code: string; 11 | 12 | // The overall container 'pre' tag that holds the frame, buttons and code. 13 | preElement: HTMLPreElement; 14 | 15 | // The 'copy code' button, we use this to quickly get a handle on the toolbar 16 | // and insert adjacent buttons. 17 | copyCodeButton: HTMLButtonElement; 18 | 19 | // The div that contains the 'code' element with the actual code. 20 | codeContainerElement: HTMLDivElement; 21 | }; 22 | 23 | /** 24 | * queryFindExactlyOneElement. 25 | * 26 | * @param {} document - the DOM document 27 | * @param {} xpathQuery - the XPath query to run 28 | * @param {} contextNode - the context node to start the query from, or null 29 | */ 30 | export function queryFindExactlyOneElement( 31 | document: Document, 32 | xpathQuery: string, 33 | contextNode: Element 34 | ) { 35 | // Run the xpath query, retrieving a snapshop. 36 | const snapshot = document.evaluate( 37 | xpathQuery, 38 | contextNode, 39 | null, 40 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, 41 | null 42 | ); 43 | 44 | // If we did not find the expected number of results, bail. 45 | if (snapshot.snapshotLength !== 1) { 46 | const errorMessage = `failed to find exactly one element when running query '${xpathQuery}' - ${snapshot.snapshotLength} element(s) were found`; 47 | throw new Error(errorMessage); 48 | } 49 | 50 | // Return the element we found. 51 | return snapshot.snapshotItem(0); 52 | } 53 | 54 | export function findCodeBlocks(document: Document): Array { 55 | // This function takes the containing pre tag for the code sample, and 56 | // returns the child elements we will be using to manipulate the DOM. 57 | const preToDOM = (preTag: HTMLPreElement, index: number) => { 58 | // Get the 'copy code' button - I've not found a clean way to do consistently 59 | // with query selectors, so use XPath. 60 | const copyCodeButton = queryFindExactlyOneElement( 61 | document, 62 | './/button[contains(text(), "Copy")]', 63 | preTag 64 | ); 65 | // Get the 'code' element that has the actual mermaid code sample. 66 | const codeElement = preTag.querySelector("code") as HTMLElement; 67 | 68 | return { 69 | isProcessed: preTag.classList.contains("chatgpt-diagrams-processed"), 70 | index, 71 | code: codeElement.textContent?.trim() || "", 72 | preElement: preTag, 73 | copyCodeButton: copyCodeButton as HTMLButtonElement, 74 | codeContainerElement: codeElement.parentNode as HTMLDivElement, 75 | }; 76 | }; 77 | 78 | // Find all of the 'pre' tags, then break into the specific code dom objects. 79 | const results = document.evaluate( 80 | "//pre", 81 | document, 82 | null, 83 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE 84 | ); 85 | return Array.from( 86 | { 87 | length: results.snapshotLength, 88 | }, 89 | (_, index) => results.snapshotItem(index) as HTMLPreElement 90 | ) 91 | .map(preToDOM) 92 | .filter((element) => element); // filter out null elements 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/prepare-code-block.ts: -------------------------------------------------------------------------------- 1 | import { ChatGPTCodeDOM } from "./chatgpt-dom"; 2 | import { renderDiagram } from "./render-diagram"; 3 | 4 | export type PreparedCodeBlock = { 5 | // The 'show diagram' button. 6 | showDiagramButton: HTMLButtonElement; 7 | showCodeButton: HTMLButtonElement; 8 | diagramTabContainer: HTMLDivElement; 9 | }; 10 | 11 | export function prepareCodeBlock( 12 | document: Document, 13 | codeBlock: ChatGPTCodeDOM 14 | ): PreparedCodeBlock { 15 | // Create the diagram tab container. 16 | const diagramTabContainer = document.createElement("div"); 17 | diagramTabContainer.id = `chatgpt-diagram-container-${codeBlock.index}`; 18 | diagramTabContainer.classList.add("p-4", "overflow-y-auto"); 19 | diagramTabContainer.style.backgroundColor = "#FFFFFF"; 20 | diagramTabContainer.style.display = "none"; 21 | codeBlock.codeContainerElement.after(diagramTabContainer); 22 | 23 | // // Create the diagram 'after code' container. 24 | // const diagramContainer = document.createElement("div"); 25 | // diagramContainer.id = `chatgpt-diagram-container-${codeBlock.index}`; 26 | // switch (displayMode) { 27 | // case DisplayMode.BelowDiagram: 28 | // // Put the digram after the 'pre' element. 29 | // codeBlock.preElement.after(diagramContainer); 30 | // break; 31 | // case DisplayMode.AsTabs: 32 | // // Set the style of the container to match the code block, then 33 | // // put into the code div. 34 | // diagramContainer.classList.add("p-4", "overflow-y-auto"); 35 | // codeBlock.codeContainerElement.after(diagramContainer); 36 | // // Style the code block tab. 37 | // codeBlock.codeContainerElement.style.display = "none"; 38 | // // Style the diagram tab. 39 | // diagramContainer.style.backgroundColor = "#FFFFFF"; 40 | // break; 41 | // default: 42 | // throw new Error(`Unknown diagram display mode '${displayMode}'`); 43 | 44 | // Create the 'show diagram' button. 45 | const showDiagramButton = document.createElement("button"); 46 | showDiagramButton.innerText = "Show diagram"; 47 | showDiagramButton.classList.add("flex", "ml-auto", "gap-2"); 48 | showDiagramButton.onclick = () => { 49 | renderDiagram(diagramTabContainer, `${codeBlock.index}`, codeBlock.code); 50 | codeBlock.codeContainerElement.style.display = "none"; 51 | diagramTabContainer.style.display = "block"; 52 | showDiagramButton.style.display = "none"; 53 | showCodeButton.style.display = "inline-block"; 54 | }; 55 | codeBlock.copyCodeButton.before(showDiagramButton); 56 | 57 | // Create the 'show code' button. 58 | const showCodeButton = document.createElement("button"); 59 | showCodeButton.innerText = "Show code"; 60 | showCodeButton.classList.add("flex", "ml-auto", "gap-2"); 61 | showCodeButton.style.display = "none"; 62 | showCodeButton.onclick = () => { 63 | codeBlock.codeContainerElement.style.display = "block"; 64 | diagramTabContainer.style.display = "none"; 65 | showDiagramButton.style.display = "inline-block"; 66 | showCodeButton.style.display = "none"; 67 | }; 68 | codeBlock.copyCodeButton.before(showCodeButton); 69 | 70 | // Add the 'chatgpt-diagrams' class to the code block - this means we will 71 | // exclude it from later searches. 72 | codeBlock.preElement.classList.add("chatgpt-diagrams-processed"); 73 | 74 | return { 75 | showDiagramButton, 76 | showCodeButton, 77 | diagramTabContainer, 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /src/lib/chatgpt-dom.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "@jest/globals"; 2 | import { findCodeBlocks } from "./chatgpt-dom"; 3 | import { JSDOM, VirtualConsole } from "jsdom"; 4 | 5 | // Create a virtual console that suppresses the CSS errors we get loading the 6 | // ChatGPT sample (they can be safely ignored and pollute the console output 7 | // too much). 8 | const virtualConsole = new VirtualConsole(); 9 | virtualConsole.sendTo(console, { omitJSDOMErrors: true }); 10 | virtualConsole.on("jsdomError", (err) => { 11 | if (/Could not parse CSS stylesheet/.test(err.message)) { 12 | return; 13 | } 14 | console.error(`Error uncaught: ${err.message.substring(0, 1024)}`); 15 | // When I'm comfortable I've caught these JDOM issues we can log the error 16 | // fully as below. 17 | // console.error(err); 18 | }); 19 | 20 | describe("chatgpt-dom", () => { 21 | describe("findCodeBlocks", () => { 22 | test("finds the correct number of code blocks in the sample", async () => { 23 | // Assert that we find the four code blocks in the sample file. 24 | const dom = await JSDOM.fromFile("./src/lib/__test_files__/sample.html", { 25 | virtualConsole, 26 | }); 27 | const codeBlocks = findCodeBlocks(dom.window.document); 28 | expect(codeBlocks.length).toEqual(4); 29 | }); 30 | 31 | test("finds the correct DOM elements that make up each code block", async () => { 32 | // Assert that we find the four code blocks in the sample file. 33 | const dom = await JSDOM.fromFile("./src/lib/__test_files__/sample.html", { 34 | virtualConsole, 35 | }); 36 | const codeBlocks = findCodeBlocks(dom.window.document); 37 | 38 | // We're going to dig into each code block a bit more and assert the 39 | // elements found are actually correct. 40 | const [ 41 | sendRequestBlock, 42 | foodDeliveryBlock, 43 | messagingBlock, 44 | retryLogicBock, 45 | ] = codeBlocks; 46 | 47 | // Assert we've found the 'send request' code sample elements. 48 | expect(sendRequestBlock.index).toEqual(0); 49 | const [srl1, srl2] = sendRequestBlock.code.split("\n"); 50 | expect(srl1).toEqual(`graph LR`); 51 | expect(srl2).toEqual(` A[Browser] --> B{Send HTTP Request}`); 52 | expect( 53 | sendRequestBlock.preElement.contains(sendRequestBlock.copyCodeButton) 54 | ).toEqual(true); 55 | expect(sendRequestBlock.copyCodeButton.textContent).toEqual("Copy code"); 56 | 57 | // Assert we've found the 'food delivery' code sample elements. 58 | expect(foodDeliveryBlock.index).toEqual(1); 59 | const [fdl1, fdl2] = foodDeliveryBlock.code.split("\n"); 60 | expect(fdl1).toEqual(`classDiagram`); 61 | expect(fdl2).toEqual(` class User {`); 62 | expect( 63 | foodDeliveryBlock.preElement.contains(foodDeliveryBlock.copyCodeButton) 64 | ).toEqual(true); 65 | expect(foodDeliveryBlock.copyCodeButton.textContent).toEqual("Copy code"); 66 | 67 | // Assert we've found the 'messaging architecture' code sample elements. 68 | expect(messagingBlock.index).toEqual(2); 69 | const [mal1, mal2] = messagingBlock.code.split("\n"); 70 | expect(mal1).toEqual(`graph TB`); 71 | expect(mal2).toEqual(` subgraph User Interface`); 72 | expect( 73 | messagingBlock.preElement.contains(messagingBlock.copyCodeButton) 74 | ).toEqual(true); 75 | expect(messagingBlock.copyCodeButton.textContent).toEqual("Copy code"); 76 | 77 | // Assert we've found the 'retry logic' code sample elements. 78 | expect(retryLogicBock.index).toEqual(3); 79 | const [rll1, rll2] = retryLogicBock.code.split("\n"); 80 | expect(rll1).toEqual(`sequenceDiagram`); 81 | expect(rll2).toEqual(` participant Producer`); 82 | expect( 83 | retryLogicBock.preElement.contains(retryLogicBock.copyCodeButton) 84 | ).toEqual(true); 85 | expect(retryLogicBock.copyCodeButton.textContent).toEqual("Copy code"); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/lib/prepare-code-block.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "@jest/globals"; 2 | import { elementByTestId } from "./test-utils"; 3 | import { findCodeBlocks } from "./chatgpt-dom"; 4 | import { prepareCodeBlock } from "./prepare-code-block"; 5 | 6 | describe("prepare-code-block", () => { 7 | test("will not double-process code blocks", () => { 8 | document.body.innerHTML = ` 9 |
10 |

Here's a simple diagram:

11 |
12 |               
13 |
14 | mermaid 15 | 16 |
17 |
18 | graph LR 19 | A[Browser] --> B{Send HTTP Request} 20 | 21 |
22 |
23 |
24 | 25 |

This flowchart illustrates a basic web request.

26 |
27 | `; 28 | 29 | // Grab the code block from the sample. 30 | const [codeBlock] = findCodeBlocks(document); 31 | expect(codeBlock.isProcessed).toBeFalsy(); 32 | 33 | // The pre tag should have a class added to show it's been prepared. 34 | prepareCodeBlock(document, codeBlock); 35 | const preTag = elementByTestId(document, "block"); 36 | expect(preTag.classList).toContain("chatgpt-diagrams-processed"); 37 | 38 | // If we search for code blocks again, this block should be marked as 39 | // 'processed'. 40 | const [codeBlockUpdated] = findCodeBlocks(document); 41 | expect(codeBlockUpdated.isProcessed).toBeTruthy(); 42 | }); 43 | 44 | test("can update the buttons in the toolbar and prep container divs", () => { 45 | document.body.innerHTML = ` 46 |
47 |

Here's a simple diagram:

48 |
49 |               
50 |
51 | mermaid 52 | 53 | 54 | 55 |
56 |
57 | graph LR 58 | A[Browser] --> B{Send HTTP Request} 59 | 60 |
61 | 62 |
63 |
64 | 65 |

This flowchart illustrates a basic web request.

66 |
67 | `; 68 | 69 | // Grab the code block from the sample. 70 | const [codeBlock] = findCodeBlocks(document); 71 | 72 | // Prepare the code block, which will add all of our diagram scaffolding 73 | // elements. 74 | const { showDiagramButton, showCodeButton, diagramTabContainer } = 75 | prepareCodeBlock(document, codeBlock); 76 | 77 | // Assert the button text, style, location. 78 | const toolbar = elementByTestId(document, "toolbar"); 79 | const copyCodeButton = elementByTestId(document, "copy"); 80 | const showDiagramStyle = window.getComputedStyle(showDiagramButton); 81 | const showCodeStyle = window.getComputedStyle(showCodeButton); 82 | const tabContainerStyle = window.getComputedStyle(diagramTabContainer); 83 | 84 | expect(showDiagramButton.parentElement).toEqual(toolbar); 85 | expect(showDiagramButton.nextElementSibling).toEqual(showCodeButton); 86 | expect(showDiagramButton.innerText).toEqual("Show diagram"); 87 | expect(showDiagramStyle.display).toEqual("inline-block"); 88 | expect(showCodeButton.parentElement).toEqual(toolbar); 89 | expect(showCodeButton.nextElementSibling).toEqual(copyCodeButton); 90 | expect(showCodeButton.innerText).toEqual("Show code"); 91 | expect(showCodeStyle.display).toEqual("none"); 92 | expect(tabContainerStyle.display).toEqual("none"); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/lib/render-diagram.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, test } from "@jest/globals"; 2 | import { JSDOM } from "jsdom"; 3 | import { elementByTestId } from "./test-utils"; 4 | import { renderDiagram } from "./render-diagram"; 5 | 6 | describe("render-diagram", () => { 7 | describe("renderDiagram", () => { 8 | beforeEach(() => { 9 | // It seems that jsdom doesn't handle the SVGElement 'getBBox' function. 10 | // Return a box of any old size and the tests will function. 11 | // @ts-expect-error - TS knows SVGElement doesn't have getBBox 12 | window.SVGElement.prototype.getBBox = () => ({ 13 | x: 0, 14 | y: 0, 15 | width: 100, 16 | height: 100, 17 | }); 18 | }); 19 | 20 | afterEach(() => { 21 | // @ts-expect-error - TS knows SVGElement doesn't have getBBox 22 | delete window.SVGElement.prototype.getBBox; 23 | 24 | // Clean out any content we've created so far. 25 | document.body.innerHTML = ""; 26 | }); 27 | 28 | test("can render simple graph in 'BelowDiagram' mode", async () => { 29 | document.body.innerHTML = ` 30 |
31 |

Here's a simple diagram:

32 |
 33 |                 
34 |
35 | mermaid 36 | 37 | 38 |
39 |
40 | graph LR 41 | A[Browser] --> B{Send HTTP Request} 42 | 43 |
44 |
45 |
46 | 47 |
48 |

This flowchart illustrates a basic web request.

49 |
50 | `; 51 | 52 | const id = "1"; 53 | const container = elementByTestId(document, "container"); 54 | const code = document.querySelector("code")?.textContent || ""; 55 | 56 | await renderDiagram(container, id, code); 57 | 58 | const svg = container.querySelector("svg") as SVGElement; 59 | expect(svg.id).toContain(`chatgpt-diagram-${id}`); 60 | 61 | // We don't need to go overboard, but check that the SVG at least 62 | // contains two node labels as expected (otherwise an empty SVG would 63 | // pass the test, fixes GH issue #22). 64 | const [label1, label2] = svg.querySelectorAll(".nodeLabel"); 65 | expect(label1.textContent).toEqual("Browser"); 66 | expect(label2.textContent).toEqual("Send HTTP Request"); 67 | }); 68 | 69 | test("does not pollute the global docucment body when rendering fails", async () => { 70 | const chatHTML = ` 71 |
72 |

Here's an invalid diagram:

73 |
 74 |                 
75 |
76 | mermaid 77 | 78 | 79 |
80 |
81 | 82 | type Vector2D = { 83 | x: number; 84 | y: number; 85 | }; 86 | 87 |
88 |
89 |
90 | 91 |
92 |

This flowchart illustrates a basic web request.

93 |
94 | `; 95 | 96 | // Ensure the global document has not had error content added by mermaid, 97 | // which is its default behaviour. 98 | expect(global.document.body.innerHTML).toEqual(""); 99 | const dom = new JSDOM(chatHTML); 100 | const id = "1"; 101 | const container = elementByTestId(dom.window.document, "container"); 102 | const code = dom.window.document.querySelector("code")?.textContent || ""; 103 | 104 | await renderDiagram(container, id, code); 105 | 106 | expect(global.document.body.innerHTML).toEqual(""); 107 | }); 108 | 109 | test("shows mermaidjs error content in the diagram container when rendering fails", async () => { 110 | const chatHTML = ` 111 |
112 |

Here's an invalid diagram:

113 |
114 |                 
115 |
116 | mermaid 117 | 118 | 119 |
120 |
121 | 122 | type Vector2D = { 123 | x: number; 124 | y: number; 125 | }; 126 | 127 |
128 |
129 |
130 | 131 |
132 |

This flowchart illustrates a basic web request.

133 |
134 | `; 135 | 136 | // Setup the dom, get the code block, render the diagram. 137 | const dom = new JSDOM(chatHTML); 138 | const id = "1"; 139 | const container = elementByTestId(dom.window.document, "container"); 140 | const code = dom.window.document.querySelector("code")?.textContent || ""; 141 | 142 | await renderDiagram(container, id, code); 143 | 144 | // Check for the mermaid.js error output in the container. 145 | expect(container.querySelector(".error-text")?.textContent).toMatch( 146 | /Syntax error in text/ 147 | ); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chatgpt-diagrams 2 | 3 | [![main](https://github.com/dwmkerr/chatgpt-diagrams-extension/actions/workflows/main.yaml/badge.svg)](https://github.com/dwmkerr/chatgpt-diagrams-extension/actions/workflows/main.yaml) 4 | [![codecov](https://codecov.io/gh/dwmkerr/chatgpt-diagrams-extension/branch/main/graph/badge.svg?token=6Wj5EwCVqf)](https://codecov.io/gh/dwmkerr/chatgpt-diagrams-extension) 5 | 6 | A Chrome browser extension that renders diagrams in the ChatGPT website inline: 7 | 8 | ![Demo Recording of ChatGPT Diagrams Extension](./docs/demo-recording.gif) 9 | 10 | Chrome Web Store: [Install ChatGPT Diagrams](https://chrome.google.com/webstore/detail/chatgpt-diagrams/gllophmfnbdpgfnbmbndlihdlcgohcpn) 11 | 12 | 13 | 14 | - [Quickstart](#quickstart) 15 | - [Developer Guide](#developer-guide) 16 | - [Developer Commands](#developer-commands) 17 | - [Code Structure](#code-structure) 18 | - [Mermaid.js Hacks and Notes](#mermaidjs-hacks-and-notes) 19 | - [Running the Sample Pages](#running-the-sample-pages) 20 | - [Manifest](#manifest) 21 | - [Formatting and Code Quality Rules](#formatting-and-code-quality-rules) 22 | - [Pre-Commit Hooks](#pre-commit-hooks) 23 | - [Testing](#testing) 24 | - [Debugging](#debugging) 25 | - [Reloading the Extension](#reloading-the-extension) 26 | - [Verifying Pull Requests](#verifying-pull-requests) 27 | - [Versioning](#versioning) 28 | - [Releasing](#releasing) 29 | - [Extension Screenshots](#extension-screenshots) 30 | - [Useful Resources](#useful-resources) 31 | - [Task List](#task-list) 32 | - [Version 0.2 Features](#version-02-features) 33 | - [Cosmetic Improvements](#cosmetic-improvements) 34 | - [Performance Improvements / Developer Experience](#performance-improvements--developer-experience) 35 | - [Options Page](#options-page) 36 | - [Extension Popup](#extension-popup) 37 | 38 | 39 | 40 | ## Quickstart 41 | 42 | Clone, install dependencies and build the extension: 43 | 44 | ```bash 45 | git clone git@github.com:dwmkerr/chatgpt-diagrams-extension.git 46 | npm install 47 | npm run build 48 | ``` 49 | 50 | Open [Chrome Extensions](chrome://extensions), choose 'Load Unpacked' and select the `./dist` folder. Now open https://chat.openai.com/ and enter a prompt such as: 51 | 52 | > Use mermaid.js to create a sequence diagram showing how state can be persisted for a chrome extension, and how state can be passed between files. 53 | 54 | Press the 'Show Diagram' button in the code sample to render the diagram inline: 55 | 56 | ![Screenshot of the 'Show Diagram' button and the inline diagram](./docs/demo-show-diagram.png) 57 | 58 | ## Developer Guide 59 | 60 | [Node Version Manager](https://github.com/nvm-sh/nvm) is recommended to ensure that you are using the latest long-term support version of node. 61 | 62 | Ensure you are using Node LTS, then install dependencies: 63 | 64 | ```bash 65 | nvm use --lts 66 | npm install 67 | ``` 68 | 69 | To run in local development mode, which will automatically reload when changes are made, use: 70 | 71 | ```bash 72 | npm start 73 | ``` 74 | 75 | Load the unpacked extension in your browser from the `./dist` folder. 76 | 77 | ### Developer Commands 78 | 79 | The following commands can be used to help development: 80 | 81 | | Command | Description | 82 | | -------------------------- | ------------------------------------------------------------------------------- | 83 | | `npm start` | Run in development mode. Updates `./dist` on changes to `./src`. | 84 | | `npm run build` | Build the production bundle to `./dist`. | 85 | | `npm run tsc` | Run the TypeScript compiler, verifies the correctness of the TypeScript code. | 86 | | -------------------------- | ------------------------------------------------------------------------------- | 87 | | `npm test` | Run unit tests, output coverage to `./coverage`. | 88 | | `npm run test:watch` | Run unit tests, coverage only on files that test run on, watch mode. | 89 | | `npm run test:debug` | Run unit tests, with the Chrome Inspector, initially 'break', watch mode. | 90 | | `npm run prettier` | Check formatting of all files. | 91 | | `npm run prettier:fix` | Fix formatting of all files. | 92 | | `npm run lint` | Check linting of all files. | 93 | | `npm run lint:fix` | Fix linting issues in all files. | 94 | | -------------------------- | ------------------------------------------------------------------------------- | 95 | | `make build` | Create the release package. | 96 | | `make test` | Validate the code, running `tsc` and unit tests. | 97 | 98 | ### Code Structure 99 | 100 | The code is structured in such a way that you should be able to immediately see the key files that make up the extension. 101 | 102 | At root level are the essential files that make up an extension, all other code is kept in the [`./lib`](./lib) folder. 103 | 104 | ``` 105 | manifest.json # the extension definition and metadata 106 | content.ts # the content script, runs on chatgpt browser tabs, renders the diagrams 107 | options.html # the UI for the options page 108 | options.ts # the logic for the options page 109 | setup-jest.js # utility to configure testing environment 110 | lib/ # bulk of the logic for the extension 111 | ``` 112 | 113 | ### Mermaid.js Hacks and Notes 114 | 115 | When Mermaid.js encounters an error, it attempts to render error content visually in the DOM. It is possible to provide an HTML Element that will be the container for this output. However, it does not appear to be possible to set this container to an element created with the JSDOM virtual DOM. 116 | 117 | This means that when Mermaid.js encounters rendering errors, we copy the raw HTML of the error content it writes from the global document object, and then move it into our own container - this works both in the browser and for unit tests. 118 | 119 | You can see this approach by searching for the text 'Hack' in the `./src/lib/chatgpt-dom.ts` code. There may be a better way but this managed to solve some issues like https://github.com/dwmkerr/chatgpt-diagrams-extension/issues/20 and others, quickly and without too much complexity in the tests. 120 | 121 | ### Running the Sample Pages 122 | 123 | The following command runs a local webserver, serving the content at [`./samples`](./samples). This makes it easy to test locally, without internet connectivity and without having to regularly log into ChatGPT: 124 | 125 | ```bash 126 | make serve-samples 127 | ``` 128 | 129 | The sample page is served at `http://localhost:3000`. 130 | 131 | ### Manifest 132 | 133 | Note that the `version` field is omitted from [`manifest.json`](./src/manifest.json). The version in the manifest file is set to the current value in the [`package.json`](package.json) file as part of the build process. 134 | 135 | ### Formatting and Code Quality Rules 136 | 137 | [Prettier](https://prettier.io/) is used for formatting. Pre-commit hooks are used to enforce code style. 138 | 139 | [ESLint](https://eslint.org/) is used for code-quality checks and rules. 140 | 141 | To understand why both are used, check ["Prettier vs Linters"](https://prettier.io/docs/en/comparison.html). 142 | 143 | ### Pre-Commit Hooks 144 | 145 | [Husky](https://github.com/typicode/husky) and [lint-staged](https://github.com/okonet/lint-staged) are used to run formatting and code quality checks on staged changes before they are committed. 146 | 147 | The configuration for lint-staged is in the [`package.json`](./package.json) file. 148 | 149 | ### Testing 150 | 151 | [Jest](https://jestjs.io/) is used as the testing framework. ChatGPT sample pages are loaded into the environment using [js-dom](https://github.com/jsdom/jsdom) and we then verify that the ChatGPT code elements are identified and processed correctly. 152 | 153 | Check the [Developer Commands](#developer-commands) section to see the various test commands that can be run. It is possible to watch tests, run tests in the debugger, and so on. 154 | 155 | ### Debugging 156 | 157 | In development mode, open source maps by navigating to the "Sources > Content Scripts > chatgpt-diagrams" folder. These are inline source maps. You can also use "Command + P" and search for a file such as `content.ts`. 158 | 159 | In production mode, source maps are generated as standalone files in the `./dist` folder. 160 | 161 | ### Reloading the Extension 162 | 163 | There is no 'live reload' on file changes. The fastest way to reload is to run locally with `npm start`. Webpack will rebuild the extension on file changes. Then just press the "Refresh" button in the `chrome://extensions` page and reload the site you are debugging. 164 | 165 | ### Verifying Pull Requests 166 | 167 | To verify that the code builds, the tests pass and the release package can be created run the commands below: 168 | 169 | ```bash 170 | make build 171 | make test 172 | make release 173 | ``` 174 | 175 | These commands will be executed for pull requests. 176 | 177 | ## Versioning 178 | 179 | The version of the extension is defined in the [`package.json`](./package.json) file. 180 | 181 | Releasing in managed via [Release Please](https://github.com/googleapis/release-please) in the [`main.yaml`](./.github/workflows/main.yaml) workflow file. 182 | 183 | If you need to manually trigger a release, run: 184 | 185 | ```bash 186 | git commit --allow-empty -m "chore: release 2.0.0" -m "Release-As: 2.0.0" 187 | ``` 188 | 189 | ## Releasing 190 | 191 | When uploading a new version, follow the steps below. 192 | 193 | ### Extension Screenshots 194 | 195 | If needed, update the screenshots. Screenshots should be 1280x800 pixels, set this in the Developer Tools (which can also be used to capture the screenshot to the Downloads folder. 196 | 197 | Currently screenshots do not include a browser frame. 198 | 199 | Screenshots do not have the ChatGPT sidebar, avoiding distractions. 200 | 201 | Screenshots after the first one do not have the code sample, avoiding distractions. 202 | 203 | Open Developer Tools, use the 'device size' button to set the responsive screen size, adjust the size to 1280x800, delete the sidebar from the nodes view, press Command+Shift+P and select 'Capture Screenshot'. 204 | 205 | Prompts for screenshots so far are: 206 | 207 | 1. Render a flowchart showing how a browser makes a web request and a server responds. Use mermaid.js. 208 | 2. Create a UML class diagram showing relationships for the data model for a simple food delivery database. Use mermaid.js. 209 | 3. Create an architecture diagram that would show the key components in an instant messaging application, use mermaidjs. 210 | 4. Create a sequence diagram showing how retry logic with retry queues is typically implemented when using Apache Kafka, use mermaidjs for the diagram 211 | 212 | Resize screenshots with: 213 | 214 | ```bash 215 | brew install imagemagick 216 | 217 | new_width=1280 218 | for input in ./docs/screenshots/*.png; do 219 | [[ -f "$input" ]] || continue 220 | output="${input/\.png/-${new_width}.png}" 221 | echo "Convert: ${input} -> ${output}" 222 | convert "${input}" -resize "${new_width}x" "${output}" 223 | done 224 | ``` 225 | 226 | ## Useful Resources 227 | 228 | https://joshisa.ninja/2021/10/22/browser-extension-with-rollup-omnibox-chatter.html 229 | 230 | ## Task List 231 | 232 | A quick-and-dirty list of improvements and next steps: 233 | 234 | - [ ] bug: button is inserted multiple times while chatgpt is writing (add the class to the dom element _before_ start processing? note that the code language text (e.g. 'mermaid') is overwritten 235 | - [x] build: test to ensure that mermaid doesn't add error content - or if it does that we at least control it better. 236 | - [x] improvement: render DOM using this method: https://crxjs.dev/vite-plugin/getting-started/vanilla-js/content-script-hmr#vite-hmr-for-javascript 237 | - [x] build: slow bundling, debugging fails: https://github.com/dwmkerr/chatgpt-diagrams-extension/issues/10 238 | - [x] bug: debugger doesn't work on chrome, seems to be a sourcemaps issue (raised as https://github.com/crxjs/chrome-extension-tools/issues/691) 239 | - [x] build: basic test for DOM manipulation 240 | - [x] build: coverage badge 241 | - [x] build: eslint for code quality rules 242 | - [x] build: pipeline to create package 243 | - [x] build: prettier for formatting 244 | - [x] build: release please 245 | - [x] build: resolve test issues https://github.com/dwmkerr/chatgpt-diagrams-extension/issues/6 246 | - [x] build: tests 247 | - [x] create a much more representative sample page, use the examples from the description, no sidebar, use as a fixture for tests, update queries to use selectors to find elements. 248 | - [x] docs: better icon - just a simple 50/50 split of the two logos down the middle, or diagonal 249 | - [x] docs: table of local commands 250 | - [x] refactor: change xpath queries to query selectors, add tests, fixtures 251 | - [x] testing: better sample that doesn't have sidebar and includes more representative group of diagrams 252 | - [x] build: commitlint 253 | 254 | ### Version 0.2 Features 255 | 256 | - [ ] bug: tab container is not initially hidden 257 | - [ ] refactor: hide 'options' page 258 | - [ ] feat: 'copy' button for diagrams 259 | - [ ] feat: Lightbox for diagrams 260 | - [ ] feat: more descriptive error messages and improved error presentation 261 | - [ ] build: auto deploy to chrome webstore: https://jam.dev/blog/automating-chrome-extension-publishing/ 262 | - [ ] feat: share feedback link (GH issue, or Chrome Web Store, grab some content from the DOM?) 263 | - [ ] feat: hints on the error screen (e.g. "Did you include "use mermaid.js" in the text") 264 | - [ ] chore: issue template (prompt text, code output, screenshot) 265 | 266 | ### Cosmetic Improvements 267 | 268 | - [ ] improvement: icon for 'show diagram' button 269 | 270 | ### Performance Improvements / Developer Experience 271 | 272 | - [ ] consider webpack dev server to serve sample page in local dev mode 273 | - [ ] build: Create script to open a new chrome window, with the appropriate command line flags to load the dist unpacked 274 | - [ ] feat: start/stop/pause buttons 275 | - [ ] improvement: use the mutation observer (see ./src/observe.js) to watch for new code samples, rather than scanning the DOM on a timer 276 | 277 | ### Options Page 278 | 279 | - [ ] feat: options based, based on popup code extracted from options script 280 | - [ ] check options UI works in extension screen as well as inline in tab 281 | - [ ] feat: edit xpath queries via options page 282 | - [ ] refactor: MD5 diagram text, use as a key for diagrams in a background page so that we don't recreate each time 283 | - [ ] refactor: move rendering logic to background page (so error content is hidden in tabs) 284 | 285 | ### Extension Popup 286 | 287 | - [ ] feat: counter for extension icon that shows number of diagrams processed 288 | 289 | ### Other 290 | 291 | - [] fix: review the fix to the mermaidjs issue and see if it helps with bundling/packaging etc https://github.com/parcel-bundler/parcel/issues/8935#event-14676821933 292 | --------------------------------------------------------------------------------