├── .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 |
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 |
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/