├── .gitignore ├── LICENSE ├── README.md ├── build-manifests.js ├── content.js ├── icons ├── icon128.png ├── icon16.png ├── icon256.png └── icon48.png ├── manifest-base.json ├── media ├── badgeFeature.png ├── darkThemeFeature.png ├── demo.gif ├── hero.png ├── marqueePromoTile.png └── smallPromoTile.png ├── package.json └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | build/ 3 | 4 | # IDE and editor files 5 | .vscode/ 6 | .idea/ 7 | *.swp 8 | *.swo 9 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 TOPUS SOFTWARE SL 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Conventional Comments - by Pullpo 2 | 3 |

4 | Conventional Comments Logo 5 |

6 | 7 | > A browser extension that enhances code reviews by implementing the Conventional Comments standard directly in GitHub and GitLab interfaces. 8 | 9 |

10 | Conventional Comments Demo 11 |

12 | 13 | ## 🎯 The Problem 14 | 15 | Code reviews are crucial for maintaining code quality, but they often suffer from: 16 | - Ambiguous or unclear feedback 17 | - Misunderstandings about comment severity 18 | - Difficulty in parsing and tracking different types of feedback 19 | - Inconsistent commenting styles across team members 20 | 21 | ## 💡 The Solution 22 | 23 | *Conventional Comments - by Pullpo* brings the power of [Conventional Comments](https://conventionalcomments.org/) directly into your GitHub and GitLab workflow. It adds a sleek, intuitive toolbar to every comment box, making it easy to: 24 | 25 | - Add standardized labels to your comments (praise, suggestion, issue, etc.) 26 | - Include decorators for additional context (non-blocking, blocking, if-minor) 27 | - Maintain consistent formatting across all review comments 28 | - Toggle between plain text and badge-style formatting 29 | 30 | ## ✨ Features 31 | 32 | - 🎨 **Intuitive Toolbar**: Seamlessly integrated into GitHub and GitLab interfaces 33 | - 🏷️ **Standard Labels**: 34 | - `praise`: Highlight something positive 35 | - `nitpick`: Minor, non-blocking issues 36 | - `suggestion`: Suggest specific improvements 37 | - `issue`: Point out blocking problems 38 | - `question`: Ask for clarification 39 | - `thought`: Share a reflection or idea 40 | - `chore`: Request minor, non-code tasks 41 | - 🎯 **Decorations**: 42 | - `(non-blocking)`: Optional changes 43 | - `(blocking)`: Must be addressed 44 | - `(if-minor)`: Address if the effort is small 45 | - 🔄 **Toggle Functionality**: Easily remove labels or decorations 46 | - 🎨 **Badge Style Option**: Switch between text and visual badge formats 47 | - 🌓 **Dark Mode Support**: Seamlessly works with both GitHub and GitLab themes 48 | 49 | ## 📥 Installation 50 | 51 | ### Chrome 52 | 1. Visit the [Chrome Web Store](https://chromewebstore.google.com/detail/gelgbjildgbbfgfgpibgcnolcipinmlp?utm_source=github_readme) 53 | 2. Click "Add to Chrome" 54 | 3. The extension will automatically activate on GitHub.com and GitLab.com 55 | 56 | ### Firefox 57 | 1. Visit the [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/conventional-comments-pullpo/) 58 | 2. Click "Add to Firefox" 59 | 3. The extension will automatically activate on GitHub.com and GitLab.com 60 | 61 | ## 🚀 Usage 62 | 63 | 1. Navigate to any pull request or merge request on GitHub or GitLab 64 | 2. Click on the comment box 65 | 3. Use the toolbar that appears above the comment box: 66 | - Select a label type (e.g., "suggestion", "issue") 67 | - Optionally add a decoration 68 | - Write your comment 69 | 4. Your comment will be automatically formatted according to the Conventional Comments standard 70 | 71 | ## 🤝 Contributing 72 | 73 | Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. 74 | 75 | 1. Fork the repository 76 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 77 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 78 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 79 | 5. Open a Pull Request 80 | 81 | ## 🛠️ Building from Source 82 | 83 | The extension can be built for both Chrome and Firefox using our build system: 84 | 85 | 1. Clone the repository and install dependencies: 86 | ```bash 87 | git clone https://github.com/pullpo/conventional-comments-helper.git 88 | cd conventional-comments-helper 89 | npm install 90 | ``` 91 | 92 | 2. Build for your target browser: 93 | - For Chrome: 94 | ```bash 95 | npm run build:chrome 96 | ``` 97 | - For Firefox: 98 | ```bash 99 | npm run build:firefox 100 | ``` 101 | - For both browsers: 102 | ```bash 103 | npm run build 104 | ``` 105 | 106 | 3. Load the extension: 107 | - Chrome: Load the `build/chrome` directory as an unpacked extension 108 | - Firefox: Load the `build/firefox` directory as a temporary add-on 109 | 110 | 111 | ## 📄 License 112 | 113 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 114 | 115 | ## 🙏 Acknowledgments 116 | 117 | - Inspired by [conventionalcomments.org](https://conventionalcomments.org/) 118 | - Built with love for the open source community 119 | -------------------------------------------------------------------------------- /build-manifests.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | // Function to ensure directory exists 5 | function ensureDirectoryExists(dirPath) { 6 | if (!fs.existsSync(dirPath)) { 7 | fs.mkdirSync(dirPath, { recursive: true }); 8 | } 9 | } 10 | 11 | // Function to copy a file 12 | function copyFile(source, target) { 13 | const targetDir = path.dirname(target); 14 | ensureDirectoryExists(targetDir); 15 | fs.copyFileSync(source, target); 16 | } 17 | 18 | // Function to build Chrome extension 19 | function buildChrome() { 20 | const chromeDir = path.join(__dirname, 'build', 'chrome'); 21 | ensureDirectoryExists(chromeDir); 22 | 23 | // Create Chrome manifest 24 | const baseManifest = JSON.parse(fs.readFileSync('./manifest-base.json', 'utf8')); 25 | fs.writeFileSync( 26 | path.join(chromeDir, 'manifest.json'), 27 | JSON.stringify(baseManifest, null, 2) 28 | ); 29 | 30 | // Copy required files 31 | copyFile('./content.js', path.join(chromeDir, 'content.js')); 32 | copyFile('./style.css', path.join(chromeDir, 'style.css')); 33 | 34 | // Copy icons 35 | const iconsDir = path.join(chromeDir, 'icons'); 36 | ensureDirectoryExists(iconsDir); 37 | copyFile('./icons/icon16.png', path.join(iconsDir, 'icon16.png')); 38 | copyFile('./icons/icon48.png', path.join(iconsDir, 'icon48.png')); 39 | copyFile('./icons/icon128.png', path.join(iconsDir, 'icon128.png')); 40 | 41 | console.log('Chrome build completed successfully!'); 42 | } 43 | 44 | // Function to build Firefox extension 45 | function buildFirefox() { 46 | const firefoxDir = path.join(__dirname, 'build', 'firefox'); 47 | ensureDirectoryExists(firefoxDir); 48 | 49 | // Create Firefox manifest 50 | const baseManifest = JSON.parse(fs.readFileSync('./manifest-base.json', 'utf8')); 51 | const firefoxManifest = { 52 | ...baseManifest, 53 | browser_specific_settings: { 54 | gecko: { 55 | id: "conventional-comments-addon@pullpo.io" 56 | } 57 | } 58 | }; 59 | 60 | fs.writeFileSync( 61 | path.join(firefoxDir, 'manifest.json'), 62 | JSON.stringify(firefoxManifest, null, 2) 63 | ); 64 | 65 | // Copy required files 66 | copyFile('./content.js', path.join(firefoxDir, 'content.js')); 67 | copyFile('./style.css', path.join(firefoxDir, 'style.css')); 68 | 69 | // Copy icons 70 | const iconsDir = path.join(firefoxDir, 'icons'); 71 | ensureDirectoryExists(iconsDir); 72 | copyFile('./icons/icon16.png', path.join(iconsDir, 'icon16.png')); 73 | copyFile('./icons/icon48.png', path.join(iconsDir, 'icon48.png')); 74 | copyFile('./icons/icon128.png', path.join(iconsDir, 'icon128.png')); 75 | 76 | console.log('Firefox build completed successfully!'); 77 | } 78 | 79 | // Get the build type from command line arguments 80 | const buildType = process.argv[2]; 81 | 82 | if (buildType === 'chrome') { 83 | buildChrome(); 84 | } else if (buildType === 'firefox') { 85 | buildFirefox(); 86 | } else { 87 | console.error('Please specify either "chrome" or "firefox" as an argument'); 88 | process.exit(1); 89 | } -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | // --- Selector Constants --- 2 | 3 | const TOOLBAR_ID_PREFIX = 'conventional-comments-toolbar-'; // Use prefix for uniqueness 4 | const TOOLBAR_MARKER_CLASS = 'cc-toolbar-added'; 5 | const SETTINGS_BUTTON_ID_PREFIX = 'cc-settings-button-'; // Prefix for settings button IDs 6 | 7 | // --- Global Selector for Textareas --- 8 | 9 | // Selectors for GitHub textareas where the toolbar should appear 10 | const TARGET_TEXTAREA_SELECTORS = [ 11 | // GitHub selectors 12 | 'textarea[name="comment[body]"]', // Standard issue/PR comments 13 | 'textarea[name="issue_comment[body]"]', // Editing existing PR issue comments 14 | 'textarea[name="pull_request_review_comment[body]"]', // Editing existing PR review line comments 15 | 'textarea[name="pull_request_review[body]"]', // Review Changes modal/popup form 16 | // GitLab selectors 17 | 'textarea[name="note[note]"]', // MR discussions (incl. line comments) 18 | 'textarea[name="work-item-add-or-edit-comment"]' // Issue discussions (incl. new descriptions & comments) 19 | ]; 20 | 21 | // Combine selectors with :not(.cc-toolbar-added) for querying unprocessed textareas 22 | const UNPROCESSED_TEXTAREA_QUERY = TARGET_TEXTAREA_SELECTORS.map( 23 | sel => `${sel}:not(.${TOOLBAR_MARKER_CLASS})` 24 | ).join(', '); 25 | 26 | // Selector matching any target textarea 27 | const ANY_TARGET_TEXTAREA_QUERY = TARGET_TEXTAREA_SELECTORS.join(', '); 28 | 29 | // --- Components of a Conventional Comment --- 30 | 31 | const LABELS = [ 32 | { label: 'praise', desc: 'Highlight something positive.', color: '#28A745' }, // Green - Standard for success/positive feedback 33 | { label: 'nitpick', desc: 'Minor, non-blocking issues (style, naming...).', color: '#F59E0B' }, // Amber/Dark Yellow - Suggests caution, minor warning 34 | { label: 'suggestion', desc: 'Suggest specific improvements.', color: '#3B82F6' }, // Blue - Often used for informational/suggestions 35 | { label: 'issue', desc: 'Point out a blocking problem.', color: '#EF4444' }, // Red - Standard for errors/critical problems 36 | { label: 'question', desc: 'Ask for clarification.', color: '#8B5CF6' }, // Violet/Purple - Distinct color often used for queries/info 37 | { label: 'thought', desc: 'Share a reflection or idea.', color: '#6B7280' }, // Cool Gray - Neutral, less prominent, for reflections 38 | { label: 'chore', desc: 'Request a minor, non-code task.', color: '#F97316' }, // Orange - Action-oriented but distinct from critical red/amber 39 | ]; 40 | 41 | const DECORATIONS = [ 42 | { label: 'non-blocking', desc: 'Optional change, doesn\'t block merge.', color: '#9CA3AF' }, // Light/Medium Gray - Indicates reduced severity/optionality 43 | { label: 'blocking', desc: 'Must be addressed before merge.', color: '#374151' }, // Dark Gray/Charcoal - Serious, indicates high importance/blocker 44 | { label: 'if-minor', desc: 'Address if the effort is small.', color: '#14B8A6' } // Teal - Represents conditionality, distinct suggestion tone 45 | ]; 46 | 47 | // --- Selector for formatted Conventional Comments --- 48 | 49 | const PLAIN_CC_REGEX = /^\s*(?:(praise|nitpick|suggestion|issue|question|thought|chore)\s*(?:\((non-blocking|blocking|if-minor)\))?:)\s*/; 50 | const BADGE_CC_REGEX = /^\s*\[\!\[(?:(praise|nitpick|suggestion|issue|question|thought|chore)(?:\((non-blocking|blocking|if-minor)\))?)\]\(https?:\/\/img\.shields\.io\/badge\/.*?\)\]\(https?:\/\/pullpo\.io\/cc\?.*?\)\s*/; 51 | 52 | // --- Global Couters --- 53 | 54 | let toolbarCounter = 0; // Ensure unique IDs if multiple textareas load simultaneously 55 | let settingsCounter = 0; // Unique IDs for settings elements 56 | 57 | // --- Badge Helpers --- 58 | 59 | // Helper function for badge colors using hex values 60 | function getBadgeColor(type) { 61 | // Find the label object 62 | const label = LABELS.find(l => l.label === type); 63 | // Return the color if found, otherwise default to gray 64 | return label ? label.color.substring(1) : '6B7280'; // Remove # from hex color for shields.io 65 | } 66 | 67 | // Helper function to create badge markdown 68 | function createBadgeMarkdown(type, decoration) { 69 | // Get the label color (without #) 70 | const labelColor = getBadgeColor(type); 71 | let label = type; 72 | let message = decoration || ''; // Use decoration as message if present 73 | let decorationColor = ''; 74 | 75 | // Get decoration color if a decoration is specified 76 | if (decoration) { 77 | const decorObj = DECORATIONS.find(d => d.label === decoration); 78 | if (decorObj) { 79 | decorationColor = decorObj.color.substring(1); // Remove # from hex color 80 | } 81 | } 82 | 83 | let badgeUrl; 84 | 85 | // Simple URL encoding for badge parts 86 | const encode = (str) => encodeURIComponent(str.replace(/-/g, '--').replace(/_/g, '__')); // Shields.io escaping 87 | 88 | if (message) { 89 | // If we have both a label and decoration with their colors 90 | if (decorationColor) { 91 | // Format: https://img.shields.io/badge/