Initial commit
This commit is contained in:
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
$err,
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInitc501b52c18a78741dc0d2bdb0f412b76::getLoader();
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [7.2, 7.3, 7.4, 8.0, 8.1]
|
||||
composer-flags: [ "" ]
|
||||
symfony-version: [ "" ]
|
||||
include:
|
||||
- php: 7.2
|
||||
symfony-version: '3.*'
|
||||
- php: 7.3
|
||||
symfony-version: '4.*'
|
||||
- php: 7.4
|
||||
symfony-version: '5.*'
|
||||
- php: 8.0
|
||||
symfony-version: '5.*'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "${{ matrix.php }}"
|
||||
coverage: none
|
||||
|
||||
- name: Update Symfony version
|
||||
if: matrix.symfony-version != ''
|
||||
run: composer require --no-update "symfony/symfony:${{ matrix.symfony-version }}"
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer update ${{ matrix.composer-flags }}
|
||||
|
||||
- name: Run tests (phpunit)
|
||||
run: ./vendor/bin/phpunit
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Update Cucumber
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 7 * * *'
|
||||
|
||||
jobs:
|
||||
cucumber-update:
|
||||
runs-on: ubuntu-latest
|
||||
name: Upstream cucumber update
|
||||
steps:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 7.4
|
||||
coverage: none
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer update
|
||||
|
||||
- name: Update cucumber tag
|
||||
id: cucumber
|
||||
run: bin/update_cucumber
|
||||
|
||||
- name: Re-install dependencies
|
||||
run: composer update
|
||||
if: steps.cucumber.outputs.cucumber_version
|
||||
|
||||
- name: Update translations
|
||||
run: bin/update_i18n
|
||||
if: steps.cucumber.outputs.cucumber_version
|
||||
|
||||
- name: Find changelog
|
||||
id: changelog
|
||||
run: bin/cucumber_changelog ${{ steps.cucumber.outputs.cucumber_version }}
|
||||
if: steps.cucumber.outputs.cucumber_version
|
||||
|
||||
- name: Open a PR
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
if: steps.cucumber.outputs.cucumber_version
|
||||
with:
|
||||
commit-message: Automatic Cucumber tag update to ${{ steps.cucumber.outputs.cucumber_version }}
|
||||
branch: cucumber-update-${{ steps.cucumber.outputs.cucumber_version }}
|
||||
delete-branch: true
|
||||
title: Cucumber update ${{ steps.cucumber.outputs.cucumber_version }}
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
base: 'master'
|
||||
Vendored
+442
@@ -0,0 +1,442 @@
|
||||
4.9.0 / 2021-10-12
|
||||
==================
|
||||
|
||||
* Simplify the boolean condition for the tag matching by @stof in https://github.com/Behat/Gherkin/pull/219
|
||||
* Remove symfony phpunit bridge by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/220
|
||||
* Ignore the bin folder in archives by @stof in https://github.com/Behat/Gherkin/pull/226
|
||||
* Cast table node exceptions into ParserExceptions when throwing by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/216
|
||||
* Cucumber changelog in PRs and using correct hash by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/225
|
||||
* Support alternative docstrings format (```) by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/214
|
||||
* Fix DocBlocks (Boolean -> bool) by @simonhammes in https://github.com/Behat/Gherkin/pull/237
|
||||
* Tag parsing by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/215
|
||||
* Remove test - cucumber added an example with Rule which is not supported by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/239
|
||||
* Add PHP 8.1 support by @javer in https://github.com/Behat/Gherkin/pull/242
|
||||
* Fix main branch alias version by @mvorisek in https://github.com/Behat/Gherkin/pull/244
|
||||
|
||||
4.8.0 / 2021-02-04
|
||||
==================
|
||||
|
||||
* Drop support for PHP before version 7.2
|
||||
|
||||
4.7.3 / 2021-02-04
|
||||
==================
|
||||
|
||||
* Refactored comments parsing to avoid Maximum function nesting level errors
|
||||
|
||||
4.7.2 / 2021-02-03
|
||||
==================
|
||||
|
||||
* Issue where Scenario Outline title was not populated into Examples
|
||||
* Updated translations from cucumber 16.0.0
|
||||
|
||||
4.7.1 / 2021-01-26
|
||||
==================
|
||||
|
||||
* Issue parsing comments before scenarios when following an Examples table
|
||||
|
||||
4.7.0 / 2021-01-24
|
||||
==================
|
||||
|
||||
* Provides better messages for TableNode construct errors
|
||||
* Now allows single character steps
|
||||
* Supports multiple Example Tables with tags
|
||||
|
||||
4.6.2 / 2020-03-17
|
||||
==================
|
||||
|
||||
* Fixed issues due to incorrect cache key
|
||||
|
||||
4.6.1 / 2020-02-27
|
||||
==================
|
||||
|
||||
* Fix AZ translations
|
||||
* Correctly filter features, now that the base path is correctly set
|
||||
|
||||
4.6.0 / 2019-01-16
|
||||
==================
|
||||
|
||||
* Updated translations (including 'Example' as synonym for 'Scenario' in `en`)
|
||||
|
||||
4.5.1 / 2017-08-30
|
||||
==================
|
||||
|
||||
* Fix regression in `PathsFilter`
|
||||
|
||||
4.5.0 / 2017-08-30
|
||||
==================
|
||||
|
||||
* Sync i18n with Cucumber Gherkin
|
||||
* Drop support for HHVM tests on Travis
|
||||
* Add `TableNode::fromList()` method (thanks @TravisCarden)
|
||||
* Add `ExampleNode::getOutlineTitle()` method (thanks @duxet)
|
||||
* Use realpath, so the feature receives the cwd prefixed (thanks @glennunipro)
|
||||
* Explicitly handle non-two-dimensional arrays in TableNode (thanks @TravisCarden)
|
||||
* Fix to line/linefilter scenario runs which take relative paths to files (thanks @generalconsensus)
|
||||
|
||||
4.4.5 / 2016-10-30
|
||||
==================
|
||||
|
||||
* Fix partial paths matching in `PathsFilter`
|
||||
|
||||
4.4.4 / 2016-09-18
|
||||
==================
|
||||
|
||||
* Provide clearer exception for non-writeable cache directories
|
||||
|
||||
4.4.3 / 2016-09-18
|
||||
==================
|
||||
|
||||
* Ensure we reset tags between features
|
||||
|
||||
4.4.2 / 2016-09-03
|
||||
==================
|
||||
|
||||
* Sync 18n with gherkin 3
|
||||
|
||||
4.4.1 / 2015-12-30
|
||||
==================
|
||||
|
||||
* Ensure keywords are trimmed when syncing translations
|
||||
* Sync 18n with cucumber
|
||||
|
||||
4.4.0 / 2015-09-19
|
||||
==================
|
||||
|
||||
* Added validation enforcing that all rows of a `TableNode` have the same number of columns
|
||||
* Added `TableNode::getColumn` to get a column from the table
|
||||
* Sync 18n with cucumber
|
||||
|
||||
4.3.0 / 2014-06-06
|
||||
==================
|
||||
|
||||
* Added `setFilters(array)` method to `Gherkin` class
|
||||
* Added `NarrativeFilter` for non-english `RoleFilter` lovers
|
||||
|
||||
4.2.1 / 2014-06-06
|
||||
==================
|
||||
|
||||
* Fix parsing of features without line feed at the end
|
||||
|
||||
4.2.0 / 2014-05-27
|
||||
==================
|
||||
|
||||
* Added `getKeyword()` and `getKeywordType()` methods to `StepNode`, deprecated `getType()`.
|
||||
Thanks to @kibao
|
||||
|
||||
4.1.3 / 2014-05-25
|
||||
==================
|
||||
|
||||
* Properly handle tables with rows terminating in whitespace
|
||||
|
||||
4.1.2 / 2014-05-14
|
||||
==================
|
||||
|
||||
* Handle case where Gherkin cache is broken
|
||||
|
||||
4.1.1 / 2014-05-05
|
||||
==================
|
||||
|
||||
* Fixed the compatibility with PHP 5.6-beta by avoiding to use the broken PHP array function
|
||||
* The YamlFileLoader no longer extend from ArrayLoader but from AbstractFileLoader
|
||||
|
||||
4.1.0 / 2014-04-20
|
||||
==================
|
||||
|
||||
* Fixed scenario tag filtering
|
||||
* Do not allow multiple multiline step arguments
|
||||
* Sync 18n with cucumber
|
||||
|
||||
4.0.0 / 2014-01-05
|
||||
==================
|
||||
|
||||
* Changed the behavior when no loader can be found for the resource. Instead of throwing an exception, the
|
||||
Gherkin class now returns an empty array.
|
||||
|
||||
3.1.3 / 2014-01-04
|
||||
==================
|
||||
|
||||
* Dropped the dependency on the Symfony Finder by using SPL iterators directly
|
||||
* Added testing on HHVM on Travis. HHVM is officially supported (previous release was actually already compatible)
|
||||
|
||||
3.1.2 / 2014-01-01
|
||||
==================
|
||||
|
||||
* All paths passed to PathsFilter are converted using realpath
|
||||
|
||||
3.1.1 / 2013-12-31
|
||||
==================
|
||||
|
||||
* Add `ComplexFilterInterace` that has complex behavior for scenarios and requires to pass
|
||||
feature too
|
||||
* `TagFilter` is an instance of a `ComplexFilterInterace` now
|
||||
|
||||
3.1.0 / 2013-12-31
|
||||
==================
|
||||
|
||||
* Example node is a scenario
|
||||
* Nodes do not have uprefs (memory usage fix)
|
||||
* Scenario filters do not depend on feature nodes
|
||||
|
||||
3.0.5 / 2014-01-01
|
||||
==================
|
||||
|
||||
* All paths passed to PathsFilter are converted using realpath
|
||||
|
||||
3.0.4 / 2013-12-31
|
||||
==================
|
||||
|
||||
* TableNode is now traversable using foreach
|
||||
* All possibly thrown exceptions implement Gherkin\Exception interface
|
||||
* Sync i18n with cucumber
|
||||
|
||||
3.0.3 / 2013-09-15
|
||||
==================
|
||||
|
||||
* Extend ExampleNode with additional methods
|
||||
|
||||
3.0.2 / 2013-09-14
|
||||
==================
|
||||
|
||||
* Extract `KeywordNodeInterface` and `ScenarioLikeInterface`
|
||||
* Add `getIndex()` methods to scenarios, outlines, steps and examples
|
||||
* Throw proper exception for fractured node tree
|
||||
|
||||
3.0.1 / 2013-09-14
|
||||
==================
|
||||
|
||||
* Use versioned subfolder in FileCache
|
||||
|
||||
3.0.0 / 2013-09-14
|
||||
==================
|
||||
|
||||
* A lot of optimizations in Parser and Lexer
|
||||
* Node tree is now immutable by nature (no setters)
|
||||
* Example nodes are now part of the node tree. They are lazily generated by Outline node
|
||||
* Sync with latest cucumber i18n
|
||||
|
||||
2.3.4 / 2013-08-11
|
||||
==================
|
||||
|
||||
* Fix leaks in memory cache
|
||||
|
||||
2.3.3 / 2013-08-11
|
||||
==================
|
||||
|
||||
* Fix encoding bug introduced with previous release
|
||||
* Sync i18n with cucumber
|
||||
|
||||
2.3.2 / 2013-08-11
|
||||
==================
|
||||
|
||||
* Explicitly use utf8 encoding
|
||||
|
||||
2.3.1 / 2013-08-10
|
||||
==================
|
||||
|
||||
* Support `an` prefix with RoleFilter
|
||||
|
||||
2.3.0 / 2013-08-04
|
||||
==================
|
||||
|
||||
* Add RoleFilter
|
||||
* Add PathsFilter
|
||||
* Add MemoryCache
|
||||
|
||||
2.2.9 / 2013-03-02
|
||||
==================
|
||||
|
||||
* Fix dependency version requirement
|
||||
|
||||
2.2.8 / 2013-03-02
|
||||
==================
|
||||
|
||||
* Features filtering behavior change. Now emptified (by filtering) features
|
||||
that do not match filter themselves are removed from resultset.
|
||||
* Small potential bug fix in TableNode
|
||||
|
||||
2.2.7 / 2013-01-27
|
||||
==================
|
||||
|
||||
* Fixed bug in i18n syncing script
|
||||
* Resynced Gherkin i18n
|
||||
|
||||
2.2.6 / 2013-01-26
|
||||
==================
|
||||
|
||||
* Support long row hashes in tables ([see](https://github.com/Behat/Gherkin/issues/40))
|
||||
* Synced Gherkin i18n
|
||||
|
||||
2.2.5 / 2012-09-26
|
||||
==================
|
||||
|
||||
* Fixed issue with loading empty features
|
||||
* Synced Gherkin i18n
|
||||
|
||||
2.2.4 / 2012-08-03
|
||||
==================
|
||||
|
||||
* Fixed exception message for "no loader found"
|
||||
|
||||
2.2.3 / 2012-08-03
|
||||
==================
|
||||
|
||||
* Fixed minor loader bug with empty base path
|
||||
* Synced Gherkin i18n
|
||||
|
||||
2.2.2 / 2012-07-01
|
||||
==================
|
||||
|
||||
* Added ability to filter outline scenarios by line and range filters
|
||||
* Synced Gherkin i18n
|
||||
* Refactored table parser to read row line numbers too
|
||||
|
||||
2.2.1 / 2012-05-04
|
||||
==================
|
||||
|
||||
* Fixed StepNode `getLanguage()` and `getFile()`
|
||||
|
||||
2.2.0 / 2012-05-03
|
||||
==================
|
||||
|
||||
* Features freeze after parsing
|
||||
* Implemented GherkinDumper (@Halleck45)
|
||||
* Synced i18n with Cucumber
|
||||
* Updated inline documentation
|
||||
|
||||
2.1.1 / 2012-03-09
|
||||
==================
|
||||
|
||||
* Fixed caching bug, where `isFresh()` always returned false
|
||||
|
||||
2.1.0 / 2012-03-09
|
||||
==================
|
||||
|
||||
* Added parser caching layer
|
||||
* Added support for table delimiter escaping (use `\|` for that)
|
||||
* Added LineRangeFilter (thanks @headrevision)
|
||||
* Synced i18n dictionary with cucumber/gherkin
|
||||
|
||||
2.0.2 / 2012-02-04
|
||||
==================
|
||||
|
||||
* Synced i18n dictionary with cucumber/gherkin
|
||||
|
||||
2.0.1 / 2012-01-26
|
||||
==================
|
||||
|
||||
* Fixed issue about parsing features without indentation
|
||||
|
||||
2.0.0 / 2012-01-19
|
||||
==================
|
||||
|
||||
* Background titles support
|
||||
* Correct parsing of titles/descriptions (hirarchy lexing)
|
||||
* Migration to the cucumber/gherkin i18n dictionary
|
||||
* Speed optimizations
|
||||
* Refactored KeywordsDumper
|
||||
* New loaders
|
||||
* Bugfixes
|
||||
|
||||
1.1.4 / 2012-01-08
|
||||
==================
|
||||
|
||||
* Read feature description even if it looks like a step
|
||||
|
||||
1.1.3 / 2011-12-14
|
||||
==================
|
||||
|
||||
* Removed file loading routines from Parser (fixes `is_file()` issue on some systems - thanks
|
||||
@flodocteurklein)
|
||||
|
||||
1.1.2 / 2011-12-01
|
||||
==================
|
||||
|
||||
* Updated spanish trasnaltion (@anbotero)
|
||||
* Integration with Composer and Travis CI
|
||||
|
||||
1.1.1 / 2011-07-29
|
||||
==================
|
||||
|
||||
* Updated pt language step types (@danielcsgomes)
|
||||
* Updated vendors
|
||||
|
||||
1.1.0 / 2011-07-16
|
||||
==================
|
||||
|
||||
* Return all tags, including inherited in `Scenario::getTags()`
|
||||
* New `Feature::getOwnTags()` and `Scenario::getOwnTags()` method added,
|
||||
which returns only own tags
|
||||
|
||||
1.0.8 / 2011-06-29
|
||||
==================
|
||||
|
||||
* Fixed comments parsing.
|
||||
You can’t have comments at the end of a line # like this
|
||||
# But you can still have comments at the beginning of a line
|
||||
|
||||
1.0.7 / 2011-06-28
|
||||
==================
|
||||
|
||||
* Added `getRaw()` method to PyStringNode
|
||||
* Updated vendors
|
||||
|
||||
1.0.6 / 2011-06-17
|
||||
==================
|
||||
|
||||
* Updated vendors
|
||||
|
||||
1.0.5 / 2011-06-10
|
||||
==================
|
||||
|
||||
* Fixed bug, introduced with 1.0.4 - hash in PyStrings
|
||||
|
||||
1.0.4 / 2011-06-10
|
||||
==================
|
||||
|
||||
* Fixed inability to comment pystrings
|
||||
|
||||
1.0.3 / 2011-04-21
|
||||
==================
|
||||
|
||||
* Fixed introduced with 1.0.2 pystring parsing bug
|
||||
|
||||
1.0.2 / 2011-04-18
|
||||
==================
|
||||
|
||||
* Fixed bugs in text with comments parsing
|
||||
|
||||
1.0.1 / 2011-04-01
|
||||
==================
|
||||
|
||||
* Updated vendors
|
||||
|
||||
1.0.0 / 2011-03-08
|
||||
==================
|
||||
|
||||
* Updated vendors
|
||||
|
||||
1.0.0RC2 / 2011-02-25
|
||||
=====================
|
||||
|
||||
* Windows support
|
||||
* Missing phpunit config
|
||||
|
||||
1.0.0RC1 / 2011-02-15
|
||||
=====================
|
||||
|
||||
* Huge optimizations to Lexer & Parser
|
||||
* Additional loaders (Yaml, Array, Directory)
|
||||
* Filters (Tag, Name, Line)
|
||||
* Code refactoring
|
||||
* Nodes optimizations
|
||||
* Additional tests for exceptions and translations
|
||||
* Keywords dumper
|
||||
|
||||
0.2.0 / 2011-01-05
|
||||
==================
|
||||
|
||||
* New Parser & Lexer (based on AST)
|
||||
* New verbose parsing exception handling
|
||||
* New translation mechanics
|
||||
* 47 brand new translations (see i18n)
|
||||
* Full test suite for everything from AST nodes to translations
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Gherkin is an open source, community-driven project. If you'd like to contribute, feel free to do this, but remember to follow this few simple rules:
|
||||
|
||||
- Make your feature addition or bug fix,
|
||||
- Always use the `master` branch as base for your changes (all new development happens in `master`),
|
||||
- Add tests for those changes (please look into `tests/` folder for some examples). This is important so we don't break it in a future version unintentionally,
|
||||
- Commit your code, but do not mess with `CHANGES.md`,
|
||||
- __Remember__: when you create Pull Request, always select `master` branch as target (done by default), otherwise it will be closed.
|
||||
|
||||
Running tests
|
||||
-------------
|
||||
|
||||
Make sure that you don't break anything with your changes by running:
|
||||
|
||||
```bash
|
||||
$> phpunit
|
||||
```
|
||||
|
||||
Contributing to Gherkin Translations
|
||||
------------------------------------
|
||||
|
||||
Gherkin supports →40 different languages and you could add more! You might notice
|
||||
`i18n.php` file in the root of the library. This file is downloaded and **autogenerated**
|
||||
from original [cucumber/gherkin translations](https://github.com/cucumber/cucumber/blob/master/gherkin/gherkin-languages.json).
|
||||
So, in order to fix/update/add some translation, you should send Pull Request to the
|
||||
`cucumber/gherkin` repository. `Behat\Gherkin` will redownload/regenerate translations
|
||||
from there before each release.
|
||||
|
||||
It might sounds difficult, but this way of dictionary sharing gives you ability to
|
||||
migrate your `*.feature` files from language to language and library to library without
|
||||
the need to rewrite/modify them - same dictionary (Gherkin) used everywhere.
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2011-2013 Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
Behat Gherkin Parser
|
||||
====================
|
||||
|
||||
This is the php Gherkin parser for Behat. It comes bundled with more than 40 native languages
|
||||
(see `i18n.php`) support & clean architecture.
|
||||
|
||||
Useful Links
|
||||
------------
|
||||
|
||||
- Official Google Group is at [http://groups.google.com/group/behat](http://groups.google.com/group/behat)
|
||||
- IRC channel on [#freenode](http://freenode.net/) is `#behat`
|
||||
- [Note on Patches/Pull Requests](CONTRIBUTING.md)
|
||||
|
||||
Usage Example
|
||||
-------------
|
||||
|
||||
``` php
|
||||
<?php
|
||||
|
||||
$keywords = new Behat\Gherkin\Keywords\ArrayKeywords(array(
|
||||
'en' => array(
|
||||
'feature' => 'Feature',
|
||||
'background' => 'Background',
|
||||
'scenario' => 'Scenario',
|
||||
'scenario_outline' => 'Scenario Outline|Scenario Template',
|
||||
'examples' => 'Examples|Scenarios',
|
||||
'given' => 'Given',
|
||||
'when' => 'When',
|
||||
'then' => 'Then',
|
||||
'and' => 'And',
|
||||
'but' => 'But'
|
||||
),
|
||||
'en-pirate' => array(
|
||||
'feature' => 'Ahoy matey!',
|
||||
'background' => 'Yo-ho-ho',
|
||||
'scenario' => 'Heave to',
|
||||
'scenario_outline' => 'Shiver me timbers',
|
||||
'examples' => 'Dead men tell no tales',
|
||||
'given' => 'Gangway!',
|
||||
'when' => 'Blimey!',
|
||||
'then' => 'Let go and haul',
|
||||
'and' => 'Aye',
|
||||
'but' => 'Avast!'
|
||||
)
|
||||
));
|
||||
$lexer = new Behat\Gherkin\Lexer($keywords);
|
||||
$parser = new Behat\Gherkin\Parser($lexer);
|
||||
|
||||
$feature = $parser->parse(file_get_contents('some.feature'));
|
||||
```
|
||||
|
||||
Installing Dependencies
|
||||
-----------------------
|
||||
|
||||
``` bash
|
||||
$> curl http://getcomposer.org/installer | php
|
||||
$> php composer.phar update
|
||||
```
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Konstantin Kudryashov [everzet](http://github.com/everzet) [lead developer]
|
||||
* Other [awesome developers](https://github.com/Behat/Gherkin/graphs/contributors)
|
||||
Vendored
+67
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "behat/gherkin",
|
||||
"description": "Gherkin DSL parser for PHP",
|
||||
"keywords": ["BDD", "parser", "DSL", "Behat", "Gherkin", "Cucumber"],
|
||||
"homepage": "http://behat.org/",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Kudryashov",
|
||||
"email": "ever.zet@gmail.com",
|
||||
"homepage": "http://everzet.com"
|
||||
}
|
||||
],
|
||||
|
||||
"require": {
|
||||
"php": "~7.2|~8.0"
|
||||
},
|
||||
|
||||
"require-dev": {
|
||||
"symfony/yaml": "~3|~4|~5",
|
||||
"phpunit/phpunit": "~8|~9",
|
||||
"cucumber/cucumber": "dev-gherkin-22.0.0"
|
||||
},
|
||||
|
||||
"suggest": {
|
||||
"symfony/yaml": "If you want to parse features, represented in YAML files"
|
||||
},
|
||||
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Behat\\Gherkin": "src/"
|
||||
}
|
||||
},
|
||||
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\Behat\\": "tests/Behat/"
|
||||
}
|
||||
},
|
||||
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.x-dev"
|
||||
}
|
||||
},
|
||||
|
||||
"repositories": [
|
||||
{
|
||||
"type": "package",
|
||||
"package": {
|
||||
"name": "cucumber/cucumber",
|
||||
"version": "dev-gherkin-22.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cucumber/cucumber.git",
|
||||
"reference": "379280b6b98ee9a1bf79444e7c75e23265ccb3e0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cucumber/cucumber/zipball/379280b6b98ee9a1bf79444e7c75e23265ccb3e0",
|
||||
"reference": "379280b6b98ee9a1bf79444e7c75e23265ccb3e0"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+1245
File diff suppressed because it is too large
Load Diff
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
return __DIR__;
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Cache;
|
||||
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
|
||||
/**
|
||||
* Parser cache interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface CacheInterface
|
||||
{
|
||||
/**
|
||||
* Checks that cache for feature exists and is fresh.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
* @param integer $timestamp The last time feature was updated
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFresh($path, $timestamp);
|
||||
|
||||
/**
|
||||
* Reads feature cache from path.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function read($path);
|
||||
|
||||
/**
|
||||
* Caches feature node.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
* @param FeatureNode $feature Feature instance
|
||||
*/
|
||||
public function write($path, FeatureNode $feature);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Cache;
|
||||
|
||||
use Behat\Gherkin\Exception\CacheException;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Gherkin;
|
||||
|
||||
/**
|
||||
* File cache.
|
||||
* Caches feature into a file.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class FileCache implements CacheInterface
|
||||
{
|
||||
private $path;
|
||||
|
||||
/**
|
||||
* Initializes file cache.
|
||||
*
|
||||
* @param string $path Path to the folder where to store caches.
|
||||
*
|
||||
* @throws CacheException
|
||||
*/
|
||||
public function __construct($path)
|
||||
{
|
||||
$this->path = rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'v'.Gherkin::VERSION;
|
||||
|
||||
if (!is_dir($this->path)) {
|
||||
@mkdir($this->path, 0777, true);
|
||||
}
|
||||
|
||||
if (!is_writeable($this->path)) {
|
||||
throw new CacheException(sprintf('Cache path "%s" is not writeable. Check your filesystem permissions or disable Gherkin file cache.', $this->path));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that cache for feature exists and is fresh.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
* @param integer $timestamp The last time feature was updated
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFresh($path, $timestamp)
|
||||
{
|
||||
$cachePath = $this->getCachePathFor($path);
|
||||
|
||||
if (!file_exists($cachePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filemtime($cachePath) > $timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads feature cache from path.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
*
|
||||
* @return FeatureNode
|
||||
*
|
||||
* @throws CacheException
|
||||
*/
|
||||
public function read($path)
|
||||
{
|
||||
$cachePath = $this->getCachePathFor($path);
|
||||
$feature = unserialize(file_get_contents($cachePath));
|
||||
|
||||
if (!$feature instanceof FeatureNode) {
|
||||
throw new CacheException(sprintf('Can not load cache for a feature "%s" from "%s".', $path, $cachePath ));
|
||||
}
|
||||
|
||||
return $feature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches feature node.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
* @param FeatureNode $feature Feature instance
|
||||
*/
|
||||
public function write($path, FeatureNode $feature)
|
||||
{
|
||||
file_put_contents($this->getCachePathFor($path), serialize($feature));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature cache file path from features path.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getCachePathFor($path)
|
||||
{
|
||||
return $this->path.'/'.md5($path).'.feature.cache';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Cache;
|
||||
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
|
||||
/**
|
||||
* Memory cache.
|
||||
* Caches feature into a memory.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class MemoryCache implements CacheInterface
|
||||
{
|
||||
private $features = array();
|
||||
private $timestamps = array();
|
||||
|
||||
/**
|
||||
* Checks that cache for feature exists and is fresh.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
* @param integer $timestamp The last time feature was updated
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFresh($path, $timestamp)
|
||||
{
|
||||
if (!isset($this->features[$path])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->timestamps[$path] > $timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads feature cache from path.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function read($path)
|
||||
{
|
||||
return $this->features[$path];
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches feature node.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
* @param FeatureNode $feature Feature instance
|
||||
*/
|
||||
public function write($path, FeatureNode $feature)
|
||||
{
|
||||
$this->features[$path] = $feature;
|
||||
$this->timestamps[$path] = time();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Cache exception.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class CacheException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Exception;
|
||||
|
||||
interface Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class LexerException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class NodeException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class ParserException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Filter;
|
||||
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
|
||||
/**
|
||||
* Abstract filter class.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
abstract class ComplexFilter implements ComplexFilterInterface
|
||||
{
|
||||
/**
|
||||
* Filters feature according to the filter.
|
||||
*
|
||||
* @param FeatureNode $feature
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function filterFeature(FeatureNode $feature)
|
||||
{
|
||||
$scenarios = array();
|
||||
foreach ($feature->getScenarios() as $scenario) {
|
||||
if (!$this->isScenarioMatch($feature, $scenario)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
|
||||
return new FeatureNode(
|
||||
$feature->getTitle(),
|
||||
$feature->getDescription(),
|
||||
$feature->getTags(),
|
||||
$feature->getBackground(),
|
||||
$scenarios,
|
||||
$feature->getKeyword(),
|
||||
$feature->getLanguage(),
|
||||
$feature->getFile(),
|
||||
$feature->getLine()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Filter;
|
||||
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
|
||||
/**
|
||||
* Filter interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface ComplexFilterInterface extends FeatureFilterInterface
|
||||
{
|
||||
/**
|
||||
* Checks if scenario or outline matches specified filter.
|
||||
*
|
||||
* @param FeatureNode $feature Feature node instance
|
||||
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isScenarioMatch(FeatureNode $feature, ScenarioInterface $scenario);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Filter;
|
||||
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
|
||||
/**
|
||||
* Feature filter interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface FeatureFilterInterface
|
||||
{
|
||||
/**
|
||||
* Checks if Feature matches specified filter.
|
||||
*
|
||||
* @param FeatureNode $feature Feature instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFeatureMatch(FeatureNode $feature);
|
||||
|
||||
/**
|
||||
* Filters feature according to the filter and returns new one.
|
||||
*
|
||||
* @param FeatureNode $feature
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function filterFeature(FeatureNode $feature);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Filter;
|
||||
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
|
||||
/**
|
||||
* Filter interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface FilterInterface extends FeatureFilterInterface
|
||||
{
|
||||
/**
|
||||
* Checks if scenario or outline matches specified filter.
|
||||
*
|
||||
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isScenarioMatch(ScenarioInterface $scenario);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Filter;
|
||||
|
||||
use Behat\Gherkin\Node\ExampleTableNode;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Node\OutlineNode;
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
|
||||
/**
|
||||
* Filters scenarios by definition line number.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class LineFilter implements FilterInterface
|
||||
{
|
||||
protected $filterLine;
|
||||
|
||||
/**
|
||||
* Initializes filter.
|
||||
*
|
||||
* @param string $filterLine Line of the scenario to filter on
|
||||
*/
|
||||
public function __construct($filterLine)
|
||||
{
|
||||
$this->filterLine = intval($filterLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Feature matches specified filter.
|
||||
*
|
||||
* @param FeatureNode $feature Feature instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFeatureMatch(FeatureNode $feature)
|
||||
{
|
||||
return $this->filterLine === $feature->getLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if scenario or outline matches specified filter.
|
||||
*
|
||||
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||
{
|
||||
if ($this->filterLine === $scenario->getLine()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||
return $this->filterLine === $scenario->getLine()
|
||||
|| in_array($this->filterLine, $scenario->getExampleTable()->getLines());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters feature according to the filter and returns new one.
|
||||
*
|
||||
* @param FeatureNode $feature
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function filterFeature(FeatureNode $feature)
|
||||
{
|
||||
$scenarios = array();
|
||||
foreach ($feature->getScenarios() as $scenario) {
|
||||
if (!$this->isScenarioMatch($scenario)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||
foreach ($scenario->getExampleTables() as $exampleTable) {
|
||||
$table = $exampleTable->getTable();
|
||||
$lines = array_keys($table);
|
||||
|
||||
if (in_array($this->filterLine, $lines)) {
|
||||
$filteredTable = array($lines[0] => $table[$lines[0]]);
|
||||
|
||||
if ($lines[0] !== $this->filterLine) {
|
||||
$filteredTable[$this->filterLine] = $table[$this->filterLine];
|
||||
}
|
||||
|
||||
$scenario = new OutlineNode(
|
||||
$scenario->getTitle(),
|
||||
$scenario->getTags(),
|
||||
$scenario->getSteps(),
|
||||
array(new ExampleTableNode($filteredTable, $exampleTable->getKeyword(), $exampleTable->getTags())),
|
||||
$scenario->getKeyword(),
|
||||
$scenario->getLine()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
|
||||
return new FeatureNode(
|
||||
$feature->getTitle(),
|
||||
$feature->getDescription(),
|
||||
$feature->getTags(),
|
||||
$feature->getBackground(),
|
||||
$scenarios,
|
||||
$feature->getKeyword(),
|
||||
$feature->getLanguage(),
|
||||
$feature->getFile(),
|
||||
$feature->getLine()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Filter;
|
||||
|
||||
use Behat\Gherkin\Node\ExampleTableNode;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Node\OutlineNode;
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
|
||||
/**
|
||||
* Filters scenarios by definition line number range.
|
||||
*
|
||||
* @author Fabian Kiss <headrevision@gmail.com>
|
||||
*/
|
||||
class LineRangeFilter implements FilterInterface
|
||||
{
|
||||
protected $filterMinLine;
|
||||
protected $filterMaxLine;
|
||||
|
||||
/**
|
||||
* Initializes filter.
|
||||
*
|
||||
* @param string $filterMinLine Minimum line of a scenario to filter on
|
||||
* @param string $filterMaxLine Maximum line of a scenario to filter on
|
||||
*/
|
||||
public function __construct($filterMinLine, $filterMaxLine)
|
||||
{
|
||||
$this->filterMinLine = intval($filterMinLine);
|
||||
if ($filterMaxLine == '*') {
|
||||
$this->filterMaxLine = PHP_INT_MAX;
|
||||
} else {
|
||||
$this->filterMaxLine = intval($filterMaxLine);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Feature matches specified filter.
|
||||
*
|
||||
* @param FeatureNode $feature Feature instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFeatureMatch(FeatureNode $feature)
|
||||
{
|
||||
return $this->filterMinLine <= $feature->getLine()
|
||||
&& $this->filterMaxLine >= $feature->getLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if scenario or outline matches specified filter.
|
||||
*
|
||||
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||
{
|
||||
if ($this->filterMinLine <= $scenario->getLine() && $this->filterMaxLine >= $scenario->getLine()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||
foreach ($scenario->getExampleTable()->getLines() as $line) {
|
||||
if ($this->filterMinLine <= $line && $this->filterMaxLine >= $line) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters feature according to the filter.
|
||||
*
|
||||
* @param FeatureNode $feature
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function filterFeature(FeatureNode $feature)
|
||||
{
|
||||
$scenarios = array();
|
||||
foreach ($feature->getScenarios() as $scenario) {
|
||||
if (!$this->isScenarioMatch($scenario)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||
// first accumulate examples and then create scenario
|
||||
$exampleTableNodes = array();
|
||||
|
||||
foreach ($scenario->getExampleTables() as $exampleTable) {
|
||||
$table = $exampleTable->getTable();
|
||||
$lines = array_keys($table);
|
||||
|
||||
$filteredTable = array($lines[0] => $table[$lines[0]]);
|
||||
unset($table[$lines[0]]);
|
||||
|
||||
foreach ($table as $line => $row) {
|
||||
if ($this->filterMinLine <= $line && $this->filterMaxLine >= $line) {
|
||||
$filteredTable[$line] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($filteredTable) > 1) {
|
||||
$exampleTableNodes[] = new ExampleTableNode($filteredTable, $exampleTable->getKeyword(), $exampleTable->getTags());
|
||||
}
|
||||
}
|
||||
|
||||
$scenario = new OutlineNode(
|
||||
$scenario->getTitle(),
|
||||
$scenario->getTags(),
|
||||
$scenario->getSteps(),
|
||||
$exampleTableNodes,
|
||||
$scenario->getKeyword(),
|
||||
$scenario->getLine()
|
||||
);
|
||||
}
|
||||
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
|
||||
return new FeatureNode(
|
||||
$feature->getTitle(),
|
||||
$feature->getDescription(),
|
||||
$feature->getTags(),
|
||||
$feature->getBackground(),
|
||||
$scenarios,
|
||||
$feature->getKeyword(),
|
||||
$feature->getLanguage(),
|
||||
$feature->getFile(),
|
||||
$feature->getLine()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Filter;
|
||||
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
|
||||
/**
|
||||
* Filters scenarios by feature/scenario name.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class NameFilter extends SimpleFilter
|
||||
{
|
||||
protected $filterString;
|
||||
|
||||
/**
|
||||
* Initializes filter.
|
||||
*
|
||||
* @param string $filterString Name filter string
|
||||
*/
|
||||
public function __construct($filterString)
|
||||
{
|
||||
$this->filterString = trim($filterString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Feature matches specified filter.
|
||||
*
|
||||
* @param FeatureNode $feature Feature instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFeatureMatch(FeatureNode $feature)
|
||||
{
|
||||
if (null === $feature->getTitle()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('/' === $this->filterString[0]) {
|
||||
return 1 === preg_match($this->filterString, $feature->getTitle());
|
||||
}
|
||||
|
||||
return false !== mb_strpos($feature->getTitle(), $this->filterString, 0, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if scenario or outline matches specified filter.
|
||||
*
|
||||
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||
{
|
||||
if (null === $scenario->getTitle()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('/' === $this->filterString[0] && 1 === preg_match($this->filterString, $scenario->getTitle())) {
|
||||
return true;
|
||||
} elseif (false !== mb_strpos($scenario->getTitle(), $this->filterString, 0, 'utf8')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Filter;
|
||||
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
|
||||
/**
|
||||
* Filters features by their narrative using regular expression.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class NarrativeFilter extends SimpleFilter
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $regex;
|
||||
|
||||
/**
|
||||
* Initializes filter.
|
||||
*
|
||||
* @param string $regex
|
||||
*/
|
||||
public function __construct($regex)
|
||||
{
|
||||
$this->regex = $regex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Feature matches specified filter.
|
||||
*
|
||||
* @param FeatureNode $feature Feature instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFeatureMatch(FeatureNode $feature)
|
||||
{
|
||||
return 1 === preg_match($this->regex, $feature->getDescription() ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if scenario or outline matches specified filter.
|
||||
*
|
||||
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Filter;
|
||||
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
|
||||
/**
|
||||
* Filters features by their paths.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class PathsFilter extends SimpleFilter
|
||||
{
|
||||
protected $filterPaths = array();
|
||||
|
||||
/**
|
||||
* Initializes filter.
|
||||
*
|
||||
* @param string[] $paths List of approved paths
|
||||
*/
|
||||
public function __construct(array $paths)
|
||||
{
|
||||
$this->filterPaths = array_map(
|
||||
function ($realpath) {
|
||||
return rtrim($realpath, DIRECTORY_SEPARATOR) .
|
||||
(is_dir($realpath) ? DIRECTORY_SEPARATOR : '');
|
||||
},
|
||||
array_filter(
|
||||
array_map('realpath', $paths)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Feature matches specified filter.
|
||||
*
|
||||
* @param FeatureNode $feature Feature instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFeatureMatch(FeatureNode $feature)
|
||||
{
|
||||
foreach ($this->filterPaths as $path) {
|
||||
if (0 === strpos(realpath($feature->getFile()), $path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if scenario or outline matches specified filter.
|
||||
*
|
||||
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||
*
|
||||
* @return false This filter is designed to work only with features
|
||||
*/
|
||||
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Filter;
|
||||
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
|
||||
/**
|
||||
* Filters features by their actors role.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class RoleFilter extends SimpleFilter
|
||||
{
|
||||
protected $pattern;
|
||||
|
||||
/**
|
||||
* Initializes filter.
|
||||
*
|
||||
* @param string $role Approved role wildcard
|
||||
*/
|
||||
public function __construct($role)
|
||||
{
|
||||
$this->pattern = '/as an? ' . strtr(preg_quote($role, '/'), array(
|
||||
'\*' => '.*',
|
||||
'\?' => '.',
|
||||
'\[' => '[',
|
||||
'\]' => ']'
|
||||
)) . '[$\n]/i';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Feature matches specified filter.
|
||||
*
|
||||
* @param FeatureNode $feature Feature instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFeatureMatch(FeatureNode $feature)
|
||||
{
|
||||
return 1 === preg_match($this->pattern, $feature->getDescription() ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if scenario or outline matches specified filter.
|
||||
*
|
||||
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||
*
|
||||
* @return false This filter is designed to work only with features
|
||||
*/
|
||||
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Filter;
|
||||
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
|
||||
/**
|
||||
* Abstract filter class.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
abstract class SimpleFilter implements FilterInterface
|
||||
{
|
||||
/**
|
||||
* Filters feature according to the filter.
|
||||
*
|
||||
* @param FeatureNode $feature
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function filterFeature(FeatureNode $feature)
|
||||
{
|
||||
if ($this->isFeatureMatch($feature)) {
|
||||
return $feature;
|
||||
}
|
||||
|
||||
$scenarios = array();
|
||||
foreach ($feature->getScenarios() as $scenario) {
|
||||
if (!$this->isScenarioMatch($scenario)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
|
||||
return new FeatureNode(
|
||||
$feature->getTitle(),
|
||||
$feature->getDescription(),
|
||||
$feature->getTags(),
|
||||
$feature->getBackground(),
|
||||
$scenarios,
|
||||
$feature->getKeyword(),
|
||||
$feature->getLanguage(),
|
||||
$feature->getFile(),
|
||||
$feature->getLine()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Filter;
|
||||
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Node\OutlineNode;
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
|
||||
/**
|
||||
* Filters scenarios by feature/scenario tag.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class TagFilter extends ComplexFilter
|
||||
{
|
||||
protected $filterString;
|
||||
|
||||
/**
|
||||
* Initializes filter.
|
||||
*
|
||||
* @param string $filterString Name filter string
|
||||
*/
|
||||
public function __construct($filterString)
|
||||
{
|
||||
$this->filterString = trim($filterString);
|
||||
|
||||
if(preg_match('/\s/u', $this->filterString)) {
|
||||
trigger_error(
|
||||
"Tags with whitespace are deprecated and may be removed in a future version",
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters feature according to the filter.
|
||||
*
|
||||
* @param FeatureNode $feature
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function filterFeature(FeatureNode $feature)
|
||||
{
|
||||
$scenarios = array();
|
||||
foreach ($feature->getScenarios() as $scenario) {
|
||||
if (!$this->isScenarioMatch($feature, $scenario)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||
|
||||
$exampleTables = array();
|
||||
|
||||
foreach ($scenario->getExampleTables() as $exampleTable) {
|
||||
if ($this->isTagsMatchCondition(array_merge($feature->getTags(), $scenario->getTags(), $exampleTable->getTags()))) {
|
||||
$exampleTables[] = $exampleTable;
|
||||
}
|
||||
}
|
||||
|
||||
$scenario = new OutlineNode(
|
||||
$scenario->getTitle(),
|
||||
$scenario->getTags(),
|
||||
$scenario->getSteps(),
|
||||
$exampleTables,
|
||||
$scenario->getKeyword(),
|
||||
$scenario->getLine()
|
||||
);
|
||||
}
|
||||
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
|
||||
return new FeatureNode(
|
||||
$feature->getTitle(),
|
||||
$feature->getDescription(),
|
||||
$feature->getTags(),
|
||||
$feature->getBackground(),
|
||||
$scenarios,
|
||||
$feature->getKeyword(),
|
||||
$feature->getLanguage(),
|
||||
$feature->getFile(),
|
||||
$feature->getLine()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Feature matches specified filter.
|
||||
*
|
||||
* @param FeatureNode $feature Feature instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFeatureMatch(FeatureNode $feature)
|
||||
{
|
||||
return $this->isTagsMatchCondition($feature->getTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if scenario or outline matches specified filter.
|
||||
*
|
||||
* @param FeatureNode $feature Feature node instance
|
||||
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isScenarioMatch(FeatureNode $feature, ScenarioInterface $scenario)
|
||||
{
|
||||
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||
foreach ($scenario->getExampleTables() as $example) {
|
||||
if ($this->isTagsMatchCondition(array_merge($feature->getTags(), $scenario->getTags(), $example->getTags()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isTagsMatchCondition(array_merge($feature->getTags(), $scenario->getTags()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that node matches condition.
|
||||
*
|
||||
* @param string[] $tags
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isTagsMatchCondition($tags)
|
||||
{
|
||||
$satisfies = true;
|
||||
|
||||
foreach (explode('&&', $this->filterString) as $andTags) {
|
||||
$satisfiesComma = false;
|
||||
|
||||
foreach (explode(',', $andTags) as $tag) {
|
||||
$tag = str_replace('@', '', trim($tag));
|
||||
|
||||
if ('~' === $tag[0]) {
|
||||
$tag = mb_substr($tag, 1, mb_strlen($tag, 'utf8') - 1, 'utf8');
|
||||
$satisfiesComma = !in_array($tag, $tags) || $satisfiesComma;
|
||||
} else {
|
||||
$satisfiesComma = in_array($tag, $tags) || $satisfiesComma;
|
||||
}
|
||||
}
|
||||
|
||||
$satisfies = $satisfiesComma && $satisfies;
|
||||
}
|
||||
|
||||
return $satisfies;
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin;
|
||||
|
||||
use Behat\Gherkin\Filter\FeatureFilterInterface;
|
||||
use Behat\Gherkin\Filter\LineFilter;
|
||||
use Behat\Gherkin\Filter\LineRangeFilter;
|
||||
use Behat\Gherkin\Loader\FileLoaderInterface;
|
||||
use Behat\Gherkin\Loader\LoaderInterface;
|
||||
|
||||
/**
|
||||
* Gherkin manager.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class Gherkin
|
||||
{
|
||||
const VERSION = '4.8.0';
|
||||
|
||||
/**
|
||||
* @var LoaderInterface[]
|
||||
*/
|
||||
protected $loaders = array();
|
||||
/**
|
||||
* @var FeatureFilterInterface[]
|
||||
*/
|
||||
protected $filters = array();
|
||||
|
||||
/**
|
||||
* Adds loader to manager.
|
||||
*
|
||||
* @param LoaderInterface $loader Feature loader
|
||||
*/
|
||||
public function addLoader(LoaderInterface $loader)
|
||||
{
|
||||
$this->loaders[] = $loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds filter to manager.
|
||||
*
|
||||
* @param FeatureFilterInterface $filter Feature filter
|
||||
*/
|
||||
public function addFilter(FeatureFilterInterface $filter)
|
||||
{
|
||||
$this->filters[] = $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets filters to the parser.
|
||||
*
|
||||
* @param FeatureFilterInterface[] $filters
|
||||
*/
|
||||
public function setFilters(array $filters)
|
||||
{
|
||||
$this->filters = array();
|
||||
array_map(array($this, 'addFilter'), $filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets base features path.
|
||||
*
|
||||
* @param string $path Loaders base path
|
||||
*/
|
||||
public function setBasePath($path)
|
||||
{
|
||||
foreach ($this->loaders as $loader) {
|
||||
if ($loader instanceof FileLoaderInterface) {
|
||||
$loader->setBasePath($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads & filters resource with added loaders.
|
||||
*
|
||||
* @param mixed $resource Resource to load
|
||||
* @param FeatureFilterInterface[] $filters Additional filters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function load($resource, array $filters = array())
|
||||
{
|
||||
$filters = array_merge($this->filters, $filters);
|
||||
|
||||
$matches = array();
|
||||
if (preg_match('/^(.*)\:(\d+)-(\d+|\*)$/', $resource, $matches)) {
|
||||
$resource = $matches[1];
|
||||
$filters[] = new LineRangeFilter($matches[2], $matches[3]);
|
||||
} elseif (preg_match('/^(.*)\:(\d+)$/', $resource, $matches)) {
|
||||
$resource = $matches[1];
|
||||
$filters[] = new LineFilter($matches[2]);
|
||||
}
|
||||
|
||||
$loader = $this->resolveLoader($resource);
|
||||
|
||||
if (null === $loader) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$features = array();
|
||||
foreach ($loader->load($resource) as $feature) {
|
||||
foreach ($filters as $filter) {
|
||||
$feature = $filter->filterFeature($feature);
|
||||
|
||||
if (!$feature->hasScenarios() && !$filter->isFeatureMatch($feature)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$features[] = $feature;
|
||||
}
|
||||
|
||||
return $features;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves loader by resource.
|
||||
*
|
||||
* @param mixed $resource Resource to load
|
||||
*
|
||||
* @return LoaderInterface
|
||||
*/
|
||||
public function resolveLoader($resource)
|
||||
{
|
||||
foreach ($this->loaders as $loader) {
|
||||
if ($loader->supports($resource)) {
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Keywords;
|
||||
|
||||
/**
|
||||
* Array initializable keywords holder.
|
||||
*
|
||||
* $keywords = new Behat\Gherkin\Keywords\ArrayKeywords(array(
|
||||
* 'en' => array(
|
||||
* 'feature' => 'Feature',
|
||||
* 'background' => 'Background',
|
||||
* 'scenario' => 'Scenario',
|
||||
* 'scenario_outline' => 'Scenario Outline|Scenario Template',
|
||||
* 'examples' => 'Examples|Scenarios',
|
||||
* 'given' => 'Given',
|
||||
* 'when' => 'When',
|
||||
* 'then' => 'Then',
|
||||
* 'and' => 'And',
|
||||
* 'but' => 'But'
|
||||
* ),
|
||||
* 'ru' => array(
|
||||
* 'feature' => 'Функционал',
|
||||
* 'background' => 'Предыстория',
|
||||
* 'scenario' => 'Сценарий',
|
||||
* 'scenario_outline' => 'Структура сценария',
|
||||
* 'examples' => 'Примеры',
|
||||
* 'given' => 'Допустим',
|
||||
* 'when' => 'Если',
|
||||
* 'then' => 'То',
|
||||
* 'and' => 'И',
|
||||
* 'but' => 'Но'
|
||||
* )
|
||||
* ));
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class ArrayKeywords implements KeywordsInterface
|
||||
{
|
||||
private $keywords = array();
|
||||
private $keywordString = array();
|
||||
private $language;
|
||||
|
||||
/**
|
||||
* Initializes holder with keywords.
|
||||
*
|
||||
* @param array $keywords Keywords array
|
||||
*/
|
||||
public function __construct(array $keywords)
|
||||
{
|
||||
$this->keywords = $keywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets keywords holder language.
|
||||
*
|
||||
* @param string $language Language name
|
||||
*/
|
||||
public function setLanguage($language)
|
||||
{
|
||||
if (!isset($this->keywords[$language])) {
|
||||
$this->language = 'en';
|
||||
} else {
|
||||
$this->language = $language;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Feature keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFeatureKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['feature'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Background keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBackgroundKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['background'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Scenario keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getScenarioKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['scenario'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Scenario Outline keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOutlineKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['scenario_outline'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Examples keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExamplesKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['examples'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Given keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getGivenKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['given'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns When keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWhenKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['when'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Then keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getThenKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['then'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns And keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAndKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['and'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns But keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getButKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['but'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all step keywords (Given, When, Then, And, But).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getStepKeywords()
|
||||
{
|
||||
if (!isset($this->keywordString[$this->language])) {
|
||||
$keywords = array_merge(
|
||||
explode('|', $this->getGivenKeywords()),
|
||||
explode('|', $this->getWhenKeywords()),
|
||||
explode('|', $this->getThenKeywords()),
|
||||
explode('|', $this->getAndKeywords()),
|
||||
explode('|', $this->getButKeywords())
|
||||
);
|
||||
|
||||
usort($keywords, function ($keyword1, $keyword2) {
|
||||
return mb_strlen($keyword2, 'utf8') - mb_strlen($keyword1, 'utf8');
|
||||
});
|
||||
|
||||
$this->keywordString[$this->language] = implode('|', $keywords);
|
||||
}
|
||||
|
||||
return $this->keywordString[$this->language];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Keywords;
|
||||
|
||||
/**
|
||||
* File initializable keywords holder.
|
||||
*
|
||||
* $keywords = new Behat\Gherkin\Keywords\CachedArrayKeywords($file);
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class CachedArrayKeywords extends ArrayKeywords
|
||||
{
|
||||
/**
|
||||
* Initializes holder with file.
|
||||
*
|
||||
* @param string $file Cached array path
|
||||
*/
|
||||
public function __construct($file)
|
||||
{
|
||||
parent::__construct(include($file));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Keywords;
|
||||
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* Cucumber-translations reader.
|
||||
*
|
||||
* $keywords = new Behat\Gherkin\Keywords\CucumberKeywords($i18nYmlPath);
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class CucumberKeywords extends ArrayKeywords
|
||||
{
|
||||
/**
|
||||
* Initializes holder with yaml string OR file.
|
||||
*
|
||||
* @param string $yaml Yaml string or file path
|
||||
*/
|
||||
public function __construct($yaml)
|
||||
{
|
||||
// Handle filename explicitly for BC reasons, as Symfony Yaml 3.0 does not do it anymore
|
||||
$file = null;
|
||||
if (strpos($yaml, "\n") === false && is_file($yaml)) {
|
||||
if (false === is_readable($yaml)) {
|
||||
throw new ParseException(sprintf('Unable to parse "%s" as the file is not readable.', $yaml));
|
||||
}
|
||||
|
||||
$file = $yaml;
|
||||
$yaml = file_get_contents($file);
|
||||
}
|
||||
|
||||
try {
|
||||
$content = Yaml::parse($yaml);
|
||||
} catch (ParseException $e) {
|
||||
if ($file) {
|
||||
$e->setParsedFile($file);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
parent::__construct($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Feature keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getGivenKeywords()
|
||||
{
|
||||
return $this->prepareStepString(parent::getGivenKeywords());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns When keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWhenKeywords()
|
||||
{
|
||||
return $this->prepareStepString(parent::getWhenKeywords());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Then keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getThenKeywords()
|
||||
{
|
||||
return $this->prepareStepString(parent::getThenKeywords());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns And keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAndKeywords()
|
||||
{
|
||||
return $this->prepareStepString(parent::getAndKeywords());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns But keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getButKeywords()
|
||||
{
|
||||
return $this->prepareStepString(parent::getButKeywords());
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim *| from the begining of the list.
|
||||
*
|
||||
* @param string $keywordsString Keywords string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function prepareStepString($keywordsString)
|
||||
{
|
||||
if (0 === mb_strpos($keywordsString, '*|', 0, 'UTF-8')) {
|
||||
$keywordsString = mb_substr($keywordsString, 2, mb_strlen($keywordsString, 'utf8') - 2, 'utf8');
|
||||
}
|
||||
|
||||
return $keywordsString;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Keywords;
|
||||
|
||||
/**
|
||||
* Gherkin keywords dumper.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class KeywordsDumper
|
||||
{
|
||||
private $keywords;
|
||||
private $keywordsDumper;
|
||||
|
||||
/**
|
||||
* Initializes dumper.
|
||||
*
|
||||
* @param KeywordsInterface $keywords Keywords instance
|
||||
*/
|
||||
public function __construct(KeywordsInterface $keywords)
|
||||
{
|
||||
$this->keywords = $keywords;
|
||||
$this->keywordsDumper = array($this, 'dumpKeywords');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets keywords mapper function.
|
||||
*
|
||||
* Callable should accept 2 arguments (array $keywords and bool $isShort)
|
||||
*
|
||||
* @param callable $mapper Mapper function
|
||||
*/
|
||||
public function setKeywordsDumperFunction($mapper)
|
||||
{
|
||||
$this->keywordsDumper = $mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults keywords dumper.
|
||||
*
|
||||
* @param array $keywords Keywords list
|
||||
* @param bool $isShort Is short version
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dumpKeywords(array $keywords, $isShort)
|
||||
{
|
||||
if ($isShort) {
|
||||
return 1 < count($keywords) ? '(' . implode('|', $keywords) . ')' : $keywords[0];
|
||||
}
|
||||
|
||||
return $keywords[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps keyworded feature into string.
|
||||
*
|
||||
* @param string $language Keywords language
|
||||
* @param bool $short Dump short version
|
||||
* @param bool $excludeAsterisk
|
||||
*
|
||||
* @return string|array String for short version and array of features for extended
|
||||
*/
|
||||
public function dump($language, $short = true, $excludeAsterisk = false)
|
||||
{
|
||||
$this->keywords->setLanguage($language);
|
||||
$languageComment = '';
|
||||
if ('en' !== $language) {
|
||||
$languageComment = "# language: $language\n";
|
||||
}
|
||||
|
||||
$keywords = explode('|', $this->keywords->getFeatureKeywords());
|
||||
|
||||
if ($short) {
|
||||
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
|
||||
|
||||
return trim($languageComment . $this->dumpFeature($keywords, $short, $excludeAsterisk));
|
||||
}
|
||||
|
||||
$features = array();
|
||||
foreach ($keywords as $keyword) {
|
||||
$keyword = call_user_func($this->keywordsDumper, array($keyword), $short);
|
||||
$features[] = trim($languageComment . $this->dumpFeature($keyword, $short, $excludeAsterisk));
|
||||
}
|
||||
|
||||
return $features;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps feature example.
|
||||
*
|
||||
* @param string $keyword Item keyword
|
||||
* @param bool $short Dump short version?
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function dumpFeature($keyword, $short = true, $excludeAsterisk = false)
|
||||
{
|
||||
$dump = <<<GHERKIN
|
||||
{$keyword}: Internal operations
|
||||
In order to stay secret
|
||||
As a secret organization
|
||||
We need to be able to erase past agents' memory
|
||||
|
||||
|
||||
GHERKIN;
|
||||
|
||||
// Background
|
||||
$keywords = explode('|', $this->keywords->getBackgroundKeywords());
|
||||
if ($short) {
|
||||
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
|
||||
$dump .= $this->dumpBackground($keywords, $short, $excludeAsterisk);
|
||||
} else {
|
||||
$keyword = call_user_func($this->keywordsDumper, array($keywords[0]), $short);
|
||||
$dump .= $this->dumpBackground($keyword, $short, $excludeAsterisk);
|
||||
}
|
||||
|
||||
// Scenario
|
||||
$keywords = explode('|', $this->keywords->getScenarioKeywords());
|
||||
if ($short) {
|
||||
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
|
||||
$dump .= $this->dumpScenario($keywords, $short, $excludeAsterisk);
|
||||
} else {
|
||||
foreach ($keywords as $keyword) {
|
||||
$keyword = call_user_func($this->keywordsDumper, array($keyword), $short);
|
||||
$dump .= $this->dumpScenario($keyword, $short, $excludeAsterisk);
|
||||
}
|
||||
}
|
||||
|
||||
// Outline
|
||||
$keywords = explode('|', $this->keywords->getOutlineKeywords());
|
||||
if ($short) {
|
||||
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
|
||||
$dump .= $this->dumpOutline($keywords, $short, $excludeAsterisk);
|
||||
} else {
|
||||
foreach ($keywords as $keyword) {
|
||||
$keyword = call_user_func($this->keywordsDumper, array($keyword), $short);
|
||||
$dump .= $this->dumpOutline($keyword, $short, $excludeAsterisk);
|
||||
}
|
||||
}
|
||||
|
||||
return $dump;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps background example.
|
||||
*
|
||||
* @param string $keyword Item keyword
|
||||
* @param bool $short Dump short version?
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function dumpBackground($keyword, $short = true, $excludeAsterisk = false)
|
||||
{
|
||||
$dump = <<<GHERKIN
|
||||
{$keyword}:
|
||||
|
||||
GHERKIN;
|
||||
|
||||
// Given
|
||||
$dump .= $this->dumpStep(
|
||||
$this->keywords->getGivenKeywords(),
|
||||
'there is agent A',
|
||||
$short,
|
||||
$excludeAsterisk
|
||||
);
|
||||
|
||||
// And
|
||||
$dump .= $this->dumpStep(
|
||||
$this->keywords->getAndKeywords(),
|
||||
'there is agent B',
|
||||
$short,
|
||||
$excludeAsterisk
|
||||
);
|
||||
|
||||
return $dump . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps scenario example.
|
||||
*
|
||||
* @param string $keyword Item keyword
|
||||
* @param bool $short Dump short version?
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function dumpScenario($keyword, $short = true, $excludeAsterisk = false)
|
||||
{
|
||||
$dump = <<<GHERKIN
|
||||
{$keyword}: Erasing agent memory
|
||||
|
||||
GHERKIN;
|
||||
|
||||
// Given
|
||||
$dump .= $this->dumpStep(
|
||||
$this->keywords->getGivenKeywords(),
|
||||
'there is agent J',
|
||||
$short,
|
||||
$excludeAsterisk
|
||||
);
|
||||
|
||||
// And
|
||||
$dump .= $this->dumpStep(
|
||||
$this->keywords->getAndKeywords(),
|
||||
'there is agent K',
|
||||
$short,
|
||||
$excludeAsterisk
|
||||
);
|
||||
|
||||
// When
|
||||
$dump .= $this->dumpStep(
|
||||
$this->keywords->getWhenKeywords(),
|
||||
'I erase agent K\'s memory',
|
||||
$short,
|
||||
$excludeAsterisk
|
||||
);
|
||||
|
||||
// Then
|
||||
$dump .= $this->dumpStep(
|
||||
$this->keywords->getThenKeywords(),
|
||||
'there should be agent J',
|
||||
$short,
|
||||
$excludeAsterisk
|
||||
);
|
||||
|
||||
// But
|
||||
$dump .= $this->dumpStep(
|
||||
$this->keywords->getButKeywords(),
|
||||
'there should not be agent K',
|
||||
$short,
|
||||
$excludeAsterisk
|
||||
);
|
||||
|
||||
return $dump . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps outline example.
|
||||
*
|
||||
* @param string $keyword Item keyword
|
||||
* @param bool $short Dump short version?
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function dumpOutline($keyword, $short = true, $excludeAsterisk = false)
|
||||
{
|
||||
$dump = <<<GHERKIN
|
||||
{$keyword}: Erasing other agents' memory
|
||||
|
||||
GHERKIN;
|
||||
|
||||
// Given
|
||||
$dump .= $this->dumpStep(
|
||||
$this->keywords->getGivenKeywords(),
|
||||
'there is agent <agent1>',
|
||||
$short,
|
||||
$excludeAsterisk
|
||||
);
|
||||
|
||||
// And
|
||||
$dump .= $this->dumpStep(
|
||||
$this->keywords->getAndKeywords(),
|
||||
'there is agent <agent2>',
|
||||
$short,
|
||||
$excludeAsterisk
|
||||
);
|
||||
|
||||
// When
|
||||
$dump .= $this->dumpStep(
|
||||
$this->keywords->getWhenKeywords(),
|
||||
'I erase agent <agent2>\'s memory',
|
||||
$short,
|
||||
$excludeAsterisk
|
||||
);
|
||||
|
||||
// Then
|
||||
$dump .= $this->dumpStep(
|
||||
$this->keywords->getThenKeywords(),
|
||||
'there should be agent <agent1>',
|
||||
$short,
|
||||
$excludeAsterisk
|
||||
);
|
||||
|
||||
// But
|
||||
$dump .= $this->dumpStep(
|
||||
$this->keywords->getButKeywords(),
|
||||
'there should not be agent <agent2>',
|
||||
$short,
|
||||
$excludeAsterisk
|
||||
);
|
||||
|
||||
$keywords = explode('|', $this->keywords->getExamplesKeywords());
|
||||
if ($short) {
|
||||
$keyword = call_user_func($this->keywordsDumper, $keywords, $short);
|
||||
} else {
|
||||
$keyword = call_user_func($this->keywordsDumper, array($keywords[0]), $short);
|
||||
}
|
||||
|
||||
$dump .= <<<GHERKIN
|
||||
|
||||
{$keyword}:
|
||||
| agent1 | agent2 |
|
||||
| D | M |
|
||||
|
||||
GHERKIN;
|
||||
|
||||
return $dump . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps step example.
|
||||
*
|
||||
* @param string $keywords Item keyword
|
||||
* @param string $text Step text
|
||||
* @param bool $short Dump short version?
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function dumpStep($keywords, $text, $short = true, $excludeAsterisk = false)
|
||||
{
|
||||
$dump = '';
|
||||
|
||||
$keywords = explode('|', $keywords);
|
||||
if ($short) {
|
||||
$keywords = array_map(
|
||||
function ($keyword) {
|
||||
return str_replace('<', '', $keyword);
|
||||
},
|
||||
$keywords
|
||||
);
|
||||
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
|
||||
$dump .= <<<GHERKIN
|
||||
{$keywords} {$text}
|
||||
|
||||
GHERKIN;
|
||||
} else {
|
||||
foreach ($keywords as $keyword) {
|
||||
if ($excludeAsterisk && '*' === $keyword) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$indent = ' ';
|
||||
if (false !== mb_strpos($keyword, '<', 0, 'utf8')) {
|
||||
$keyword = mb_substr($keyword, 0, -1, 'utf8');
|
||||
$indent = '';
|
||||
}
|
||||
$keyword = call_user_func($this->keywordsDumper, array($keyword), $short);
|
||||
$dump .= <<<GHERKIN
|
||||
{$keyword}{$indent}{$text}
|
||||
|
||||
GHERKIN;
|
||||
}
|
||||
}
|
||||
|
||||
return $dump;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Keywords;
|
||||
|
||||
/**
|
||||
* Keywords holder interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface KeywordsInterface
|
||||
{
|
||||
/**
|
||||
* Sets keywords holder language.
|
||||
*
|
||||
* @param string $language Language name
|
||||
*/
|
||||
public function setLanguage($language);
|
||||
|
||||
/**
|
||||
* Returns Feature keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFeatureKeywords();
|
||||
|
||||
/**
|
||||
* Returns Background keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBackgroundKeywords();
|
||||
|
||||
/**
|
||||
* Returns Scenario keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getScenarioKeywords();
|
||||
|
||||
/**
|
||||
* Returns Scenario Outline keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOutlineKeywords();
|
||||
|
||||
/**
|
||||
* Returns Examples keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExamplesKeywords();
|
||||
|
||||
/**
|
||||
* Returns Given keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getGivenKeywords();
|
||||
|
||||
/**
|
||||
* Returns When keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWhenKeywords();
|
||||
|
||||
/**
|
||||
* Returns Then keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getThenKeywords();
|
||||
|
||||
/**
|
||||
* Returns And keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAndKeywords();
|
||||
|
||||
/**
|
||||
* Returns But keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getButKeywords();
|
||||
|
||||
/**
|
||||
* Returns all step keywords (splitted by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getStepKeywords();
|
||||
}
|
||||
+653
@@ -0,0 +1,653 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin;
|
||||
|
||||
use Behat\Gherkin\Exception\LexerException;
|
||||
use Behat\Gherkin\Keywords\KeywordsInterface;
|
||||
|
||||
/**
|
||||
* Gherkin lexer.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class Lexer
|
||||
{
|
||||
private $language;
|
||||
private $lines;
|
||||
private $linesCount;
|
||||
private $line;
|
||||
private $trimmedLine;
|
||||
private $lineNumber;
|
||||
private $eos;
|
||||
private $keywords;
|
||||
private $keywordsCache = array();
|
||||
private $stepKeywordTypesCache = array();
|
||||
private $deferredObjects = array();
|
||||
private $deferredObjectsCount = 0;
|
||||
private $stashedToken;
|
||||
private $inPyString = false;
|
||||
private $pyStringSwallow = 0;
|
||||
private $featureStarted = false;
|
||||
private $allowMultilineArguments = false;
|
||||
private $allowSteps = false;
|
||||
private $pyStringDelimiter = null;
|
||||
|
||||
/**
|
||||
* Initializes lexer.
|
||||
*
|
||||
* @param KeywordsInterface $keywords Keywords holder
|
||||
*/
|
||||
public function __construct(KeywordsInterface $keywords)
|
||||
{
|
||||
$this->keywords = $keywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets lexer input.
|
||||
*
|
||||
* @param string $input Input string
|
||||
* @param string $language Language name
|
||||
*
|
||||
* @throws Exception\LexerException
|
||||
*/
|
||||
public function analyse($input, $language = 'en')
|
||||
{
|
||||
// try to detect unsupported encoding
|
||||
if ('UTF-8' !== mb_detect_encoding($input, 'UTF-8', true)) {
|
||||
throw new LexerException('Feature file is not in UTF8 encoding');
|
||||
}
|
||||
|
||||
$input = strtr($input, array("\r\n" => "\n", "\r" => "\n"));
|
||||
|
||||
$this->lines = explode("\n", $input);
|
||||
$this->linesCount = count($this->lines);
|
||||
$this->line = $this->lines[0];
|
||||
$this->lineNumber = 1;
|
||||
$this->trimmedLine = null;
|
||||
$this->eos = false;
|
||||
|
||||
$this->deferredObjects = array();
|
||||
$this->deferredObjectsCount = 0;
|
||||
$this->stashedToken = null;
|
||||
$this->inPyString = false;
|
||||
$this->pyStringSwallow = 0;
|
||||
|
||||
$this->featureStarted = false;
|
||||
$this->allowMultilineArguments = false;
|
||||
$this->allowSteps = false;
|
||||
|
||||
$this->keywords->setLanguage($this->language = $language);
|
||||
$this->keywordsCache = array();
|
||||
$this->stepKeywordTypesCache = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current lexer language.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next token or previously stashed one.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAdvancedToken()
|
||||
{
|
||||
return $this->getStashedToken() ?: $this->getNextToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defers token.
|
||||
*
|
||||
* @param array $token Token to defer
|
||||
*/
|
||||
public function deferToken(array $token)
|
||||
{
|
||||
$token['deferred'] = true;
|
||||
$this->deferredObjects[] = $token;
|
||||
++$this->deferredObjectsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicts the upcoming token without passing over it.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function predictToken()
|
||||
{
|
||||
if (null === $this->stashedToken) {
|
||||
$this->stashedToken = $this->getNextToken();
|
||||
}
|
||||
|
||||
return $this->stashedToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips over the currently-predicted token, if any.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function skipPredictedToken()
|
||||
{
|
||||
$this->stashedToken = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs token with specified parameters.
|
||||
*
|
||||
* @param string $type Token type
|
||||
* @param string $value Token value
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function takeToken($type, $value = null)
|
||||
{
|
||||
return array(
|
||||
'type' => $type,
|
||||
'line' => $this->lineNumber,
|
||||
'value' => $value ?: null,
|
||||
'deferred' => false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes line from input & increments line counter.
|
||||
*/
|
||||
protected function consumeLine()
|
||||
{
|
||||
++$this->lineNumber;
|
||||
|
||||
if (($this->lineNumber - 1) === $this->linesCount) {
|
||||
$this->eos = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->line = $this->lines[$this->lineNumber - 1];
|
||||
$this->trimmedLine = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes first part of line from input without incrementing the line number
|
||||
*/
|
||||
protected function consumeLineUntil(int $trimmedOffset)
|
||||
{
|
||||
$this->line = mb_substr(ltrim($this->line), $trimmedOffset, null, 'utf-8');
|
||||
$this->trimmedLine = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns trimmed version of line.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getTrimmedLine()
|
||||
{
|
||||
return null !== $this->trimmedLine ? $this->trimmedLine : $this->trimmedLine = trim($this->line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stashed token or null if hasn't.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getStashedToken()
|
||||
{
|
||||
$stashedToken = $this->stashedToken;
|
||||
$this->stashedToken = null;
|
||||
|
||||
return $stashedToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns deferred token or null if hasn't.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getDeferredToken()
|
||||
{
|
||||
if (!$this->deferredObjectsCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
--$this->deferredObjectsCount;
|
||||
|
||||
return array_shift($this->deferredObjects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next token from input.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getNextToken()
|
||||
{
|
||||
return $this->getDeferredToken()
|
||||
?: $this->scanEOS()
|
||||
?: $this->scanLanguage()
|
||||
?: $this->scanComment()
|
||||
?: $this->scanPyStringOp()
|
||||
?: $this->scanPyStringContent()
|
||||
?: $this->scanStep()
|
||||
?: $this->scanScenario()
|
||||
?: $this->scanBackground()
|
||||
?: $this->scanOutline()
|
||||
?: $this->scanExamples()
|
||||
?: $this->scanFeature()
|
||||
?: $this->scanTags()
|
||||
?: $this->scanTableRow()
|
||||
?: $this->scanNewline()
|
||||
?: $this->scanText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans for token with specified regex.
|
||||
*
|
||||
* @param string $regex Regular expression
|
||||
* @param string $type Expected token type
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanInput($regex, $type)
|
||||
{
|
||||
if (!preg_match($regex, $this->line, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = $this->takeToken($type, $matches[1]);
|
||||
$this->consumeLine();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans for token with specified keywords.
|
||||
*
|
||||
* @param string $keywords Keywords (splitted with |)
|
||||
* @param string $type Expected token type
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanInputForKeywords($keywords, $type)
|
||||
{
|
||||
if (!preg_match('/^(\s*)(' . $keywords . '):\s*(.*)/u', $this->line, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = $this->takeToken($type, $matches[3]);
|
||||
$token['keyword'] = $matches[2];
|
||||
$token['indent'] = mb_strlen($matches[1], 'utf8');
|
||||
|
||||
$this->consumeLine();
|
||||
|
||||
// turn off language searching
|
||||
if ('Feature' === $type) {
|
||||
$this->featureStarted = true;
|
||||
}
|
||||
|
||||
// turn off PyString and Table searching
|
||||
if ('Feature' === $type || 'Scenario' === $type || 'Outline' === $type) {
|
||||
$this->allowMultilineArguments = false;
|
||||
} elseif ('Examples' === $type) {
|
||||
$this->allowMultilineArguments = true;
|
||||
}
|
||||
|
||||
// turn on steps searching
|
||||
if ('Scenario' === $type || 'Background' === $type || 'Outline' === $type) {
|
||||
$this->allowSteps = true;
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans EOS from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanEOS()
|
||||
{
|
||||
if (!$this->eos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->takeToken('EOS');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns keywords for provided type.
|
||||
*
|
||||
* @param string $type Keyword type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getKeywords($type)
|
||||
{
|
||||
if (!isset($this->keywordsCache[$type])) {
|
||||
$getter = 'get' . $type . 'Keywords';
|
||||
$keywords = $this->keywords->$getter();
|
||||
|
||||
if ('Step' === $type) {
|
||||
$padded = array();
|
||||
foreach (explode('|', $keywords) as $keyword) {
|
||||
$padded[] = false !== mb_strpos($keyword, '<', 0, 'utf8')
|
||||
? preg_quote(mb_substr($keyword, 0, -1, 'utf8'), '/') . '\s*'
|
||||
: preg_quote($keyword, '/') . '\s+';
|
||||
}
|
||||
|
||||
$keywords = implode('|', $padded);
|
||||
}
|
||||
|
||||
$this->keywordsCache[$type] = $keywords;
|
||||
}
|
||||
|
||||
return $this->keywordsCache[$type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Feature from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanFeature()
|
||||
{
|
||||
return $this->scanInputForKeywords($this->getKeywords('Feature'), 'Feature');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Background from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanBackground()
|
||||
{
|
||||
return $this->scanInputForKeywords($this->getKeywords('Background'), 'Background');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Scenario from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanScenario()
|
||||
{
|
||||
return $this->scanInputForKeywords($this->getKeywords('Scenario'), 'Scenario');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Scenario Outline from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanOutline()
|
||||
{
|
||||
return $this->scanInputForKeywords($this->getKeywords('Outline'), 'Outline');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Scenario Outline Examples from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanExamples()
|
||||
{
|
||||
return $this->scanInputForKeywords($this->getKeywords('Examples'), 'Examples');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Step from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanStep()
|
||||
{
|
||||
if (!$this->allowSteps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$keywords = $this->getKeywords('Step');
|
||||
if (!preg_match('/^\s*(' . $keywords . ')([^\s].*)/u', $this->line, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$keyword = trim($matches[1]);
|
||||
$token = $this->takeToken('Step', $keyword);
|
||||
$token['keyword_type'] = $this->getStepKeywordType($keyword);
|
||||
$token['text'] = $matches[2];
|
||||
|
||||
$this->consumeLine();
|
||||
$this->allowMultilineArguments = true;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans PyString from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanPyStringOp()
|
||||
{
|
||||
if (!$this->allowMultilineArguments) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!preg_match('/^\s*(?<delimiter>"""|```)/u', $this->line, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
['delimiter' => [0 => $delimiter, 1 => $indent]] = $matches;
|
||||
|
||||
if ($this->inPyString) {
|
||||
if ($this->pyStringDelimiter !== $delimiter) {
|
||||
return null;
|
||||
}
|
||||
$this->pyStringDelimiter = null;
|
||||
}
|
||||
else {
|
||||
$this->pyStringDelimiter= $delimiter;
|
||||
}
|
||||
|
||||
$this->inPyString = !$this->inPyString;
|
||||
$token = $this->takeToken('PyStringOp');
|
||||
$this->pyStringSwallow = $indent;
|
||||
|
||||
$this->consumeLine();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans PyString content.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanPyStringContent()
|
||||
{
|
||||
if (!$this->inPyString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = $this->scanText();
|
||||
// swallow trailing spaces
|
||||
$token['value'] = preg_replace('/^\s{0,' . $this->pyStringSwallow . '}/u', '', $token['value'] ?? '');
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Table Row from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanTableRow()
|
||||
{
|
||||
if (!$this->allowMultilineArguments) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$line = $this->getTrimmedLine();
|
||||
if (!isset($line[0]) || '|' !== $line[0] || '|' !== substr($line, -1)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = $this->takeToken('TableRow');
|
||||
$line = mb_substr($line, 1, mb_strlen($line, 'utf8') - 2, 'utf8');
|
||||
$columns = array_map(function ($column) {
|
||||
return trim(str_replace('\\|', '|', $column));
|
||||
}, preg_split('/(?<!\\\)\|/u', $line));
|
||||
$token['columns'] = $columns;
|
||||
|
||||
$this->consumeLine();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Tags from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanTags()
|
||||
{
|
||||
$line = $this->getTrimmedLine();
|
||||
|
||||
if (!isset($line[0]) || '@' !== $line[0]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(preg_match('/^(?<line>.*)\s+#.*$/', $line, $matches)) {
|
||||
['line' => $line] = $matches;
|
||||
$this->consumeLineUntil(mb_strlen($line, 'utf-8'));
|
||||
} else {
|
||||
$this->consumeLine();
|
||||
}
|
||||
|
||||
$token = $this->takeToken('Tag');
|
||||
$tags = explode('@', mb_substr($line, 1, mb_strlen($line, 'utf8') - 1, 'utf8'));
|
||||
$tags = array_map('trim', $tags);
|
||||
$token['tags'] = $tags;
|
||||
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Language specifier from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanLanguage()
|
||||
{
|
||||
if ($this->featureStarted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->inPyString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (0 !== mb_strpos(ltrim($this->line), '#', 0, 'utf8')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->scanInput('/^\s*\#\s*language:\s*([\w_\-]+)\s*$/', 'Language');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Comment from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanComment()
|
||||
{
|
||||
if ($this->inPyString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$line = $this->getTrimmedLine();
|
||||
if (0 !== mb_strpos($line, '#', 0, 'utf8')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = $this->takeToken('Comment', $line);
|
||||
$this->consumeLine();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Newline from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanNewline()
|
||||
{
|
||||
if ('' !== $this->getTrimmedLine()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = $this->takeToken('Newline', mb_strlen($this->line, 'utf8'));
|
||||
$this->consumeLine();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans text from input & returns it if found.
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function scanText()
|
||||
{
|
||||
$token = $this->takeToken('Text', $this->line);
|
||||
$this->consumeLine();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns step type keyword (Given, When, Then, etc.).
|
||||
*
|
||||
* @param string $native Step keyword in provided language
|
||||
* @return string
|
||||
*/
|
||||
private function getStepKeywordType($native)
|
||||
{
|
||||
// Consider "*" as a AND keyword so that it is normalized to the previous step type
|
||||
if ('*' === $native) {
|
||||
return 'And';
|
||||
}
|
||||
|
||||
if (empty($this->stepKeywordTypesCache)) {
|
||||
$this->stepKeywordTypesCache = array(
|
||||
'Given' => explode('|', $this->keywords->getGivenKeywords()),
|
||||
'When' => explode('|', $this->keywords->getWhenKeywords()),
|
||||
'Then' => explode('|', $this->keywords->getThenKeywords()),
|
||||
'And' => explode('|', $this->keywords->getAndKeywords()),
|
||||
'But' => explode('|', $this->keywords->getButKeywords())
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($this->stepKeywordTypesCache as $type => $keywords) {
|
||||
if (in_array($native, $keywords) || in_array($native . '<', $keywords)) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
return 'Given';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Loader;
|
||||
|
||||
/**
|
||||
* Abstract filesystem loader.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
abstract class AbstractFileLoader implements FileLoaderInterface
|
||||
{
|
||||
protected $basePath;
|
||||
|
||||
/**
|
||||
* Sets base features path.
|
||||
*
|
||||
* @param string $path Base loader path
|
||||
*/
|
||||
public function setBasePath($path)
|
||||
{
|
||||
$this->basePath = realpath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds relative path for provided absolute (relative to base features path).
|
||||
*
|
||||
* @param string $path Absolute path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function findRelativePath($path)
|
||||
{
|
||||
if (null !== $this->basePath) {
|
||||
return strtr($path, array($this->basePath . DIRECTORY_SEPARATOR => ''));
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds absolute path for provided relative (relative to base features path).
|
||||
*
|
||||
* @param string $path Relative path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function findAbsolutePath($path)
|
||||
{
|
||||
if (is_file($path) || is_dir($path)) {
|
||||
return realpath($path);
|
||||
}
|
||||
|
||||
if (null === $this->basePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_file($this->basePath . DIRECTORY_SEPARATOR . $path)
|
||||
|| is_dir($this->basePath . DIRECTORY_SEPARATOR . $path)) {
|
||||
return realpath($this->basePath . DIRECTORY_SEPARATOR . $path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Loader;
|
||||
|
||||
use Behat\Gherkin\Node\BackgroundNode;
|
||||
use Behat\Gherkin\Node\ExampleTableNode;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Node\OutlineNode;
|
||||
use Behat\Gherkin\Node\PyStringNode;
|
||||
use Behat\Gherkin\Node\ScenarioNode;
|
||||
use Behat\Gherkin\Node\StepNode;
|
||||
use Behat\Gherkin\Node\TableNode;
|
||||
|
||||
/**
|
||||
* From-array loader.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class ArrayLoader implements LoaderInterface
|
||||
{
|
||||
/**
|
||||
* Checks if current loader supports provided resource.
|
||||
*
|
||||
* @param mixed $resource Resource to load
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function supports($resource)
|
||||
{
|
||||
return is_array($resource) && (isset($resource['features']) || isset($resource['feature']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads features from provided resource.
|
||||
*
|
||||
* @param mixed $resource Resource to load
|
||||
*
|
||||
* @return FeatureNode[]
|
||||
*/
|
||||
public function load($resource)
|
||||
{
|
||||
$features = array();
|
||||
|
||||
if (isset($resource['features'])) {
|
||||
foreach ($resource['features'] as $iterator => $hash) {
|
||||
$feature = $this->loadFeatureHash($hash, $iterator);
|
||||
$features[] = $feature;
|
||||
}
|
||||
} elseif (isset($resource['feature'])) {
|
||||
$feature = $this->loadFeatureHash($resource['feature']);
|
||||
$features[] = $feature;
|
||||
}
|
||||
|
||||
return $features;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads feature from provided feature hash.
|
||||
*
|
||||
* @param array $hash Feature hash
|
||||
* @param integer $line
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
protected function loadFeatureHash(array $hash, $line = 0)
|
||||
{
|
||||
$hash = array_merge(
|
||||
array(
|
||||
'title' => null,
|
||||
'description' => null,
|
||||
'tags' => array(),
|
||||
'keyword' => 'Feature',
|
||||
'language' => 'en',
|
||||
'line' => $line,
|
||||
'scenarios' => array(),
|
||||
),
|
||||
$hash
|
||||
);
|
||||
$background = isset($hash['background']) ? $this->loadBackgroundHash($hash['background']) : null;
|
||||
|
||||
$scenarios = array();
|
||||
foreach ((array) $hash['scenarios'] as $scenarioIterator => $scenarioHash) {
|
||||
if (isset($scenarioHash['type']) && 'outline' === $scenarioHash['type']) {
|
||||
$scenarios[] = $this->loadOutlineHash($scenarioHash, $scenarioIterator);
|
||||
} else {
|
||||
$scenarios[] = $this->loadScenarioHash($scenarioHash, $scenarioIterator);
|
||||
}
|
||||
}
|
||||
|
||||
return new FeatureNode($hash['title'], $hash['description'], $hash['tags'], $background, $scenarios, $hash['keyword'], $hash['language'], null, $hash['line']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads background from provided hash.
|
||||
*
|
||||
* @param array $hash Background hash
|
||||
*
|
||||
* @return BackgroundNode
|
||||
*/
|
||||
protected function loadBackgroundHash(array $hash)
|
||||
{
|
||||
$hash = array_merge(
|
||||
array(
|
||||
'title' => null,
|
||||
'keyword' => 'Background',
|
||||
'line' => 0,
|
||||
'steps' => array(),
|
||||
),
|
||||
$hash
|
||||
);
|
||||
|
||||
$steps = $this->loadStepsHash($hash['steps']);
|
||||
|
||||
return new BackgroundNode($hash['title'], $steps, $hash['keyword'], $hash['line']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads scenario from provided scenario hash.
|
||||
*
|
||||
* @param array $hash Scenario hash
|
||||
* @param integer $line Scenario definition line
|
||||
*
|
||||
* @return ScenarioNode
|
||||
*/
|
||||
protected function loadScenarioHash(array $hash, $line = 0)
|
||||
{
|
||||
$hash = array_merge(
|
||||
array(
|
||||
'title' => null,
|
||||
'tags' => array(),
|
||||
'keyword' => 'Scenario',
|
||||
'line' => $line,
|
||||
'steps' => array(),
|
||||
),
|
||||
$hash
|
||||
);
|
||||
|
||||
$steps = $this->loadStepsHash($hash['steps']);
|
||||
|
||||
return new ScenarioNode($hash['title'], $hash['tags'], $steps, $hash['keyword'], $hash['line']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads outline from provided outline hash.
|
||||
*
|
||||
* @param array $hash Outline hash
|
||||
* @param integer $line Outline definition line
|
||||
*
|
||||
* @return OutlineNode
|
||||
*/
|
||||
protected function loadOutlineHash(array $hash, $line = 0)
|
||||
{
|
||||
$hash = array_merge(
|
||||
array(
|
||||
'title' => null,
|
||||
'tags' => array(),
|
||||
'keyword' => 'Scenario Outline',
|
||||
'line' => $line,
|
||||
'steps' => array(),
|
||||
'examples' => array(),
|
||||
),
|
||||
$hash
|
||||
);
|
||||
|
||||
$steps = $this->loadStepsHash($hash['steps']);
|
||||
|
||||
if (isset($hash['examples']['keyword'])) {
|
||||
$examplesKeyword = $hash['examples']['keyword'];
|
||||
unset($hash['examples']['keyword']);
|
||||
} else {
|
||||
$examplesKeyword = 'Examples';
|
||||
}
|
||||
|
||||
$exHash = $hash['examples'];
|
||||
$examples = array();
|
||||
|
||||
if ($this->examplesAreInArray($exHash)) {
|
||||
$examples = $this->processExamplesArray($exHash, $examplesKeyword, $examples);
|
||||
} else {
|
||||
// examples as a single table - we create an array with the only one element
|
||||
$examples[] = new ExampleTableNode($exHash, $examplesKeyword);;
|
||||
}
|
||||
|
||||
return new OutlineNode($hash['title'], $hash['tags'], $steps, $examples, $hash['keyword'], $hash['line']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads steps from provided hash.
|
||||
*
|
||||
* @param array $hash
|
||||
*
|
||||
* @return StepNode[]
|
||||
*/
|
||||
private function loadStepsHash(array $hash)
|
||||
{
|
||||
$steps = array();
|
||||
foreach ($hash as $stepIterator => $stepHash) {
|
||||
$steps[] = $this->loadStepHash($stepHash, $stepIterator);
|
||||
}
|
||||
|
||||
return $steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads step from provided hash.
|
||||
*
|
||||
* @param array $hash Step hash
|
||||
* @param integer $line Step definition line
|
||||
*
|
||||
* @return StepNode
|
||||
*/
|
||||
protected function loadStepHash(array $hash, $line = 0)
|
||||
{
|
||||
$hash = array_merge(
|
||||
array(
|
||||
'keyword_type' => 'Given',
|
||||
'type' => 'Given',
|
||||
'text' => null,
|
||||
'keyword' => 'Scenario',
|
||||
'line' => $line,
|
||||
'arguments' => array(),
|
||||
),
|
||||
$hash
|
||||
);
|
||||
|
||||
$arguments = array();
|
||||
foreach ($hash['arguments'] as $argumentHash) {
|
||||
if ('table' === $argumentHash['type']) {
|
||||
$arguments[] = $this->loadTableHash($argumentHash['rows']);
|
||||
} elseif ('pystring' === $argumentHash['type']) {
|
||||
$arguments[] = $this->loadPyStringHash($argumentHash, $hash['line'] + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return new StepNode($hash['type'], $hash['text'], $arguments, $hash['line'], $hash['keyword_type']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads table from provided hash.
|
||||
*
|
||||
* @param array $hash Table hash
|
||||
*
|
||||
* @return TableNode
|
||||
*/
|
||||
protected function loadTableHash(array $hash)
|
||||
{
|
||||
return new TableNode($hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads PyString from provided hash.
|
||||
*
|
||||
* @param array $hash PyString hash
|
||||
* @param integer $line
|
||||
*
|
||||
* @return PyStringNode
|
||||
*/
|
||||
protected function loadPyStringHash(array $hash, $line = 0)
|
||||
{
|
||||
$line = isset($hash['line']) ? $hash['line'] : $line;
|
||||
|
||||
$strings = array();
|
||||
foreach (explode("\n", $hash['text']) as $string) {
|
||||
$strings[] = $string;
|
||||
}
|
||||
|
||||
return new PyStringNode($strings, $line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if examples node is an array
|
||||
* @param $exHash object hash
|
||||
* @return bool
|
||||
*/
|
||||
private function examplesAreInArray($exHash)
|
||||
{
|
||||
return isset($exHash[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes cases when examples are in the form of array of arrays
|
||||
* OR in the form of array of objects
|
||||
*
|
||||
* @param $exHash array hash
|
||||
* @param $examplesKeyword string
|
||||
* @param $examples array
|
||||
* @return array
|
||||
*/
|
||||
private function processExamplesArray($exHash, $examplesKeyword, $examples)
|
||||
{
|
||||
for ($i = 0; $i < count($exHash); $i++) {
|
||||
if (isset($exHash[$i]['table'])) {
|
||||
// we have examples as objects, hence there could be tags
|
||||
$exHashTags = isset($exHash[$i]['tags']) ? $exHash[$i]['tags'] : array();
|
||||
$examples[] = new ExampleTableNode($exHash[$i]['table'], $examplesKeyword, $exHashTags);
|
||||
} else {
|
||||
// we have examples as arrays
|
||||
$examples[] = new ExampleTableNode($exHash[$i], $examplesKeyword);
|
||||
}
|
||||
}
|
||||
|
||||
return $examples;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace Behat\Gherkin\Loader;
|
||||
|
||||
use Behat\Gherkin\Node\BackgroundNode;
|
||||
use Behat\Gherkin\Node\ExampleTableNode;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Node\OutlineNode;
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
use Behat\Gherkin\Node\ScenarioNode;
|
||||
use Behat\Gherkin\Node\StepNode;
|
||||
|
||||
/**
|
||||
* Loads a feature from cucumber's protobuf JSON format
|
||||
*/
|
||||
class CucumberNDJsonAstLoader implements LoaderInterface
|
||||
{
|
||||
|
||||
public function supports($resource)
|
||||
{
|
||||
return is_string($resource);
|
||||
}
|
||||
|
||||
public function load($resource)
|
||||
{
|
||||
return array_values(array_filter(array_map(
|
||||
static function ($line) use ($resource) {
|
||||
return self::getFeature(json_decode($line, true), $resource);
|
||||
},
|
||||
file($resource)
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FeatureNode|null
|
||||
*/
|
||||
private static function getFeature(array $json, $filePath)
|
||||
{
|
||||
if (!isset($json['gherkinDocument']['feature'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$featureJson = $json['gherkinDocument']['feature'];
|
||||
|
||||
$feature = new FeatureNode(
|
||||
isset($featureJson['name']) ? $featureJson['name'] : null,
|
||||
$featureJson['description'] ? trim($featureJson['description']) : null,
|
||||
self::getTags($featureJson),
|
||||
self::getBackground($featureJson),
|
||||
self::getScenarios($featureJson),
|
||||
$featureJson['keyword'],
|
||||
$featureJson['language'],
|
||||
preg_replace('/(?<=\\.feature).*$/', '', $filePath),
|
||||
$featureJson['location']['line']
|
||||
);
|
||||
|
||||
return $feature;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private static function getTags(array $json)
|
||||
{
|
||||
return array_map(
|
||||
static function(array $tag) { return preg_replace('/^@/', '', $tag['name']); },
|
||||
isset($json['tags']) ? $json['tags'] : []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ScenarioInterface[]
|
||||
*/
|
||||
private static function getScenarios(array $json)
|
||||
{
|
||||
|
||||
return array_values(
|
||||
array_map(
|
||||
static function ($child) {
|
||||
|
||||
if ($child['scenario']['examples']) {
|
||||
return new OutlineNode(
|
||||
isset($child['scenario']['name']) ? $child['scenario']['name'] : null,
|
||||
self::getTags($child['scenario']),
|
||||
self::getSteps(isset($child['scenario']['steps']) ? $child['scenario']['steps'] : []),
|
||||
self::getTables($child['scenario']['examples']),
|
||||
$child['scenario']['keyword'],
|
||||
$child['scenario']['location']['line']
|
||||
);
|
||||
}
|
||||
else {
|
||||
return new ScenarioNode(
|
||||
$child['scenario']['name'],
|
||||
self::getTags($child['scenario']),
|
||||
self::getSteps(isset($child['scenario']['steps']) ? $child['scenario']['steps'] : []),
|
||||
$child['scenario']['keyword'],
|
||||
$child['scenario']['location']['line']
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
array_filter(
|
||||
isset($json['children']) ? $json['children'] : [],
|
||||
static function ($child) {
|
||||
return isset($child['scenario']);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackgroundNode|null
|
||||
*/
|
||||
private static function getBackground(array $json)
|
||||
{
|
||||
$backgrounds = array_values(
|
||||
array_map(
|
||||
static function ($child) {
|
||||
return new BackgroundNode(
|
||||
$child['background']['name'],
|
||||
self::getSteps(isset($child['background']['steps']) ? $child['background']['steps'] : []),
|
||||
$child['background']['keyword'],
|
||||
$child['background']['location']['line']
|
||||
);
|
||||
},
|
||||
array_filter(
|
||||
isset($json['children']) ? $json['children'] : [],
|
||||
static function ($child) {
|
||||
return isset($child['background']);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return count($backgrounds) == 1 ? $backgrounds[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return StepNode[]
|
||||
*/
|
||||
private static function getSteps(array $json)
|
||||
{
|
||||
return array_map(
|
||||
static function(array $json) {
|
||||
return new StepNode(
|
||||
trim($json['keyword']),
|
||||
$json['text'],
|
||||
[],
|
||||
$json['location']['line'],
|
||||
trim($json['keyword'])
|
||||
);
|
||||
},
|
||||
$json
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ExampleTableNode[]
|
||||
*/
|
||||
private static function getTables(array $json)
|
||||
{
|
||||
return array_map(
|
||||
static function($tableJson) {
|
||||
|
||||
$table = [];
|
||||
|
||||
$table[$tableJson['tableHeader']['location']['line']] = array_map(
|
||||
static function($cell) {
|
||||
return $cell['value'];
|
||||
},
|
||||
$tableJson['tableHeader']['cells']
|
||||
);
|
||||
|
||||
foreach ($tableJson['tableBody'] as $bodyRow) {
|
||||
$table[$bodyRow['location']['line']] = array_map(
|
||||
static function($cell) {
|
||||
return $cell['value'];
|
||||
},
|
||||
$bodyRow['cells']
|
||||
);
|
||||
}
|
||||
|
||||
return new ExampleTableNode(
|
||||
$table,
|
||||
$tableJson['keyword'],
|
||||
self::getTags($tableJson)
|
||||
);
|
||||
},
|
||||
$json
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Loader;
|
||||
|
||||
use Behat\Gherkin\Gherkin;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
/**
|
||||
* Directory contents loader.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class DirectoryLoader extends AbstractFileLoader
|
||||
{
|
||||
protected $gherkin;
|
||||
|
||||
/**
|
||||
* Initializes loader.
|
||||
*
|
||||
* @param Gherkin $gherkin Gherkin manager
|
||||
*/
|
||||
public function __construct(Gherkin $gherkin)
|
||||
{
|
||||
$this->gherkin = $gherkin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current loader supports provided resource.
|
||||
*
|
||||
* @param mixed $path Resource to load
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function supports($path)
|
||||
{
|
||||
return is_string($path)
|
||||
&& is_dir($this->findAbsolutePath($path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads features from provided resource.
|
||||
*
|
||||
* @param string $path Resource to load
|
||||
*
|
||||
* @return FeatureNode[]
|
||||
*/
|
||||
public function load($path)
|
||||
{
|
||||
$path = $this->findAbsolutePath($path);
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
$paths = array_map('strval', iterator_to_array($iterator));
|
||||
uasort($paths, 'strnatcasecmp');
|
||||
|
||||
$features = array();
|
||||
|
||||
foreach ($paths as $path) {
|
||||
$path = (string) $path;
|
||||
$loader = $this->gherkin->resolveLoader($path);
|
||||
|
||||
if (null !== $loader) {
|
||||
$features = array_merge($features, $loader->load($path));
|
||||
}
|
||||
}
|
||||
|
||||
return $features;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Loader;
|
||||
|
||||
/**
|
||||
* File Loader interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface FileLoaderInterface extends LoaderInterface
|
||||
{
|
||||
/**
|
||||
* Sets base features path.
|
||||
*
|
||||
* @param string $path Base loader path
|
||||
*/
|
||||
public function setBasePath($path);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Loader;
|
||||
|
||||
use Behat\Gherkin\Cache\CacheInterface;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Parser;
|
||||
|
||||
/**
|
||||
* Gherkin *.feature files loader.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class GherkinFileLoader extends AbstractFileLoader
|
||||
{
|
||||
protected $parser;
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* Initializes loader.
|
||||
*
|
||||
* @param Parser $parser Parser
|
||||
* @param CacheInterface $cache Cache layer
|
||||
*/
|
||||
public function __construct(Parser $parser, CacheInterface $cache = null)
|
||||
{
|
||||
$this->parser = $parser;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets cache layer.
|
||||
*
|
||||
* @param CacheInterface $cache Cache layer
|
||||
*/
|
||||
public function setCache(CacheInterface $cache)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current loader supports provided resource.
|
||||
*
|
||||
* @param mixed $path Resource to load
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function supports($path)
|
||||
{
|
||||
return is_string($path)
|
||||
&& is_file($absolute = $this->findAbsolutePath($path))
|
||||
&& 'feature' === pathinfo($absolute, PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads features from provided resource.
|
||||
*
|
||||
* @param string $path Resource to load
|
||||
*
|
||||
* @return FeatureNode[]
|
||||
*/
|
||||
public function load($path)
|
||||
{
|
||||
$path = $this->findAbsolutePath($path);
|
||||
|
||||
if ($this->cache) {
|
||||
if ($this->cache->isFresh($path, filemtime($path))) {
|
||||
$feature = $this->cache->read($path);
|
||||
} elseif (null !== $feature = $this->parseFeature($path)) {
|
||||
$this->cache->write($path, $feature);
|
||||
}
|
||||
} else {
|
||||
$feature = $this->parseFeature($path);
|
||||
}
|
||||
|
||||
return null !== $feature ? array($feature) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses feature at provided absolute path.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
protected function parseFeature($path)
|
||||
{
|
||||
$content = file_get_contents($path);
|
||||
$feature = $this->parser->parse($content, $path);
|
||||
|
||||
return $feature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Loader;
|
||||
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
|
||||
/**
|
||||
* Loader interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface LoaderInterface
|
||||
{
|
||||
/**
|
||||
* Checks if current loader supports provided resource.
|
||||
*
|
||||
* @param mixed $resource Resource to load
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function supports($resource);
|
||||
|
||||
/**
|
||||
* Loads features from provided resource.
|
||||
*
|
||||
* @param mixed $resource Resource to load
|
||||
*
|
||||
* @return FeatureNode[]
|
||||
*/
|
||||
public function load($resource);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Loader;
|
||||
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* Yaml files loader.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class YamlFileLoader extends AbstractFileLoader
|
||||
{
|
||||
private $loader;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->loader = new ArrayLoader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current loader supports provided resource.
|
||||
*
|
||||
* @param mixed $path Resource to load
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function supports($path)
|
||||
{
|
||||
return is_string($path)
|
||||
&& is_file($absolute = $this->findAbsolutePath($path))
|
||||
&& 'yml' === pathinfo($absolute, PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads features from provided resource.
|
||||
*
|
||||
* @param string $path Resource to load
|
||||
*
|
||||
* @return FeatureNode[]
|
||||
*/
|
||||
public function load($path)
|
||||
{
|
||||
$path = $this->findAbsolutePath($path);
|
||||
$hash = Yaml::parse(file_get_contents($path));
|
||||
|
||||
$features = $this->loader->load($hash);
|
||||
|
||||
return array_map(function (FeatureNode $feature) use ($path) {
|
||||
return new FeatureNode(
|
||||
$feature->getTitle(),
|
||||
$feature->getDescription(),
|
||||
$feature->getTags(),
|
||||
$feature->getBackground(),
|
||||
$feature->getScenarios(),
|
||||
$feature->getKeyword(),
|
||||
$feature->getLanguage(),
|
||||
$path,
|
||||
$feature->getLine()
|
||||
);
|
||||
}, $features);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Gherkin arguments interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface ArgumentInterface extends NodeInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Represents Gherkin Background.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class BackgroundNode implements ScenarioLikeInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $title;
|
||||
/**
|
||||
* @var StepNode[]
|
||||
*/
|
||||
private $steps = array();
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $keyword;
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $line;
|
||||
|
||||
/**
|
||||
* Initializes background.
|
||||
*
|
||||
* @param null|string $title
|
||||
* @param StepNode[] $steps
|
||||
* @param string $keyword
|
||||
* @param integer $line
|
||||
*/
|
||||
public function __construct($title, array $steps, $keyword, $line)
|
||||
{
|
||||
$this->title = $title;
|
||||
$this->steps = $steps;
|
||||
$this->keyword = $keyword;
|
||||
$this->line = $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Background';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns background title.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if background has steps.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSteps()
|
||||
{
|
||||
return 0 < count($this->steps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns background steps.
|
||||
*
|
||||
* @return StepNode[]
|
||||
*/
|
||||
public function getSteps()
|
||||
{
|
||||
return $this->steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns background keyword.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyword()
|
||||
{
|
||||
return $this->keyword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns background declaration line number.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Represents Gherkin Outline Example.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class ExampleNode implements ScenarioInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $title;
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $tags;
|
||||
/**
|
||||
* @var StepNode[]
|
||||
*/
|
||||
private $outlineSteps;
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $tokens;
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $line;
|
||||
/**
|
||||
* @var null|StepNode[]
|
||||
*/
|
||||
private $steps;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $outlineTitle;
|
||||
|
||||
/**
|
||||
* Initializes outline.
|
||||
*
|
||||
* @param string $title
|
||||
* @param string[] $tags
|
||||
* @param StepNode[] $outlineSteps
|
||||
* @param string[] $tokens
|
||||
* @param integer $line
|
||||
* @param string|null $outlineTitle
|
||||
*/
|
||||
public function __construct($title, array $tags, $outlineSteps, array $tokens, $line, $outlineTitle = null)
|
||||
{
|
||||
$this->title = $title;
|
||||
$this->tags = $tags;
|
||||
$this->outlineSteps = $outlineSteps;
|
||||
$this->tokens = $tokens;
|
||||
$this->line = $line;
|
||||
$this->outlineTitle = $outlineTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Example';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node keyword.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyword()
|
||||
{
|
||||
return $this->getNodeType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns example title.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if outline is tagged with tag.
|
||||
*
|
||||
* @param string $tag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTag($tag)
|
||||
{
|
||||
return in_array($tag, $this->getTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if outline has tags (both inherited from feature and own).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTags()
|
||||
{
|
||||
return 0 < count($this->getTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline tags (including inherited from feature).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if outline has steps.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSteps()
|
||||
{
|
||||
return 0 < count($this->outlineSteps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline steps.
|
||||
*
|
||||
* @return StepNode[]
|
||||
*/
|
||||
public function getSteps()
|
||||
{
|
||||
return $this->steps = $this->steps ? : $this->createExampleSteps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns example tokens.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTokens()
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline declaration line number.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline title.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOutlineTitle()
|
||||
{
|
||||
return $this->outlineTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates steps for this example from abstract outline steps.
|
||||
*
|
||||
* @return StepNode[]
|
||||
*/
|
||||
protected function createExampleSteps()
|
||||
{
|
||||
$steps = array();
|
||||
foreach ($this->outlineSteps as $outlineStep) {
|
||||
$keyword = $outlineStep->getKeyword();
|
||||
$keywordType = $outlineStep->getKeywordType();
|
||||
$text = $this->replaceTextTokens($outlineStep->getText());
|
||||
$args = $this->replaceArgumentsTokens($outlineStep->getArguments());
|
||||
$line = $outlineStep->getLine();
|
||||
|
||||
$steps[] = new StepNode($keyword, $text, $args, $line, $keywordType);
|
||||
}
|
||||
|
||||
return $steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces tokens in arguments with row values.
|
||||
*
|
||||
* @param ArgumentInterface[] $arguments
|
||||
*
|
||||
* @return ArgumentInterface[]
|
||||
*/
|
||||
protected function replaceArgumentsTokens(array $arguments)
|
||||
{
|
||||
foreach ($arguments as $num => $argument) {
|
||||
if ($argument instanceof TableNode) {
|
||||
$arguments[$num] = $this->replaceTableArgumentTokens($argument);
|
||||
}
|
||||
if ($argument instanceof PyStringNode) {
|
||||
$arguments[$num] = $this->replacePyStringArgumentTokens($argument);
|
||||
}
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces tokens in table with row values.
|
||||
*
|
||||
* @param TableNode $argument
|
||||
*
|
||||
* @return TableNode
|
||||
*/
|
||||
protected function replaceTableArgumentTokens(TableNode $argument)
|
||||
{
|
||||
$table = $argument->getTable();
|
||||
foreach ($table as $line => $row) {
|
||||
foreach (array_keys($row) as $col) {
|
||||
$table[$line][$col] = $this->replaceTextTokens($table[$line][$col]);
|
||||
}
|
||||
}
|
||||
|
||||
return new TableNode($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces tokens in PyString with row values.
|
||||
*
|
||||
* @param PyStringNode $argument
|
||||
*
|
||||
* @return PyStringNode
|
||||
*/
|
||||
protected function replacePyStringArgumentTokens(PyStringNode $argument)
|
||||
{
|
||||
$strings = $argument->getStrings();
|
||||
foreach ($strings as $line => $string) {
|
||||
$strings[$line] = $this->replaceTextTokens($strings[$line]);
|
||||
}
|
||||
|
||||
return new PyStringNode($strings, $argument->getLine());
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces tokens in text with row values.
|
||||
*
|
||||
* @param string $text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function replaceTextTokens($text)
|
||||
{
|
||||
foreach ($this->tokens as $key => $val) {
|
||||
$text = str_replace('<' . $key . '>', $val, $text);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Represents Gherkin Outline Example Table.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class ExampleTableNode extends TableNode
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $tags;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $keyword;
|
||||
|
||||
/**
|
||||
* Initializes example table.
|
||||
*
|
||||
* @param array $table Table in form of [$rowLineNumber => [$val1, $val2, $val3]]
|
||||
* @param string $keyword
|
||||
* @param string[] $tags
|
||||
*/
|
||||
public function __construct(array $table, $keyword, array $tags = array())
|
||||
{
|
||||
$this->keyword = $keyword;
|
||||
$this->tags = $tags;
|
||||
|
||||
parent::__construct($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'ExampleTable';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns attached tags
|
||||
* @return \string[]
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns example table keyword.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyword()
|
||||
{
|
||||
return $this->keyword;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Represents Gherkin Feature.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class FeatureNode implements KeywordNodeInterface, TaggedNodeInterface
|
||||
{
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
private $title;
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
private $description;
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $tags = array();
|
||||
/**
|
||||
* @var null|BackgroundNode
|
||||
*/
|
||||
private $background;
|
||||
/**
|
||||
* @var ScenarioInterface[]
|
||||
*/
|
||||
private $scenarios = array();
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $keyword;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $language;
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
private $file;
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $line;
|
||||
|
||||
/**
|
||||
* Initializes feature.
|
||||
*
|
||||
* @param null|string $title
|
||||
* @param null|string $description
|
||||
* @param string[] $tags
|
||||
* @param null|BackgroundNode $background
|
||||
* @param ScenarioInterface[] $scenarios
|
||||
* @param string $keyword
|
||||
* @param string $language
|
||||
* @param null|string $file The absolute path to the feature file.
|
||||
* @param integer $line
|
||||
*/
|
||||
public function __construct(
|
||||
$title,
|
||||
$description,
|
||||
array $tags,
|
||||
BackgroundNode $background = null,
|
||||
array $scenarios,
|
||||
$keyword,
|
||||
$language,
|
||||
$file,
|
||||
$line
|
||||
) {
|
||||
// Verify that the feature file is an absolute path.
|
||||
if (!empty($file) && !$this->isAbsolutePath($file)) {
|
||||
throw new \InvalidArgumentException('The file should be an absolute path.');
|
||||
}
|
||||
$this->title = $title;
|
||||
$this->description = $description;
|
||||
$this->tags = $tags;
|
||||
$this->background = $background;
|
||||
$this->scenarios = $scenarios;
|
||||
$this->keyword = $keyword;
|
||||
$this->language = $language;
|
||||
$this->file = $file;
|
||||
$this->line = $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Feature';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature title.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if feature has a description.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDescription()
|
||||
{
|
||||
return !empty($this->description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature description.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if feature is tagged with tag.
|
||||
*
|
||||
* @param string $tag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTag($tag)
|
||||
{
|
||||
return in_array($tag, $this->tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if feature has tags.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTags()
|
||||
{
|
||||
return 0 < count($this->tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature tags.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if feature has background.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasBackground()
|
||||
{
|
||||
return null !== $this->background;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature background.
|
||||
*
|
||||
* @return null|BackgroundNode
|
||||
*/
|
||||
public function getBackground()
|
||||
{
|
||||
return $this->background;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if feature has scenarios.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasScenarios()
|
||||
{
|
||||
return 0 < count($this->scenarios);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature scenarios.
|
||||
*
|
||||
* @return ScenarioInterface[]
|
||||
*/
|
||||
public function getScenarios()
|
||||
{
|
||||
return $this->scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature keyword.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyword()
|
||||
{
|
||||
return $this->keyword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature language.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature file as an absolute path.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getFile()
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature declaration line number.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the file path is an absolute path.
|
||||
*
|
||||
* @param string $file A file path
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @see https://github.com/symfony/filesystem/blob/master/Filesystem.php
|
||||
*/
|
||||
protected function isAbsolutePath($file)
|
||||
{
|
||||
if (null === $file) {
|
||||
@trigger_error(sprintf('Calling "%s()" with a null in the $file argument is deprecated since Symfony 4.4.', __METHOD__), E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
return strspn($file, '/\\', 0, 1)
|
||||
|| (\strlen($file) > 3 && ctype_alpha($file[0])
|
||||
&& ':' === $file[1]
|
||||
&& strspn($file, '/\\', 2, 1)
|
||||
)
|
||||
|| null !== parse_url($file, PHP_URL_SCHEME)
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Gherkin keyword node interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface KeywordNodeInterface extends NodeInterface
|
||||
{
|
||||
/**
|
||||
* Returns node keyword.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyword();
|
||||
|
||||
/**
|
||||
* Returns node title.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getTitle();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Gherkin node interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface NodeInterface
|
||||
{
|
||||
/**
|
||||
* Returns node type string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType();
|
||||
|
||||
/**
|
||||
* Returns feature declaration line number.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getLine();
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Represents Gherkin Outline.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class OutlineNode implements ScenarioInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $title;
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $tags;
|
||||
/**
|
||||
* @var StepNode[]
|
||||
*/
|
||||
private $steps;
|
||||
/**
|
||||
* @var ExampleTableNode|ExampleTableNode[]
|
||||
*/
|
||||
private $tables;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $keyword;
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $line;
|
||||
/**
|
||||
* @var null|ExampleNode[]
|
||||
*/
|
||||
private $examples;
|
||||
|
||||
/**
|
||||
* Initializes outline.
|
||||
*
|
||||
* @param null|string $title
|
||||
* @param string[] $tags
|
||||
* @param StepNode[] $steps
|
||||
* @param ExampleTableNode|ExampleTableNode[] $tables
|
||||
* @param string $keyword
|
||||
* @param integer $line
|
||||
*/
|
||||
public function __construct(
|
||||
$title,
|
||||
array $tags,
|
||||
array $steps,
|
||||
$tables,
|
||||
$keyword,
|
||||
$line
|
||||
) {
|
||||
$this->title = $title;
|
||||
$this->tags = $tags;
|
||||
$this->steps = $steps;
|
||||
$this->keyword = $keyword;
|
||||
$this->line = $line;
|
||||
if (!is_array($tables)) {
|
||||
$this->tables = array($tables);
|
||||
} else {
|
||||
$this->tables = $tables;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Outline';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline title.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if outline is tagged with tag.
|
||||
*
|
||||
* @param string $tag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTag($tag)
|
||||
{
|
||||
return in_array($tag, $this->getTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if outline has tags (both inherited from feature and own).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTags()
|
||||
{
|
||||
return 0 < count($this->getTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline tags (including inherited from feature).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if outline has steps.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSteps()
|
||||
{
|
||||
return 0 < count($this->steps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline steps.
|
||||
*
|
||||
* @return StepNode[]
|
||||
*/
|
||||
public function getSteps()
|
||||
{
|
||||
return $this->steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if outline has examples.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasExamples()
|
||||
{
|
||||
return 0 < count($this->tables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns examples table for the outline.
|
||||
*
|
||||
* WARNING: it returns a merged table with tags lost.
|
||||
*
|
||||
* @deprecated use getExampleTables instead
|
||||
* @return ExampleTableNode
|
||||
*/
|
||||
public function getExampleTable()
|
||||
{
|
||||
$table = array();
|
||||
foreach ($this->tables[0]->getTable() as $k => $v) {
|
||||
$table[$k] = $v;
|
||||
}
|
||||
|
||||
/** @var ExampleTableNode $exampleTableNode */
|
||||
$exampleTableNode = new ExampleTableNode($table, $this->tables[0]->getKeyword());
|
||||
for ($i = 1; $i < count($this->tables); $i++) {
|
||||
$exampleTableNode->mergeRowsFromTable($this->tables[$i]);
|
||||
}
|
||||
return $exampleTableNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of examples for the outline.
|
||||
* @return ExampleNode[]
|
||||
*/
|
||||
public function getExamples()
|
||||
{
|
||||
return $this->examples = $this->examples ?: $this->createExamples();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns examples tables array for the outline.
|
||||
* @return ExampleTableNode[]
|
||||
*/
|
||||
public function getExampleTables()
|
||||
{
|
||||
return $this->tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline keyword.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyword()
|
||||
{
|
||||
return $this->keyword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline declaration line number.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates examples for this outline using examples table.
|
||||
*
|
||||
* @return ExampleNode[]
|
||||
*/
|
||||
protected function createExamples()
|
||||
{
|
||||
$examples = array();
|
||||
|
||||
foreach ($this->getExampleTables() as $exampleTable) {
|
||||
foreach ($exampleTable->getColumnsHash() as $rowNum => $row) {
|
||||
$examples[] = new ExampleNode(
|
||||
$exampleTable->getRowAsString($rowNum + 1),
|
||||
array_merge($this->tags, $exampleTable->getTags()),
|
||||
$this->getSteps(),
|
||||
$row,
|
||||
$exampleTable->getRowLine($rowNum + 1),
|
||||
$this->getTitle()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $examples;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Represents Gherkin PyString argument.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class PyStringNode implements ArgumentInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $strings = array();
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $line;
|
||||
|
||||
/**
|
||||
* Initializes PyString.
|
||||
*
|
||||
* @param array $strings String in form of [$stringLine]
|
||||
* @param integer $line Line number where string been started
|
||||
*/
|
||||
public function __construct(array $strings, $line)
|
||||
{
|
||||
$this->strings = $strings;
|
||||
$this->line = $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'PyString';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entire PyString lines set.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getStrings()
|
||||
{
|
||||
return $this->strings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns raw string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRaw()
|
||||
{
|
||||
return implode("\n", $this->strings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts PyString into string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getRaw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns line number at which PyString was started.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Gherkin scenario interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface ScenarioInterface extends ScenarioLikeInterface, TaggedNodeInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Gherkin scenario-like interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface ScenarioLikeInterface extends KeywordNodeInterface, StepContainerInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Represents Gherkin Scenario.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class ScenarioNode implements ScenarioInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $title;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $tags = array();
|
||||
/**
|
||||
* @var StepNode[]
|
||||
*/
|
||||
private $steps = array();
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $keyword;
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $line;
|
||||
|
||||
/**
|
||||
* Initializes scenario.
|
||||
*
|
||||
* @param null|string $title
|
||||
* @param array $tags
|
||||
* @param StepNode[] $steps
|
||||
* @param string $keyword
|
||||
* @param integer $line
|
||||
*/
|
||||
public function __construct($title, array $tags, array $steps, $keyword, $line)
|
||||
{
|
||||
$this->title = $title;
|
||||
$this->tags = $tags;
|
||||
$this->steps = $steps;
|
||||
$this->keyword = $keyword;
|
||||
$this->line = $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Scenario';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns scenario title.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if scenario is tagged with tag.
|
||||
*
|
||||
* @param string $tag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTag($tag)
|
||||
{
|
||||
return in_array($tag, $this->getTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if scenario has tags (both inherited from feature and own).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTags()
|
||||
{
|
||||
return 0 < count($this->getTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns scenario tags (including inherited from feature).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if scenario has steps.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSteps()
|
||||
{
|
||||
return 0 < count($this->steps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns scenario steps.
|
||||
*
|
||||
* @return StepNode[]
|
||||
*/
|
||||
public function getSteps()
|
||||
{
|
||||
return $this->steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns scenario keyword.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyword()
|
||||
{
|
||||
return $this->keyword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns scenario declaration line number.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Gherkin step container interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface StepContainerInterface extends NodeInterface
|
||||
{
|
||||
/**
|
||||
* Checks if container has steps.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSteps();
|
||||
|
||||
/**
|
||||
* Returns container steps.
|
||||
*
|
||||
* @return StepNode[]
|
||||
*/
|
||||
public function getSteps();
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
use Behat\Gherkin\Exception\NodeException;
|
||||
|
||||
/**
|
||||
* Represents Gherkin Step.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class StepNode implements NodeInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $keyword;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $keywordType;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $text;
|
||||
/**
|
||||
* @var ArgumentInterface[]
|
||||
*/
|
||||
private $arguments = array();
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $line;
|
||||
|
||||
/**
|
||||
* Initializes step.
|
||||
*
|
||||
* @param string $keyword
|
||||
* @param string $text
|
||||
* @param ArgumentInterface[] $arguments
|
||||
* @param integer $line
|
||||
* @param string $keywordType
|
||||
*/
|
||||
public function __construct($keyword, $text, array $arguments, $line, $keywordType = null)
|
||||
{
|
||||
if (count($arguments) > 1) {
|
||||
throw new NodeException(sprintf(
|
||||
'Steps could have only one argument, but `%s %s` have %d.',
|
||||
$keyword,
|
||||
$text,
|
||||
count($arguments)
|
||||
));
|
||||
}
|
||||
|
||||
$this->keyword = $keyword;
|
||||
$this->text = $text;
|
||||
$this->arguments = $arguments;
|
||||
$this->line = $line;
|
||||
$this->keywordType = $keywordType ?: 'Given';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Step';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns step keyword in provided language (Given, When, Then, etc.).
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @deprecated use getKeyword() instead
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->getKeyword();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns step keyword in provided language (Given, When, Then, etc.).
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function getKeyword()
|
||||
{
|
||||
return $this->keyword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns step type keyword (Given, When, Then, etc.).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeywordType()
|
||||
{
|
||||
return $this->keywordType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns step text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getText()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if step has arguments.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasArguments()
|
||||
{
|
||||
return 0 < count($this->arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns step arguments.
|
||||
*
|
||||
* @return ArgumentInterface[]
|
||||
*/
|
||||
public function getArguments()
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns step declaration line number.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
use ArrayIterator;
|
||||
use Behat\Gherkin\Exception\NodeException;
|
||||
use Iterator;
|
||||
use IteratorAggregate;
|
||||
use ReturnTypeWillChange;
|
||||
|
||||
/**
|
||||
* Represents Gherkin Table argument.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class TableNode implements ArgumentInterface, IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $table;
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $maxLineLength = array();
|
||||
|
||||
/**
|
||||
* Initializes table.
|
||||
*
|
||||
* @param array $table Table in form of [$rowLineNumber => [$val1, $val2, $val3]]
|
||||
*
|
||||
* @throws NodeException If the given table is invalid
|
||||
*/
|
||||
public function __construct(array $table)
|
||||
{
|
||||
$this->table = $table;
|
||||
$columnCount = null;
|
||||
|
||||
foreach ($this->getRows() as $ridx => $row) {
|
||||
|
||||
if (!is_array($row)) {
|
||||
throw new NodeException(sprintf(
|
||||
"Table row '%s' is expected to be array, got %s",
|
||||
$ridx,
|
||||
gettype($row)
|
||||
));
|
||||
}
|
||||
|
||||
if ($columnCount === null) {
|
||||
$columnCount = count($row);
|
||||
}
|
||||
|
||||
if (count($row) !== $columnCount) {
|
||||
throw new NodeException(sprintf(
|
||||
"Table row '%s' is expected to have %s columns, got %s",
|
||||
$ridx,
|
||||
$columnCount,
|
||||
count($row)
|
||||
));
|
||||
}
|
||||
|
||||
foreach ($row as $column => $string) {
|
||||
if (!isset($this->maxLineLength[$column])) {
|
||||
$this->maxLineLength[$column] = 0;
|
||||
}
|
||||
|
||||
if (!is_scalar($string)) {
|
||||
throw new NodeException(sprintf(
|
||||
"Table cell at row '%s', col '%s' is expected to be scalar, got %s",
|
||||
$ridx,
|
||||
$column,
|
||||
gettype($string)
|
||||
));
|
||||
}
|
||||
|
||||
$this->maxLineLength[$column] = max($this->maxLineLength[$column], mb_strlen($string, 'utf8'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table from a given list.
|
||||
*
|
||||
* @param array $list One-dimensional array
|
||||
*
|
||||
* @return TableNode
|
||||
*
|
||||
* @throws NodeException If the given list is not a one-dimensional array
|
||||
*/
|
||||
public static function fromList(array $list)
|
||||
{
|
||||
if (count($list) !== count($list, COUNT_RECURSIVE)) {
|
||||
throw new NodeException('List is not a one-dimensional array.');
|
||||
}
|
||||
|
||||
array_walk($list, function (&$item) {
|
||||
$item = array($item);
|
||||
});
|
||||
return new self($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Table';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns table hash, formed by columns (ColumnsHash).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getHash()
|
||||
{
|
||||
return $this->getColumnsHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns table hash, formed by columns.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getColumnsHash()
|
||||
{
|
||||
$rows = $this->getRows();
|
||||
$keys = array_shift($rows);
|
||||
|
||||
$hash = array();
|
||||
foreach ($rows as $row) {
|
||||
$hash[] = array_combine($keys, $row);
|
||||
}
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns table hash, formed by rows.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRowsHash()
|
||||
{
|
||||
$hash = array();
|
||||
|
||||
foreach ($this->getRows() as $row) {
|
||||
$hash[array_shift($row)] = (1 == count($row)) ? $row[0] : $row;
|
||||
}
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns numerated table lines.
|
||||
* Line numbers are keys, lines are values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTable()
|
||||
{
|
||||
return $this->table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns table rows.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRows()
|
||||
{
|
||||
return array_values($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns table definition lines.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLines()
|
||||
{
|
||||
return array_keys($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns specific row in a table.
|
||||
*
|
||||
* @param integer $index Row number
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws NodeException If row with specified index does not exist
|
||||
*/
|
||||
public function getRow($index)
|
||||
{
|
||||
$rows = $this->getRows();
|
||||
|
||||
if (!isset($rows[$index])) {
|
||||
throw new NodeException(sprintf('Rows #%d does not exist in table.', $index));
|
||||
}
|
||||
|
||||
return $rows[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns specific column in a table.
|
||||
*
|
||||
* @param integer $index Column number
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws NodeException If column with specified index does not exist
|
||||
*/
|
||||
public function getColumn($index)
|
||||
{
|
||||
if ($index >= count($this->getRow(0))) {
|
||||
throw new NodeException(sprintf('Column #%d does not exist in table.', $index));
|
||||
}
|
||||
|
||||
$rows = $this->getRows();
|
||||
$column = array();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$column[] = $row[$index];
|
||||
}
|
||||
|
||||
return $column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns line number at which specific row was defined.
|
||||
*
|
||||
* @param integer $index
|
||||
*
|
||||
* @return integer
|
||||
*
|
||||
* @throws NodeException If row with specified index does not exist
|
||||
*/
|
||||
public function getRowLine($index)
|
||||
{
|
||||
$lines = array_keys($this->table);
|
||||
|
||||
if (!isset($lines[$index])) {
|
||||
throw new NodeException(sprintf('Rows #%d does not exist in table.', $index));
|
||||
}
|
||||
|
||||
return $lines[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts row into delimited string.
|
||||
*
|
||||
* @param integer $rowNum Row number
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRowAsString($rowNum)
|
||||
{
|
||||
$values = array();
|
||||
foreach ($this->getRow($rowNum) as $column => $value) {
|
||||
$values[] = $this->padRight(' ' . $value . ' ', $this->maxLineLength[$column] + 2);
|
||||
}
|
||||
|
||||
return sprintf('|%s|', implode('|', $values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts row into delimited string.
|
||||
*
|
||||
* @param integer $rowNum Row number
|
||||
* @param callable $wrapper Wrapper function
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRowAsStringWithWrappedValues($rowNum, $wrapper)
|
||||
{
|
||||
$values = array();
|
||||
foreach ($this->getRow($rowNum) as $column => $value) {
|
||||
$value = $this->padRight(' ' . $value . ' ', $this->maxLineLength[$column] + 2);
|
||||
|
||||
$values[] = call_user_func($wrapper, $value, $column);
|
||||
}
|
||||
|
||||
return sprintf('|%s|', implode('|', $values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts entire table into string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTableAsString()
|
||||
{
|
||||
$lines = array();
|
||||
for ($i = 0; $i < count($this->getRows()); $i++) {
|
||||
$lines[] = $this->getRowAsString($i);
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns line number at which table was started.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->getRowLine(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts table into string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getTableAsString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a hash iterator.
|
||||
*
|
||||
* @return Iterator
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
return new ArrayIterator($this->getHash());
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains and adds rows from another table to the current table.
|
||||
* The second table should have the same structure as the current one.
|
||||
* @param TableNode $node
|
||||
*
|
||||
* @deprecated remove together with OutlineNode::getExampleTable
|
||||
*/
|
||||
public function mergeRowsFromTable(TableNode $node)
|
||||
{
|
||||
// check structure
|
||||
if ($this->getRow(0) !== $node->getRow(0)) {
|
||||
throw new NodeException("Tables have different structure. Cannot merge one into another");
|
||||
}
|
||||
|
||||
$firstLine = $node->getLine();
|
||||
foreach ($node->getTable() as $line => $value) {
|
||||
if ($line === $firstLine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->table[$line] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads string right.
|
||||
*
|
||||
* @param string $text Text to pad
|
||||
* @param integer $length Length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function padRight($text, $length)
|
||||
{
|
||||
while ($length > mb_strlen($text, 'utf8')) {
|
||||
$text = $text . ' ';
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Node;
|
||||
|
||||
/**
|
||||
* Gherkin tagged node interface.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
interface TaggedNodeInterface extends NodeInterface
|
||||
{
|
||||
/**
|
||||
* Checks if node is tagged with tag.
|
||||
*
|
||||
* @param string $tag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTag($tag);
|
||||
|
||||
/**
|
||||
* Checks if node has tags (both inherited from feature and own).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTags();
|
||||
|
||||
/**
|
||||
* Returns node tags (including inherited from feature).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTags();
|
||||
}
|
||||
+771
@@ -0,0 +1,771 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin;
|
||||
|
||||
use Behat\Gherkin\Exception\LexerException;
|
||||
use Behat\Gherkin\Exception\NodeException;
|
||||
use Behat\Gherkin\Exception\ParserException;
|
||||
use Behat\Gherkin\Node\BackgroundNode;
|
||||
use Behat\Gherkin\Node\ExampleTableNode;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Node\OutlineNode;
|
||||
use Behat\Gherkin\Node\PyStringNode;
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
use Behat\Gherkin\Node\ScenarioNode;
|
||||
use Behat\Gherkin\Node\StepNode;
|
||||
use Behat\Gherkin\Node\TableNode;
|
||||
|
||||
/**
|
||||
* Gherkin parser.
|
||||
*
|
||||
* $lexer = new Behat\Gherkin\Lexer($keywords);
|
||||
* $parser = new Behat\Gherkin\Parser($lexer);
|
||||
* $featuresArray = $parser->parse('/path/to/feature.feature');
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class Parser
|
||||
{
|
||||
private $lexer;
|
||||
private $input;
|
||||
private $file;
|
||||
private $tags = array();
|
||||
private $languageSpecifierLine;
|
||||
|
||||
private $passedNodesStack = array();
|
||||
|
||||
/**
|
||||
* Initializes parser.
|
||||
*
|
||||
* @param Lexer $lexer Lexer instance
|
||||
*/
|
||||
public function __construct(Lexer $lexer)
|
||||
{
|
||||
$this->lexer = $lexer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses input & returns features array.
|
||||
*
|
||||
* @param string $input Gherkin string document
|
||||
* @param string $file File name
|
||||
*
|
||||
* @return FeatureNode|null
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
public function parse($input, $file = null)
|
||||
{
|
||||
$this->languageSpecifierLine = null;
|
||||
$this->input = $input;
|
||||
$this->file = $file;
|
||||
$this->tags = array();
|
||||
|
||||
try {
|
||||
$this->lexer->analyse($this->input, 'en');
|
||||
} catch (LexerException $e) {
|
||||
throw new ParserException(
|
||||
sprintf('Lexer exception "%s" thrown for file %s', $e->getMessage(), $file),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
|
||||
$feature = null;
|
||||
while ('EOS' !== ($predicted = $this->predictTokenType())) {
|
||||
$node = $this->parseExpression();
|
||||
|
||||
if (null === $node || "\n" === $node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$feature && $node instanceof FeatureNode) {
|
||||
$feature = $node;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($feature && $node instanceof FeatureNode) {
|
||||
throw new ParserException(sprintf(
|
||||
'Only one feature is allowed per feature file. But %s got multiple.',
|
||||
$this->file
|
||||
));
|
||||
}
|
||||
|
||||
if (is_string($node)) {
|
||||
throw new ParserException(sprintf(
|
||||
'Expected Feature, but got text: "%s"%s',
|
||||
$node,
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
|
||||
if (!$node instanceof FeatureNode) {
|
||||
throw new ParserException(sprintf(
|
||||
'Expected Feature, but got %s on line: %d%s',
|
||||
$node->getKeyword(),
|
||||
$node->getLine(),
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return $feature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next token if it's type equals to expected.
|
||||
*
|
||||
* @param string $type Token type
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws Exception\ParserException
|
||||
*/
|
||||
protected function expectTokenType($type)
|
||||
{
|
||||
$types = (array) $type;
|
||||
if (in_array($this->predictTokenType(), $types)) {
|
||||
return $this->lexer->getAdvancedToken();
|
||||
}
|
||||
|
||||
$token = $this->lexer->predictToken();
|
||||
|
||||
throw new ParserException(sprintf(
|
||||
'Expected %s token, but got %s on line: %d%s',
|
||||
implode(' or ', $types),
|
||||
$this->predictTokenType(),
|
||||
$token['line'],
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next token if it's type equals to expected.
|
||||
*
|
||||
* @param string $type Token type
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
protected function acceptTokenType($type)
|
||||
{
|
||||
if ($type !== $this->predictTokenType()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->lexer->getAdvancedToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next token type without real input reading (prediction).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function predictTokenType()
|
||||
{
|
||||
$token = $this->lexer->predictToken();
|
||||
|
||||
return $token['type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses current expression & returns Node.
|
||||
*
|
||||
* @return string|FeatureNode|BackgroundNode|ScenarioNode|OutlineNode|TableNode|StepNode
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
protected function parseExpression()
|
||||
{
|
||||
$type = $this->predictTokenType();
|
||||
|
||||
while ($type === 'Comment') {
|
||||
$this->expectTokenType('Comment');
|
||||
|
||||
$type = $this->predictTokenType();
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'Feature':
|
||||
return $this->parseFeature();
|
||||
case 'Background':
|
||||
return $this->parseBackground();
|
||||
case 'Scenario':
|
||||
return $this->parseScenario();
|
||||
case 'Outline':
|
||||
return $this->parseOutline();
|
||||
case 'Examples':
|
||||
return $this->parseExamples();
|
||||
case 'TableRow':
|
||||
return $this->parseTable();
|
||||
case 'PyStringOp':
|
||||
return $this->parsePyString();
|
||||
case 'Step':
|
||||
return $this->parseStep();
|
||||
case 'Text':
|
||||
return $this->parseText();
|
||||
case 'Newline':
|
||||
return $this->parseNewline();
|
||||
case 'Tag':
|
||||
return $this->parseTags();
|
||||
case 'Language':
|
||||
return $this->parseLanguage();
|
||||
case 'EOS':
|
||||
return '';
|
||||
}
|
||||
|
||||
throw new ParserException(sprintf('Unknown token type: %s', $type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses feature token & returns it's node.
|
||||
*
|
||||
* @return FeatureNode
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
protected function parseFeature()
|
||||
{
|
||||
$token = $this->expectTokenType('Feature');
|
||||
|
||||
$title = trim($token['value'] ?? '');
|
||||
$description = null;
|
||||
$tags = $this->popTags();
|
||||
$background = null;
|
||||
$scenarios = array();
|
||||
$keyword = $token['keyword'];
|
||||
$language = $this->lexer->getLanguage();
|
||||
$file = $this->file;
|
||||
$line = $token['line'];
|
||||
|
||||
array_push($this->passedNodesStack, 'Feature');
|
||||
|
||||
// Parse description, background, scenarios & outlines
|
||||
while ('EOS' !== $this->predictTokenType()) {
|
||||
$node = $this->parseExpression();
|
||||
|
||||
if (is_string($node)) {
|
||||
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
|
||||
$description .= (null !== $description ? "\n" : '') . $text;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$background && $node instanceof BackgroundNode) {
|
||||
$background = $node;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($node instanceof ScenarioInterface) {
|
||||
$scenarios[] = $node;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($background instanceof BackgroundNode && $node instanceof BackgroundNode) {
|
||||
throw new ParserException(sprintf(
|
||||
'Each Feature could have only one Background, but found multiple on lines %d and %d%s',
|
||||
$background->getLine(),
|
||||
$node->getLine(),
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
|
||||
if (!$node instanceof ScenarioNode) {
|
||||
throw new ParserException(sprintf(
|
||||
'Expected Scenario, Outline or Background, but got %s on line: %d%s',
|
||||
$node->getNodeType(),
|
||||
$node->getLine(),
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return new FeatureNode(
|
||||
rtrim($title) ?: null,
|
||||
rtrim($description ?? '') ?: null,
|
||||
$tags,
|
||||
$background,
|
||||
$scenarios,
|
||||
$keyword,
|
||||
$language,
|
||||
$file,
|
||||
$line
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses background token & returns it's node.
|
||||
*
|
||||
* @return BackgroundNode
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
protected function parseBackground()
|
||||
{
|
||||
$token = $this->expectTokenType('Background');
|
||||
|
||||
$title = trim($token['value'] ?? '');
|
||||
$keyword = $token['keyword'];
|
||||
$line = $token['line'];
|
||||
|
||||
if (count($this->popTags())) {
|
||||
throw new ParserException(sprintf(
|
||||
'Background can not be tagged, but it is on line: %d%s',
|
||||
$line,
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
|
||||
// Parse description and steps
|
||||
$steps = array();
|
||||
$allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
|
||||
while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
|
||||
$node = $this->parseExpression();
|
||||
|
||||
if ($node instanceof StepNode) {
|
||||
$steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!count($steps) && is_string($node)) {
|
||||
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
|
||||
$title .= "\n" . $text;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("\n" === $node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($node)) {
|
||||
throw new ParserException(sprintf(
|
||||
'Expected Step, but got text: "%s"%s',
|
||||
$node,
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
|
||||
if (!$node instanceof StepNode) {
|
||||
throw new ParserException(sprintf(
|
||||
'Expected Step, but got %s on line: %d%s',
|
||||
$node->getNodeType(),
|
||||
$node->getLine(),
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return new BackgroundNode(rtrim($title) ?: null, $steps, $keyword, $line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses scenario token & returns it's node.
|
||||
*
|
||||
* @return ScenarioNode
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
protected function parseScenario()
|
||||
{
|
||||
$token = $this->expectTokenType('Scenario');
|
||||
|
||||
$title = trim($token['value'] ?? '');
|
||||
$tags = $this->popTags();
|
||||
$keyword = $token['keyword'];
|
||||
$line = $token['line'];
|
||||
|
||||
array_push($this->passedNodesStack, 'Scenario');
|
||||
|
||||
// Parse description and steps
|
||||
$steps = array();
|
||||
while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
|
||||
$node = $this->parseExpression();
|
||||
|
||||
if ($node instanceof StepNode) {
|
||||
$steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!count($steps) && is_string($node)) {
|
||||
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
|
||||
$title .= "\n" . $text;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("\n" === $node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($node)) {
|
||||
throw new ParserException(sprintf(
|
||||
'Expected Step, but got text: "%s"%s',
|
||||
$node,
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
|
||||
if (!$node instanceof StepNode) {
|
||||
throw new ParserException(sprintf(
|
||||
'Expected Step, but got %s on line: %d%s',
|
||||
$node->getNodeType(),
|
||||
$node->getLine(),
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
array_pop($this->passedNodesStack);
|
||||
|
||||
return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses scenario outline token & returns it's node.
|
||||
*
|
||||
* @return OutlineNode
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
protected function parseOutline()
|
||||
{
|
||||
$token = $this->expectTokenType('Outline');
|
||||
|
||||
$title = trim($token['value'] ?? '');
|
||||
$tags = $this->popTags();
|
||||
$keyword = $token['keyword'];
|
||||
|
||||
/** @var ExampleTableNode $examples */
|
||||
$examples = array();
|
||||
$line = $token['line'];
|
||||
|
||||
// Parse description, steps and examples
|
||||
$steps = array();
|
||||
|
||||
array_push($this->passedNodesStack, 'Outline');
|
||||
|
||||
while (in_array($nextTokenType = $this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment', 'Tag'))) {
|
||||
if ($nextTokenType === 'Comment') {
|
||||
$this->lexer->skipPredictedToken();
|
||||
continue;
|
||||
}
|
||||
|
||||
$node = $this->parseExpression();
|
||||
|
||||
if ($node instanceof StepNode) {
|
||||
$steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($node instanceof ExampleTableNode) {
|
||||
$examples[] = $node;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!count($steps) && is_string($node)) {
|
||||
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
|
||||
$title .= "\n" . $text;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("\n" === $node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($node)) {
|
||||
throw new ParserException(sprintf(
|
||||
'Expected Step or Examples table, but got text: "%s"%s',
|
||||
$node,
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
|
||||
if (!$node instanceof StepNode) {
|
||||
throw new ParserException(sprintf(
|
||||
'Expected Step or Examples table, but got %s on line: %d%s',
|
||||
$node->getNodeType(),
|
||||
$node->getLine(),
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($examples)) {
|
||||
throw new ParserException(sprintf(
|
||||
'Outline should have examples table, but got none for outline "%s" on line: %d%s',
|
||||
rtrim($title),
|
||||
$line,
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
|
||||
return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses step token & returns it's node.
|
||||
*
|
||||
* @return StepNode
|
||||
*/
|
||||
protected function parseStep()
|
||||
{
|
||||
$token = $this->expectTokenType('Step');
|
||||
|
||||
$keyword = $token['value'];
|
||||
$keywordType = $token['keyword_type'];
|
||||
$text = trim($token['text']);
|
||||
$line = $token['line'];
|
||||
|
||||
array_push($this->passedNodesStack, 'Step');
|
||||
|
||||
$arguments = array();
|
||||
while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
|
||||
if ('Comment' === $predicted || 'Newline' === $predicted) {
|
||||
$this->acceptTokenType($predicted);
|
||||
continue;
|
||||
}
|
||||
|
||||
$node = $this->parseExpression();
|
||||
|
||||
if ($node instanceof PyStringNode || $node instanceof TableNode) {
|
||||
$arguments[] = $node;
|
||||
}
|
||||
}
|
||||
|
||||
array_pop($this->passedNodesStack);
|
||||
|
||||
return new StepNode($keyword, $text, $arguments, $line, $keywordType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses examples table node.
|
||||
*
|
||||
* @return ExampleTableNode
|
||||
*/
|
||||
protected function parseExamples()
|
||||
{
|
||||
$keyword = ($this->expectTokenType('Examples'))['keyword'];
|
||||
$tags = empty($this->tags) ? array() : $this->popTags();
|
||||
$table = $this->parseTableRows();
|
||||
|
||||
try {
|
||||
return new ExampleTableNode($table, $keyword, $tags);
|
||||
} catch(NodeException $e) {
|
||||
$this->rethrowNodeException($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses table token & returns it's node.
|
||||
*
|
||||
* @return TableNode
|
||||
*/
|
||||
protected function parseTable()
|
||||
{
|
||||
$table = $this->parseTableRows();
|
||||
|
||||
try {
|
||||
return new TableNode($table);
|
||||
} catch(NodeException $e) {
|
||||
$this->rethrowNodeException($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses PyString token & returns it's node.
|
||||
*
|
||||
* @return PyStringNode
|
||||
*/
|
||||
protected function parsePyString()
|
||||
{
|
||||
$token = $this->expectTokenType('PyStringOp');
|
||||
|
||||
$line = $token['line'];
|
||||
|
||||
$strings = array();
|
||||
while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) {
|
||||
$token = $this->expectTokenType('Text');
|
||||
|
||||
$strings[] = $token['value'];
|
||||
}
|
||||
|
||||
$this->expectTokenType('PyStringOp');
|
||||
|
||||
return new PyStringNode($strings, $line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses tags.
|
||||
*
|
||||
* @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
|
||||
*/
|
||||
protected function parseTags()
|
||||
{
|
||||
$token = $this->expectTokenType('Tag');
|
||||
|
||||
$this->guardTags($token['tags']);
|
||||
|
||||
$this->tags = array_merge($this->tags, $token['tags']);
|
||||
|
||||
$possibleTransitions = array(
|
||||
'Outline' => array(
|
||||
'Examples',
|
||||
'Step'
|
||||
)
|
||||
);
|
||||
|
||||
$currentType = '-1';
|
||||
// check if that is ok to go inside:
|
||||
if (!empty($this->passedNodesStack)) {
|
||||
$currentType = $this->passedNodesStack[count($this->passedNodesStack) - 1];
|
||||
}
|
||||
|
||||
$nextType = $this->predictTokenType();
|
||||
if (!isset($possibleTransitions[$currentType]) || in_array($nextType, $possibleTransitions[$currentType])) {
|
||||
return $this->parseExpression();
|
||||
}
|
||||
|
||||
return "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current set of tags and clears tag buffer.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function popTags()
|
||||
{
|
||||
$tags = $this->tags;
|
||||
$this->tags = array();
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the tags fit the required format
|
||||
*
|
||||
* @param string[] $tags
|
||||
*/
|
||||
protected function guardTags(array $tags)
|
||||
{
|
||||
foreach ($tags as $tag) {
|
||||
if (preg_match('/\s/', $tag)) {
|
||||
trigger_error('Whitespace in tags is deprecated, found "$tag"', E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next text line & returns it.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function parseText()
|
||||
{
|
||||
$token = $this->expectTokenType('Text');
|
||||
|
||||
return $token['value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next newline & returns \n.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function parseNewline()
|
||||
{
|
||||
$this->expectTokenType('Newline');
|
||||
|
||||
return "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses language block and updates lexer configuration based on it.
|
||||
*
|
||||
* @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
protected function parseLanguage()
|
||||
{
|
||||
$token = $this->expectTokenType('Language');
|
||||
|
||||
if (null === $this->languageSpecifierLine) {
|
||||
$this->lexer->analyse($this->input, $token['value']);
|
||||
$this->languageSpecifierLine = $token['line'];
|
||||
} elseif ($token['line'] !== $this->languageSpecifierLine) {
|
||||
throw new ParserException(sprintf(
|
||||
'Ambiguous language specifiers on lines: %d and %d%s',
|
||||
$this->languageSpecifierLine,
|
||||
$token['line'],
|
||||
$this->file ? ' in file: ' . $this->file : ''
|
||||
));
|
||||
}
|
||||
|
||||
return $this->parseExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the rows of a table
|
||||
*
|
||||
* @return string[][]
|
||||
*/
|
||||
private function parseTableRows()
|
||||
{
|
||||
$table = array();
|
||||
while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
|
||||
if ('Comment' === $predicted || 'Newline' === $predicted) {
|
||||
$this->acceptTokenType($predicted);
|
||||
continue;
|
||||
}
|
||||
|
||||
$token = $this->expectTokenType('TableRow');
|
||||
|
||||
$table[$token['line']] = $token['columns'];
|
||||
}
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes step node type for types But, And to type of previous step if it exists else sets to Given
|
||||
*
|
||||
* @param StepNode $node
|
||||
* @param StepNode[] $steps
|
||||
* @return StepNode
|
||||
*/
|
||||
private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
|
||||
{
|
||||
if (in_array($node->getKeywordType(), array('And', 'But'))) {
|
||||
if (($prev = end($steps))) {
|
||||
$keywordType = $prev->getKeywordType();
|
||||
} else {
|
||||
$keywordType = 'Given';
|
||||
}
|
||||
|
||||
$node = new StepNode(
|
||||
$node->getKeyword(),
|
||||
$node->getText(),
|
||||
$node->getArguments(),
|
||||
$node->getLine(),
|
||||
$keywordType
|
||||
);
|
||||
}
|
||||
return $node;
|
||||
}
|
||||
|
||||
private function rethrowNodeException(NodeException $e): void
|
||||
{
|
||||
throw new ParserException(
|
||||
$e->getMessage() . ($this->file ? ' in file ' . $this->file : ''),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../codeception/codeception/codecept)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
include("phpvfscomposer://" . __DIR__ . '/..'.'/codeception/codeception/codecept');
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
include __DIR__ . '/..'.'/codeception/codeception/codecept';
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/codecept
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
||||
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../cebe/markdown/bin/markdown)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
include("phpvfscomposer://" . __DIR__ . '/..'.'/cebe/markdown/bin/markdown');
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
include __DIR__ . '/..'.'/cebe/markdown/bin/markdown';
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/markdown
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
||||
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../nikic/php-parser/bin/php-parse)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse');
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/php-parse
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
||||
Vendored
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../phpunit/phpunit/phpunit)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath(__DIR__ . '/..'.'/phpunit/phpunit/phpunit'));
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = 'phpvfscomposer://'.$this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
$data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);
|
||||
$data = str_replace('__FILE__', var_export($this->realpath, true), $data);
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/phpunit
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
||||
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../symfony/yaml/Resources/bin/yaml-lint)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint');
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
include __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint';
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/yaml-lint
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
||||
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../yiisoft/yii2/yii)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
include("phpvfscomposer://" . __DIR__ . '/..'.'/yiisoft/yii2/yii');
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
include __DIR__ . '/..'.'/yiisoft/yii2/yii';
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
@ECHO OFF
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
SET BIN_TARGET=%~dp0/yii
|
||||
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||
php "%BIN_TARGET%" %*
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
loose: true,
|
||||
bugfixes: true,
|
||||
modules: false
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# https://github.com/browserslist/browserslist#readme
|
||||
|
||||
>= 0.5%
|
||||
last 2 major versions
|
||||
not dead
|
||||
Chrome >= 60
|
||||
Firefox >= 60
|
||||
Firefox ESR
|
||||
iOS >= 12
|
||||
Safari >= 12
|
||||
not Explorer <= 11
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"path": "./dist/css/bootstrap-grid.css",
|
||||
"maxSize": "7.5 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/bootstrap-grid.min.css",
|
||||
"maxSize": "6.55 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/bootstrap-reboot.css",
|
||||
"maxSize": "2.75 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/bootstrap-reboot.min.css",
|
||||
"maxSize": "2.5 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/bootstrap-utilities.css",
|
||||
"maxSize": "9.25 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/bootstrap-utilities.min.css",
|
||||
"maxSize": "8.5 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/bootstrap.css",
|
||||
"maxSize": "28.75 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/bootstrap.min.css",
|
||||
"maxSize": "26.75 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/js/bootstrap.bundle.js",
|
||||
"maxSize": "43.25 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/js/bootstrap.bundle.min.js",
|
||||
"maxSize": "22.75 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/js/bootstrap.esm.js",
|
||||
"maxSize": "28.0 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/js/bootstrap.esm.min.js",
|
||||
"maxSize": "18.5 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/js/bootstrap.js",
|
||||
"maxSize": "28.75 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/js/bootstrap.min.js",
|
||||
"maxSize": "16.25 kB"
|
||||
}
|
||||
],
|
||||
"ci": {
|
||||
"trackBranches": [
|
||||
"main",
|
||||
"v4-dev"
|
||||
]
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"version": "0.2",
|
||||
"words": [
|
||||
"affordance",
|
||||
"allowfullscreen",
|
||||
"Analyser",
|
||||
"autohide",
|
||||
"autohiding",
|
||||
"autoplay",
|
||||
"autoplays",
|
||||
"blazingly",
|
||||
"Blockquotes",
|
||||
"Bootstrappers",
|
||||
"borderless",
|
||||
"Brotli",
|
||||
"browserslist",
|
||||
"browserslistrc",
|
||||
"btncheck",
|
||||
"btnradio",
|
||||
"callout",
|
||||
"callouts",
|
||||
"camelCase",
|
||||
"clearfix",
|
||||
"Codesniffer",
|
||||
"combinator",
|
||||
"Contentful",
|
||||
"Cpath",
|
||||
"Crossfade",
|
||||
"crossfading",
|
||||
"cssgrid",
|
||||
"Csvg",
|
||||
"Datalists",
|
||||
"Deque",
|
||||
"discoverability",
|
||||
"docsearch",
|
||||
"docsref",
|
||||
"dropend",
|
||||
"dropleft",
|
||||
"dropright",
|
||||
"dropstart",
|
||||
"dropup",
|
||||
"errorf",
|
||||
"favicon",
|
||||
"favicons",
|
||||
"fieldsets",
|
||||
"flexbox",
|
||||
"fullscreen",
|
||||
"getbootstrap",
|
||||
"Grayscale",
|
||||
"Hoverable",
|
||||
"hreflang",
|
||||
"hstack",
|
||||
"importmap",
|
||||
"jsdelivr",
|
||||
"Jumpstart",
|
||||
"keyframes",
|
||||
"libera",
|
||||
"libman",
|
||||
"Libsass",
|
||||
"lightboxes",
|
||||
"Lowercased",
|
||||
"markdownify",
|
||||
"mediaqueries",
|
||||
"minifiers",
|
||||
"misfunction",
|
||||
"mkdir",
|
||||
"monospace",
|
||||
"mouseleave",
|
||||
"navbars",
|
||||
"navs",
|
||||
"Neue",
|
||||
"noindex",
|
||||
"Noto",
|
||||
"offcanvas",
|
||||
"offcanvases",
|
||||
"Packagist",
|
||||
"popperjs",
|
||||
"prebuild",
|
||||
"prefersreducedmotion",
|
||||
"prepended",
|
||||
"printf",
|
||||
"rects",
|
||||
"relref",
|
||||
"rgba",
|
||||
"roboto",
|
||||
"RTLCSS",
|
||||
"ruleset",
|
||||
"screenreaders",
|
||||
"scrollbars",
|
||||
"scrollspy",
|
||||
"Segoe",
|
||||
"semibold",
|
||||
"socio",
|
||||
"srcset",
|
||||
"stackblitz",
|
||||
"stickied",
|
||||
"Stylelint",
|
||||
"subnav",
|
||||
"tabbable",
|
||||
"textareas",
|
||||
"toggleable",
|
||||
"topbar",
|
||||
"touchend",
|
||||
"twbs",
|
||||
"unitless",
|
||||
"unstylable",
|
||||
"unstyled",
|
||||
"Uppercased",
|
||||
"urlize",
|
||||
"vbtn",
|
||||
"viewports",
|
||||
"Vite",
|
||||
"vstack",
|
||||
"walkthroughs",
|
||||
"WCAG",
|
||||
"zindex"
|
||||
],
|
||||
"language": "en-US",
|
||||
"files": [
|
||||
"**/*.md"
|
||||
],
|
||||
"ignorePaths": [
|
||||
".cspell.json",
|
||||
"dist/",
|
||||
"*.min.*",
|
||||
"**/*rtl*",
|
||||
"**/tests/**"
|
||||
],
|
||||
"useGitignore": true
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
**/*.min.js
|
||||
**/dist/
|
||||
**/vendor/
|
||||
/_site/
|
||||
/js/coverage/
|
||||
/js/tests/integration/
|
||||
/site/static/sw.js
|
||||
/site/layouts/
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:unicorn/recommended",
|
||||
"xo",
|
||||
"xo/browser"
|
||||
],
|
||||
"rules": {
|
||||
"arrow-body-style": "off",
|
||||
"capitalized-comments": "off",
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"MemberExpression": "off",
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"max-params": [
|
||||
"warn",
|
||||
5
|
||||
],
|
||||
"multiline-ternary": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"new-cap": [
|
||||
"error",
|
||||
{
|
||||
"properties": false
|
||||
}
|
||||
],
|
||||
"no-console": "error",
|
||||
"no-negated-condition": "off",
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"operator-linebreak": [
|
||||
"error",
|
||||
"after"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"unicorn/explicit-length-check": "off",
|
||||
"unicorn/no-array-callback-reference": "off",
|
||||
"unicorn/no-array-method-this-argument": "off",
|
||||
"unicorn/no-null": "off",
|
||||
"unicorn/no-unused-properties": "error",
|
||||
"unicorn/prefer-array-flat": "off",
|
||||
"unicorn/prefer-dom-node-dataset": "off",
|
||||
"unicorn/prefer-module": "off",
|
||||
"unicorn/prefer-query-selector": "off",
|
||||
"unicorn/prefer-spread": "off",
|
||||
"unicorn/prevent-abbreviations": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Enforce Unix newlines
|
||||
* text=auto eol=lf
|
||||
|
||||
# Don't diff or textually merge source maps
|
||||
*.map binary
|
||||
|
||||
bootstrap.css linguist-vendored=false
|
||||
bootstrap.js linguist-vendored=false
|
||||
@@ -0,0 +1,3 @@
|
||||
*.js @twbs/js-review
|
||||
*.css @twbs/css-review
|
||||
*.scss @twbs/css-review
|
||||
@@ -0,0 +1,237 @@
|
||||
# Contributing to Bootstrap
|
||||
|
||||
Looking to contribute something to Bootstrap? **Here's how you can help.**
|
||||
|
||||
Please take a moment to review this document in order to make the contribution
|
||||
process easy and effective for everyone involved.
|
||||
|
||||
Following these guidelines helps to communicate that you respect the time of
|
||||
the developers managing and developing this open source project. In return,
|
||||
they should reciprocate that respect in addressing your issue or assessing
|
||||
patches and features.
|
||||
|
||||
|
||||
## Using the issue tracker
|
||||
|
||||
The [issue tracker](https://github.com/twbs/bootstrap/issues) is
|
||||
the preferred channel for [bug reports](#bug-reports), [features requests](#feature-requests)
|
||||
and [submitting pull requests](#pull-requests), but please respect the following
|
||||
restrictions:
|
||||
|
||||
* Please **do not** use the issue tracker for personal support requests. Stack Overflow ([`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag), [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions) or [IRC](/README.md#community) are better places to get help.
|
||||
|
||||
* Please **do not** derail or troll issues. Keep the discussion on topic and
|
||||
respect the opinions of others.
|
||||
|
||||
* Please **do not** post comments consisting solely of "+1" or ":thumbsup:".
|
||||
Use [GitHub's "reactions" feature](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/)
|
||||
instead. We reserve the right to delete comments which violate this rule.
|
||||
|
||||
* Please **do not** open issues regarding the official themes offered on <https://themes.getbootstrap.com/>.
|
||||
Instead, please email any questions or feedback regarding those themes to `themes AT getbootstrap DOT com`.
|
||||
|
||||
|
||||
## Issues and labels
|
||||
|
||||
Our bug tracker utilizes several labels to help organize and identify issues. Here's what they represent and how we use them:
|
||||
|
||||
- `browser bug` - Issues that are reported to us, but actually are the result of a browser-specific bug. These are diagnosed with reduced test cases and result in an issue opened on that browser's own bug tracker.
|
||||
- `confirmed` - Issues that have been confirmed with a reduced test case and identify a bug in Bootstrap.
|
||||
- `css` - Issues stemming from our compiled CSS or source Sass files.
|
||||
- `docs` - Issues for improving or updating our documentation.
|
||||
- `examples` - Issues involving the example templates included in our docs.
|
||||
- `feature` - Issues asking for a new feature to be added, or an existing one to be extended or modified. New features require a minor version bump (e.g., `v3.0.0` to `v3.1.0`).
|
||||
- `build` - Issues with our build system, which is used to run all our tests, concatenate and compile source files, and more.
|
||||
- `help wanted` - Issues we need or would love help from the community to resolve.
|
||||
- `js` - Issues stemming from our compiled or source JavaScript files.
|
||||
- `meta` - Issues with the project itself or our GitHub repository.
|
||||
|
||||
For a complete look at our labels, see the [project labels page](https://github.com/twbs/bootstrap/labels).
|
||||
|
||||
|
||||
## Bug reports
|
||||
|
||||
A bug is a _demonstrable problem_ that is caused by the code in the repository.
|
||||
Good bug reports are extremely helpful, so thanks!
|
||||
|
||||
Guidelines for bug reports:
|
||||
|
||||
0. **[Validate your HTML](https://html5.validator.nu/)** to ensure your
|
||||
problem isn't caused by a simple error in your own code.
|
||||
|
||||
1. **Use the GitHub issue search** — check if the issue has already been
|
||||
reported.
|
||||
|
||||
2. **Check if the issue has been fixed** — try to reproduce it using the
|
||||
latest `main` (or `v4-dev` branch if the issue is about v4) in the repository.
|
||||
|
||||
3. **Isolate the problem** — ideally create a [reduced test
|
||||
case](https://css-tricks.com/reduced-test-cases/) and a live example.
|
||||
These [v4 CodePen](https://codepen.io/team/bootstrap/pen/yLabNQL) and [v5 CodePen](https://codepen.io/team/bootstrap/pen/qBamdLj) are helpful templates.
|
||||
|
||||
|
||||
A good bug report shouldn't leave others needing to chase you up for more
|
||||
information. Please try to be as detailed as possible in your report. What is
|
||||
your environment? What steps will reproduce the issue? What browser(s) and OS
|
||||
experience the problem? Do other browsers show the bug differently? What
|
||||
would you expect to be the outcome? All these details will help people to fix
|
||||
any potential bugs.
|
||||
|
||||
Example:
|
||||
|
||||
> Short and descriptive example bug report title
|
||||
>
|
||||
> A summary of the issue and the browser/OS environment in which it occurs. If
|
||||
> suitable, include the steps required to reproduce the bug.
|
||||
>
|
||||
> 1. This is the first step
|
||||
> 2. This is the second step
|
||||
> 3. Further steps, etc.
|
||||
>
|
||||
> `<url>` - a link to the reduced test case
|
||||
>
|
||||
> Any other information you want to share that is relevant to the issue being
|
||||
> reported. This might include the lines of code that you have identified as
|
||||
> causing the bug, and potential solutions (and your opinions on their
|
||||
> merits).
|
||||
|
||||
### Reporting upstream browser bugs
|
||||
|
||||
Sometimes bugs reported to us are actually caused by bugs in the browser(s) themselves, not bugs in Bootstrap per se.
|
||||
|
||||
| Vendor(s) | Browser(s) | Rendering engine | Bug reporting website(s) | Notes |
|
||||
| ------------- | ---------------------------- | ---------------- | ------------------------------------------------------ | -------------------------------------------------------- |
|
||||
| Mozilla | Firefox | Gecko | https://bugzilla.mozilla.org/enter_bug.cgi | "Core" is normally the right product option to choose. |
|
||||
| Apple | Safari | WebKit | https://bugs.webkit.org/enter_bug.cgi?product=WebKit | In Apple's bug reporter, choose "Safari" as the product. |
|
||||
| Google, Opera | Chrome, Chromium, Opera v15+ | Blink | https://bugs.chromium.org/p/chromium/issues/list | Click the "New issue" button. |
|
||||
| Microsoft | Edge | Blink | https://developer.microsoft.com/en-us/microsoft-edge/ | Go to "Help > Send Feedback" from the browser |
|
||||
|
||||
|
||||
## Feature requests
|
||||
|
||||
Feature requests are welcome. But take a moment to find out whether your idea
|
||||
fits with the scope and aims of the project. It's up to *you* to make a strong
|
||||
case to convince the project's developers of the merits of this feature. Please
|
||||
provide as much detail and context as possible.
|
||||
|
||||
|
||||
## Pull requests
|
||||
|
||||
Good pull requests—patches, improvements, new features—are a fantastic
|
||||
help. They should remain focused in scope and avoid containing unrelated
|
||||
commits.
|
||||
|
||||
**Please ask first** before embarking on any **significant** pull request (e.g.
|
||||
implementing features, refactoring code, porting to a different language),
|
||||
otherwise you risk spending a lot of time working on something that the
|
||||
project's developers might not want to merge into the project. For trivial
|
||||
things, or things that don't require a lot of your time, you can go ahead and
|
||||
make a PR.
|
||||
|
||||
Please adhere to the [coding guidelines](#code-guidelines) used throughout the
|
||||
project (indentation, accurate comments, etc.) and any other requirements
|
||||
(such as test coverage).
|
||||
|
||||
**Do not edit `bootstrap.css` or `bootstrap.js`, and do not commit
|
||||
any dist files (`dist/` or `js/dist`).** Those files are automatically generated by our build tools. You should
|
||||
edit the source files in [`/bootstrap/scss/`](https://github.com/twbs/bootstrap/tree/main/scss)
|
||||
and/or [`/bootstrap/js/src/`](https://github.com/twbs/bootstrap/tree/main/js/src) instead.
|
||||
|
||||
Similarly, when contributing to Bootstrap's documentation, you should edit the
|
||||
documentation source files in
|
||||
[the `/bootstrap/site/content/docs/` directory of the `main` branch](https://github.com/twbs/bootstrap/tree/main/site/content/docs).
|
||||
**Do not edit the `gh-pages` branch.** That branch is generated from the
|
||||
documentation source files and is managed separately by the Bootstrap Core Team.
|
||||
|
||||
Adhering to the following process is the best way to get your work
|
||||
included in the project:
|
||||
|
||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork,
|
||||
and configure the remotes:
|
||||
|
||||
```bash
|
||||
# Clone your fork of the repo into the current directory
|
||||
git clone https://github.com/<your-username>/bootstrap.git
|
||||
# Navigate to the newly cloned directory
|
||||
cd bootstrap
|
||||
# Assign the original repo to a remote called "upstream"
|
||||
git remote add upstream https://github.com/twbs/bootstrap.git
|
||||
```
|
||||
|
||||
2. If you cloned a while ago, get the latest changes from upstream:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull upstream main
|
||||
```
|
||||
|
||||
3. Create a new topic branch (off the main project development branch) to
|
||||
contain your feature, change, or fix:
|
||||
|
||||
```bash
|
||||
git checkout -b <topic-branch-name>
|
||||
```
|
||||
|
||||
4. Commit your changes in logical chunks. Please adhere to these [git commit
|
||||
message guidelines](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
|
||||
or your code is unlikely be merged into the main project. Use Git's
|
||||
[interactive rebase](https://help.github.com/articles/about-git-rebase/)
|
||||
feature to tidy up your commits before making them public.
|
||||
|
||||
5. Locally merge (or rebase) the upstream development branch into your topic branch:
|
||||
|
||||
```bash
|
||||
git pull [--rebase] upstream main
|
||||
```
|
||||
|
||||
6. Push your topic branch up to your fork:
|
||||
|
||||
```bash
|
||||
git push origin <topic-branch-name>
|
||||
```
|
||||
|
||||
7. [Open a Pull Request](https://help.github.com/articles/about-pull-requests/)
|
||||
with a clear title and description against the `main` branch.
|
||||
|
||||
**IMPORTANT**: By submitting a patch, you agree to allow the project owners to
|
||||
license your work under the terms of the [MIT License](../LICENSE) (if it
|
||||
includes code changes) and under the terms of the
|
||||
[Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/)
|
||||
(if it includes documentation changes).
|
||||
|
||||
|
||||
## Code guidelines
|
||||
|
||||
### HTML
|
||||
|
||||
[Adhere to the Code Guide.](https://codeguide.co/#html)
|
||||
|
||||
- Use tags and elements appropriate for an HTML5 doctype (e.g., self-closing tags).
|
||||
- Use CDNs and HTTPS for third-party JS when possible. We don't use protocol-relative URLs in this case because they break when viewing the page locally via `file://`.
|
||||
- Use [WAI-ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes in documentation examples to promote accessibility.
|
||||
|
||||
### CSS
|
||||
|
||||
[Adhere to the Code Guide.](https://codeguide.co/#css)
|
||||
|
||||
- When feasible, default color palettes should comply with [WCAG color contrast guidelines](https://www.w3.org/TR/WCAG20/#visual-audio-contrast).
|
||||
- Except in rare cases, don't remove default `:focus` styles (via e.g. `outline: none;`) without providing alternative styles. See [this A11Y Project post](https://www.a11yproject.com/posts/2013-01-25-never-remove-css-outlines/) for more details.
|
||||
|
||||
### JS
|
||||
|
||||
- No semicolons (in client-side JS)
|
||||
- 2 spaces (no tabs)
|
||||
- strict mode
|
||||
- "Attractive"
|
||||
|
||||
### Checking coding style
|
||||
|
||||
Run `npm run test` before committing to ensure your changes follow our coding standards.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
By contributing your code, you agree to license your contribution under the [MIT License](../LICENSE).
|
||||
By contributing to the documentation, you agree to license your contribution under the [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/).
|
||||
|
||||
Prior to v3.1.0, Bootstrap's code was released under the Apache License v2.0.
|
||||
@@ -0,0 +1,62 @@
|
||||
name: Report a bug
|
||||
description: Tell us about a bug or issue you may have identified in Bootstrap.
|
||||
title: "Provide a general summary of the issue"
|
||||
labels: [bug]
|
||||
assignees: "-"
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
description: Take a couple minutes to help our maintainers work faster.
|
||||
options:
|
||||
- label: I have [searched](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue) for duplicate or closed issues
|
||||
required: true
|
||||
- label: I have [validated](https://html5.validator.nu/) any HTML to avoid common problems
|
||||
required: true
|
||||
- label: I have read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md)
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Describe the issue
|
||||
description: Provide a summary of the issue and what you expected to happen, including specific steps to reproduce.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reduced-test-case
|
||||
attributes:
|
||||
label: Reduced test cases
|
||||
description: Include links [reduced test case](https://css-tricks.com/reduced-test-cases/) links or suggested fixes using CodePen ([v4 template](https://codepen.io/team/bootstrap/pen/yLabNQL) or [v5 template](https://codepen.io/team/bootstrap/pen/qBamdLj)).
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: What operating system(s) are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Android
|
||||
- iOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browser
|
||||
attributes:
|
||||
label: What browser(s) are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Chrome
|
||||
- Safari
|
||||
- Firefox
|
||||
- Microsoft Edge
|
||||
- Opera
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: What version of Bootstrap are you using?
|
||||
placeholder: "e.g., v5.1.0 or v4.5.2"
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: Ask the community
|
||||
url: https://github.com/twbs/bootstrap/discussions/new
|
||||
about: Ask and discuss questions with other Bootstrap community members.
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Feature request
|
||||
description: Suggest new or updated features to include in Bootstrap.
|
||||
title: "Suggest a new feature"
|
||||
labels: [feature]
|
||||
assignees: []
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
description: Take a couple minutes to help our maintainers work faster.
|
||||
options:
|
||||
- label: I have [searched](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue) for duplicate or closed feature requests
|
||||
required: true
|
||||
- label: I have read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md)
|
||||
required: true
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposal
|
||||
description: Provide detailed information for what we should add, including relevant links to prior art, screenshots, or live demos whenever possible.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: motivation
|
||||
attributes:
|
||||
label: Motivation and context
|
||||
description: Tell us why this change is needed or helpful, and what problems it may help solve.
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,38 @@
|
||||
### Description
|
||||
|
||||
<!-- Describe your changes in detail -->
|
||||
|
||||
### Motivation & Context
|
||||
|
||||
<!-- Why is this change required? What problem does it solve? -->
|
||||
|
||||
### Type of changes
|
||||
|
||||
<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. -->
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Refactoring (non-breaking change)
|
||||
- [ ] Breaking change (fix or feature that would change existing functionality)
|
||||
|
||||
### Checklist
|
||||
|
||||
<!-- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||
|
||||
- [ ] I have read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md)
|
||||
- [ ] My code follows the code style of the project _(using `npm run lint`)_
|
||||
- [ ] My change introduces changes to the documentation
|
||||
- [ ] I have updated the documentation accordingly
|
||||
- [ ] I have added tests to cover my changes
|
||||
- [ ] All new and existing tests passed
|
||||
|
||||
#### Live previews
|
||||
|
||||
<!-- Please add direct links where your modifications can be seen in the documentation -->
|
||||
|
||||
* https://deploy-preview-{your pr number}--twbs-bootstrap.netlify.app/
|
||||
|
||||
### Related issues
|
||||
|
||||
<!-- Please link any related issues here. -->
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
### Bug reports
|
||||
|
||||
See the [contributing guidelines](CONTRIBUTING.md) for sharing bug reports.
|
||||
|
||||
### How-to
|
||||
|
||||
For general troubleshooting or help getting started:
|
||||
|
||||
- Ask and explore [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions).
|
||||
- Chat with fellow Bootstrappers in IRC. On the `irc.libera.chat` server, in the `#bootstrap` channel.
|
||||
- Ask and explore Stack Overflow with the [`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag.
|
||||
@@ -0,0 +1,24 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: tuesday
|
||||
time: "12:00"
|
||||
timezone: Europe/Athens
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- XhmikosR
|
||||
labels:
|
||||
- dependencies
|
||||
- v5
|
||||
versioning-strategy: increase
|
||||
rebase-strategy: disabled
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: tuesday
|
||||
time: "12:00"
|
||||
timezone: Europe/Athens
|
||||
@@ -0,0 +1,60 @@
|
||||
name-template: 'v$NEXT_MAJOR_VERSION'
|
||||
tag-template: 'v$NEXT_MAJOR_VERSION'
|
||||
prerelease: true
|
||||
exclude-labels:
|
||||
- 'skip-changelog'
|
||||
categories:
|
||||
- title: '❗ Breaking Changes'
|
||||
labels:
|
||||
- 'breaking-change'
|
||||
- title: '🚀 Highlights'
|
||||
labels:
|
||||
- 'release-highlight'
|
||||
- title: '🚀 Features'
|
||||
labels:
|
||||
- 'new-feature'
|
||||
- 'feature'
|
||||
- 'enhancement'
|
||||
- title: '🐛 Bug fixes'
|
||||
labels:
|
||||
- 'fix'
|
||||
- 'bugfix'
|
||||
- 'bug'
|
||||
- title: '⚡ Performance improvements'
|
||||
labels:
|
||||
- 'performance'
|
||||
- title: '🎨 CSS'
|
||||
labels:
|
||||
- 'css'
|
||||
- title: '☕️ JavaScript'
|
||||
labels:
|
||||
- 'js'
|
||||
- title: '📖 Docs'
|
||||
labels:
|
||||
- 'docs'
|
||||
- title: '🛠 Examples'
|
||||
labels:
|
||||
- 'examples'
|
||||
- title: '🌎 Accessibility'
|
||||
labels:
|
||||
- 'accessibility'
|
||||
- title: '🔧 Utility API'
|
||||
labels:
|
||||
- 'utility API'
|
||||
- 'utilities'
|
||||
- title: '🏭 Tests'
|
||||
labels:
|
||||
- 'tests'
|
||||
- title: '🧰 Misc'
|
||||
labels:
|
||||
- 'build'
|
||||
- 'meta'
|
||||
- 'chore'
|
||||
- 'CI'
|
||||
- title: '📦 Dependencies'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
change-template: '- #$NUMBER: $TITLE'
|
||||
template: |
|
||||
## Changes
|
||||
$CHANGES
|
||||
@@ -0,0 +1,38 @@
|
||||
name: BrowserStack
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 2
|
||||
NODE: 16
|
||||
|
||||
jobs:
|
||||
browserstack:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'twbs/bootstrap' && (!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]'))
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "${{ env.NODE }}"
|
||||
cache: npm
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run dist
|
||||
run: npm run dist
|
||||
|
||||
- name: Run BrowserStack tests
|
||||
run: npm run js-test-cloud
|
||||
env:
|
||||
BROWSER_STACK_ACCESS_KEY: "${{ secrets.BROWSER_STACK_ACCESS_KEY }}"
|
||||
BROWSER_STACK_USERNAME: "${{ secrets.BROWSER_STACK_USERNAME }}"
|
||||
GITHUB_SHA: "${{ github.sha }}"
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Bundlewatch
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "dependabot/**"
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 2
|
||||
NODE: 16
|
||||
|
||||
jobs:
|
||||
bundlewatch:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "${{ env.NODE }}"
|
||||
cache: npm
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run dist
|
||||
run: npm run dist
|
||||
|
||||
- name: Run bundlewatch
|
||||
run: npm run bundlewatch
|
||||
env:
|
||||
BUNDLEWATCH_GITHUB_TOKEN: "${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}"
|
||||
CI_BRANCH_BASE: main
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Compress Images
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.jpg'
|
||||
- '**.jpeg'
|
||||
- '**.png'
|
||||
- '**.webp'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# Only run on Pull Requests within the same repository, and not from forks.
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: calibreapp/image-actions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Compress Images
|
||||
uses: calibreapp/image-actions@1.1.0
|
||||
with:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,38 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v4-dev
|
||||
- "!dependabot/**"
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches:
|
||||
- main
|
||||
- v4-dev
|
||||
- "!dependabot/**"
|
||||
schedule:
|
||||
- cron: "0 2 * * 5"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: "javascript"
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
@@ -0,0 +1,28 @@
|
||||
name: cspell
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "dependabot/**"
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 2
|
||||
NODE: 16
|
||||
|
||||
jobs:
|
||||
cspell:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run cspell
|
||||
uses: streetsidesoftware/cspell-action@v2
|
||||
with:
|
||||
config: ".cspell.json"
|
||||
files: "**/*.md"
|
||||
inline: error
|
||||
incremental_files_only: false
|
||||
@@ -0,0 +1,32 @@
|
||||
name: CSS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "dependabot/**"
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 2
|
||||
NODE: 16
|
||||
|
||||
jobs:
|
||||
css:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "${{ env.NODE }}"
|
||||
cache: npm
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build CSS
|
||||
run: npm run css
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "dependabot/**"
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 2
|
||||
NODE: 16
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "${{ env.NODE }}"
|
||||
cache: npm
|
||||
|
||||
- run: java -version
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build docs
|
||||
run: npm run docs-build
|
||||
|
||||
- name: Validate HTML
|
||||
run: npm run docs-vnu
|
||||
|
||||
- name: Run linkinator
|
||||
uses: JustinBeckwith/linkinator-action@v1
|
||||
with:
|
||||
paths: _site
|
||||
recurse: true
|
||||
verbosity: error
|
||||
skip: "^(?!http://localhost)"
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Close Issue Awaiting Reply
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
issue-close-require:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'twbs/bootstrap'
|
||||
steps:
|
||||
- name: awaiting reply
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
actions: "close-issues"
|
||||
labels: "awaiting-reply"
|
||||
inactive-day: 14
|
||||
body: |
|
||||
As the issue was labeled with `awaiting-reply`, but there has been no response in 14 days, this issue will be closed. If you have any questions, you can comment/reply.
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Issue Labeled
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
issue-labeled:
|
||||
if: github.repository == 'twbs/bootstrap'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: awaiting reply
|
||||
if: github.event.label.name == 'needs-example'
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
actions: "create-comment"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
body: |
|
||||
Hello @${{ github.event.issue.user.login }}. Bug reports must include a **live demo** of the issue. Per our [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md), please create a reduced test case on [CodePen](https://codepen.io/) or [StackBlitz](https://stackblitz.com/) and report back with your link, Bootstrap version, and specific browser and Operating System details.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user