├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── release.yml │ └── secrets-scan.yml ├── .gitignore ├── .prettierrc.json ├── .whitesource ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASE.md ├── dev └── index.js ├── jest.config.js ├── jest.setup.ts ├── package.json ├── scripts └── release.js ├── src ├── components │ ├── GherkinEditor │ │ ├── index.jsx │ │ └── styled.js │ └── Toolbar │ │ ├── index.tsx │ │ └── styled.ts ├── index.ts ├── lib │ ├── gherkin-languages │ │ └── index.ts │ └── gherkin-linter │ │ └── index.ts ├── modules │ ├── dialects │ │ ├── gherkin_background_i18n.js │ │ ├── gherkin_i18n.js │ │ └── gherkin_scenario_i18n.js │ ├── gherkin-annotator │ │ ├── __mocks__ │ │ │ └── index.ts │ │ └── index.ts │ ├── keyword-completer │ │ └── index.js │ ├── mode │ │ ├── gherkin_background_i18n.js │ │ ├── gherkin_i18n.js │ │ └── gherkin_scenario_i18n.js │ └── step-completer │ │ └── index.js └── themes │ ├── cucumber.js │ └── jira.js ├── test ├── components │ ├── GherkinEditor │ │ └── index.spec.js │ └── Toolbar │ │ └── index.spec.tsx ├── lib │ └── gherkin-linter │ │ └── index.spec.js └── modules │ ├── gherkin-annotator │ └── index.spec.ts │ ├── keyword-completer │ └── index.spec.js │ └── step-completer │ └── index.spec.js ├── tsconfig.build.json ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true 5 | }, 6 | settings: { 7 | react: { 8 | version: 'detect' 9 | } 10 | }, 11 | extends: ['standard', 'plugin:react/recommended', 'plugin:jest/recommended'], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true 16 | }, 17 | ecmaVersion: 2018, 18 | sourceType: 'module' 19 | }, 20 | plugins: ['react', 'jest'], 21 | rules: { 22 | 'no-use-before-define': 'off', 23 | 'arrow-parens': ['error', 'as-needed'], 24 | camelcase: 'warn', 25 | 'id-length': ['error', { exceptions: ['i', 'v'] }], 26 | 'jest/expect-expect': [ 27 | 'error', 28 | { 29 | assertFunctionNames: ['expect', '*.expect'] 30 | } 31 | ], 32 | 'jsx-quotes': ['error', 'prefer-single'], 33 | 'multiline-ternary': 'off', 34 | 'no-case-declarations': 'off', 35 | 'prefer-const': [ 36 | 'error', 37 | { 38 | destructuring: 'any', 39 | ignoreReadBeforeAssign: false 40 | } 41 | ], 42 | 'react/display-name': 'off', 43 | 'react/jsx-closing-bracket-location': [ 44 | 'error', 45 | { 46 | nonEmpty: 'line-aligned', 47 | selfClosing: 'line-aligned' 48 | } 49 | ], 50 | 'react/jsx-closing-tag-location': 'error', 51 | 'react/jsx-curly-brace-presence': ['error', 'never'], 52 | 'react/jsx-curly-spacing': [ 53 | 'error', 54 | { 55 | when: 'never', 56 | attributes: true, 57 | children: true 58 | } 59 | ], 60 | 'react/jsx-equals-spacing': 'error', 61 | 'react/jsx-indent': ['error', 2], 62 | 'react/jsx-indent-props': ['error', 2], 63 | 'react/jsx-no-useless-fragment': 'error', 64 | 'react/jsx-tag-spacing': [ 65 | 'error', 66 | { 67 | closingSlash: 'never', 68 | beforeSelfClosing: 'always', 69 | afterOpening: 'never', 70 | beforeClosing: 'never' 71 | } 72 | ], 73 | 'react/prop-types': 'off', 74 | 'react/self-closing-comp': [ 75 | 'error', 76 | { 77 | component: true, 78 | html: true 79 | } 80 | ], 81 | 'space-before-function-paren': 'off' 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: npm 6 | directory: '/' 7 | schedule: 8 | interval: 'daily' 9 | time: '08:00' 10 | timezone: 'Europe/Paris' 11 | open-pull-requests-limit: 100 12 | versioning-strategy: increase 13 | - package-ecosystem: github-actions 14 | directory: '/' 15 | schedule: 16 | interval: 'daily' 17 | time: '08:00' 18 | timezone: 'Europe/Paris' 19 | open-pull-requests-limit: 100 20 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Auto Merge 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - reopened 8 | - synchronize 9 | 10 | jobs: 11 | auto-merge: 12 | name: Auto merge 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Use auto merge action 20 | uses: ahmadnassri/action-dependabot-auto-merge@v2 21 | with: 22 | target: minor 23 | github-token: ${{ secrets.PAT }} 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Use Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: '14.x' 17 | 18 | - name: Cache node modules 19 | uses: actions/cache@v4 20 | with: 21 | path: node_modules 22 | key: ${{ runner.os }}-cache-${{ hashFiles('**/yarn.lock') }} 23 | 24 | - name: Run yarn 25 | run: yarn --frozen-lockfile 26 | 27 | - name: Run yarn lint 28 | run: yarn lint 29 | 30 | - name: Run yarn test 31 | run: yarn test 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: 'CodeQL' 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: '30 7 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: ['javascript'] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 68 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | workflow_dispatch: 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Node.js to publish to npm 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: '14.x' 21 | registry-url: 'https://registry.npmjs.com' 22 | 23 | - name: Cache node modules 24 | uses: actions/cache@v4 25 | with: 26 | path: node_modules 27 | key: ${{ runner.os }}-cache-${{ hashFiles('**/yarn.lock') }} 28 | 29 | - name: Install dependencies 30 | run: yarn 31 | 32 | - name: Run tests 33 | run: | 34 | yarn lint 35 | yarn test 36 | 37 | - name: Get package version 38 | id: package-version 39 | uses: martinbeentjes/npm-get-version-action@master 40 | 41 | - name: Get changelog 42 | id: changelog 43 | uses: mindsers/changelog-reader-action@v2 44 | with: 45 | version: ${{ steps.package-version.outputs.current-version }} 46 | 47 | - name: Publish npm package 48 | run: yarn publish 49 | env: 50 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 51 | 52 | - name: Setup Node.js to publish to GitHub Packages 53 | uses: actions/setup-node@v4 54 | with: 55 | node-version: '14.x' 56 | registry-url: 'https://npm.pkg.github.com' 57 | scope: '@smartbear' 58 | 59 | - name: Create GitHub release 60 | uses: actions/create-release@v1.1.4 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | with: 64 | tag_name: v${{ steps.package-version.outputs.current-version }} 65 | release_name: Release ${{ steps.package-version.outputs.current-version }} 66 | body: ${{ steps.changelog.outputs.changes }} 67 | 68 | - name: Publish GitHub package 69 | run: yarn publish 70 | env: 71 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | -------------------------------------------------------------------------------- /.github/workflows/secrets-scan.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Secrets Scan 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: 10 | - master 11 | - release/* 12 | pull_request: 13 | branches: [master] 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | 18 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 19 | jobs: 20 | # This workflow contains a single job called "build" 21 | scan: 22 | # The type of runner that the job will run on 23 | runs-on: ubuntu-latest 24 | 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - uses: actions/checkout@v4 29 | 30 | - name: Gitleaks 31 | # You may pin to the exact commit or the version. 32 | # uses: zricethezav/gitleaks-action@8b6cb34fcc59c3497958ce5a6d03675682029ef7 33 | uses: zricethezav/gitleaks-action@v1.6.0 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Build output 19 | dist/ 20 | 21 | # Dependency directories 22 | node_modules/ 23 | 24 | # Optional npm cache directory 25 | .npm 26 | 27 | # Optional eslint cache 28 | .eslintcache 29 | 30 | # Output of 'npm pack' 31 | *.tgz 32 | 33 | # Yarn Integrity file 34 | .yarn-integrity 35 | 36 | # dotenv environment variables file 37 | .env 38 | .env.test 39 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "endOfLine": "lf", 5 | "htmlWhitespaceSensitivity": "css", 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": true, 8 | "overrides": [ 9 | { 10 | "files": "*.json", 11 | "options": { 12 | "printWidth": 200 13 | } 14 | } 15 | ], 16 | "printWidth": 80, 17 | "proseWrap": "preserve", 18 | "requirePragma": false, 19 | "semi": false, 20 | "singleQuote": true, 21 | "tabWidth": 2, 22 | "trailingComma": "none", 23 | "useTabs": false 24 | } 25 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "settingsInheritedFrom": "SmartBear/whitesource-config@main", 3 | "checkRunSettings": { 4 | "vulnerableCheckRunConclusionLevel": "success", 5 | "displayMode": "diff" 6 | }, 7 | "issueSettings": { 8 | "minSeverityLevel": "MEDIUM" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [2.4.14] 6 | 7 | - Upgrade dependencies 8 | 9 | ## [2.4.13] 10 | 11 | - Add support for React 18 12 | 13 | ## [2.4.12] 14 | 15 | - Fix lodash import 16 | - Upgrade dependencies 17 | 18 | ## [2.4.11] 19 | 20 | - Upgrade dependencies 21 | 22 | ## [2.4.10] 23 | 24 | - Setup a new package release process that will also release on GitHub Packages 25 | 26 | ## [2.4.9] 27 | 28 | Build package using `tsc` instead of `babel`. 29 | Also use ts-jest to run tests. 30 | 31 | ## [2.4.8] 32 | 33 | ## [2.4.7] 34 | 35 | Add `onParse` property that exposes a callback used to rely on the internal Gherkin parser runs. 36 | 37 | ## [2.4.3] 38 | 39 | Remove window from codebase. 40 | 41 | ## [2.4.2] 42 | 43 | Update readme. 44 | 45 | ## [2.4.1] 46 | 47 | Reduce the maximum width of Gherkin linter annotations. 48 | 49 | ## [2.4.0] 50 | 51 | - Change the package structure from a single file generated using webpack to a `lib` folder generated using babel. 52 | - Upgrade dependencies 53 | 54 | ## [2.3.2] 55 | 56 | Fix linter was not triggered properly sometimes when the component was rerendered 57 | 58 | ## [2.3.1] 59 | 60 | Fix linting was not triggered when changing mode or locale 61 | 62 | ## [2.3.0] 63 | 64 | Add linting capabilities 65 | 66 | ## [2.2.2] 67 | 68 | + Update README informations 69 | + Fix semi-column keyword highlighting using the 'gherkin_background_i18n' mode 70 | + Rename 'c4j' theme by 'cucumber' 71 | 72 | ## [2.2.1] 73 | 74 | - Remove altaskit dependencies 75 | - Prevent overloading ace options 76 | - Improve codebase 77 | 78 | ## [2.2.0] 79 | 80 | - Add support for Gherkin Background/Scenario only highlighting 81 | - Add a `mode` props to customize the highlighting mode 82 | - GherkinEditor improvements 83 | 84 | ## [2.1.0] 85 | 86 | - Security fix 87 | - Dropped react v15 support. 88 | 89 | ## [2.0.7] 90 | 91 | Fix loading of the gherkin_i18n mode 92 | 93 | ## [2.0.6] 94 | 95 | - Upgrade react-ace dependency 96 | - Migrate code to use the new react-ace version 97 | - Create a new C4J theme 98 | 99 | ## [2.0.5] 100 | 101 | - Allow to hide language toolbar with 'hideToolbar' props 102 | - We can pass a callback 'onSubmit' to the editor which is triggered when pressing Ctrl+Enter in the editor 103 | 104 | ## [2.0.4] 105 | 106 | - Fix language selector when the language prop is changed from the GherkinEditor component 107 | - Disable language selector when readOnly prop is set 108 | 109 | ## [2.0.3] 110 | 111 | Editor can be resized horizontally 112 | 113 | ## [2.0.2] 114 | 115 | Fix indentation in I18n context 116 | 117 | ## [2.0.1] 118 | 119 | Breaking changes: 120 | - OnValueChange => onChange 121 | 122 | Features: 123 | - Added I18n for gherkin syntax & keyword autocompletions 124 | 125 | ## [0.1.0] 126 | 127 | Initial release 128 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SmartBear Software 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 | # React Gherkin Editor 2 | 3 | A Gherkin language editor for React. 4 | 5 | [![Version][version-badge]][package] 6 | [![CI][ci-badge]][ci] 7 | [![Dependencies][dependencies-badge]][dependencies] 8 | [![License][license-badge]][license] 9 | 10 | ## Introduction 11 | 12 | React Gherkin Editor is a wrapper arround [React Ace Editor](https://github.com/securingsincity/react-ace) specially designed for Gherkin. 13 | 14 | Features: 15 | 16 | - Gherkin syntax highlighting 17 | - Gherkin keyword snippets 18 | - Easy to provide step autocompletions (Promise based) 19 | - Gherkin I18n support 20 | - Special themes for Jira and Cucumber 21 | 22 | ## Installation 23 | 24 | Using npm: 25 | 26 | ``` 27 | npm install --save @smartbear/react-gherkin-editor 28 | ``` 29 | 30 | Using yarn: 31 | 32 | ``` 33 | yarn add @smartbear/react-gherkin-editor 34 | ``` 35 | 36 | ## Basic Usage 37 | 38 | ```javascript 39 | import React from 'react' 40 | import { render } from 'react-dom' 41 | import ReactGherkinEditor from '@smartbear/react-gherkin-editor' 42 | 43 | const root = document.createElement('div') 44 | document.body.appendChild(root) 45 | 46 | const initialValue = `Feature: Serve coffee 47 | As a coffee lover 48 | I can get coffee from the machine 49 | So I can enjoy the rest of the day 50 | 51 | Scenario: Simple use 52 | # Well, sometimes, you just get a coffee. 53 | Given the coffee machine is started 54 | When I take a coffee 55 | Then coffee should be served 56 | And message "Please take your coffee" should be printed` 57 | 58 | const steps = [ 59 | 'I start the coffee machine using language "lang"', 60 | 'I shutdown the coffee machine', 61 | 'message "message" should be displayed' 62 | ] 63 | 64 | const onValueChange = console.log 65 | 66 | const autoCompleteFunction = async (_keyword, text) => { 67 | const matches = steps.filter(step => step.startsWith(text)) 68 | 69 | const completions = matches.map(match => ({ 70 | caption: match, 71 | value: match, 72 | score: Math.floor(Math.random() * Math.floor(100)), 73 | meta: 'Step' 74 | })) 75 | 76 | return completions 77 | } 78 | 79 | render( 80 | , 87 | root 88 | ) 89 | ``` 90 | 91 | ## Ace Documentation 92 | 93 | [React Ace Editor](https://github.com/securingsincity/react-ace) 94 | 95 | [version-badge]: https://img.shields.io/npm/v/@smartbear/react-gherkin-editor 96 | [package]: https://www.npmjs.com/package/@smartbear/react-gherkin-editor 97 | [ci-badge]: https://img.shields.io/github/workflow/status/smartbear/react-gherkin-editor/CI?logo=github 98 | [ci]: https://github.com/SmartBear/react-gherkin-editor/actions?query=workflow%3ACI 99 | [dependencies-badge]: https://img.shields.io/david/smartbear/react-gherkin-editor 100 | [dependencies]: https://david-dm.org/smartbear/react-gherkin-editor 101 | [license-badge]: https://img.shields.io/npm/l/@smartbear/react-gherkin-editor 102 | [license]: https://github.com/SmartBear/react-gherkin-editor/blob/master/LICENSE 103 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # How to release a new version 2 | 3 | ## Keep a changelog 4 | 5 | As you add features or fix bugs, keep the `CHANGELOG.md` file up to date. 6 | Add any new information in the **Unreleased** section. 7 | 8 | **Do not put the new information in a new section with your future version number.** 9 | The release script will take care of that. 10 | 11 | If you have pending changes to the changelog file, no need to commit it, the release script will include these changes. 12 | 13 | ## Release a new version 14 | 15 | - Ensure the `CHANGELOG.md` file has the new content in the **Unreleased** section, **these will become the release notes** 16 | - Run `yarn release` to create a new version with the correct release type: `yarn release --major`, `yarn release --minor` or `yarn release --patch` 17 | - If the operation is a success, a new commit and tag will be created and pushed for the new version 18 | 19 | That's it. A GitHub action will make the GitHub release and publish automatically. 20 | 21 | ## Release process information 22 | 23 | The release process is automated using a release script then a GitHub action: 24 | - The `yarn release` script will: 25 | - Move content in the changelog from **Unreleased** to a new section, then add the file to the future commit 26 | - Use `yarn version` to bump the version in `package.json`, create a release commit and tag 27 | - Use `git push --follow-tags` to push the commit 28 | - A `release` GitHub action will make the release and publish the package: 29 | - The action is triggered if a push includes a tag that starts as `v*` 30 | - It runs all tests before the release 31 | - It creates a GitHub release, settings the release notes based on the changelog 32 | - It publishes the package to both npm and GitHub Packages 33 | 34 | You can check the release script in the `scripts/release.js` file and the GitHub action in the `.github/workflows/release.yml` file to see how they work. 35 | -------------------------------------------------------------------------------- /dev/index.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import React, { useState } from 'react' 3 | import ReactDOM from 'react-dom/client' 4 | import GherkinEditor from 'components/GherkinEditor' 5 | import Select from 'react-select' 6 | 7 | const steps = [ 8 | 'I start the coffee machine using language "lang"', 9 | 'I shutdown the coffee machine', 10 | 'message "message" should be displayed', 11 | 'coffee should be served', 12 | 'coffee should not be served', 13 | 'I take a coffee', 14 | 'I empty the coffee grounds', 15 | 'I fill the beans tank', 16 | 'I fill the water tank', 17 | 'I take "coffee_number" coffees', 18 | 'the coffee machine is started', 19 | 'I handle everything except the water tank', 20 | 'I handle water tank', 21 | 'I handle beans', 22 | 'I handle coffee grounds', 23 | 'I handle everything except the beans', 24 | 'I handle everything except the grounds', 25 | 'displayed message is:', 26 | 'I switch to settings mode', 27 | 'settings should be:' 28 | ] 29 | 30 | const autoCompleteFunction = (_keyword, text) => { 31 | const matches = steps.filter(step => step.startsWith(text)) 32 | const completions = matches.map(match => ({ 33 | caption: match, 34 | value: match, 35 | score: Math.floor(Math.random() * Math.floor(100)), 36 | meta: 'Step' 37 | })) 38 | return Promise.resolve(completions) 39 | } 40 | 41 | const Preview = () => { 42 | const [theme, setTheme] = useState('jira') 43 | const [mode, setMode] = useState('gherkin_i18n') 44 | const [language, setLanguage] = useState('en') 45 | const [value, setValue] = useState(`Feature: Support internationalisation 46 | As a polyglot coffee lover 47 | I can select the language on the coffee machine 48 | So I can practice my use of greetings in several languages 49 | 50 | Scenario: No messages are displayed when machine is shut down 51 | Given the coffee machine is started 52 | When I shutdown the coffee machine 53 | Then message "Bye" should be displayed 54 | 55 | Scenario Outline: Messages are based on language 56 | # Well, sometimes, you just get a coffee. 57 | When I start the coffee machine using language 58 | Then message should be displayed 59 | 60 | Examples: 61 | | language | ready_message | 62 | | en | Ready | 63 | | fr | Pret | 64 | `) 65 | 66 | const onChange = value => { 67 | console.log(value) 68 | setValue(value) 69 | } 70 | 71 | const themeOptions = [ 72 | { label: 'jira', value: 'jira' }, 73 | { label: 'cucumber', value: 'cucumber' } 74 | ] 75 | const modeOptions = [ 76 | { label: 'gherkin i18n', value: 'gherkin_i18n' }, 77 | { label: 'gherkin background i18n', value: 'gherkin_background_i18n' }, 78 | { label: 'gherkin scenario i18n', value: 'gherkin_scenario_i18n' } 79 | ] 80 | 81 | const ToolbarContent = ( 82 |
83 | Theme 84 | ({ display: 'flex' }) 97 | }} 98 | onChange={option => setMode(option.value)} 99 | options={modeOptions} 100 | defaultValue={modeOptions[0]} 101 | /> 102 |
103 | ) 104 | 105 | return ( 106 |
107 | setLanguage(option.value)} 113 | theme={theme} 114 | mode={mode} 115 | autoCompleteFunction={autoCompleteFunction} 116 | toolbarContent={ToolbarContent} 117 | autoFocus 118 | activateLinter 119 | showGutter 120 | setOptions={{ 121 | showLineNumbers: true 122 | }} 123 | /> 124 |
125 | ) 126 | } 127 | 128 | const root = document.createElement('div') 129 | document.body.appendChild(root) 130 | 131 | ReactDOM.createRoot(root).render() 132 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/js-with-ts', 3 | setupFilesAfterEnv: ['./jest.setup.ts'], 4 | testEnvironment: 'jsdom', 5 | transformIgnorePatterns: ['node_modules/(?!escape-string-regexp|uuid/)'] 6 | } 7 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@smartbear/react-gherkin-editor", 3 | "version": "2.4.14", 4 | "description": "A Gherkin language editor for React.", 5 | "homepage": "https://github.com/SmartBear/react-gherkin-editor", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/SmartBear/react-gherkin-editor" 9 | }, 10 | "main": "./dist/index.js", 11 | "types": "./dist/index.d.ts", 12 | "author": "SmartBear Software", 13 | "license": "MIT", 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "start": "webpack serve", 19 | "pretty": "prettier --write .", 20 | "test": "jest", 21 | "lint": "concurrently yarn:eslint yarn:type-check", 22 | "eslint": "eslint --ext js,jsx,ts,tsx .", 23 | "type-check": "tsc --noEmit", 24 | "build": "tsc --project tsconfig.build.json", 25 | "release": "node scripts/release.js", 26 | "prepack": "rm -rf dist && yarn build", 27 | "postpack": "rm -rf dist", 28 | "postversion": "git push --follow-tags" 29 | }, 30 | "devDependencies": { 31 | "@testing-library/jest-dom": "^6.6.3", 32 | "@testing-library/react": "^14.3.0", 33 | "@types/jest": "^29.5.14", 34 | "@types/react": "^18.2.79", 35 | "@typescript-eslint/parser": "^5.61.0", 36 | "buffer": "^6.0.3", 37 | "concurrently": "^8.2.2", 38 | "eslint": "^8.57.1", 39 | "eslint-config-standard": "^17.1.0", 40 | "eslint-plugin-import": "^2.31.0", 41 | "eslint-plugin-jest": "^27.9.0", 42 | "eslint-plugin-n": "^15.7.0", 43 | "eslint-plugin-promise": "^6.6.0", 44 | "eslint-plugin-react": "^7.37.5", 45 | "html-webpack-plugin": "^5.6.3", 46 | "jest": "^29.7.0", 47 | "jest-environment-jsdom": "^29.7.0", 48 | "prettier": "^3.5.3", 49 | "process": "^0.11.10", 50 | "prop-types": "^15.8.1", 51 | "react": "^18.2.0", 52 | "react-dom": "^18.2.0", 53 | "react-is": "^18.3.1", 54 | "react-select-event": "^5.5.1", 55 | "semver": "^7.7.2", 56 | "stream-browserify": "^3.0.0", 57 | "styled-components": "^5.3.11", 58 | "ts-jest": "^29.2.6", 59 | "ts-loader": "^9.5.2", 60 | "typescript": "^5.5.4", 61 | "webpack": "^5.99.9", 62 | "webpack-cli": "^5.1.4", 63 | "webpack-dev-server": "^4.15.1" 64 | }, 65 | "dependencies": { 66 | "@cucumber/gherkin": "^26.2.0", 67 | "ace-builds": "^1.42.0", 68 | "calculate-size": "^1.1.1", 69 | "escape-string-regexp": "^5.0.0", 70 | "lodash": "^4.17.21", 71 | "re-resizable": "^6.11.2", 72 | "react-ace": "^10.1.0", 73 | "react-select": "^5.10.1" 74 | }, 75 | "peerDependencies": { 76 | "react": ">=16.8.0 <19.0.0", 77 | "react-dom": ">=16.8.0 <19.0.0", 78 | "styled-components": ">=3.0.0 <6.0.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver') 2 | const fs = require('fs') 3 | const { execSync } = require('child_process') 4 | 5 | const releaseTypes = ['major', 'minor', 'patch'] 6 | const changelogFile = 'CHANGELOG.md' 7 | 8 | const exec = command => { 9 | try { 10 | command() 11 | } catch (error) { 12 | console.log(`Error: ${error.message}`) 13 | process.exitCode = 1 14 | } 15 | } 16 | 17 | const release = () => { 18 | const releaseType = computeReleaseType() 19 | 20 | const oldVersion = process.env.npm_package_version 21 | const newVersion = semver.inc(oldVersion, releaseType) 22 | 23 | console.log(`Preparing ${releaseType} release from v${oldVersion} to v${newVersion}...`) 24 | 25 | console.log('Updating changelog...') 26 | 27 | updateChangelog(newVersion) 28 | 29 | console.log(`Creating a new version using "yarn version --${releaseType}"...`) 30 | 31 | createVersion(releaseType) 32 | 33 | console.log('Release done! GitHub will publish it.') 34 | } 35 | 36 | const computeReleaseType = () => { 37 | const args = process.argv.slice(2) 38 | 39 | const requestedReleaseTypes = releaseTypes.filter(releaseType => args.includes(`--${releaseType}`)) 40 | 41 | if (requestedReleaseTypes.length === 0) { 42 | throw new Error('No release type specified. Please specify a new version using --major, --minor or --patch.') 43 | } 44 | 45 | if (requestedReleaseTypes.length > 1) { 46 | throw new Error('Multiple release types specified. Please specify a new version using --major, --minor or --patch.') 47 | } 48 | 49 | return requestedReleaseTypes[0] 50 | } 51 | 52 | const updateChangelog = newVersion => { 53 | const changelog = fs.readFileSync(changelogFile, 'utf8') 54 | const newChangelog = changelog.replace('## [Unreleased]', `## [Unreleased]\n\n## [${newVersion}]`) 55 | 56 | fs.writeFileSync(changelogFile, newChangelog, 'utf8') 57 | execSync(`git add ${changelogFile}`) 58 | } 59 | 60 | const createVersion = releaseType => { 61 | const command = `yarn version --${releaseType}` 62 | 63 | execSync(command, { stdio: 'inherit' }) 64 | } 65 | 66 | exec(release) 67 | -------------------------------------------------------------------------------- /src/components/GherkinEditor/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useImperativeHandle } from 'react' 2 | import PropTypes from 'prop-types' 3 | import AceEditor from 'react-ace' 4 | import { require as acequire } from 'ace-builds' 5 | import { Resizable } from 're-resizable' 6 | import KeywordCompleter from '../../modules/keyword-completer' 7 | import StepCompleter from '../../modules/step-completer' 8 | import { 9 | setGherkinDialect as setDialect, 10 | getGherkinDialect as getDialect 11 | } from '../../modules/dialects/gherkin_i18n' 12 | import { 13 | setGherkinDialect as setBackgroundDialect, 14 | getGherkinDialect as getBackgroundDialect 15 | } from '../../modules/dialects/gherkin_background_i18n' 16 | import { 17 | setGherkinDialect as setScenarioDialect, 18 | getGherkinDialect as getScenarioDialect 19 | } from '../../modules/dialects/gherkin_scenario_i18n' 20 | import GherkinAnnotator from '../../modules/gherkin-annotator' 21 | import Toolbar from '../Toolbar' 22 | import { EditorWrapper } from './styled' 23 | 24 | import 'ace-builds/src-noconflict/ext-language_tools' 25 | 26 | import '../../themes/jira' 27 | import '../../themes/cucumber' 28 | 29 | import '../../modules/mode/gherkin_i18n' 30 | import '../../modules/mode/gherkin_background_i18n' 31 | import '../../modules/mode/gherkin_scenario_i18n' 32 | 33 | const setGherkinDialectFunctions = { 34 | gherkin_i18n: setDialect, 35 | gherkin_background_i18n: setBackgroundDialect, 36 | gherkin_scenario_i18n: setScenarioDialect 37 | } 38 | 39 | const getGherkinDialectFunctions = { 40 | gherkin_i18n: getDialect, 41 | gherkin_background_i18n: getBackgroundDialect, 42 | gherkin_scenario_i18n: getScenarioDialect 43 | } 44 | 45 | const defaultOptions = { 46 | fontFamily: [ 47 | "'SFMono-Medium'", 48 | "'SF Mono'", 49 | "'Segoe UI Mono'", 50 | "'Roboto Mono'", 51 | "'Ubuntu Mono'", 52 | 'Menlo', 53 | 'Consolas', 54 | 'Courier', 55 | 'monospace' 56 | ].join(', '), 57 | enableBasicAutocompletion: true, 58 | enableLiveAutocompletion: true, 59 | showLineNumbers: false, 60 | displayIndentGuides: false, 61 | tabSize: 2 62 | } 63 | 64 | let gherkinAnnotator = null 65 | 66 | const GherkinEditor = React.forwardRef((props, ref) => { 67 | const [currentLanguage, setCurrentLanguage] = useState(props.language) 68 | const [height, setHeight] = useState(props.initialHeight) 69 | 70 | const aceEditorRef = useRef() 71 | 72 | const { 73 | activateLinter, 74 | autoCompleteFunction, 75 | autoFocus, 76 | hideToolbar, 77 | initialValue, 78 | language, 79 | mode, 80 | onLanguageChange, 81 | onParse, 82 | onSubmit, 83 | readOnly, 84 | setOptions, 85 | showGutter, 86 | theme, 87 | toolbarContent, 88 | uniqueId 89 | } = props 90 | 91 | const setGherkinDialect = setGherkinDialectFunctions[mode] || setDialect 92 | const getGherkinDialect = getGherkinDialectFunctions[mode] || getDialect 93 | const isLinterActivated = activateLinter && showGutter 94 | 95 | useEffect(() => { 96 | if (autoFocus) { 97 | aceEditorRef.current.editor.focus() 98 | } 99 | }, [autoFocus]) 100 | 101 | useEffect(() => { 102 | const keywordCompleter = new KeywordCompleter(getGherkinDialect) 103 | const stepCompleter = new StepCompleter( 104 | autoCompleteFunction, 105 | getGherkinDialect 106 | ) 107 | const langTools = acequire('ace/ext/language_tools') 108 | 109 | langTools.setCompleters([keywordCompleter, stepCompleter]) 110 | }, [autoCompleteFunction, getGherkinDialect]) 111 | 112 | useEffect(() => { 113 | setCurrentLanguage(language) 114 | }, [language]) 115 | 116 | useEffect(() => { 117 | setGherkinDialect(currentLanguage) 118 | 119 | aceEditorRef.current.editor.session.setMode({ 120 | path: `ace/mode/${mode}`, 121 | v: Date.now() 122 | }) 123 | }, [setGherkinDialect, currentLanguage, mode]) 124 | 125 | useEffect(() => { 126 | if (!isLinterActivated) { 127 | gherkinAnnotator = null 128 | return 129 | } 130 | 131 | const session = aceEditorRef.current.editor.getSession() 132 | 133 | if (!gherkinAnnotator) { 134 | gherkinAnnotator = new GherkinAnnotator(session, onParse) 135 | } else { 136 | gherkinAnnotator.setSession(session) 137 | } 138 | }, [isLinterActivated]) 139 | 140 | useEffect(() => { 141 | if (gherkinAnnotator) { 142 | gherkinAnnotator.setLanguage(currentLanguage) 143 | gherkinAnnotator.setMode(mode) 144 | gherkinAnnotator.annotate(initialValue) 145 | } 146 | }, [currentLanguage, mode, initialValue]) 147 | 148 | useImperativeHandle(ref, () => ({ 149 | editor: aceEditorRef.current.editor 150 | })) 151 | 152 | if (activateLinter && !showGutter) { 153 | console.warn('activateLinter requires showGutter to be true') 154 | } 155 | 156 | const onResizeStop = (_event, _direction, _refToElement, delta) => { 157 | setHeight(height + delta.height) 158 | } 159 | 160 | const languageChangeHandler = option => { 161 | setCurrentLanguage(option.value) 162 | onLanguageChange(option) 163 | } 164 | 165 | const onChangeHandler = (newValue, ...args) => { 166 | if (gherkinAnnotator) { 167 | gherkinAnnotator.annotate(newValue) 168 | } 169 | 170 | return props.onChange(newValue, ...args) 171 | } 172 | 173 | const options = { ...defaultOptions, ...setOptions } 174 | 175 | return ( 176 | 177 | {!hideToolbar && ( 178 | 184 | )} 185 | 199 | onSubmit(editor.getValue()) 214 | } 215 | ]} 216 | /> 217 | 218 | 219 | ) 220 | }) 221 | 222 | GherkinEditor.propTypes = { 223 | initialValue: PropTypes.string, 224 | language: PropTypes.string, 225 | hideToolbar: PropTypes.bool, 226 | readOnly: PropTypes.bool, 227 | uniqueId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 228 | toolbarContent: PropTypes.node, 229 | onChange: PropTypes.func, 230 | onSubmit: PropTypes.func, 231 | autoCompleteFunction: PropTypes.func, 232 | onLanguageChange: PropTypes.func, 233 | autoFocus: PropTypes.bool, 234 | initialHeight: PropTypes.number, 235 | theme: PropTypes.string, 236 | mode: PropTypes.oneOf([ 237 | 'gherkin_i18n', 238 | 'gherkin_background_i18n', 239 | 'gherkin_scenario_i18n' 240 | ]), 241 | fontSize: PropTypes.number, 242 | width: PropTypes.string, 243 | showPrintMargin: PropTypes.bool, 244 | showGutter: PropTypes.bool, 245 | highlightActiveLine: PropTypes.bool, 246 | activateLinter: PropTypes.bool, 247 | setOptions: PropTypes.object 248 | } 249 | 250 | GherkinEditor.defaultProps = { 251 | initialValue: '', 252 | language: 'en', 253 | hideToolbar: false, 254 | readOnly: false, 255 | uniqueId: Math.random().toString(36).substr(2, 9), 256 | onChange: () => {}, 257 | onSubmit: () => {}, 258 | autoCompleteFunction: () => Promise.resolve([]), 259 | onLanguageChange: () => {}, 260 | autoFocus: false, 261 | initialHeight: 500, 262 | theme: 'jira', 263 | mode: 'gherkin_i18n', 264 | fontSize: 14, 265 | width: '100%', 266 | showPrintMargin: false, 267 | showGutter: false, 268 | highlightActiveLine: false, 269 | activateLinter: false, 270 | setOptions: {} 271 | } 272 | 273 | export default GherkinEditor 274 | -------------------------------------------------------------------------------- /src/components/GherkinEditor/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const EditorWrapper = styled.div` 4 | border-width: 1px; 5 | border-style: solid; 6 | border-color: rgb(223, 225, 230); 7 | border-radius: 3px; 8 | ` 9 | -------------------------------------------------------------------------------- /src/components/Toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import gherkinLanguages from '../../lib/gherkin-languages' 2 | import _find from 'lodash/find' 3 | import React from 'react' 4 | import Select from 'react-select' 5 | 6 | import { LanguageDropdownContainer, ToolbarContainer } from './styled' 7 | 8 | interface ToolbarProps { 9 | content?: React.ReactNode 10 | language?: string 11 | readOnly?: boolean 12 | onLanguageChange?(option: object): void 13 | } 14 | 15 | const availableLanguages = Object.entries(gherkinLanguages).map( 16 | ([key, language]) => ({ 17 | value: key, 18 | label: (language as any).native 19 | }) 20 | ) 21 | 22 | const languageSelectStyles = { 23 | container: styles => ({ ...styles, 'z-index': 5 }) 24 | } 25 | 26 | const Toolbar = ({ 27 | content, 28 | language = 'en', 29 | readOnly = false, 30 | onLanguageChange = () => {} 31 | }: ToolbarProps) => { 32 | const gherkinLanguage = _find(availableLanguages, { value: language }) 33 | 34 | return ( 35 | 36 | 37 |