Full Code of netgen/query-translator for AI

master 9f42602061cf cached
72 files
334.1 KB
74.2k tokens
243 symbols
1 requests
Download .txt
Showing preview only (357K chars total). Download the full file or copy to clipboard to get everything.
Repository: netgen/query-translator
Branch: master
Commit: 9f42602061cf
Files: 72
Total size: 334.1 KB

Directory structure:
gitextract_wpaijre9/

├── .gitattributes
├── .github/
│   └── workflows/
│       └── tests.yml
├── .gitignore
├── .php_cs.dist
├── LICENSE
├── README.md
├── composer.json
├── lib/
│   ├── Languages/
│   │   └── Galach/
│   │       ├── Generators/
│   │       │   ├── Common/
│   │       │   │   ├── Aggregate.php
│   │       │   │   └── Visitor.php
│   │       │   ├── ExtendedDisMax.php
│   │       │   ├── Lucene/
│   │       │   │   ├── Common/
│   │       │   │   │   ├── Group.php
│   │       │   │   │   ├── LogicalAnd.php
│   │       │   │   │   ├── LogicalNot.php
│   │       │   │   │   ├── LogicalOr.php
│   │       │   │   │   ├── Mandatory.php
│   │       │   │   │   ├── Phrase.php
│   │       │   │   │   ├── Prohibited.php
│   │       │   │   │   ├── Query.php
│   │       │   │   │   ├── Tag.php
│   │       │   │   │   ├── User.php
│   │       │   │   │   └── WordBase.php
│   │       │   │   ├── ExtendedDisMax/
│   │       │   │   │   └── Word.php
│   │       │   │   └── QueryString/
│   │       │   │       └── Word.php
│   │       │   ├── Native/
│   │       │   │   ├── BinaryOperator.php
│   │       │   │   ├── Group.php
│   │       │   │   ├── Phrase.php
│   │       │   │   ├── Query.php
│   │       │   │   ├── Tag.php
│   │       │   │   ├── UnaryOperator.php
│   │       │   │   ├── User.php
│   │       │   │   └── Word.php
│   │       │   ├── Native.php
│   │       │   └── QueryString.php
│   │       ├── Parser.php
│   │       ├── README.md
│   │       ├── SYNTAX.md
│   │       ├── TokenExtractor/
│   │       │   ├── Full.php
│   │       │   └── Text.php
│   │       ├── TokenExtractor.php
│   │       ├── Tokenizer.php
│   │       └── Values/
│   │           ├── Node/
│   │           │   ├── Group.php
│   │           │   ├── LogicalAnd.php
│   │           │   ├── LogicalNot.php
│   │           │   ├── LogicalOr.php
│   │           │   ├── Mandatory.php
│   │           │   ├── Prohibited.php
│   │           │   ├── Query.php
│   │           │   └── Term.php
│   │           └── Token/
│   │               ├── GroupBegin.php
│   │               ├── Phrase.php
│   │               ├── Tag.php
│   │               ├── User.php
│   │               └── Word.php
│   ├── Parsing.php
│   ├── Tokenizing.php
│   └── Values/
│       ├── Correction.php
│       ├── Node.php
│       ├── SyntaxTree.php
│       ├── Token.php
│       └── TokenSequence.php
├── phpunit.xml
└── tests/
    ├── Galach/
    │   ├── Generators/
    │   │   ├── AggregateVisitorDispatchTest.php
    │   │   ├── ExtendedDisMaxTest.php
    │   │   ├── LuceneVisitorDispatchTest.php
    │   │   ├── NativeVisitorDispatchTest.php
    │   │   └── QueryStringTest.php
    │   ├── IntegrationTest.php
    │   ├── Tokenizer/
    │   │   ├── FullTokenizerTest.php
    │   │   ├── TextTokenizerTest.php
    │   │   └── TokenExtractorTest.php
    │   └── Values/
    │       └── NodeTraversalTest.php
    └── bootstrap.php

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
/tests export-ignore


================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests

on:
    push:
        branches: ['master']
    pull_request:

jobs:
    tests:
        name: PHP ${{ matrix.php }}
        runs-on: ubuntu-latest

        strategy:
            fail-fast: false
            matrix:
                include:
                    - php: '7.0'
                      coverage: none
                    - php: '7.1'
                      coverage: none
                    - php: '7.2'
                      coverage: none
                    - php: '7.3'
                      coverage: none
                    - php: '7.4'
                      coverage: none
                    - php: '8.0'
                      coverage: none
                    - php: '8.1'
                      coverage: none
                    - php: '8.2'
                      coverage: xdebug
                      upload_coverage: true
                    - php: '8.3'
                      coverage: none
                    - php: '8.4'
                      coverage: none
                    - php: '8.5'
                      coverage: none

        steps:
            - name: Checkout
              uses: actions/checkout@v4
              with:
                  fetch-depth: 0

            - name: Setup PHP
              uses: shivammathur/setup-php@v2
              with:
                  php-version: ${{ matrix.php }}
                  coverage: ${{ matrix.coverage }}

            - name: Configure Composer (public deps)
              run: |
                  composer config -g github-protocols https
                  composer config -g use-github-api false

            - name: Composer version
              run: composer --version

            - name: Validate composer.json
              run: composer validate --strict

            - name: Install dependencies
              run: composer update --prefer-dist

            - name: Run PHPUnit (no coverage)
              if: ${{ !matrix.upload_coverage }}
              run: vendor/bin/phpunit -c phpunit.xml --colors=always

            - name: Run PHPUnit with coverage
              if: ${{ matrix.upload_coverage }}
              run: vendor/bin/phpunit -c phpunit.xml --colors=always --coverage-clover=coverage.xml

            - name: Upload coverage to Codecov
              if: ${{ matrix.upload_coverage }}
              uses: codecov/codecov-action@v4
              with:
                  files: ./coverage.xml
                  flags: all
                  fail_ci_if_error: true
              env:
                  CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}


================================================
FILE: .gitignore
================================================
composer.lock
.php_cs.cache
.phpunit.result.cache
/vendor/


================================================
FILE: .php_cs.dist
================================================
<?php

$finder = PhpCsFixer\Finder::create()
    ->in(__DIR__)
    ->exclude([])
    ->files()->name('*.php')
;

return PhpCsFixer\Config::create()
    ->setRules([
        '@Symfony' => true,
        '@Symfony:risky' => true,
        'concat_space' => ['spacing' => 'one'],
        'array_syntax' => ['syntax' => 'short'],
        'simplified_null_return' => false,
        'phpdoc_align' => false,
        'phpdoc_separation' => false,
        'phpdoc_to_comment' => false,
        'no_useless_else' => true,
        'no_useless_return' => true,
        'ordered_class_elements' => true,
        'ordered_imports' => true,
        'cast_spaces' => false,
        'blank_line_after_opening_tag' => false,
        'single_blank_line_before_namespace' => false,
        'phpdoc_annotation_without_dot' => false,
        'phpdoc_no_alias_tag' => false,
        'space_after_semicolon' => false,
    ])
    ->setRiskyAllowed(true)
    ->setFinder($finder)
;


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2017 Petar Španja <petar@spanja.info>

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.


================================================
FILE: README.md
================================================
# Query Translator

[![Build Status](https://img.shields.io/github/actions/workflow/status/netgen/query-translator/tests.yml?branch=master&&style=flat-square)](https://github.com/netgen/query-translator/actions?query=workflow%3ATests)
[![Code Coverage](https://img.shields.io/codecov/c/github/netgen/query-translator.svg?style=flat-square)](https://codecov.io/gh/netgen/query-translator)
[![Downloads](https://img.shields.io/packagist/dt/netgen/query-translator.svg?style=flat-square)](https://packagist.org/packages/netgen/query-translator)
[![Latest stable](https://img.shields.io/packagist/v/netgen/query-translator.svg?style=flat-square)](https://packagist.org/packages/netgen/query-translator)
[![License](https://img.shields.io/packagist/l/netgen/query-translator.svg?style=flat-square)](https://packagist.org/packages/netgen/query-translator)
[![PHP](https://img.shields.io/badge/php-%3E%3D%205.6-8892BF.svg?style=flat-square)](https://secure.php.net/)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/netgen/query-translator)

Query Translator takes a search string as user input and converts it into something a search backend
can understand. Technically, it's a search query
[translator](https://en.wikipedia.org/wiki/Translator_(computing)) with
[abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) representation. From the
produced syntax tree, translation target can be anything you need. Usually it's a search backend,
like Solr and Elasticsearch, or a database abstraction layer.

A set of interfaces for implementing a language processor is provided, with a single implemented
language named [Galach](lib/Languages/Galach). Galach implements a syntax that is based on what
seems to be the unofficial standard for search query as user input. Quick cheat sheet:

`word` `"phrase"` `(group)` `+mandatory` `-prohibited` `AND` `&&` `OR` `||` `NOT` `!` `#tag` `@user`
`domain:term`

### Error handling

User input means you have to expect errors and handle them gracefully. Because of that, the parser
is completely resistant to errors. Syntax tree will contain detailed information about corrections
applied to make sense of the user input. This can be useful to clean up the input or implement rich
input interface, with features like suggestions, syntax highlighting and error feedback.

### Customization

The implementation was made with customization in mind. You can change the special characters which
will be used as part of the syntax, pick out elements of the language you want to use, implement
your own term clauses, or change how the syntax tree is converted to the target output.

### Some use cases

- User-level query language on top of your search backend
- Common query language on top of different search backends
- Control over options of the query language that is already provided by the search backend
- Better error handling than provided by the search backend
- Analysis and manipulation of the query before sending to the backend
- Customized query language (while remaining within the base syntax)
- Implementing rich input interface (with suggestions, syntax highlighting, error feedback)

Note: This implementation is intended as a
[library](https://en.wikipedia.org/wiki/Library_(computing)), meaning it doesn't try to solve
specific use cases for query translation. Instead, it's meant to be a base that you can use in
implementing such a use case.

### How to use

First add the library to your project:

```
composer require netgen/query-translator:^1.0
```

After that, make use of the features provided out of the box. If those are not enough, use extension
points to customize various parts of the translator to fit your needs. See
[Galach documentation](lib/Languages/Galach) to find out more.

## Run the demo

Demo is available as a separate repository at [netgen/query-translator-demo](https://github.com/netgen/query-translator-demo).

Steps for running the demo:

1. Create the demo project using composer `composer create-project netgen/query-translator-demo`
2. Position into the demo project directory `cd query-translator-demo`
3. Start the web server with `src` as the document root `php -S localhost:8005 -t src`
4. Open [http://localhost:8005](http://localhost:8005) in your browser ![Query Translator demo](https://raw.githubusercontent.com/netgen/query-translator-demo/master/src/animation.gif)


================================================
FILE: composer.json
================================================
{
    "name": "netgen/query-translator",
    "description": "Query Translator is a search query translator with AST representation",
    "keywords": [
        "search",
        "query",
        "tokenizer",
        "parser",
        "generator",
        "translator",
        "ast",
        "solr",
        "edismax",
        "elasticsearch"
    ],
    "type": "library",
    "homepage": "https://github.com/netgen/query-translator",
    "license": "MIT",
    "authors": [
        {
            "name": "Petar Španja",
            "email": "petar@spanja.info"
        }
    ],
    "require": {
        "php": "^7.0||^8.0"
    },
    "require-dev": {
        "phpunit/phpunit": "<10",
        "symfony/phpunit-bridge": "*",
        "friendsofphp/php-cs-fixer": "^2.11"
    },
    "autoload": {
        "psr-4": {
            "QueryTranslator\\": "lib"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "QueryTranslator\\Tests\\": "tests"
        }
    },
    "scripts": {
        "test": [
            "./vendor/bin/phpunit"
        ]
    },
    "extra": {
        "branch-alias": {
            "dev-master": "1.0.x-dev"
        }
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Common/Aggregate.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Common;

use QueryTranslator\Values\Node;
use RuntimeException;

/**
 * Common Aggregate Visitor implementation.
 */
final class Aggregate extends Visitor
{
    /**
     * @var \QueryTranslator\Languages\Galach\Generators\Common\Visitor[]
     */
    private $visitors = [];

    /**
     * Construct from the optional array of $visitors.
     *
     * @param \QueryTranslator\Languages\Galach\Generators\Common\Visitor[] $visitors
     */
    public function __construct(array $visitors = [])
    {
        foreach ($visitors as $visitor) {
            $this->addVisitor($visitor);
        }
    }

    /**
     * Add a $visitor to the aggregated collection.
     *
     * @param \QueryTranslator\Languages\Galach\Generators\Common\Visitor $visitor
     */
    public function addVisitor(Visitor $visitor)
    {
        $this->visitors[] = $visitor;
    }

    public function accept(Node $node)
    {
        return true;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        foreach ($this->visitors as $visitor) {
            if ($visitor->accept($node)) {
                return $visitor->visit($node, $this, $options);
            }
        }

        throw new RuntimeException('No visitor available for ' . get_class($node));
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Common/Visitor.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Common;

use QueryTranslator\Values\Node;

/**
 * Common base class for AST visitor implementations.
 */
abstract class Visitor
{
    /**
     * Check if visitor accepts the given $node.
     *
     * @param \QueryTranslator\Values\Node $node
     *
     * @return bool
     */
    abstract public function accept(Node $node);

    /**
     * Visit the given $node.
     *
     * @param \QueryTranslator\Values\Node $node
     * @param \QueryTranslator\Languages\Galach\Generators\Common\Visitor $subVisitor
     * @param mixed $options
     *
     * @return string
     */
    abstract public function visit(Node $node, Visitor $subVisitor = null, $options = null);
}


================================================
FILE: lib/Languages/Galach/Generators/ExtendedDisMax.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators;

use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Values\SyntaxTree;

/**
 * ExtendedDisMax generator generates query string in Solr Extended DisMax query parser format.
 *
 * @link https://cwiki.apache.org/confluence/display/solr/The+Extended+DisMax+Query+Parser
 */
final class ExtendedDisMax
{
    /**
     * @var \QueryTranslator\Languages\Galach\Generators\Common\Visitor
     */
    private $visitor;

    public function __construct(Visitor $visitor)
    {
        $this->visitor = $visitor;
    }

    /**
     * Generate query string in Solr Extended DisMax format from the given $syntaxTree.
     *
     * @param \QueryTranslator\Values\SyntaxTree $syntaxTree
     * @param mixed $options
     *
     * @return string
     */
    public function generate(SyntaxTree $syntaxTree, $options = null)
    {
        return $this->visitor->visit($syntaxTree->rootNode, null, $options);
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/Common/Group.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\Common;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Group as GroupNode;
use QueryTranslator\Languages\Galach\Values\Token\GroupBegin;
use QueryTranslator\Values\Node;

/**
 * Group Node Visitor implementation.
 */
final class Group extends Visitor
{
    /**
     * Mapping of token domain to Solr field name.
     *
     * @var array
     */
    private $domainFieldMap = [];

    /**
     * Solr field name to be used when no mapping for a domain is found.
     *
     * @var string
     */
    private $defaultFieldName;

    /**
     * @param array|null $domainFieldMap
     * @param string|null $defaultFieldName
     */
    public function __construct(array $domainFieldMap = null, $defaultFieldName = null)
    {
        if ($domainFieldMap !== null) {
            $this->domainFieldMap = $domainFieldMap;
        }

        $this->defaultFieldName = $defaultFieldName;
    }

    public function accept(Node $node)
    {
        return $node instanceof GroupNode;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof GroupNode) {
            throw new LogicException(
                'Implementation accepts instance of Group Node'
            );
        }

        if ($subVisitor === null) {
            throw new LogicException('Implementation requires sub-visitor');
        }

        $clauses = [];

        foreach ($node->nodes as $subNode) {
            $clauses[] = $subVisitor->visit($subNode, $subVisitor, $options);
        }

        $fieldPrefix = $this->getSolrFieldPrefix($node->tokenLeft);
        $clauses = implode(' ', $clauses);

        return "{$fieldPrefix}({$clauses})";
    }

    /**
     * Return Solr backend field name prefix for the given $token.
     *
     * @param \QueryTranslator\Languages\Galach\Values\Token\GroupBegin $token
     *
     * @return string
     */
    private function getSolrFieldPrefix(GroupBegin $token)
    {
        if ($token->domain === '') {
            return '';
        }

        if (isset($this->domainFieldMap[$token->domain])) {
            return $this->domainFieldMap[$token->domain] . ':';
        }

        return $this->defaultFieldName . ':';
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/Common/LogicalAnd.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\Common;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\LogicalAnd as LogicalAndNode;
use QueryTranslator\Values\Node;

/**
 * LogicalAnd operator Node Visitor implementation.
 */
final class LogicalAnd extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof LogicalAndNode;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof LogicalAndNode) {
            throw new LogicException(
                'Implementation accepts instance of LogicalAnd Node'
            );
        }

        if ($subVisitor === null) {
            throw new LogicException('Implementation requires sub-visitor');
        }

        $clauses = [
            $subVisitor->visit($node->leftOperand, $subVisitor, $options),
            $subVisitor->visit($node->rightOperand, $subVisitor, $options),
        ];

        return implode(' AND ', $clauses);
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/Common/LogicalNot.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\Common;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\LogicalNot as LogicalNotNode;
use QueryTranslator\Values\Node;

/**
 * LogicalNot operator Node Visitor implementation.
 */
final class LogicalNot extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof LogicalNotNode;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof LogicalNotNode) {
            throw new LogicException(
                'Implementation accepts instance of LogicalNot Node'
            );
        }

        if ($subVisitor === null) {
            throw new LogicException('Implementation requires sub-visitor');
        }

        $clause = $subVisitor->visit($node->operand, $subVisitor, $options);

        return "NOT {$clause}";
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/Common/LogicalOr.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\Common;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\LogicalOr as LogicalOrNode;
use QueryTranslator\Values\Node;

/**
 * LogicalOr operator Node Visitor implementation.
 */
final class LogicalOr extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof LogicalOrNode;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof LogicalOrNode) {
            throw new LogicException(
                'Implementation accepts instance of LogicalOr Node'
            );
        }

        if ($subVisitor === null) {
            throw new LogicException('Implementation requires sub-visitor');
        }

        $clauses = [
            $subVisitor->visit($node->leftOperand, $subVisitor, $options),
            $subVisitor->visit($node->rightOperand, $subVisitor, $options),
        ];

        return implode(' OR ', $clauses);
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/Common/Mandatory.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\Common;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Mandatory as MandatoryNode;
use QueryTranslator\Values\Node;

/**
 * Mandatory operator Node Visitor implementation.
 */
final class Mandatory extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof MandatoryNode;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof MandatoryNode) {
            throw new LogicException(
                'Implementation accepts instance of Mandatory Node'
            );
        }

        if ($subVisitor === null) {
            throw new LogicException('Implementation requires sub-visitor');
        }

        $clause = $subVisitor->visit($node->operand, $subVisitor, $options);

        return "+{$clause}";
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/Common/Phrase.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\Common;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Term;
use QueryTranslator\Languages\Galach\Values\Token\Phrase as PhraseToken;
use QueryTranslator\Values\Node;

/**
 * Phrase Node Visitor implementation.
 */
final class Phrase extends Visitor
{
    /**
     * Mapping of token domain to Solr field name.
     *
     * @var array
     */
    private $domainFieldMap = [];

    /**
     * Solr field name to be used when no mapping for a domain is found.
     *
     * @var string
     */
    private $defaultFieldName;

    /**
     * @param array|null $domainFieldMap
     * @param string|null $defaultFieldName
     */
    public function __construct(array $domainFieldMap = null, $defaultFieldName = null)
    {
        if ($domainFieldMap !== null) {
            $this->domainFieldMap = $domainFieldMap;
        }

        $this->defaultFieldName = $defaultFieldName;
    }

    public function accept(Node $node)
    {
        return $node instanceof Term && $node->token instanceof PhraseToken;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof Term) {
            throw new LogicException(
                'Implementation accepts instance of Term Node'
            );
        }

        $token = $node->token;

        if (!$token instanceof PhraseToken) {
            throw new LogicException(
                'Implementation accepts instance of Phrase Token'
            );
        }

        $fieldPrefix = $this->getSolrFieldPrefix($token);
        $phraseEscaped = preg_replace("/([\\{$token->quote}])/", '\\\\$1', $token->phrase);

        return "{$fieldPrefix}\"{$phraseEscaped}\"";
    }

    /**
     * Return Solr backend field name prefix for the given $token.
     *
     * @param \QueryTranslator\Languages\Galach\Values\Token\Phrase $token
     *
     * @return string
     */
    private function getSolrFieldPrefix(PhraseToken $token)
    {
        if ($token->domain === '') {
            return '';
        }

        if (isset($this->domainFieldMap[$token->domain])) {
            return $this->domainFieldMap[$token->domain] . ':';
        }

        return $this->defaultFieldName . ':';
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/Common/Prohibited.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\Common;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Prohibited as ProhibitedNode;
use QueryTranslator\Values\Node;

/**
 * Prohibited operator Node Visitor implementation.
 */
final class Prohibited extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof ProhibitedNode;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof ProhibitedNode) {
            throw new LogicException(
                'Implementation accepts instance of Prohibited Node'
            );
        }

        if ($subVisitor === null) {
            throw new LogicException('Implementation requires sub-visitor');
        }

        $clause = $subVisitor->visit($node->operand, $subVisitor, $options);

        return "-{$clause}";
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/Common/Query.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\Common;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Query as QueryNode;
use QueryTranslator\Values\Node;

/**
 * Query Node Visitor implementation.
 */
final class Query extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof QueryNode;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof QueryNode) {
            throw new LogicException(
                'Implementation accepts instance of Query Node'
            );
        }

        if ($subVisitor === null) {
            throw new LogicException('Implementation requires sub-visitor');
        }

        $clauses = [];

        foreach ($node->nodes as $subNode) {
            $clauses[] = $subVisitor->visit($subNode, $subVisitor, $options);
        }

        return implode(' ', $clauses);
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/Common/Tag.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\Common;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Term;
use QueryTranslator\Languages\Galach\Values\Token\Tag as TagToken;
use QueryTranslator\Values\Node;

/**
 * User Node Visitor implementation.
 */
final class Tag extends Visitor
{
    /**
     * @var string
     */
    private $fieldName;

    /**
     * @param string $fieldName
     */
    public function __construct($fieldName = null)
    {
        $this->fieldName = $fieldName;
    }

    public function accept(Node $node)
    {
        return $node instanceof Term && $node->token instanceof TagToken;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof Term) {
            throw new LogicException(
                'Implementation accepts instance of Term Node'
            );
        }

        $token = $node->token;

        if (!$token instanceof TagToken) {
            throw new LogicException(
                'Implementation accepts instance of Tag Token'
            );
        }

        $fieldPrefix = $this->fieldName === null ? '' : "{$this->fieldName}:";

        return "{$fieldPrefix}{$token->tag}";
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/Common/User.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\Common;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Term;
use QueryTranslator\Languages\Galach\Values\Token\User as UserToken;
use QueryTranslator\Values\Node;

/**
 * User Node Visitor implementation.
 */
final class User extends Visitor
{
    /**
     * @var string
     */
    private $fieldName;

    /**
     * @param string $fieldName
     */
    public function __construct($fieldName = null)
    {
        $this->fieldName = $fieldName;
    }

    public function accept(Node $node)
    {
        return $node instanceof Term && $node->token instanceof UserToken;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof Term) {
            throw new LogicException(
                'Implementation accepts instance of Term Node'
            );
        }

        $token = $node->token;

        if (!$token instanceof UserToken) {
            throw new LogicException(
                'Implementation accepts instance of User Token'
            );
        }

        $fieldPrefix = $this->fieldName === null ? '' : "{$this->fieldName}:";

        return "{$fieldPrefix}{$token->user}";
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/Common/WordBase.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\Common;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Term;
use QueryTranslator\Languages\Galach\Values\Token\Word as WordToken;
use QueryTranslator\Values\Node;

/**
 * Base Word Node Visitor implementation.
 */
abstract class WordBase extends Visitor
{
    /**
     * Mapping of token domain to the backend field name.
     *
     * @var array
     */
    private $domainFieldMap = [];

    /**
     * Solr field name to be used when no mapping for a domain is found.
     *
     * @var string
     */
    private $defaultFieldName;

    /**
     * @param array|null $domainFieldMap
     * @param string|null $defaultFieldName
     */
    public function __construct(array $domainFieldMap = null, $defaultFieldName = null)
    {
        if ($domainFieldMap !== null) {
            $this->domainFieldMap = $domainFieldMap;
        }

        $this->defaultFieldName = $defaultFieldName;
    }

    public function accept(Node $node)
    {
        return $node instanceof Term && $node->token instanceof WordToken;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof Term) {
            throw new LogicException(
                'Implementation accepts instance of Term Node'
            );
        }

        $token = $node->token;

        if (!$token instanceof WordToken) {
            throw new LogicException(
                'Implementation accepts instance of Word Token'
            );
        }

        $fieldPrefix = $this->getSolrFieldPrefix($token);
        $wordEscaped = $this->escapeWord($token->word);

        return "{$fieldPrefix}{$wordEscaped}";
    }

    /**
     * Escape special characters in the given word $string.
     *
     * @param string $string
     *
     * @return string
     */
    abstract protected function escapeWord($string);

    /**
     * Return backend field name prefix for the given $token.
     *
     * @param \QueryTranslator\Languages\Galach\Values\Token\Word $token
     *
     * @return string
     */
    private function getSolrFieldPrefix(WordToken $token)
    {
        if ($token->domain === '') {
            return '';
        }

        if (isset($this->domainFieldMap[$token->domain])) {
            return $this->domainFieldMap[$token->domain] . ':';
        }

        return $this->defaultFieldName . ':';
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/ExtendedDisMax/Word.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\ExtendedDisMax;

use QueryTranslator\Languages\Galach\Generators\Lucene\Common\WordBase;

/**
 * Word Node Visitor implementation.
 */
final class Word extends WordBase
{
    /**
     * {@inheritdoc}
     *
     * @link http://lucene.apache.org/core/5_0_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#Escaping_Special_Characters
     *
     * Note: additionally to what is defined above we also escape blank space.
     */
    protected function escapeWord($string)
    {
        return preg_replace(
            '/(\\+|-|&&|\\|\\||!|\\(|\\)|\\{|}|\\[|]|\\^|"|~|\\*|\\?|:|\\/|\\\\| )/',
            '\\\\$1',
            $string
        );
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Lucene/QueryString/Word.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Lucene\QueryString;

use QueryTranslator\Languages\Galach\Generators\Lucene\Common\WordBase;

/**
 * Word Node Visitor implementation.
 */
final class Word extends WordBase
{
    /**
     * {@inheritdoc}
     *
     * @link http://lucene.apache.org/core/6_5_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#Escaping_Special_Characters
     *
     * Note: additionally to what is defined above we also escape blank space.
     */
    protected function escapeWord($string)
    {
        return preg_replace(
            '/(\\+|-|\\=|&&|\\|\\||\\>|\\<|!|\\(|\\)|\\{|}|\\[|]|\\^|"|~|\\*|\\?|:|\\/|\\\\| )/',
            '\\\\$1',
            $string
        );
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Native/BinaryOperator.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Native;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\LogicalAnd;
use QueryTranslator\Languages\Galach\Values\Node\LogicalOr as LogicalOrNode;
use QueryTranslator\Values\Node;

/**
 * BinaryOperator operator Node Visitor implementation.
 */
final class BinaryOperator extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof LogicalAnd || $node instanceof LogicalOrNode;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof LogicalAnd && !$node instanceof LogicalOrNode) {
            throw new LogicException(
                'Implementation accepts instance of LogicalAnd or LogicalOr Node'
            );
        }

        if ($subVisitor === null) {
            throw new LogicException('Implementation requires sub-visitor');
        }

        $clauses = [
            $subVisitor->visit($node->leftOperand, $subVisitor, $options),
            $subVisitor->visit($node->rightOperand, $subVisitor, $options),
        ];

        return implode(" {$node->token->lexeme} ", $clauses);
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Native/Group.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Native;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Group as GroupNode;
use QueryTranslator\Values\Node;

/**
 * Group Node Visitor implementation.
 */
final class Group extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof GroupNode;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof GroupNode) {
            throw new LogicException(
                'Implementation accepts instance of Group Node'
            );
        }

        if ($subVisitor === null) {
            throw new LogicException('Implementation requires sub-visitor');
        }

        $clauses = [];

        foreach ($node->nodes as $subNode) {
            $clauses[] = $subVisitor->visit($subNode, $subVisitor, $options);
        }

        $clauses = implode(' ', $clauses);
        $domainPrefix = $node->tokenLeft->domain === '' ? '' : "{$node->tokenLeft->domain}:";

        return "{$domainPrefix}{$node->tokenLeft->delimiter}{$clauses}{$node->tokenRight->lexeme}";
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Native/Phrase.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Native;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Term;
use QueryTranslator\Languages\Galach\Values\Token\Phrase as PhraseToken;
use QueryTranslator\Values\Node;

/**
 * Phrase Node Visitor implementation.
 */
final class Phrase extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof Term && $node->token instanceof PhraseToken;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof Term) {
            throw new LogicException(
                'Implementation accepts instance of Term Node'
            );
        }

        $token = $node->token;

        if (!$token instanceof PhraseToken) {
            throw new LogicException(
                'Implementation accepts instance of Phrase Token'
            );
        }

        $domainPrefix = $token->domain === '' ? '' : "{$token->domain}:";
        $phraseEscaped = preg_replace("/([\\{$token->quote}])/", '\\\\$1', $token->phrase);

        return "{$domainPrefix}{$token->quote}{$phraseEscaped}{$token->quote}";
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Native/Query.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Native;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Query as QueryNode;
use QueryTranslator\Values\Node;

/**
 * Query Node Visitor implementation.
 */
final class Query extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof QueryNode;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof QueryNode) {
            throw new LogicException(
                'Implementation accepts instance of Query Node'
            );
        }

        if ($subVisitor === null) {
            throw new LogicException('Implementation requires sub-visitor');
        }

        $clauses = [];

        foreach ($node->nodes as $subNode) {
            $clauses[] = $subVisitor->visit($subNode, $subVisitor, $options);
        }

        return implode(' ', $clauses);
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Native/Tag.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Native;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Term;
use QueryTranslator\Languages\Galach\Values\Token\Tag as TagToken;
use QueryTranslator\Values\Node;

/**
 * User Node Visitor implementation.
 */
final class Tag extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof Term && $node->token instanceof TagToken;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof Term) {
            throw new LogicException(
                'Implementation accepts instance of Term Node'
            );
        }

        $token = $node->token;

        if (!$token instanceof TagToken) {
            throw new LogicException(
                'Implementation accepts instance of Tag Token'
            );
        }

        return "{$token->marker}{$token->tag}";
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Native/UnaryOperator.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Native;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Tokenizer;
use QueryTranslator\Languages\Galach\Values\Node\LogicalNot;
use QueryTranslator\Languages\Galach\Values\Node\Mandatory;
use QueryTranslator\Languages\Galach\Values\Node\Prohibited;
use QueryTranslator\Values\Node;

/**
 * Unary operator Node Visitor implementation.
 */
final class UnaryOperator extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof Mandatory || $node instanceof Prohibited || $node instanceof LogicalNot;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof Mandatory && !$node instanceof Prohibited && !$node instanceof LogicalNot) {
            throw new LogicException(
                'Implementation accepts instance of Mandatory, Prohibited or LogicalNot Node'
            );
        }

        if ($subVisitor === null) {
            throw new LogicException('Implementation requires sub-visitor');
        }

        $clause = $subVisitor->visit($node->operand, $subVisitor, $options);

        $padding = '';
        if ($node->token->type === Tokenizer::TOKEN_LOGICAL_NOT) {
            $padding = ' ';
        }

        return "{$node->token->lexeme}{$padding}{$clause}";
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Native/User.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Native;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Term;
use QueryTranslator\Languages\Galach\Values\Token\User as UserToken;
use QueryTranslator\Values\Node;

/**
 * User Node Visitor implementation.
 */
final class User extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof Term && $node->token instanceof UserToken;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof Term) {
            throw new LogicException(
                'Implementation accepts instance of Term Node'
            );
        }

        $token = $node->token;

        if (!$token instanceof UserToken) {
            throw new LogicException(
                'Implementation accepts instance of User Token'
            );
        }

        return "{$token->marker}{$token->user}";
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Native/Word.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators\Native;

use LogicException;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Values\Node\Term;
use QueryTranslator\Languages\Galach\Values\Token\Word as WordToken;
use QueryTranslator\Values\Node;

/**
 * Word Node Visitor implementation.
 */
final class Word extends Visitor
{
    public function accept(Node $node)
    {
        return $node instanceof Term && $node->token instanceof WordToken;
    }

    public function visit(Node $node, Visitor $subVisitor = null, $options = null)
    {
        if (!$node instanceof Term) {
            throw new LogicException(
                'Implementation accepts instance of Term Node'
            );
        }

        $token = $node->token;

        if (!$token instanceof WordToken) {
            throw new LogicException(
                'Implementation accepts instance of Word Token'
            );
        }

        $domainPrefix = $token->domain === '' ? '' : "{$token->domain}:";
        $wordEscaped = preg_replace('/([\\\'"+\-!():#@ ])/', '\\\\$1', $token->word);

        return "{$domainPrefix}{$wordEscaped}";
    }
}


================================================
FILE: lib/Languages/Galach/Generators/Native.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators;

use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Values\SyntaxTree;

/**
 * Native Galach generator generates query string in Galach format.
 */
final class Native
{
    /**
     * @var \QueryTranslator\Languages\Galach\Generators\Common\Visitor
     */
    private $visitor;

    public function __construct(Visitor $visitor)
    {
        $this->visitor = $visitor;
    }

    /**
     * Generate query string in Galach format from the given $syntaxTree.
     *
     * @param \QueryTranslator\Values\SyntaxTree $syntaxTree
     *
     * @return string
     */
    public function generate(SyntaxTree $syntaxTree)
    {
        return $this->visitor->visit($syntaxTree->rootNode);
    }
}


================================================
FILE: lib/Languages/Galach/Generators/QueryString.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Generators;

use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Values\SyntaxTree;

/**
 * QueryString generator generates query string in Elasticsearch Query String Query format.
 *
 * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html
 */
final class QueryString
{
    /**
     * @var \QueryTranslator\Languages\Galach\Generators\Common\Visitor
     */
    private $visitor;

    public function __construct(Visitor $visitor)
    {
        $this->visitor = $visitor;
    }

    /**
     * Generate query string in Elasticsearch Query String Query format from the given $syntaxTree.
     *
     * @param \QueryTranslator\Values\SyntaxTree $syntaxTree
     * @param mixed $options
     *
     * @return string
     */
    public function generate(SyntaxTree $syntaxTree, $options = null)
    {
        return $this->visitor->visit($syntaxTree->rootNode, null, $options);
    }
}


================================================
FILE: lib/Languages/Galach/Parser.php
================================================
<?php

namespace QueryTranslator\Languages\Galach;

use QueryTranslator\Languages\Galach\Values\Node\Group;
use QueryTranslator\Languages\Galach\Values\Node\LogicalAnd;
use QueryTranslator\Languages\Galach\Values\Node\LogicalNot;
use QueryTranslator\Languages\Galach\Values\Node\LogicalOr;
use QueryTranslator\Languages\Galach\Values\Node\Mandatory;
use QueryTranslator\Languages\Galach\Values\Node\Prohibited;
use QueryTranslator\Languages\Galach\Values\Node\Query;
use QueryTranslator\Languages\Galach\Values\Node\Term;
use QueryTranslator\Parsing;
use QueryTranslator\Values\Correction;
use QueryTranslator\Values\Node;
use QueryTranslator\Values\SyntaxTree;
use QueryTranslator\Values\Token;
use QueryTranslator\Values\TokenSequence;
use SplStack;

/**
 * Galach implementation of the Parsing interface.
 */
final class Parser implements Parsing
{
    /**
     * Parser ignored adjacent unary operator preceding another operator.
     */
    const CORRECTION_ADJACENT_UNARY_OPERATOR_PRECEDING_OPERATOR_IGNORED = 0;

    /**
     * Parser ignored unary operator missing an operand.
     */
    const CORRECTION_UNARY_OPERATOR_MISSING_OPERAND_IGNORED = 1;

    /**
     * Parser ignored binary operator missing left side operand.
     */
    const CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED = 2;

    /**
     * Parser ignored binary operator missing right side operand.
     */
    const CORRECTION_BINARY_OPERATOR_MISSING_RIGHT_OPERAND_IGNORED = 3;

    /**
     * Parser ignored binary operator following another operator and connecting operators.
     */
    const CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED = 4;

    /**
     * Parser ignored logical not operators preceding mandatory or prohibited operator.
     */
    const CORRECTION_LOGICAL_NOT_OPERATORS_PRECEDING_PREFERENCE_IGNORED = 5;

    /**
     * Parser ignored empty group and connecting operators.
     */
    const CORRECTION_EMPTY_GROUP_IGNORED = 6;

    /**
     * Parser ignored unmatched left side group delimiter.
     */
    const CORRECTION_UNMATCHED_GROUP_LEFT_DELIMITER_IGNORED = 7;

    /**
     * Parser ignored unmatched right side group delimiter.
     */
    const CORRECTION_UNMATCHED_GROUP_RIGHT_DELIMITER_IGNORED = 8;

    /**
     * Parser ignored bailout type token.
     *
     * @see \QueryTranslator\Languages\Galach\Tokenizer::TOKEN_BAILOUT
     */
    const CORRECTION_BAILOUT_TOKEN_IGNORED = 9;

    private static $tokenShortcuts = [
        'operatorNot' => Tokenizer::TOKEN_LOGICAL_NOT | Tokenizer::TOKEN_LOGICAL_NOT_2,
        'operatorPreference' => Tokenizer::TOKEN_MANDATORY | Tokenizer::TOKEN_PROHIBITED,
        'operatorPrefix' => Tokenizer::TOKEN_MANDATORY | Tokenizer::TOKEN_PROHIBITED | Tokenizer::TOKEN_LOGICAL_NOT_2,
        'operatorUnary' => Tokenizer::TOKEN_MANDATORY | Tokenizer::TOKEN_PROHIBITED | Tokenizer::TOKEN_LOGICAL_NOT | Tokenizer::TOKEN_LOGICAL_NOT_2,
        'operatorBinary' => Tokenizer::TOKEN_LOGICAL_AND | Tokenizer::TOKEN_LOGICAL_OR,
        'operator' => Tokenizer::TOKEN_LOGICAL_AND | Tokenizer::TOKEN_LOGICAL_OR | Tokenizer::TOKEN_MANDATORY | Tokenizer::TOKEN_PROHIBITED | Tokenizer::TOKEN_LOGICAL_NOT | Tokenizer::TOKEN_LOGICAL_NOT_2,
        'groupDelimiter' => Tokenizer::TOKEN_GROUP_BEGIN | Tokenizer::TOKEN_GROUP_END,
        'binaryOperatorAndWhitespace' => Tokenizer::TOKEN_LOGICAL_AND | Tokenizer::TOKEN_LOGICAL_OR | Tokenizer::TOKEN_WHITESPACE,
    ];

    private static $shifts = [
        Tokenizer::TOKEN_WHITESPACE => 'shiftWhitespace',
        Tokenizer::TOKEN_TERM => 'shiftTerm',
        Tokenizer::TOKEN_GROUP_BEGIN => 'shiftGroupBegin',
        Tokenizer::TOKEN_GROUP_END => 'shiftGroupEnd',
        Tokenizer::TOKEN_LOGICAL_AND => 'shiftBinaryOperator',
        Tokenizer::TOKEN_LOGICAL_OR => 'shiftBinaryOperator',
        Tokenizer::TOKEN_LOGICAL_NOT => 'shiftLogicalNot',
        Tokenizer::TOKEN_LOGICAL_NOT_2 => 'shiftLogicalNot2',
        Tokenizer::TOKEN_MANDATORY => 'shiftPreference',
        Tokenizer::TOKEN_PROHIBITED => 'shiftPreference',
        Tokenizer::TOKEN_BAILOUT => 'shiftBailout',
    ];

    private static $nodeToReductionGroup = [
        Group::class => 'group',
        LogicalAnd::class => 'logicalAnd',
        LogicalOr::class => 'logicalOr',
        LogicalNot::class => 'unaryOperator',
        Mandatory::class => 'unaryOperator',
        Prohibited::class => 'unaryOperator',
        Term::class => 'term',
    ];

    private static $reductionGroups = [
        'group' => [
            'reduceGroup',
            'reducePreference',
            'reduceLogicalNot',
            'reduceLogicalAnd',
            'reduceLogicalOr',
        ],
        'unaryOperator' => [
            'reduceLogicalNot',
            'reduceLogicalAnd',
            'reduceLogicalOr',
        ],
        'logicalOr' => [],
        'logicalAnd' => [
            'reduceLogicalOr',
        ],
        'term' => [
            'reducePreference',
            'reduceLogicalNot',
            'reduceLogicalAnd',
            'reduceLogicalOr',
        ],
    ];

    /**
     * Input tokens.
     *
     * @var \QueryTranslator\Values\Token[]
     */
    private $tokens;

    /**
     * Query stack.
     *
     * @var \SplStack
     */
    private $stack;

    /**
     * An array of applied corrections.
     *
     * @var \QueryTranslator\Values\Correction[]
     */
    private $corrections = [];

    public function parse(TokenSequence $tokenSequence)
    {
        $this->init($tokenSequence->tokens);

        while (!empty($this->tokens)) {
            $node = $this->shift();

            if ($node instanceof Node) {
                $this->reduce($node);
            }
        }

        $this->reduceQuery();

        return new SyntaxTree($this->stack->top(), $tokenSequence, $this->corrections);
    }

    private function shift()
    {
        $token = array_shift($this->tokens);
        $shift = self::$shifts[$token->type];

        return $this->{$shift}($token);
    }

    private function reduce(Node $node)
    {
        $previousNode = null;
        $reductionIndex = null;

        while ($node instanceof Node) {
            // Reset reduction index on first iteration or on Node change
            if ($node !== $previousNode) {
                $reductionIndex = 0;
            }

            // If there are no reductions to try, put the Node on the stack
            // and continue shifting
            $reduction = $this->getReduction($node, $reductionIndex);
            if ($reduction === null) {
                $this->stack->push($node);
                break;
            }

            $previousNode = $node;
            $node = $this->{$reduction}($node);
            ++$reductionIndex;
        }
    }

    protected function shiftWhitespace()
    {
        if ($this->isTopStackToken(self::$tokenShortcuts['operatorPrefix'])) {
            $this->addCorrection(
                self::CORRECTION_UNARY_OPERATOR_MISSING_OPERAND_IGNORED,
                $this->stack->pop()
            );
        }
    }

    protected function shiftPreference(Token $token)
    {
        return $this->shiftAdjacentUnaryOperator($token, self::$tokenShortcuts['operator']);
    }

    protected function shiftAdjacentUnaryOperator(Token $token, $tokenMask)
    {
        if ($this->isToken(reset($this->tokens), $tokenMask)) {
            $this->addCorrection(
                self::CORRECTION_ADJACENT_UNARY_OPERATOR_PRECEDING_OPERATOR_IGNORED,
                $token
            );

            return null;
        }

        $this->stack->push($token);
    }

    protected function shiftLogicalNot(Token $token)
    {
        $this->stack->push($token);
    }

    protected function shiftLogicalNot2(Token $token)
    {
        $tokenMask = self::$tokenShortcuts['operator'] & ~Tokenizer::TOKEN_LOGICAL_NOT_2;

        return $this->shiftAdjacentUnaryOperator($token, $tokenMask);
    }

    protected function shiftBinaryOperator(Token $token)
    {
        if ($this->stack->isEmpty() || $this->isTopStackToken(Tokenizer::TOKEN_GROUP_BEGIN)) {
            $this->addCorrection(
                self::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED,
                $token
            );

            return null;
        }

        if ($this->isTopStackToken(self::$tokenShortcuts['operator'])) {
            $this->ignoreBinaryOperatorFollowingOperator($token);

            return null;
        }

        $this->stack->push($token);
    }

    private function ignoreBinaryOperatorFollowingOperator(Token $token)
    {
        $precedingOperators = $this->ignorePrecedingOperators(self::$tokenShortcuts['operator']);
        $followingOperators = $this->ignoreFollowingOperators();

        $this->addCorrection(
            self::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED,
            ...array_merge(
                $precedingOperators,
                [$token],
                $followingOperators
            )
        );
    }

    protected function shiftTerm(Token $token)
    {
        return new Term($token);
    }

    protected function shiftGroupBegin(Token $token)
    {
        $this->stack->push($token);
    }

    protected function shiftGroupEnd(Token $token)
    {
        $this->stack->push($token);

        return new Group();
    }

    protected function shiftBailout(Token $token)
    {
        $this->addCorrection(self::CORRECTION_BAILOUT_TOKEN_IGNORED, $token);
    }

    protected function reducePreference(Node $node)
    {
        if (!$this->isTopStackToken(self::$tokenShortcuts['operatorPreference'])) {
            return $node;
        }

        $token = $this->stack->pop();

        if ($this->isToken($token, Tokenizer::TOKEN_MANDATORY)) {
            return new Mandatory($node, $token);
        }

        return new Prohibited($node, $token);
    }

    protected function reduceLogicalNot(Node $node)
    {
        if (!$this->isTopStackToken(self::$tokenShortcuts['operatorNot'])) {
            return $node;
        }

        if ($node instanceof Mandatory || $node instanceof Prohibited) {
            $this->ignoreLogicalNotOperatorsPrecedingPreferenceOperator();

            return $node;
        }

        return new LogicalNot($node, $this->stack->pop());
    }

    public function ignoreLogicalNotOperatorsPrecedingPreferenceOperator()
    {
        $precedingOperators = $this->ignorePrecedingOperators(self::$tokenShortcuts['operatorNot']);

        if (!empty($precedingOperators)) {
            $this->addCorrection(
                self::CORRECTION_LOGICAL_NOT_OPERATORS_PRECEDING_PREFERENCE_IGNORED,
                ...$precedingOperators
            );
        }
    }

    protected function reduceLogicalAnd(Node $node)
    {
        if ($this->stack->count() <= 1 || !$this->isTopStackToken(Tokenizer::TOKEN_LOGICAL_AND)) {
            return $node;
        }

        $token = $this->stack->pop();
        $leftOperand = $this->stack->pop();

        return new LogicalAnd($leftOperand, $node, $token);
    }

    /**
     * Reduce logical OR.
     *
     * @param \QueryTranslator\Values\Node $node
     * @param bool $inGroup Reduce inside a group
     *
     * @return null|\QueryTranslator\Languages\Galach\Values\Node\LogicalOr|\QueryTranslator\Values\Node
     */
    protected function reduceLogicalOr(Node $node, $inGroup = false)
    {
        if ($this->stack->count() <= 1 || !$this->isTopStackToken(Tokenizer::TOKEN_LOGICAL_OR)) {
            return $node;
        }

        // If inside a group don't look for following logical AND
        if (!$inGroup) {
            $this->popWhitespace();
            // If the next token is logical AND, put the node on stack
            // as that has precedence over logical OR
            if ($this->isToken(reset($this->tokens), Tokenizer::TOKEN_LOGICAL_AND)) {
                $this->stack->push($node);

                return null;
            }
        }

        $token = $this->stack->pop();
        $leftOperand = $this->stack->pop();

        return new LogicalOr($leftOperand, $node, $token);
    }

    protected function reduceGroup(Group $group)
    {
        $rightDelimiter = $this->stack->pop();

        // Pop dangling tokens
        $this->popTokens(~Tokenizer::TOKEN_GROUP_BEGIN);

        if ($this->isTopStackToken(Tokenizer::TOKEN_GROUP_BEGIN)) {
            $leftDelimiter = $this->stack->pop();
            $this->ignoreEmptyGroup($leftDelimiter, $rightDelimiter);
            $this->reduceRemainingLogicalOr(true);

            return null;
        }

        $this->reduceRemainingLogicalOr(true);

        $group->nodes = $this->collectTopStackNodes();
        $group->tokenLeft = $this->stack->pop();
        $group->tokenRight = $rightDelimiter;

        return $group;
    }

    /**
     * Collect all Nodes from the top of the stack.
     *
     * @return \QueryTranslator\Values\Node[]
     */
    private function collectTopStackNodes()
    {
        $nodes = [];

        while (!$this->stack->isEmpty() && $this->stack->top() instanceof Node) {
            array_unshift($nodes, $this->stack->pop());
        }

        return $nodes;
    }

    private function ignoreEmptyGroup(Token $leftDelimiter, Token $rightDelimiter)
    {
        $precedingOperators = $this->ignorePrecedingOperators(self::$tokenShortcuts['operator']);
        $followingOperators = $this->ignoreFollowingOperators();

        $this->addCorrection(
            self::CORRECTION_EMPTY_GROUP_IGNORED,
            ...array_merge(
                $precedingOperators,
                [$leftDelimiter, $rightDelimiter],
                $followingOperators
            )
        );
    }

    /**
     * Initialize the parser with given array of $tokens.
     *
     * @param \QueryTranslator\Values\Token[] $tokens
     */
    private function init(array $tokens)
    {
        $this->corrections = [];
        $this->tokens = $tokens;
        $this->cleanupGroupDelimiters($this->tokens);
        $this->stack = new SplStack();
    }

    private function getReduction(Node $node, $reductionIndex)
    {
        $reductionGroup = self::$nodeToReductionGroup[get_class($node)];

        if (isset(self::$reductionGroups[$reductionGroup][$reductionIndex])) {
            return self::$reductionGroups[$reductionGroup][$reductionIndex];
        }

        return null;
    }

    private function reduceQuery()
    {
        $this->popTokens();
        $this->reduceRemainingLogicalOr();
        $nodes = [];

        while (!$this->stack->isEmpty()) {
            array_unshift($nodes, $this->stack->pop());
        }

        $this->stack->push(new Query($nodes));
    }

    /**
     * Check if the given $token is an instance of Token.
     *
     * Optionally also checks given Token $typeMask.
     *
     * @param mixed $token
     * @param int $typeMask
     *
     * @return bool
     */
    private function isToken($token, $typeMask = null)
    {
        if (!$token instanceof Token) {
            return false;
        }

        if (null === $typeMask || $token->type & $typeMask) {
            return true;
        }

        return false;
    }

    private function isTopStackToken($type = null)
    {
        return !$this->stack->isEmpty() && $this->isToken($this->stack->top(), $type);
    }

    /**
     * Remove whitespace Tokens from the beginning of the token array.
     */
    private function popWhitespace()
    {
        while ($this->isToken(reset($this->tokens), Tokenizer::TOKEN_WHITESPACE)) {
            array_shift($this->tokens);
        }
    }

    /**
     * Remove all Tokens from the top of the query stack and log Corrections as necessary.
     *
     * Optionally also checks that Token matches given $typeMask.
     *
     * @param int $typeMask
     */
    private function popTokens($typeMask = null)
    {
        while ($this->isTopStackToken($typeMask)) {
            $token = $this->stack->pop();
            if ($token->type & self::$tokenShortcuts['operatorUnary']) {
                $this->addCorrection(
                    self::CORRECTION_UNARY_OPERATOR_MISSING_OPERAND_IGNORED,
                    $token
                );
            } else {
                $this->addCorrection(
                    self::CORRECTION_BINARY_OPERATOR_MISSING_RIGHT_OPERAND_IGNORED,
                    $token
                );
            }
        }
    }

    private function ignorePrecedingOperators($type)
    {
        $tokens = [];
        while ($this->isTopStackToken($type)) {
            array_unshift($tokens, $this->stack->pop());
        }

        return $tokens;
    }

    private function ignoreFollowingOperators()
    {
        $tokenMask = self::$tokenShortcuts['binaryOperatorAndWhitespace'];
        $tokens = [];
        while ($this->isToken(reset($this->tokens), $tokenMask)) {
            $token = array_shift($this->tokens);
            if ($token->type & self::$tokenShortcuts['operatorBinary']) {
                $tokens[] = $token;
            }
        }

        return $tokens;
    }

    /**
     * Reduce logical OR possibly remaining after reaching end of group or query.
     *
     * @param bool $inGroup Reduce inside a group
     */
    private function reduceRemainingLogicalOr($inGroup = false)
    {
        if (!$this->stack->isEmpty() && !$this->isTopStackToken()) {
            $node = $this->reduceLogicalOr($this->stack->pop(), $inGroup);
            $this->stack->push($node);
        }
    }

    /**
     * Clean up group delimiter tokens, removing unmatched left and right delimiter.
     *
     * Closest group delimiters will be matched first, unmatched remainder is removed.
     *
     * @param \QueryTranslator\Values\Token[] $tokens
     */
    private function cleanupGroupDelimiters(array &$tokens)
    {
        $indexes = $this->getUnmatchedGroupDelimiterIndexes($tokens);

        while (!empty($indexes)) {
            $lastIndex = array_pop($indexes);
            $token = $tokens[$lastIndex];
            unset($tokens[$lastIndex]);

            if ($token->type === Tokenizer::TOKEN_GROUP_BEGIN) {
                $this->addCorrection(
                    self::CORRECTION_UNMATCHED_GROUP_LEFT_DELIMITER_IGNORED,
                    $token
                );
            } else {
                $this->addCorrection(
                    self::CORRECTION_UNMATCHED_GROUP_RIGHT_DELIMITER_IGNORED,
                    $token
                );
            }
        }
    }

    private function getUnmatchedGroupDelimiterIndexes(array &$tokens)
    {
        $trackLeft = [];
        $trackRight = [];

        foreach ($tokens as $index => $token) {
            if (!$this->isToken($token, self::$tokenShortcuts['groupDelimiter'])) {
                continue;
            }

            if ($this->isToken($token, Tokenizer::TOKEN_GROUP_BEGIN)) {
                $trackLeft[] = $index;
                continue;
            }

            if (empty($trackLeft)) {
                $trackRight[] = $index;
            } else {
                array_pop($trackLeft);
            }
        }

        return array_merge($trackLeft, $trackRight);
    }

    private function addCorrection($type, Token ...$tokens)
    {
        $this->corrections[] = new Correction($type, ...$tokens);
    }
}


================================================
FILE: lib/Languages/Galach/README.md
================================================
# Galach query language

To better understand parts of the language processor described below, run the demo:

1. Create the demo project using composer `composer create-project netgen/query-translator-demo`
2. Position into the demo project directory `cd query-translator-demo`
3. Start the web server with `src` as the document root `php -S localhost:8005 -t src`
4. Open [http://localhost:8005](http://localhost:8005) in your browser

The demo will present behavior of Query Translator in an interactive way.

### Syntax

Galach is based on a syntax that seems to be the unofficial standard for search query as user input.
It should feel familiar, as the same basic syntax is used by any popular text-based search engine
out there. It is also very similar to
[Lucene Query Parser syntax](https://lucene.apache.org/core/2_9_4/queryparsersyntax.html), used by
both Solr and Elasticsearch.

Read about it more detail in the [syntax documentation](SYNTAX.md), here we'll only show a quick
cheat sheet:

`word` `"phrase"` `(group)` `+mandatory` `-prohibited` `AND` `&&` `OR` `||` `NOT` `!` `#tag` `@user`
`domain:term`

And an example:

```
cheese AND (bacon OR eggs) +type:breakfast
```

### How it works

The implementation has some of the usual language processor phases, starting with the lexical
analysis in [Tokenizer](Tokenizer.php), followed by the syntax analysis in [Parser](Parser.php), and
ending with the target code generation in a [Generator](Generators). The output of the Parser is a
hierarchical tree structure. It represents the syntax of the query in an abstract way and is easy to
process using [tree traversal](https://en.wikipedia.org/wiki/Tree_traversal). From that syntax tree,
a target output is generated.

When broken into parts, we have a sequence like this:

1. User writes a query string
2. Query string is given to Tokenizer which produces an instance of
[TokenSequence](../../Values/TokenSequence.php)
3. TokenSequence instance is given to Parser which produces an instance of
[SyntaxTree](../../Values/SyntaxTree.php)
4. SyntaxTree instance is given to the Generator to produce a target output
5. Target output is passed to its consumer

Here's how that would look in code:

```php
use QueryTranslator\Languages\Galach\Tokenizer;
use QueryTranslator\Languages\Galach\TokenExtractor\Full as FullTokenExtractor;
use QueryTranslator\Languages\Galach\Parser;
use QueryTranslator\Languages\Galach\Generators;

// 1. User writes a query string

$queryString = $_GET['query_string'];

// This is the place where you would perform some sanity checks that are out of the scope
// of this library, for example, checking the length of the query string

// 2. Query string is given to Tokenizer which produces an instance of TokenSequence

// Note that Tokenizer needs a TokenExtractor, which is an extension point
// Here we use Full TokenExtractor which provides full Galach syntax

$tokenExtractor = new FullTokenExtractor();
$tokenizer = new Tokenizer($tokenExtractor);
$tokenSequence = $tokenizer->tokenize($queryString);

// 3. TokenSequence instance is given to Parser which produces an instance of SyntaxTree

$parser = new Parser();
$syntaxTree = $parser->parse($tokenSequence);

// If needed, here you can access corrections

foreach ($syntaxTree->corrections as $correction) {
    echo $correction->type;
}
 
// 4. Now we can build a generator, in this example an ExtendedDisMax generator to target
//    Solr's Extended DisMax Query Parser

// This part is a little bit more involving since we need to build all visitors for different
// Nodes in the syntax tree

$generator = new Generators\ExtendedDisMax(
    new Generators\Common\Aggregate([
        new Generators\Lucene\Common\BinaryOperator(),
        new Generators\Lucene\Common\Group(),
        new Generators\Lucene\Common\Phrase(),
        new Generators\Lucene\Common\Query(),
        new Generators\Lucene\Common\Tag(),
        new Generators\Lucene\Common\UnaryOperator(),
        new Generators\Lucene\Common\User(),
        new Generators\Lucene\ExtendedDisMax\Word(),
    ])
);

// Now we can use the generator to generate the target output

$targetString = $generator->generate($syntaxTree);

// Finally we can send the generated string to Solr

$result = $solrClient->search($targetString);
```

### Error handling

No input is considered invalid. Both Tokenizer and Parser are made to be resistant to errors and
will try to process anything you throw at them. When input does contain an error, a correction will
be applied. This will be repeated as necessary. The corrections are applied during parsing and are
made available in the SyntaxTree as an array of [Correction](../../Values/Correction.php) instances.
They will contain information about the type of the correction and the tokens affected by it.

One type of correction starts in the Tokenizer. When no [Token](../../Values/Token.php) can be
extracted at a current position in the input string, a single character will be read as a special
`Tokenizer::TOKEN_BAILOUT` type Token. All Tokens of that type will be ignored by the parser. The
only known case where this can happen is the occurrence of an unclosed phrase delimiter `"`.

Note that, while applying the corrections, the best efforts are made to preserve the intended
meaning of the query. The following is a list of corrections, with correction type constant and an
example of an incorrect input and a corrected result.

1. Adjacent unary operator preceding another operator is ignored

    `Parser::CORRECTION_ADJACENT_UNARY_OPERATOR_PRECEDING_OPERATOR_IGNORED`

    ```
    ++one +-two
    ```
    ```
    +one -two
    ```

2. Unary operator missing an operand is ignored

    `Parser::CORRECTION_UNARY_OPERATOR_MISSING_OPERAND_IGNORED`

    ```
    one NOT
    ```
    ```
    one
    ```

3. Binary operator missing left side operand is ignored

    `Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED`

    ```
    AND two
    ```
    ```
    two
    ```

4. Binary operator missing right side operand is ignored

    `Parser::CORRECTION_BINARY_OPERATOR_MISSING_RIGHT_OPERAND_IGNORED`

    ```
    one AND
    ```
    ```
    one
    ```

5. Binary operator following another operator is ignored together with connecting operators

    `Parser::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED`

    ```
    one AND OR AND two
    ```
    ```
    one two
    ```

6. Logical not operators preceding mandatory or prohibited operator are ignored

    `Parser::CORRECTION_LOGICAL_NOT_OPERATORS_PRECEDING_PREFERENCE_IGNORED`

    ```
    NOT +one NOT -two
    ```
    ```
    +one -two
    ```

7. Empty group is ignored together with connecting operators

    `Parser::CORRECTION_EMPTY_GROUP_IGNORED`

    ```
    one AND () OR two
    ```
    ```
    one two
    ```

8. Unmatched left side group delimiter is ignored

    `Parser::CORRECTION_UNMATCHED_GROUP_LEFT_DELIMITER_IGNORED`

    ```
    one ( AND two
    ```
    ```
    one AND two
    ```

9. Unmatched right side group delimiter is ignored

    `Parser::CORRECTION_UNMATCHED_GROUP_RIGHT_DELIMITER_IGNORED`

    ```
    one AND ) two
    ```
    ```
    one AND two
    ```

10. Any Token of `Tokenizer::TOKEN_BAILOUT` type is ignored

    `Parser::CORRECTION_BAILOUT_TOKEN_IGNORED`

    ```
    one " two
    ```
    ```
    one two
    ```

### Customization

You can modify the Galach language in a limited way:

- By changing special characters and sequences of characters used as part of the language syntax:
    - operators: `AND` `&&` `OR` `||` `NOT` `!` `+` `-`
    - grouping and phrase delimiters: `(` `)` `"`
    - user and tag markers: `@` `#`
    - domain prefix: `domain:`
- By choosing parts of the language that you want to use. You might want to use only a subset of the
  full syntax, maybe without the grouping feature, using only `+` and `-` operators, disabling
  domains, and so on.
- By implementing custom `Tokenizer::TOKEN_TERM` type token. Read more on that in the text below.

Customization happens during the lexical analysis. The Tokenizer is actually marked as `final` and
is not intended for extending. You will need to implement your own
[TokenExtractor](TokenExtractor.php), a dependency to the Tokenizer. TokenExtractor controls the
syntax through regular expressions used to recognize the [Token](../../Values/Token.php), which is a
sequence of characters forming the smallest syntactic unit of the language. The following is a list
of supported Token types, together with their `Tokenizer::TOKEN_*` constants and an example:

1. Term token – represents a category of term type tokens.

    Note that [Word](Values/Token/Word.php) and [Phrase](Values/Token/Phrase.php) term tokens can
    have domain prefix. This can't be used on [User](Values/Token/User.php) and
    [Tag](Values/Token/Tag.php) term tokens, because those define implicit domains of their own.

    `Tokenizer::TOKEN_TERM`

    ```
    word
    ```
    ```
    title:word
    ```
    ```
    "this is a phrase"
    ```
    ```
    body:"this is a phrase"
    ```
    ```
    @user
    ```
    ```
    #tag
    ```

2. Whitespace token - represents the whitespace in the input string.

    `Tokenizer::TOKEN_WHITESPACE`

    ```
    one two
       ^
    ```

3. Logical AND token - combines two adjoining elements with logical AND.

    `Tokenizer::TOKEN_LOGICAL_AND`

    ```
    one AND two
        ^^^
    ```

4. Logical OR token - combines two adjoining elements with logical OR.

    `Tokenizer::TOKEN_LOGICAL_OR`

    ```
    one OR two
        ^^
    ```

5. Logical NOT token - applies logical NOT to the next (right-side) element.

    `Tokenizer::TOKEN_LOGICAL_NOT`

    ```
    NOT one
    ^^^
    ```

6. Shorthand logical NOT token - applies logical NOT to the next (right-side) element.

    This is an alternative to the `Tokenizer::TOKEN_LOGICAL_NOT` above, with the difference that
    parser will expect it's placed next (left) to the element it applies to, without the whitespace
    in between.

    `Tokenizer::TOKEN_LOGICAL_NOT_2`

    ```
    !one
    ^
    ```

7. Mandatory operator - applies mandatory inclusion to the next (right side) element.

    `Tokenizer::TOKEN_MANDATORY`

    ```
    +one
    ^
    ```

8. Prohibited operator - applies mandatory exclusion to the next (right side) element.

    `Tokenizer::TOKEN_PROHIBITED`

    ```
    -one
    ^
    ```

9. Left side delimiter of a group.

    Note that the left side group delimiter can have domain prefix.

    `Tokenizer::TOKEN_GROUP_BEGIN`

    ```
    (one AND two)
    ^
    ```
    ```
    text:(one AND two)
    ^^^^^^
    ```

10. Right side delimiter of a group.

    `Tokenizer::TOKEN_GROUP_END`

    ```
    (one AND two)
                ^
    ```

11. Bailout token.

    `Tokenizer::TOKEN_BAILOUT`

    ```
    not exactly a phrase"
                        ^
    ```

By changing the regular expressions, you can change how tokens are recognized, including special
characters used as part of the language syntax. You can also omit regular expressions for some token
types. Through that, you can control which elements of the language you want to use. There are two
abstract methods to implement when extending the base [TokenExtractor](TokenExtractor.php):

- `getExpressionTypeMap(): array`

    Here you must return a map of regular expressions to corresponding Token types. Token type
    can be one of the predefined constants `Tokenizer::TOKEN_*`.

- `createTermToken($position, array $data): Token`

    Here you receive Token data extracted through regular expression matching and a position where
    the data was extracted at. From that, you must return the corresponding Token instance of the
    `Tokenizer::TOKEN_TERM` type.

    If needed, here you can return an instance of your own Token subtype. You can use regular
    expressions with named capturing groups to extract meaning from the input string and pass it to
    the constructor method.

Optionally you can override the `createGroupBeginToken()` method. This is useful if you want to
customize token of the `Tokenizer::TOKEN_GROUP_BEGIN` type:

- `createGroupBeginToken($position, array $data): Token`

    Here you receive Token data extracted through regular expression matching and a position where
    the data was extracted at. From that, you must return the corresponding Token instance of the
    `Tokenizer::TOKEN_GROUP_BEGIN` type.

    If needed, here you can return an instance of your own Token subtype. You can use regular
    expressions with named capturing groups to extract meaning from the input string and pass it to
    the constructor method.

Two TokenExtractor implementations are provided out of the box. You can use them as an example and a
starting point to implement your own. These are:

- [Full](TokenExtractor/Full.php) TokenExtractor, supports full syntax of the language
- [Text](TokenExtractor/Text.php) TokenExtractor, supports text related subset of the language

#### Parser

The Parser is the core of the library. It's marked as `final` and is not intended for extending.
Method `Parser::parse()` accepts TokenSequence, but it only cares about the type of the Token, so it
will be oblivious to any customizations you might do in the Tokenizer. That includes both
recognizing only a subset of the full syntax and the custom `Tokenizer::TOKEN_TERM` type tokens.
While it's possible to implement a custom Parser, at that point you should consider calling it a new
language rather than a customization of Galach.

### Generators

A generator is used to generate the target output from the SyntaxTree. Three different ones are
provided out of the box:

1. [Native](Generators/Native.php)

   `Native` generator produces query string in the Galach format. This is mostly useful as an
   example and for the cleanup of the user input. In case the corrections were applied to the input,
   the output will be corrected. Also, it will not contain any superfluous whitespace and special
   characters will be explicitly escaped.

2. [ExtendedDisMax](Generators/ExtendedDisMax.php)

   Output of `ExtendedDisMax` generator is intended for the `q` parameter of the
   [Solr Extended DisMax Query Parser](https://cwiki.apache.org/confluence/display/solr/The+Extended+DisMax+Query+Parser).

3. [QueryString](Generators/QueryString.php)

   Output of `QueryString` generator is intended for the `query` parameter of the
   [Elasticsearch Query String Query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html).

All generators use the same hierarchical [Visitor](Generators/Common/Visitor.php) pattern. Each
concrete [Node](../../Values/Node.php) instance has its own visitor, dispatched by checking on the
class it implements. This enables customization per Node visitor. Since Term Node can cover
different Term tokens (including your custom ones), Term visitors should be dispatched both by the
Node instance and the type of Token it aggregates. The visit method also propagates optional
`$options` parameter. If needed, it can be used to control the behavior of the generator from the
outside.

This approach should be useful for most custom implementations.

Note that the Generator interface is not provided. That is because the generator's output can't be
assumed, because it's specific to the intended target. The main job of the Query Translator is
producing the syntax tree from which it's easy to generate anything you might need. Following from
that - if the provided generators don't meet your needs, feel free to customize them or implement
your own.


================================================
FILE: lib/Languages/Galach/SYNTAX.md
================================================
# Galach query language syntax

## Terms

1. `Word` term is a string not containing whitespace, unless that whitespace is escaped.

    ```
    word
    ```
    ```
    another\ word
    ```

2. `Phrase` term is formed by enclosing words within double quotation marks `"`.

    ```
    "reality exists"
    ```
    ```
    "what's not real doesn't exist"
    ```

3. `User` term is defined by the leading `@` character, followed by at least one alphanumeric or
    underscore character, followed by an arbitrary sequence of alphanumeric characters, hyphens,
    underscores, and dots.

    Regular expression:

    ```
    @[a-zA-Z0-9_][a-zA-Z0-9_\-.]*
    ```

    Examples:

    ```
    @joe.watt
    ```
    ```
    @_alice83
    ```
    ```
    @The-Ronald
    ```

4. `Tag` term is defined by the leading `#` character, followed by at least one alphanumeric or
    underscore character, followed by an arbitrary sequence of alphanumeric characters, hyphens,
    underscores, and dots.

    Regular expression:

    ```
    \#[a-zA-Z0-9_][a-zA-Z0-9_\-.]*
    ```

    Examples:

    ```
    #php
    ```
    ```
    #PHP-7.1
    ```
    ```
    #query_parser
    ```

## Operators

Terms can be combined or modified using binary and unary operators:

1. `Logical and` is a binary operator that combines left and right operands so that both must
    match.

    It comes in two forms: `AND`, `&&`

    In both cases, it must be separated from its operands by whitespace.

    ```
    coffee AND milk
    ```
    ```
    tea && lemon
    ```

2. `Logical or` is a binary operator that combines left and right operands so that at least one of
    them has to match.

    It comes in two forms: `OR`, `||`

    In both cases, it must be separated from its operands by whitespace.

    ```
    potato OR tomato
    ```
    ```
    true || false
    ```

3. `Logical not` is a unary operator that modifies its operand so that it must not match.

    It comes in two forms: `NOT`, `!`

    When `NOT` form is used, it must be separated from its operand by whitespace:

    ```
    NOT important
    ```

    When shorthand form `!` is used, it must be adjacent to its operand:

    ```
    !important
    ```

4. `Mandatory` is a unary operator that modifies its operand so that it must match.
    It's represented by a plus sign `+` and must be placed adjacent to its operand.

    ```
    +coffee
    ```

5. `Prohibited` is a unary operator that modifies its operand so that it must not match.
    It's represented by a minus sign `-` and must be placed adjacent to its operand.

    ```
    -cake
    ```

### Operator precedence

Unary operators are applied first. Since they apply to the first element to the left, they never
conflict. They are followed by binary operators, with `Logical and` preceding `Logical or`:

1. `Logical not`, `Mandatory`, `Prohibited`
2. `Logical and`
3. `Logical or`

## Grouping

Terms and expressions can be grouped using round brackets. A group is processed as a whole. The
following two examples will be processed as the same since grouping follows operator precedence:

```
one OR NOT two AND three
```
```
one OR ((NOT two) AND three)
```

But you can also use grouping to change the meaning that would follow from operator precedence:

```
(one OR NOT two) AND three
```
```
one OR NOT (two AND three)
```

## Domains

Domain is an abstract category on which the term or group applies. It's defined by prefixing the
term or group with a domain string, followed by a colon `:`. Domain string must start with at least
one alphanumeric or underscore character and is followed by an arbitrary sequence of alphanumeric
characters, hyphens `-`, underscores `_` and dots `.`.

Note that the domain cannot be used on `Tag` and `User` terms. These two, in fact, define implicit
domains of their own.

Regular expression for domain string:

```
[a-zA-Z_][a-zA-Z0-9_\-.]*
```

Examples:

```
type:aeroplane
```
```
title:"Language processor"
```
```
description:(wings AND propeller)
```

## Special characters

The characters that are part of the language syntax must be escaped in order not to be recognized as
such by the engine. These are:

- `(` left paren
- `)` right paren
- `+` plus
- `-` minus
- `!` exclamation mark
- `"` double quote
- `#` hash
- `@` at sign
- `:` colon
- `\` backslash
- `␣` blank space

Character used for escaping is backslash `\`:

```
joined\ word
```
```
"escaped \"double quote\""
```
```
escaped \+operator domain\:word \@user \#tag \(and so on\)
```
```
double backslash \\ is a backslash escaped
```

Aside from the quotation marks themselves, escaping is not required inside phrases. Since quotes are
used as delimiters, everything between them is taken as-is. Hence these will be interpreted as equal
in meaning:

```
"+one -two"
```
```
"\+one \-two"
```

In some cases the tokenizer will automatically assume that a special character is to be interpreted
as if it was escaped. The following pairs will be processed as the same:

1. Colon at the end of a `Word` is considered part of the `Word`

   ```
   word:
   ```
   ```
   word\:
   ```

2. Colon placed after a domain colon is considered part of the `Word`

   ```
   domain:domain:domain
   ```
   ```
   domain:domain\:domain
   ```

3. Domain can't be used on a `Tag` and `User` terms

   ```
   domain:#tag domain:@user
   ```
   ```
   domain:\#tag domain:\@user
   ```

4. Characters used for `Mandatory`, `Prohibited` and shorthand `Logical not` operators can be
   considered part of the `Word`:

   - When placed after domain colon

      ```
      domain:+word domain:-word domain:!word
      ```
      ```
      domain:\+word domain:\-word domain:\!word
      ```

   - When placed in the middle of the word

      ```
      one+two one-two one!two
      ```
      ```
      one\+two one\-two one\!two
      ```

   - When placed at the end of the `Word`

      ```
      one+ two- three!
      ```
      ```
      one\+ two\- three\!
      ```


================================================
FILE: lib/Languages/Galach/TokenExtractor/Full.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\TokenExtractor;

use QueryTranslator\Languages\Galach\TokenExtractor;
use QueryTranslator\Languages\Galach\Tokenizer;
use QueryTranslator\Languages\Galach\Values\Token\Phrase;
use QueryTranslator\Languages\Galach\Values\Token\Tag;
use QueryTranslator\Languages\Galach\Values\Token\User;
use QueryTranslator\Languages\Galach\Values\Token\Word;
use RuntimeException;

/**
 * Full implementation of the Galach token extractor.
 *
 * Supports all features of the language.
 */
final class Full extends TokenExtractor
{
    /**
     * Map of regex expressions to Token types.
     *
     * @var array
     */
    private static $expressionTypeMap = [
        '/(?<lexeme>[\s]+)/Au' => Tokenizer::TOKEN_WHITESPACE,
        '/(?<lexeme>\+)/Au' => Tokenizer::TOKEN_MANDATORY,
        '/(?<lexeme>-)/Au' => Tokenizer::TOKEN_PROHIBITED,
        '/(?<lexeme>!)/Au' => Tokenizer::TOKEN_LOGICAL_NOT_2,
        '/(?<lexeme>\))/Au' => Tokenizer::TOKEN_GROUP_END,
        '/(?<lexeme>NOT)(?:[\s"()+\-!]|$)/Au' => Tokenizer::TOKEN_LOGICAL_NOT,
        '/(?<lexeme>(?:AND|&&))(?:[\s"()+\-!]|$)/Au' => Tokenizer::TOKEN_LOGICAL_AND,
        '/(?<lexeme>(?:OR|\|\|))(?:[\s"()+\-!]|$)/Au' => Tokenizer::TOKEN_LOGICAL_OR,
        '/(?<lexeme>(?:(?<domain>[a-zA-Z_][a-zA-Z0-9_\-.]*):)?(?<delimiter>\())/Au' => Tokenizer::TOKEN_GROUP_BEGIN,
        '/(?<lexeme>(?:(?<marker>(?<!\\\\)\#)(?<tag>[a-zA-Z0-9_][a-zA-Z0-9_\-.]*)))(?:[\s"()+!]|$)/Au' => Tokenizer::TOKEN_TERM,
        '/(?<lexeme>(?:(?<marker>(?<!\\\\)@)(?<user>[a-zA-Z0-9_][a-zA-Z0-9_\-.]*)))(?:[\s"()+!]|$)/Au' => Tokenizer::TOKEN_TERM,
        '/(?<lexeme>(?:(?<domain>[a-zA-Z_][a-zA-Z0-9_\-.]*):)?(?<quote>(?<!\\\\)["])(?<phrase>.*?)(?:(?<!\\\\)(?P=quote)))/Aus' => Tokenizer::TOKEN_TERM,
        '/(?<lexeme>(?:(?<domain>[a-zA-Z_][a-zA-Z0-9_\-.]*):)?(?<word>(?:\\\\\\\\|\\\\ |\\\\\(|\\\\\)|\\\\"|[^"()\s])+?))(?:(?<!\\\\)["]|\(|\)|$|\s)/Au' => Tokenizer::TOKEN_TERM,
    ];

    protected function getExpressionTypeMap()
    {
        return self::$expressionTypeMap;
    }

    protected function createTermToken($position, array $data)
    {
        $lexeme = $data['lexeme'];

        switch (true) {
            case isset($data['word']):
                return new Word(
                    $lexeme,
                    $position,
                    $data['domain'],
                    // un-backslash special characters
                    preg_replace('/(?:\\\\(\\\\|(["+\-!():#@ ])))/', '$1', $data['word'])
                );
            case isset($data['phrase']):
                $quote = $data['quote'];

                return new Phrase(
                    $lexeme,
                    $position,
                    $data['domain'],
                    $quote,
                    // un-backslash quote
                    preg_replace('/(?:\\\\([' . $quote . ']))/', '$1', $data['phrase'])
                );
            case isset($data['tag']):
                return new Tag(
                    $lexeme,
                    $position,
                    $data['marker'],
                    $data['tag']
                );
            case isset($data['user']):
                return new User(
                    $lexeme,
                    $position,
                    $data['marker'],
                    $data['user']
                );
        }

        throw new RuntimeException('Could not extract term token from the given data');
    }
}


================================================
FILE: lib/Languages/Galach/TokenExtractor/Text.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\TokenExtractor;

use QueryTranslator\Languages\Galach\TokenExtractor;
use QueryTranslator\Languages\Galach\Tokenizer;
use QueryTranslator\Languages\Galach\Values\Token\GroupBegin;
use QueryTranslator\Languages\Galach\Values\Token\Phrase;
use QueryTranslator\Languages\Galach\Values\Token\Word;
use RuntimeException;

/**
 * Text implementation of the Galach token extractor.
 *
 * Supports text related subset of the language features.
 */
final class Text extends TokenExtractor
{
    /**
     * Map of regex expressions to Token types.
     *
     * @var array
     */
    private static $expressionTypeMap = [
        '/(?<lexeme>[\s]+)/Au' => Tokenizer::TOKEN_WHITESPACE,
        '/(?<lexeme>\+)/Au' => Tokenizer::TOKEN_MANDATORY,
        '/(?<lexeme>-)/Au' => Tokenizer::TOKEN_PROHIBITED,
        '/(?<lexeme>!)/Au' => Tokenizer::TOKEN_LOGICAL_NOT_2,
        '/(?<lexeme>\))/Au' => Tokenizer::TOKEN_GROUP_END,
        '/(?<lexeme>NOT)(?:[\s"()+\-!]|$)/Au' => Tokenizer::TOKEN_LOGICAL_NOT,
        '/(?<lexeme>(?:AND|&&))(?:[\s"()+\-!]|$)/Au' => Tokenizer::TOKEN_LOGICAL_AND,
        '/(?<lexeme>(?:OR|\|\|))(?:[\s"()+\-!]|$)/Au' => Tokenizer::TOKEN_LOGICAL_OR,
        '/(?<lexeme>\()/Au' => Tokenizer::TOKEN_GROUP_BEGIN,
        '/(?<lexeme>(?<quote>(?<!\\\\)["])(?<phrase>.*?)(?:(?<!\\\\)(?P=quote)))/Aus' => Tokenizer::TOKEN_TERM,
        '/(?<lexeme>(?<word>(?:\\\\\\\\|\\\\ |\\\\\(|\\\\\)|\\\\"|[^"()\s])+?))(?:(?<!\\\\)["]|\(|\)|$|\s)/Au' => Tokenizer::TOKEN_TERM,
    ];

    protected function getExpressionTypeMap()
    {
        return self::$expressionTypeMap;
    }

    protected function createTermToken($position, array $data)
    {
        $lexeme = $data['lexeme'];

        switch (true) {
            case isset($data['word']):
                return new Word(
                    $lexeme,
                    $position,
                    '',
                    // un-backslash special chars
                    preg_replace('/(?:\\\\(\\\\|(["+\-!() ])))/', '$1', $data['word'])
                );
            case isset($data['phrase']):
                $quote = $data['quote'];

                return new Phrase(
                    $lexeme,
                    $position,
                    '',
                    $quote,
                    // un-backslash quote
                    preg_replace('/(?:\\\\([' . $quote . ']))/', '$1', $data['phrase'])
                );
        }

        throw new RuntimeException('Could not extract term token from the given data');
    }

    protected function createGroupBeginToken($position, array $data)
    {
        return new GroupBegin($data['lexeme'], $position, $data['lexeme'], '');
    }
}


================================================
FILE: lib/Languages/Galach/TokenExtractor.php
================================================
<?php

namespace QueryTranslator\Languages\Galach;

use QueryTranslator\Languages\Galach\Values\Token\GroupBegin;
use QueryTranslator\Values\Token;
use RuntimeException;

/**
 * Token extractor is used by Tokenizer to extract tokens from the input string.
 *
 * This is the abstract implementation intended to be used as an extension point.
 */
abstract class TokenExtractor
{
    /**
     * Return the token at the given $position of the $string.
     *
     * @throws \RuntimeException On PCRE regex error
     *
     * @param string $string Input string
     * @param int $position Position in the input string to extract from
     *
     * @return \QueryTranslator\Values\Token Extracted token
     */
    final public function extract($string, $position)
    {
        $byteOffset = $this->getByteOffset($string, $position);

        foreach ($this->getExpressionTypeMap() as $expression => $type) {
            $success = preg_match($expression, $string, $matches, 0, $byteOffset);

            if (false === $success) {
                throw new RuntimeException('PCRE regex error code: ' . preg_last_error());
            }

            if (0 === $success) {
                continue;
            }

            return $this->createToken($type, $position, $matches);
        }

        return new Token(
            Tokenizer::TOKEN_BAILOUT,
            mb_substr($string, $position, 1),
            $position
        );
    }

    /**
     * Return a map of regular expressions to token types.
     *
     * The returned map must be an array where key is a regular expression
     * and value is a corresponding token type. Regular expression must define
     * named capturing group 'lexeme' that identifies part of the input string
     * recognized as token.
     *
     * @return array
     */
    abstract protected function getExpressionTypeMap();

    /**
     * Create a term type token by the given parameters.
     *
     * @throw \RuntimeException If token could not be created from the given $matches data
     *
     * @param int $position Position of the token in the input string
     * @param array $data Regex match data, depends on the matched term token
     *
     * @return \QueryTranslator\Values\Token
     */
    abstract protected function createTermToken($position, array $data);

    /**
     * Create a token object from the given parameters.
     *
     * @param int $type Token type
     * @param int $position Position of the token in the input string
     * @param array $data Regex match data, depends on the type of the token
     *
     * @return \QueryTranslator\Values\Token
     */
    private function createToken($type, $position, array $data)
    {
        if ($type === Tokenizer::TOKEN_GROUP_BEGIN) {
            return $this->createGroupBeginToken($position, $data);
        }

        if ($type === Tokenizer::TOKEN_TERM) {
            return $this->createTermToken($position, $data);
        }

        return new Token($type, $data['lexeme'], $position);
    }

    /**
     * Create an instance of Group token by the given parameters.
     *
     * @param $position
     * @param array $data
     *
     * @return \QueryTranslator\Values\Token
     */
    protected function createGroupBeginToken($position, array $data)
    {
        return new GroupBegin($data['lexeme'], $position, $data['delimiter'], $data['domain']);
    }

    /**
     * Return the offset of the given $position in the input $string, in bytes.
     *
     * Offset in bytes is needed for preg_match $offset parameter.
     *
     * @param string $string
     * @param int $position
     *
     * @return int
     */
    private function getByteOffset($string, $position)
    {
        return strlen(mb_substr($string, 0, $position));
    }
}


================================================
FILE: lib/Languages/Galach/Tokenizer.php
================================================
<?php

namespace QueryTranslator\Languages\Galach;

use QueryTranslator\Tokenizing;
use QueryTranslator\Values\TokenSequence;

/**
 * Galach implementation of the Tokenizing interface.
 */
final class Tokenizer implements Tokenizing
{
    /**
     * Represents the whitespace in the input string.
     */
    const TOKEN_WHITESPACE = 1;

    /**
     * Combines two adjoining elements with logical AND.
     */
    const TOKEN_LOGICAL_AND = 2;

    /**
     * Combines two adjoining elements with logical OR.
     */
    const TOKEN_LOGICAL_OR = 4;

    /**
     * Applies logical NOT to the next (right-side) element.
     */
    const TOKEN_LOGICAL_NOT = 8;

    /**
     * Applies logical NOT to the next (right-side) element.
     *
     * This is an alternative to the TOKEN_LOGICAL_NOT, with the difference that
     * parser will expect it's placed next (left) to the element it applies to,
     * without the whitespace in between.
     */
    const TOKEN_LOGICAL_NOT_2 = 16;

    /**
     * Mandatory operator applies to the next (right-side) element and means
     * that the element must be present. There must be no whitespace between it
     * and the element it applies to.
     */
    const TOKEN_MANDATORY = 32;

    /**
     * Prohibited operator applies to the next (right-side) element and means
     * that the element must not be present. There must be no whitespace between
     * it and the element it applies to.
     */
    const TOKEN_PROHIBITED = 64;

    /**
     * Left side delimiter of a group.
     *
     * Group is used to group elements in order to form a sub-query.
     *
     * @see \QueryTranslator\Languages\Galach\Values\Token\GroupBegin
     */
    const TOKEN_GROUP_BEGIN = 128;

    /**
     * Right side delimiter of a group.
     *
     * Group is used to group elements in order to form a sub-query.
     */
    const TOKEN_GROUP_END = 256;

    /**
     * Term token type represents a category of term type tokens.
     *
     * This type is intended to be used as an extension point through subtyping.
     *
     * @see \QueryTranslator\Languages\Galach\Values\Token\Phrase
     * @see \QueryTranslator\Languages\Galach\Values\Token\Tag
     * @see \QueryTranslator\Languages\Galach\Values\Token\User
     * @see \QueryTranslator\Languages\Galach\Values\Token\Word
     */
    const TOKEN_TERM = 512;

    /**
     * Bailout token.
     *
     * If token could not be recognized, next character is extracted into a
     * token of this type. Ignored by parser.
     */
    const TOKEN_BAILOUT = 1024;

    /**
     * @var \QueryTranslator\Languages\Galach\TokenExtractor
     */
    private $tokenExtractor;

    /**
     * @param \QueryTranslator\Languages\Galach\TokenExtractor $tokenExtractor
     */
    public function __construct(TokenExtractor $tokenExtractor)
    {
        $this->tokenExtractor = $tokenExtractor;
    }

    public function tokenize($string)
    {
        $length = mb_strlen($string);
        $position = 0;
        $tokens = [];

        while ($position < $length) {
            $token = $this->tokenExtractor->extract($string, $position);
            $position += mb_strlen($token->lexeme);
            $tokens[] = $token;
        }

        return new TokenSequence($tokens, $string);
    }
}


================================================
FILE: lib/Languages/Galach/Values/Node/Group.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Node;

use QueryTranslator\Languages\Galach\Values\Token\GroupBegin;
use QueryTranslator\Values\Node;
use QueryTranslator\Values\Token;

/**
 * Group Node Visitor implementation.
 */
final class Group extends Node
{
    /**
     * @var \QueryTranslator\Values\Node[]
     */
    public $nodes;

    /**
     * @var \QueryTranslator\Languages\Galach\Values\Token\GroupBegin
     */
    public $tokenLeft;

    /**
     * @var \QueryTranslator\Values\Token
     */
    public $tokenRight;

    /**
     * @param \QueryTranslator\Values\Node[] $nodes
     * @param \QueryTranslator\Languages\Galach\Values\Token\GroupBegin $tokenLeft
     * @param \QueryTranslator\Values\Token $tokenRight
     */
    public function __construct(
        array $nodes = [],
        GroupBegin $tokenLeft = null,
        Token $tokenRight = null
    ) {
        $this->nodes = $nodes;
        $this->tokenLeft = $tokenLeft;
        $this->tokenRight = $tokenRight;
    }

    public function getNodes()
    {
        return $this->nodes;
    }
}


================================================
FILE: lib/Languages/Galach/Values/Node/LogicalAnd.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Node;

use QueryTranslator\Values\Node;
use QueryTranslator\Values\Token;

final class LogicalAnd extends Node
{
    /**
     * @var \QueryTranslator\Values\Node
     */
    public $leftOperand;

    /**
     * @var \QueryTranslator\Values\Node
     */
    public $rightOperand;

    /**
     * @var \QueryTranslator\Values\Token
     */
    public $token;

    /**
     * @param \QueryTranslator\Values\Node $leftOperand
     * @param \QueryTranslator\Values\Node $rightOperand
     * @param \QueryTranslator\Values\Token $token
     */
    public function __construct(
        Node $leftOperand = null,
        Node $rightOperand = null,
        Token $token = null
    ) {
        $this->leftOperand = $leftOperand;
        $this->rightOperand = $rightOperand;
        $this->token = $token;
    }

    public function getNodes()
    {
        return [
            $this->leftOperand,
            $this->rightOperand,
        ];
    }
}


================================================
FILE: lib/Languages/Galach/Values/Node/LogicalNot.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Node;

use QueryTranslator\Values\Node;
use QueryTranslator\Values\Token;

final class LogicalNot extends Node
{
    /**
     * @var \QueryTranslator\Values\Node
     */
    public $operand;

    /**
     * @var \QueryTranslator\Values\Token
     */
    public $token;

    /**
     * @param \QueryTranslator\Values\Node $operand
     * @param \QueryTranslator\Values\Token $token
     */
    public function __construct(Node $operand = null, Token $token = null)
    {
        $this->operand = $operand;
        $this->token = $token;
    }

    public function getNodes()
    {
        return [$this->operand];
    }
}


================================================
FILE: lib/Languages/Galach/Values/Node/LogicalOr.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Node;

use QueryTranslator\Values\Node;
use QueryTranslator\Values\Token;

final class LogicalOr extends Node
{
    /**
     * @var \QueryTranslator\Values\Node
     */
    public $leftOperand;

    /**
     * @var \QueryTranslator\Values\Node
     */
    public $rightOperand;

    /**
     * @var \QueryTranslator\Values\Token
     */
    public $token;

    /**
     * @param \QueryTranslator\Values\Node $leftOperand
     * @param \QueryTranslator\Values\Node $rightOperand
     * @param \QueryTranslator\Values\Token $token
     */
    public function __construct(
        Node $leftOperand = null,
        Node $rightOperand = null,
        Token $token = null
    ) {
        $this->leftOperand = $leftOperand;
        $this->rightOperand = $rightOperand;
        $this->token = $token;
    }

    public function getNodes()
    {
        return [
            $this->leftOperand,
            $this->rightOperand,
        ];
    }
}


================================================
FILE: lib/Languages/Galach/Values/Node/Mandatory.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Node;

use QueryTranslator\Values\Node;
use QueryTranslator\Values\Token;

final class Mandatory extends Node
{
    /**
     * @var \QueryTranslator\Values\Node
     */
    public $operand;

    /**
     * @var \QueryTranslator\Values\Token
     */
    public $token;

    /**
     * @param \QueryTranslator\Values\Node $operand
     * @param \QueryTranslator\Values\Token $token
     */
    public function __construct(Node $operand = null, Token $token = null)
    {
        $this->operand = $operand;
        $this->token = $token;
    }

    public function getNodes()
    {
        return [$this->operand];
    }
}


================================================
FILE: lib/Languages/Galach/Values/Node/Prohibited.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Node;

use QueryTranslator\Values\Node;
use QueryTranslator\Values\Token;

final class Prohibited extends Node
{
    /**
     * @var \QueryTranslator\Values\Node
     */
    public $operand;

    /**
     * @var \QueryTranslator\Values\Token
     */
    public $token;

    /**
     * @param \QueryTranslator\Values\Node $operand
     * @param \QueryTranslator\Values\Token $token
     */
    public function __construct(Node $operand = null, Token $token = null)
    {
        $this->operand = $operand;
        $this->token = $token;
    }

    public function getNodes()
    {
        return [$this->operand];
    }
}


================================================
FILE: lib/Languages/Galach/Values/Node/Query.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Node;

use QueryTranslator\Values\Node;

final class Query extends Node
{
    /**
     * @var \QueryTranslator\Values\Node[]
     */
    public $nodes;

    /**
     * @param \QueryTranslator\Values\Node[] $nodes
     */
    public function __construct(array $nodes)
    {
        $this->nodes = $nodes;
    }

    public function getNodes()
    {
        return $this->nodes;
    }
}


================================================
FILE: lib/Languages/Galach/Values/Node/Term.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Node;

use QueryTranslator\Values\Node;
use QueryTranslator\Values\Token;

final class Term extends Node
{
    /**
     * @var \QueryTranslator\Values\Token
     */
    public $token;

    /**
     * @param \QueryTranslator\Values\Token $token
     */
    public function __construct(Token $token)
    {
        $this->token = $token;
    }

    public function getNodes()
    {
        return [];
    }
}


================================================
FILE: lib/Languages/Galach/Values/Token/GroupBegin.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Token;

use QueryTranslator\Languages\Galach\Tokenizer;
use QueryTranslator\Values\Token;

/**
 * GroupBegin token represents group's domain and left side delimiter.
 */
final class GroupBegin extends Token
{
    /**
     * Holds group's left side delimiter string.
     *
     * @var string
     */
    public $delimiter;

    /**
     * Holds domain string.
     *
     * @var string
     */
    public $domain;

    /**
     * @param string $lexeme
     * @param int $position
     * @param string $domain
     * @param string $delimiter
     */
    public function __construct($lexeme, $position, $delimiter, $domain)
    {
        $this->delimiter = $delimiter;
        $this->domain = $domain;

        parent::__construct(Tokenizer::TOKEN_GROUP_BEGIN, $lexeme, $position);
    }
}


================================================
FILE: lib/Languages/Galach/Values/Token/Phrase.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Token;

use QueryTranslator\Languages\Galach\Tokenizer;
use QueryTranslator\Values\Token;

/**
 * Phrase term token.
 *
 * @see \QueryTranslator\Languages\Galach\Tokenizer::TOKEN_TERM
 */
final class Phrase extends Token
{
    /**
     * Holds domain identifier or null if not set.
     *
     * @var null|string
     */
    public $domain;

    /**
     * @var string
     */
    public $quote;

    /**
     * @var string
     */
    public $phrase;

    /**
     * @param string $lexeme
     * @param int $position
     * @param string $domain
     * @param string $quote
     * @param string $phrase
     */
    public function __construct($lexeme, $position, $domain, $quote, $phrase)
    {
        $this->domain = $domain;
        $this->quote = $quote;
        $this->phrase = $phrase;

        parent::__construct(Tokenizer::TOKEN_TERM, $lexeme, $position);
    }
}


================================================
FILE: lib/Languages/Galach/Values/Token/Tag.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Token;

use QueryTranslator\Languages\Galach\Tokenizer;
use QueryTranslator\Values\Token;

/**
 * Tag term token.
 *
 * @see \QueryTranslator\Languages\Galach\Tokenizer::TOKEN_TERM
 */
final class Tag extends Token
{
    /**
     * @var string
     */
    public $marker;

    /**
     * @var string
     */
    public $tag;

    /**
     * @param string $lexeme
     * @param int $position
     * @param string $marker
     * @param string $tag
     */
    public function __construct($lexeme, $position, $marker, $tag)
    {
        $this->marker = $marker;
        $this->tag = $tag;

        parent::__construct(Tokenizer::TOKEN_TERM, $lexeme, $position);
    }
}


================================================
FILE: lib/Languages/Galach/Values/Token/User.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Token;

use QueryTranslator\Languages\Galach\Tokenizer;
use QueryTranslator\Values\Token;

/**
 * User term token.
 *
 * @see \QueryTranslator\Languages\Galach\Tokenizer::TOKEN_TERM
 */
final class User extends Token
{
    /**
     * @var string
     */
    public $marker;

    /**
     * @var string
     */
    public $user;

    /**
     * @param string $lexeme
     * @param int $position
     * @param string $marker
     * @param string $user
     */
    public function __construct($lexeme, $position, $marker, $user)
    {
        $this->marker = $marker;
        $this->user = $user;

        parent::__construct(Tokenizer::TOKEN_TERM, $lexeme, $position);
    }
}


================================================
FILE: lib/Languages/Galach/Values/Token/Word.php
================================================
<?php

namespace QueryTranslator\Languages\Galach\Values\Token;

use QueryTranslator\Languages\Galach\Tokenizer;
use QueryTranslator\Values\Token;

/**
 * Word term token.
 *
 * @see \QueryTranslator\Languages\Galach\Tokenizer::TOKEN_TERM
 */
final class Word extends Token
{
    /**
     * Holds domain string.
     *
     * @var string
     */
    public $domain;

    /**
     * @var string
     */
    public $word;

    /**
     * @param string $lexeme
     * @param int $position
     * @param string $domain
     * @param string $word
     */
    public function __construct($lexeme, $position, $domain, $word)
    {
        $this->domain = $domain;
        $this->word = $word;

        parent::__construct(Tokenizer::TOKEN_TERM, $lexeme, $position);
    }
}


================================================
FILE: lib/Parsing.php
================================================
<?php

namespace QueryTranslator;

use QueryTranslator\Values\TokenSequence;

/**
 * Interface for parsing a sequence of tokens into a syntax tree.
 */
interface Parsing
{
    /**
     * Parse the given $tokenSequence.
     *
     * @param \QueryTranslator\Values\TokenSequence $tokenSequence
     *
     * @return \QueryTranslator\Values\SyntaxTree
     */
    public function parse(TokenSequence $tokenSequence);
}


================================================
FILE: lib/Tokenizing.php
================================================
<?php

namespace QueryTranslator;

/**
 * Interface for tokenizing a string into a sequence of tokens.
 */
interface Tokenizing
{
    /**
     * Tokenize the given $string.
     *
     * @param string $string Input string
     *
     * @return \QueryTranslator\Values\TokenSequence
     */
    public function tokenize($string);
}


================================================
FILE: lib/Values/Correction.php
================================================
<?php

namespace QueryTranslator\Values;

/**
 * Represents a correction applied during parsing of the token sequence.
 *
 * @see \QueryTranslator\Parsing
 * @see \QueryTranslator\Values\TokenSequence
 */
class Correction
{
    /**
     * Correction type constant.
     *
     * Defined by the language implementation.
     *
     * @var mixed
     */
    public $type;

    /**
     * An array of tokens that correction affects.
     *
     * @var \QueryTranslator\Values\Token[]
     */
    public $tokens = [];

    /**
     * @param mixed $type
     * @param \QueryTranslator\Values\Token[] ...$tokens
     */
    public function __construct($type, Token ...$tokens)
    {
        $this->type = $type;
        $this->tokens = $tokens;
    }
}


================================================
FILE: lib/Values/Node.php
================================================
<?php

namespace QueryTranslator\Values;

/**
 * Node is a basic building element of the syntax tree.
 *
 * @see \QueryTranslator\Values\SyntaxTree
 */
abstract class Node
{
    /**
     * Return an array of sub-nodes.
     *
     * @return \QueryTranslator\Values\Node[]
     */
    abstract public function getNodes();
}


================================================
FILE: lib/Values/SyntaxTree.php
================================================
<?php

namespace QueryTranslator\Values;

/**
 * Syntax tree is an abstract hierarchical representation of the query syntax,
 * intended for easy conversion into different concrete formats.
 *
 * @see \QueryTranslator\Parsing::parse()
 */
class SyntaxTree
{
    /**
     * The root node of the syntax tree.
     *
     * @var \QueryTranslator\Values\Node
     */
    public $rootNode;

    /**
     * Token sequence that was parsed into this syntax tree.
     *
     * @var \QueryTranslator\Values\TokenSequence
     */
    public $tokenSequence;

    /**
     * An array of corrections performed while parsing the token sequence.
     *
     * @var \QueryTranslator\Values\Correction[]
     */
    public $corrections;

    /**
     * @param \QueryTranslator\Values\Node $rootNode
     * @param \QueryTranslator\Values\TokenSequence $tokenSequence
     * @param \QueryTranslator\Values\Correction[] $corrections
     */
    public function __construct(Node $rootNode, TokenSequence $tokenSequence, array $corrections)
    {
        $this->rootNode = $rootNode;
        $this->tokenSequence = $tokenSequence;
        $this->corrections = $corrections;
    }
}


================================================
FILE: lib/Values/Token.php
================================================
<?php

namespace QueryTranslator\Values;

/**
 * Token represents a sequence of characters which forms a syntactic unit.
 */
class Token
{
    /**
     * Token type constant.
     *
     * Categorizes the token for the purpose of parsing.
     * Defined by the language implementation.
     *
     * @var string
     */
    public $type;

    /**
     * Token lexeme is a part of the input string recognized as token.
     *
     * @var string
     */
    public $lexeme;

    /**
     * Position of the lexeme in the input string.
     *
     * @var int
     */
    public $position;

    /**
     * @param string $type
     * @param string $lexeme
     * @param int $position
     */
    public function __construct($type, $lexeme, $position)
    {
        $this->type = $type;
        $this->lexeme = $lexeme;
        $this->position = $position;
    }
}


================================================
FILE: lib/Values/TokenSequence.php
================================================
<?php

namespace QueryTranslator\Values;

/**
 * Token sequence holds an array of tokens extracted from the query string.
 *
 * @see \QueryTranslator\Tokenizing::tokenize()
 * @see \QueryTranslator\Values\Token
 */
class TokenSequence
{
    /**
     * An array of tokens extracted from the input string.
     *
     * @var \QueryTranslator\Values\Token[]
     */
    public $tokens;

    /**
     * Source query string, unmodified.
     *
     * @var string
     */
    public $source;

    /**
     * @param \QueryTranslator\Values\Token[] $tokens
     * @param string $source
     */
    public function __construct(array $tokens, $source)
    {
        $this->tokens = $tokens;
        $this->source = $source;
    }
}


================================================
FILE: phpunit.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./tests/bootstrap.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true">
    <php>
        <env name="SYMFONY_DEPRECATIONS_HELPER" value="disabled"/>
    </php>
    <testsuites>
        <testsuite name="Query parser tests">
            <directory suffix="Test.php">./tests</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./lib</directory>
        </include>
    </coverage>
</phpunit>


================================================
FILE: tests/Galach/Generators/AggregateVisitorDispatchTest.php
================================================
<?php

namespace QueryTranslator\Tests\Galach\Generators;

use PHPUnit\Framework\TestCase;
use QueryTranslator\Languages\Galach\Generators\Common\Aggregate;
use QueryTranslator\Values\Node;
use RuntimeException;

/**
 * Test case for Aggregate visitor.
 */
class AggregateVisitorDispatchTest extends TestCase
{
    public function testAccept()
    {
        /** @var \QueryTranslator\Values\Node $nodeMock */
        $nodeMock = $this->getMockBuilder(Node::class)->getMock();

        $this->assertTrue((new Aggregate())->accept($nodeMock));
    }

    public function testVisitThrowsException()
    {
        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessage('No visitor available for Mock');

        /** @var \QueryTranslator\Values\Node $nodeMock */
        $nodeMock = $this->getMockBuilder(Node::class)->getMock();

        (new Aggregate())->visit($nodeMock);
    }
}


================================================
FILE: tests/Galach/Generators/ExtendedDisMaxTest.php
================================================
<?php

namespace QueryTranslator\Tests\Galach\Generators;

use PHPUnit\Framework\TestCase;
use QueryTranslator\Languages\Galach\Generators;
use QueryTranslator\Languages\Galach\Parser;
use QueryTranslator\Languages\Galach\TokenExtractor;
use QueryTranslator\Languages\Galach\Tokenizer;

/**
 * Test case for ExtendedDisMax generator.
 */
class ExtendedDisMaxTest extends TestCase
{
    const FIELD_USER = 'user_s';
    const FIELD_TAG = 'tags_ms';
    const FIELD_TEXT_DEFAULT = 'default_text_t';
    const FIELD_TEXT_DOMAIN = 'domain';
    const FIELD_TEXT_DOMAIN_MAPPED = 'special_text_t';

    public function providerForTestTranslation()
    {
        return [
            [
                'one',
                'one',
            ],
            [
                "'one'",
                "'one'",
            ],
            [
                '"one"',
                '"one"',
            ],
            [
                'one two',
                'one two',
            ],
            [
                '(one two)',
                '(one two)',
            ],
            [
                'unexpected:(one two)',
                'default_text_t:(one two)',
            ],
            [
                'domain:(one two)',
                'special_text_t:(one two)',
            ],
            [
                'one AND two',
                'one AND two',
            ],
            [
                'one && two',
                'one AND two',
            ],
            [
                'one OR two',
                'one OR two',
            ],
            [
                'one || two',
                'one OR two',
            ],
            [
                'NOT one',
                'NOT one',
            ],
            [
                '!one',
                'NOT one',
            ],
            [
                '+one',
                '+one',
            ],
            [
                '-one',
                '-one',
            ],
            [
                '@user',
                'user_s:user',
            ],
            [
                '#tag',
                'tags_ms:tag',
            ],
            [
                'unexpected:one',
                'default_text_t:one',
            ],
            [
                'domain:one',
                'special_text_t:one',
            ],
            [
                "unexpected:'one'",
                "default_text_t:'one'",
            ],
            [
                "domain:'one'",
                "special_text_t:'one'",
            ],
            [
                'unexpected:"one"',
                'default_text_t:"one"',
            ],
            [
                'domain:"one"',
                'special_text_t:"one"',
            ],
            [
                '\\',
                '\\\\',
            ],
            [
                '\\+',
                '\\+',
            ],
            [
                '\\-',
                '\\-',
            ],
            [
                '\\&&',
                '\\\\\\&&',
            ],
            [
                '\\||',
                '\\\\\\||',
            ],
            [
                '\\!',
                '\\!',
            ],
            [
                '\\(',
                '\\(',
            ],
            [
                '\\)',
                '\\)',
            ],
            [
                '\\{',
                '\\\\\\{',
            ],
            [
                '\\}',
                '\\\\\\}',
            ],
            [
                '\\[',
                '\\\\\\[',
            ],
            [
                '\\]',
                '\\\\\\]',
            ],
            [
                '\\^',
                '\\\\\\^',
            ],
            [
                '\\"',
                '\\"',
            ],
            [
                '\\~',
                '\\\\\\~',
            ],
            [
                '\\*',
                '\\\\\\*',
            ],
            [
                '\\?',
                '\\\\\\?',
            ],
            [
                '\\:',
                '\\:',
            ],
            [
                '\\/',
                '\\\\\\/',
            ],
            [
                '\\\\',
                '\\\\',
            ],
            [
                '\\ ',
                '\\ ',
            ],
        ];
    }

    /**
     * @dataProvider providerForTestTranslation
     *
     * @param string $string
     * @param string $expectedTranslatedString
     */
    public function testTranslation($string, $expectedTranslatedString)
    {
        $tokenExtractor = new TokenExtractor\Full();
        $tokenizer = new Tokenizer($tokenExtractor);
        $parser = new Parser();
        $generator = $this->getGenerator();

        $tokenSequence = $tokenizer->tokenize($string);
        $syntaxTree = $parser->parse($tokenSequence);
        $translatedString = $generator->generate($syntaxTree);

        $this->assertEquals($expectedTranslatedString, $translatedString);
    }

    /**
     * @return \QueryTranslator\Languages\Galach\Generators\ExtendedDisMax
     */
    protected function getGenerator()
    {
        $visitors = [];

        $visitors[] = new Generators\Lucene\Common\Prohibited();
        $visitors[] = new Generators\Lucene\Common\Group(
            [
                self::FIELD_TEXT_DOMAIN => self::FIELD_TEXT_DOMAIN_MAPPED,
            ],
            self::FIELD_TEXT_DEFAULT
        );
        $visitors[] = new Generators\Lucene\Common\Mandatory();
        $visitors[] = new Generators\Lucene\Common\LogicalAnd();
        $visitors[] = new Generators\Lucene\Common\LogicalNot();
        $visitors[] = new Generators\Lucene\Common\LogicalOr();
        $visitors[] = new Generators\Lucene\Common\Phrase(
            [
                self::FIELD_TEXT_DOMAIN => self::FIELD_TEXT_DOMAIN_MAPPED,
            ],
            self::FIELD_TEXT_DEFAULT
        );
        $visitors[] = new Generators\Lucene\Common\Query();
        $visitors[] = new Generators\Lucene\Common\Tag(self::FIELD_TAG);
        $visitors[] = new Generators\Lucene\Common\User(self::FIELD_USER);
        $visitors[] = new Generators\Lucene\ExtendedDisMax\Word(
            [
                self::FIELD_TEXT_DOMAIN => self::FIELD_TEXT_DOMAIN_MAPPED,
            ],
            self::FIELD_TEXT_DEFAULT
        );

        $aggregate = new Generators\Common\Aggregate($visitors);

        return new Generators\ExtendedDisMax($aggregate);
    }
}


================================================
FILE: tests/Galach/Generators/LuceneVisitorDispatchTest.php
================================================
<?php

namespace QueryTranslator\Tests\Galach\Generators;

use LogicException;
use PHPUnit\Framework\TestCase;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Generators\Lucene\Common\Group;
use QueryTranslator\Languages\Galach\Generators\Lucene\Common\LogicalAnd;
use QueryTranslator\Languages\Galach\Generators\Lucene\Common\LogicalNot;
use QueryTranslator\Languages\Galach\Generators\Lucene\Common\LogicalOr;
use QueryTranslator\Languages\Galach\Generators\Lucene\Common\Mandatory;
use QueryTranslator\Languages\Galach\Generators\Lucene\Common\Phrase;
use QueryTranslator\Languages\Galach\Generators\Lucene\Common\Prohibited;
use QueryTranslator\Languages\Galach\Generators\Lucene\Common\Query;
use QueryTranslator\Languages\Galach\Generators\Lucene\Common\Tag;
use QueryTranslator\Languages\Galach\Generators\Lucene\Common\User;
use QueryTranslator\Languages\Galach\Generators\Lucene\ExtendedDisMax\Word as ExtendedDisMaxWord;
use QueryTranslator\Languages\Galach\Generators\Lucene\QueryString\Word as QueryStringWord;
use QueryTranslator\Languages\Galach\Values\Node\Group as GroupNode;
use QueryTranslator\Languages\Galach\Values\Node\LogicalAnd as LogicalAndNode;
use QueryTranslator\Languages\Galach\Values\Node\LogicalNot as LogicalNotNode;
use QueryTranslator\Languages\Galach\Values\Node\LogicalOr as LogicalOrNode;
use QueryTranslator\Languages\Galach\Values\Node\Mandatory as MandatoryNode;
use QueryTranslator\Languages\Galach\Values\Node\Prohibited as ProhibitedNode;
use QueryTranslator\Languages\Galach\Values\Node\Query as QueryNode;
use QueryTranslator\Languages\Galach\Values\Node\Term;
use QueryTranslator\Values\Node;
use QueryTranslator\Values\Token;

/**
 * Test case for Lucene visitors.
 */
class LuceneVisitorDispatchTest extends TestCase
{
    public function providerForTestVisitThrowsLogicExceptionNode()
    {
        $nodeMock = $this->getMockBuilder(Node::class)->getMock();

        return [
            [
                new Group(),
                $nodeMock,
                'Implementation accepts instance of Group Node',
            ],
            [
                new LogicalAnd(),
                $nodeMock,
                'Implementation accepts instance of LogicalAnd Node',
            ],
            [
                new LogicalNot(),
                $nodeMock,
                'Implementation accepts instance of LogicalNot Node',
            ],
            [
                new LogicalOr(),
                $nodeMock,
                'Implementation accepts instance of LogicalOr Node',
            ],
            [
                new Mandatory(),
                $nodeMock,
                'Implementation accepts instance of Mandatory Node',
            ],
            [
                new Phrase(),
                $nodeMock,
                'Implementation accepts instance of Term Node',
            ],
            [
                new Prohibited(),
                $nodeMock,
                'Implementation accepts instance of Prohibited Node',
            ],
            [
                new Query(),
                $nodeMock,
                'Implementation accepts instance of Query Node',
            ],
            [
                new Tag(),
                $nodeMock,
                'Implementation accepts instance of Term Node',
            ],
            [
                new User(),
                $nodeMock,
                'Implementation accepts instance of Term Node',
            ],
            [
                new ExtendedDisMaxWord(),
                $nodeMock,
                'Implementation accepts instance of Term Node',
            ],
            [
                new QueryStringWord(),
                $nodeMock,
                'Implementation accepts instance of Term Node',
            ],
        ];
    }

    /**
     * @dataProvider providerForTestVisitThrowsLogicExceptionNode
     *
     * @param \QueryTranslator\Languages\Galach\Generators\Common\Visitor $visitor
     * @param \QueryTranslator\Values\Node $node
     * @param string $expectedExceptionMessage
     */
    public function testVisitThrowsLogicExceptionNode(Visitor $visitor, Node $node, $expectedExceptionMessage)
    {
        $this->expectException(LogicException::class);

        try {
            $visitor->visit($node);
        } catch (LogicException $e) {
            $this->assertSame($expectedExceptionMessage, $e->getMessage());
            throw $e;
        }
    }

    public function providerForTestVisitThrowsLogicExceptionToken()
    {
        /** @var \QueryTranslator\Values\Token $tokenMock */
        $tokenMock = $this->getMockBuilder(Token::class)->disableOriginalConstructor()->getMock();
        $node = new Term($tokenMock);

        return [
            [
                new Phrase(),
                $node,
                'Implementation accepts instance of Phrase Token',
            ],
            [
                new Tag(),
                $node,
                'Implementation accepts instance of Tag Token',
            ],
            [
                new User(),
                $node,
                'Implementation accepts instance of User Token',
            ],
            [
                new ExtendedDisMaxWord(),
                $node,
                'Implementation accepts instance of Word Token',
            ],
            [
                new QueryStringWord(),
                $node,
                'Implementation accepts instance of Word Token',
            ],
        ];
    }

    /**
     * @dataProvider providerForTestVisitThrowsLogicExceptionToken
     *
     * @param \QueryTranslator\Languages\Galach\Generators\Common\Visitor $visitor
     * @param \QueryTranslator\Values\Node $node
     * @param string $expectedExceptionMessage
     */
    public function testVisitThrowsLogicExceptionToken(Visitor $visitor, Node $node, $expectedExceptionMessage)
    {
        $this->expectException(LogicException::class);

        try {
            $visitor->visit($node);
        } catch (LogicException $e) {
            $this->assertSame($expectedExceptionMessage, $e->getMessage());
            throw $e;
        }
    }

    public function providerForTestVisitThrowsLogicExceptionSubVisitor()
    {
        return [
            [
                new Group(),
                new GroupNode(),
            ],
            [
                new LogicalAnd(),
                new LogicalAndNode(),
            ],
            [
                new LogicalNot(),
                new LogicalNotNode(),
            ],
            [
                new LogicalOr(),
                new LogicalOrNode(),
            ],
            [
                new Mandatory(),
                new MandatoryNode(),
            ],
            [
                new Prohibited(),
                new ProhibitedNode(),
            ],
            [
                new Query(),
                new QueryNode([]),
            ],
        ];
    }

    /**
     * @dataProvider providerForTestVisitThrowsLogicExceptionSubVisitor
     *
     * @param \QueryTranslator\Languages\Galach\Generators\Common\Visitor $visitor
     * @param \QueryTranslator\Values\Node $node
     */
    public function testVisitThrowsLogicExceptionSubVisitor(Visitor $visitor, Node $node)
    {
        $this->expectException(LogicException::class);
        $this->expectExceptionMessage("Implementation requires sub-visitor");

        $visitor->visit($node);
    }
}


================================================
FILE: tests/Galach/Generators/NativeVisitorDispatchTest.php
================================================
<?php

namespace QueryTranslator\Tests\Galach\Generators;

use LogicException;
use PHPUnit\Framework\TestCase;
use QueryTranslator\Languages\Galach\Generators\Common\Visitor;
use QueryTranslator\Languages\Galach\Generators\Native\BinaryOperator;
use QueryTranslator\Languages\Galach\Generators\Native\Group;
use QueryTranslator\Languages\Galach\Generators\Native\Phrase;
use QueryTranslator\Languages\Galach\Generators\Native\Query;
use QueryTranslator\Languages\Galach\Generators\Native\Tag;
use QueryTranslator\Languages\Galach\Generators\Native\UnaryOperator;
use QueryTranslator\Languages\Galach\Generators\Native\User;
use QueryTranslator\Languages\Galach\Generators\Native\Word;
use QueryTranslator\Languages\Galach\Values\Node\Group as GroupNode;
use QueryTranslator\Languages\Galach\Values\Node\LogicalAnd as LogicalAndNode;
use QueryTranslator\Languages\Galach\Values\Node\LogicalNot as LogicalNotNode;
use QueryTranslator\Languages\Galach\Values\Node\LogicalOr as LogicalOrNode;
use QueryTranslator\Languages\Galach\Values\Node\Mandatory as MandatoryNode;
use QueryTranslator\Languages\Galach\Values\Node\Prohibited as ProhibitedNode;
use QueryTranslator\Languages\Galach\Values\Node\Query as QueryNode;
use QueryTranslator\Languages\Galach\Values\Node\Term;
use QueryTranslator\Values\Node;
use QueryTranslator\Values\Token;

/**
 * Test case for Native visitors.
 */
class NativeVisitorDispatchTest extends TestCase
{
    public function providerForTestVisitThrowsLogicExceptionNode()
    {
        $nodeMock = $this->getMockBuilder(Node::class)->getMock();

        return [
            [
                new BinaryOperator(),
                $nodeMock,
                'Implementation accepts instance of LogicalAnd or LogicalOr Node',
            ],
            [
                new Group(),
                $nodeMock,
                'Implementation accepts instance of Group Node',
            ],
            [
                new Phrase(),
                $nodeMock,
                'Implementation accepts instance of Term Node',
            ],
            [
                new Query(),
                $nodeMock,
                'Implementation accepts instance of Query Node',
            ],
            [
                new Tag(),
                $nodeMock,
                'Implementation accepts instance of Term Node',
            ],
            [
                new UnaryOperator(),
                $nodeMock,
                'Implementation accepts instance of Mandatory, Prohibited or LogicalNot Node',
            ],
            [
                new User(),
                $nodeMock,
                'Implementation accepts instance of Term Node',
            ],
            [
                new Word(),
                $nodeMock,
                'Implementation accepts instance of Term Node',
            ],
        ];
    }

    /**
     * @dataProvider providerForTestVisitThrowsLogicExceptionNode
     *
     * @param \QueryTranslator\Languages\Galach\Generators\Common\Visitor $visitor
     * @param \QueryTranslator\Values\Node $node
     * @param string $expectedExceptionMessage
     */
    public function testVisitThrowsLogicExceptionNode(Visitor $visitor, Node $node, $expectedExceptionMessage)
    {
        $this->expectException(LogicException::class);

        try {
            $visitor->visit($node);
        } catch (LogicException $e) {
            $this->assertSame($expectedExceptionMessage, $e->getMessage());
            throw $e;
        }
    }

    public function providerForTestVisitThrowsLogicExceptionToken()
    {
        /** @var \QueryTranslator\Values\Token $tokenMock */
        $tokenMock = $this->getMockBuilder(Token::class)->disableOriginalConstructor()->getMock();
        $node = new Term($tokenMock);

        return [
            [
                new Phrase(),
                $node,
                'Implementation accepts instance of Phrase Token',
            ],
            [
                new Tag(),
                $node,
                'Implementation accepts instance of Tag Token',
            ],
            [
                new User(),
                $node,
                'Implementation accepts instance of User Token',
            ],
            [
                new Word(),
                $node,
                'Implementation accepts instance of Word Token',
            ],
        ];
    }

    /**
     * @dataProvider providerForTestVisitThrowsLogicExceptionToken
     *
     * @param \QueryTranslator\Languages\Galach\Generators\Common\Visitor $visitor
     * @param \QueryTranslator\Values\Node $node
     * @param string $expectedExceptionMessage
     */
    public function testVisitThrowsLogicExceptionToken(Visitor $visitor, Node $node, $expectedExceptionMessage)
    {
        $this->expectException(LogicException::class);

        try {
            $visitor->visit($node);
        } catch (LogicException $e) {
            $this->assertSame($expectedExceptionMessage, $e->getMessage());
            throw $e;
        }
    }

    public function providerForTestVisitThrowsLogicExceptionSubVisitor()
    {
        return [
            [
                new BinaryOperator(),
                new LogicalAndNode(),
            ],
            [
                new BinaryOperator(),
                new LogicalOrNode(),
            ],
            [
                new Group(),
                new GroupNode(),
            ],
            [
                new Query(),
                new QueryNode([]),
            ],
            [
                new UnaryOperator(),
                new LogicalNotNode(),
            ],
            [
                new UnaryOperator(),
                new MandatoryNode(),
            ],
            [
                new UnaryOperator(),
                new ProhibitedNode(),
            ],
        ];
    }

    /**
     * @dataProvider providerForTestVisitThrowsLogicExceptionSubVisitor
     *
     * @param \QueryTranslator\Languages\Galach\Generators\Common\Visitor $visitor
     * @param \QueryTranslator\Values\Node $node
     */
    public function testVisitThrowsLogicExceptionSubVisitor(Visitor $visitor, Node $node)
    {
        $this->expectException(LogicException::class);
        $this->expectExceptionMessage("Implementation requires sub-visitor");

        $visitor->visit($node);
    }
}


================================================
FILE: tests/Galach/Generators/QueryStringTest.php
================================================
<?php

namespace QueryTranslator\Tests\Galach\Generators;

use QueryTranslator\Languages\Galach\Generators;
use QueryTranslator\Languages\Galach\Generators\QueryString;

/**
 * Test case for QueryString generator.
 */
class QueryStringTest extends ExtendedDisMaxTest
{
    public function providerForTestTranslation()
    {
        return array_merge(
            parent::providerForTestTranslation(),
            [
                [
                    '\\=',
                    '\\\\\\=',
                ],
                [
                    '\\>',
                    '\\\\\\>',
                ],
                [
                    '\\<',
                    '\\\\\\<',
                ],
            ]
        );
    }

    /**
     * @return \QueryTranslator\Languages\Galach\Generators\QueryString
     */
    protected function getGenerator()
    {
        $visitors = [];

        $visitors[] = new Generators\Lucene\Common\Prohibited();
        $visitors[] = new Generators\Lucene\Common\Group(
            [
                self::FIELD_TEXT_DOMAIN => self::FIELD_TEXT_DOMAIN_MAPPED,
            ],
            self::FIELD_TEXT_DEFAULT
        );
        $visitors[] = new Generators\Lucene\Common\Mandatory();
        $visitors[] = new Generators\Lucene\Common\LogicalAnd();
        $visitors[] = new Generators\Lucene\Common\LogicalNot();
        $visitors[] = new Generators\Lucene\Common\LogicalOr();
        $visitors[] = new Generators\Lucene\Common\Phrase(
            [
                self::FIELD_TEXT_DOMAIN => self::FIELD_TEXT_DOMAIN_MAPPED,
            ],
            self::FIELD_TEXT_DEFAULT
        );
        $visitors[] = new Generators\Lucene\Common\Query();
        $visitors[] = new Generators\Lucene\Common\Tag(self::FIELD_TAG);
        $visitors[] = new Generators\Lucene\Common\User(self::FIELD_USER);
        $visitors[] = new Generators\Lucene\QueryString\Word(
            [
                self::FIELD_TEXT_DOMAIN => self::FIELD_TEXT_DOMAIN_MAPPED,
            ],
            self::FIELD_TEXT_DEFAULT
        );

        $aggregate = new Generators\Common\Aggregate($visitors);

        return new QueryString($aggregate);
    }
}


================================================
FILE: tests/Galach/IntegrationTest.php
================================================
<?php

namespace QueryTranslator\Tests\Galach;

use PHPUnit\Framework\TestCase;
use QueryTranslator\Languages\Galach\Generators;
use QueryTranslator\Languages\Galach\Parser;
use QueryTranslator\Languages\Galach\TokenExtractor;
use QueryTranslator\Languages\Galach\Tokenizer;
use QueryTranslator\Languages\Galach\Values\Node\Group;
use QueryTranslator\Languages\Galach\Values\Node\LogicalAnd;
use QueryTranslator\Languages\Galach\Values\Node\LogicalNot;
use QueryTranslator\Languages\Galach\Values\Node\LogicalOr;
use QueryTranslator\Languages\Galach\Values\Node\Mandatory;
use QueryTranslator\Languages\Galach\Values\Node\Prohibited;
use QueryTranslator\Languages\Galach\Values\Node\Query;
use QueryTranslator\Languages\Galach\Values\Node\Term;
use QueryTranslator\Languages\Galach\Values\Token\GroupBegin as GroupBeginToken;
use QueryTranslator\Languages\Galach\Values\Token\Phrase as PhraseToken;
use QueryTranslator\Languages\Galach\Values\Token\Tag as TagToken;
use QueryTranslator\Languages\Galach\Values\Token\User as UserToken;
use QueryTranslator\Languages\Galach\Values\Token\Word as WordToken;
use QueryTranslator\Values\Correction;
use QueryTranslator\Values\SyntaxTree;
use QueryTranslator\Values\Token;
use QueryTranslator\Values\TokenSequence;

/**
 * Tests integration of language components.
 *
 *  - tokenization of the query string into a sequence of tokens
 *  - parsing the token sequence into a syntax tree
 *  - generating the result by traversing the syntax tree
 */
class IntegrationTest extends TestCase
{
    public function providerForTestQuery()
    {
        return [
            [
                '',
                [],
                new Query([]),
            ],
            [
                'one',
                [
                    $token = new WordToken('one', 0, '', 'one'),
                ],
                new Query(
                    [
                        new Term($token),
                    ]
                ),
            ],
            [
                'one two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new WordToken('two', 4, '', 'two'),
                ],
                new Query(
                    [
                        new Term($token1),
                        new Term($token2),
                    ]
                ),
            ],
            [
                'one AND two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 4),
                    $token3 = new WordToken('two', 8, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalAnd(
                            new Term($token1),
                            new Term($token3),
                            $token2
                        ),
                    ]
                ),
            ],
            [
                'one OR two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 4),
                    $token3 = new WordToken('two', 7, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new Term($token1),
                            new Term($token3),
                            $token2
                        ),
                    ]
                ),
            ],
            [
                'one OR two AND three',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 4),
                    $token3 = new WordToken('two', 7, '', 'two'),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 11),
                    $token5 = new WordToken('three', 15, '', 'three'),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new Term($token1),
                            new LogicalAnd(
                                new Term($token3),
                                new Term($token5),
                                $token4
                            ),
                            $token2
                        ),
                    ]
                ),
            ],
            [
                'one AND two OR three',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 4),
                    $token3 = new WordToken('two', 8, '', 'two'),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 12),
                    $token5 = new WordToken('three', 15, '', 'three'),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new LogicalAnd(
                                new Term($token1),
                                new Term($token3),
                                $token2
                            ),
                            new Term($token5),
                            $token4
                        ),
                    ]
                ),
            ],
            [
                'NOT one',
                [
                    $token1 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 0),
                    $token2 = new WordToken('one', 4, '', 'one'),
                ],
                new Query(
                    [
                        new LogicalNot(
                            new Term($token2),
                            $token1
                        ),
                    ]
                ),
            ],
            [
                'one NOT two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 4),
                    $token3 = new WordToken('two', 8, '', 'two'),
                ],
                new Query(
                    [
                        new Term($token1),
                        new LogicalNot(
                            new Term($token3),
                            $token2
                        ),
                    ]
                ),
            ],
            [
                'one AND NOT two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 4),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 8),
                    $token4 = new WordToken('two', 12, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalAnd(
                            new Term($token1),
                            new LogicalNot(
                                new Term($token4),
                                $token3
                            ),
                            $token2
                        ),
                    ]
                ),
            ],
            [
                'one OR NOT two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 4),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 7),
                    $token4 = new WordToken('two', 11, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new Term($token1),
                            new LogicalNot(
                                new Term($token4),
                                $token3
                            ),
                            $token2
                        ),
                    ]
                ),
            ],
            [
                '!one',
                [
                    $token1 = new Token(Tokenizer::TOKEN_LOGICAL_NOT_2, '!', 0),
                    $token2 = new WordToken('one', 1, '', 'one'),
                ],
                new Query(
                    [
                        new LogicalNot(
                            new Term($token2),
                            $token1
                        ),
                    ]
                ),
            ],
            [
                'one !two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_NOT_2, '!', 4),
                    $token3 = new WordToken('two', 5, '', 'two'),
                ],
                new Query(
                    [
                        new Term($token1),
                        new LogicalNot(
                            new Term($token3),
                            $token2
                        ),
                    ]
                ),
            ],
            [
                'one AND !two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 4),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_NOT_2, '!', 8),
                    $token4 = new WordToken('two', 9, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalAnd(
                            new Term($token1),
                            new LogicalNot(
                                new Term($token4),
                                $token3
                            ),
                            $token2
                        ),
                    ]
                ),
            ],
            [
                'one OR !two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 4),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_NOT_2, '!', 7),
                    $token4 = new WordToken('two', 8, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new Term($token1),
                            new LogicalNot(
                                new Term($token4),
                                $token3
                            ),
                            $token2
                        ),
                    ]
                ),
            ],
            [
                '(one two)',
                [
                    $token1 = new GroupBeginToken('(', 0, '(', null),
                    $token2 = new WordToken('one', 1, '', 'one'),
                    $token3 = new WordToken('two', 5, '', 'two'),
                    $token4 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 8),
                ],
                new Query(
                    [
                        new Group(
                            [
                                new Term($token2),
                                new Term($token3),
                            ],
                            $token1,
                            $token4
                        ),
                    ]
                ),
            ],
            [
                '(one AND two)',
                [
                    $token1 = new GroupBeginToken('(', 0, '(', null),
                    $token2 = new WordToken('one', 1, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 5),
                    $token4 = new WordToken('two', 9, '', 'two'),
                    $token5 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 12),
                ],
                new Query(
                    [
                        new Group(
                            [
                                new LogicalAnd(
                                    new Term($token2),
                                    new Term($token4),
                                    $token3
                                ),
                            ],
                            $token1,
                            $token5
                        ),
                    ]
                ),
            ],
            [
                '(NOT one OR two)',
                [
                    $token1 = new GroupBeginToken('(', 0, '(', null),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 1),
                    $token3 = new WordToken('one', 5, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 9),
                    $token5 = new WordToken('two', 12, '', 'two'),
                    $token6 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 15),
                ],
                new Query(
                    [
                        new Group(
                            [
                                new LogicalOr(
                                    new LogicalNot(
                                        new Term($token3),
                                        $token2
                                    ),
                                    new Term($token5),
                                    $token4
                                ),
                            ],
                            $token1,
                            $token6
                        ),
                    ]
                ),
            ],
            [
                'one AND (two OR three)',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 4),
                    $token3 = new GroupBeginToken('(', 8, '(', null),
                    $token4 = new WordToken('two', 9, '', 'two'),
                    $token5 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 13),
                    $token6 = new WordToken('three', 16, '', 'three'),
                    $token7 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 21),
                ],
                new Query(
                    [
                        new LogicalAnd(
                            new Term($token1),
                            new Group(
                                [
                                    new LogicalOr(
                                        new Term($token4),
                                        new Term($token6),
                                        $token5
                                    ),
                                ],
                                $token3,
                                $token7
                            ),
                            $token2
                        ),
                    ]
                ),
            ],
            [
                '((one) AND (two AND (three OR four five)))',
                [
                    $token1 = new GroupBeginToken('(', 0, '(', null),
                    $token2 = new GroupBeginToken('(', 1, '(', null),
                    $token3 = new WordToken('one', 2, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 5),
                    $token5 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 7),
                    $token6 = new GroupBeginToken('(', 11, '(', null),
                    $token7 = new WordToken('two', 12, '', 'two'),
                    $token8 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 16),
                    $token9 = new GroupBeginToken('(', 20, '(', null),
                    $token10 = new WordToken('three', 21, '', 'three'),
                    $token11 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 27),
                    $token12 = new WordToken('four', 30, '', 'four'),
                    $token13 = new WordToken('five', 35, '', 'five'),
                    $token14 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 39),
                    $token15 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 40),
                    $token16 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 41),
                ],
                new Query(
                    [
                        new Group(
                            [
                                new LogicalAnd(
                                    new Group(
                                        [
                                            new Term($token3),
                                        ],
                                        $token2,
                                        $token4
                                    ),
                                    new Group(
                                        [
                                            new LogicalAnd(
                                                new Term($token7),
                                                new Group(
                                                    [
                                                        new LogicalOr(
                                                            new Term($token10),
                                                            new Term($token12),
                                                            $token11
                                                        ),
                                                        new Term($token13),
                                                    ],
                                                    $token9,
                                                    $token14
                                                ),
                                                $token8
                                            ),
                                        ],
                                        $token6,
                                        $token15
                                    ),
                                    $token5
                                ),
                            ],
                            $token1,
                            $token16
                        ),
                    ]
                ),
            ],
            [
                '((one) (two OR three))',
                [
                    $token1 = new GroupBeginToken('(', 0, '(', null),
                    $token2 = new GroupBeginToken('(', 1, '(', null),
                    $token3 = new WordToken('one', 2, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 5),
                    $token5 = new GroupBeginToken('(', 7, '(', null),
                    $token6 = new WordToken('two', 8, '', 'two'),
                    $token7 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 12),
                    $token8 = new WordToken('three', 15, '', 'three'),
                    $token9 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 20),
                    $token10 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 21),
                ],
                new Query(
                    [
                        new Group(
                            [
                                new Group(
                                    [
                                        new Term($token3),
                                    ],
                                    $token2,
                                    $token4
                                ),
                                new Group(
                                    [
                                        new LogicalOr(
                                            new Term($token6),
                                            new Term($token8),
                                            $token7
                                        ),
                                    ],
                                    $token5,
                                    $token9
                                ),
                            ],
                            $token1,
                            $token10
                        ),
                    ]
                ),
            ],
            [
                '+one',
                [
                    $token1 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 0),
                    $token2 = new WordToken('one', 1, '', 'one'),
                ],
                new Query(
                    [
                        new Mandatory(
                            new Term($token2),
                            $token1
                        ),
                    ]
                ),
            ],
            [
                '+one AND +two',
                [
                    $token1 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 0),
                    $token2 = new WordToken('one', 1, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 5),
                    $token4 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 9),
                    $token5 = new WordToken('two', 10, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalAnd(
                            new Mandatory(
                                new Term($token2),
                                $token1
                            ),
                            new Mandatory(
                                new Term($token5),
                                $token4
                            ),
                            $token3
                        ),
                    ]
                ),
            ],
            [
                '+one OR +two',
                [
                    $token1 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 0),
                    $token2 = new WordToken('one', 1, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 5),
                    $token4 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 8),
                    $token5 = new WordToken('two', 9, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new Mandatory(
                                new Term($token2),
                                $token1
                            ),
                            new Mandatory(
                                new Term($token5),
                                $token4
                            ),
                            $token3
                        ),
                    ]
                ),
            ],
            [
                '+one OR +two AND +three',
                [
                    $token1 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 0),
                    $token2 = new WordToken('one', 1, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 5),
                    $token4 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 8),
                    $token5 = new WordToken('two', 9, '', 'two'),
                    $token6 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 13),
                    $token7 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 17),
                    $token8 = new WordToken('three', 18, '', 'three'),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new Mandatory(
                                new Term($token2),
                                $token1
                            ),
                            new LogicalAnd(
                                new Mandatory(
                                    new Term($token5),
                                    $token4
                                ),
                                new Mandatory(
                                    new Term($token8),
                                    $token7
                                ),
                                $token6
                            ),
                            $token3
                        ),
                    ]
                ),
            ],
            [
                '+one AND +two OR +three',
                [
                    $token1 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 0),
                    $token2 = new WordToken('one', 1, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 5),
                    $token4 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 9),
                    $token5 = new WordToken('two', 10, '', 'two'),
                    $token6 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 14),
                    $token7 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 17),
                    $token8 = new WordToken('three', 18, '', 'three'),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new LogicalAnd(
                                new Mandatory(
                                    new Term($token2),
                                    $token1
                                ),
                                new Mandatory(
                                    new Term($token5),
                                    $token4
                                ),
                                $token3
                            ),
                            new Mandatory(
                                new Term($token8),
                                $token7
                            ),
                            $token6
                        ),
                    ]
                ),
            ],
            [
                '+(one)',
                [
                    $token1 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 0),
                    $token2 = new GroupBeginToken('(', 1, '(', null),
                    $token3 = new WordToken('one', 2, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 5),
                ],
                new Query(
                    [
                        new Mandatory(
                            new Group(
                                [
                                    new Term($token3),
                                ],
                                $token2,
                                $token4
                            ),
                            $token1
                        ),
                    ]
                ),
            ],
            [
                '-one',
                [
                    $token1 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 0),
                    $token2 = new WordToken('one', 1, '', 'one'),
                ],
                new Query(
                    [
                        new Prohibited(
                            new Term($token2),
                            $token1
                        ),
                    ]
                ),
            ],
            [
                '-one AND -two',
                [
                    $token1 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 0),
                    $token2 = new WordToken('one', 1, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 5),
                    $token4 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 9),
                    $token5 = new WordToken('two', 10, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalAnd(
                            new Prohibited(
                                new Term($token2),
                                $token1
                            ),
                            new Prohibited(
                                new Term($token5),
                                $token4
                            ),
                            $token3
                        ),
                    ]
                ),
            ],
            [
                '-one OR -two',
                [
                    $token1 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 0),
                    $token2 = new WordToken('one', 1, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 5),
                    $token4 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 8),
                    $token5 = new WordToken('two', 9, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new Prohibited(
                                new Term($token2),
                                $token1
                            ),
                            new Prohibited(
                                new Term($token5),
                                $token4
                            ),
                            $token3
                        ),
                    ]
                ),
            ],
            [
                '-one OR -two AND -three',
                [
                    $token1 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 0),
                    $token2 = new WordToken('one', 1, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 5),
                    $token4 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 8),
                    $token5 = new WordToken('two', 9, '', 'two'),
                    $token6 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 13),
                    $token7 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 17),
                    $token8 = new WordToken('three', 18, '', 'three'),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new Prohibited(
                                new Term($token2),
                                $token1
                            ),
                            new LogicalAnd(
                                new Prohibited(
                                    new Term($token5),
                                    $token4
                                ),
                                new Prohibited(
                                    new Term($token8),
                                    $token7
                                ),
                                $token6
                            ),
                            $token3
                        ),
                    ]
                ),
            ],
            [
                '-one AND -two OR -three',
                [
                    $token1 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 0),
                    $token2 = new WordToken('one', 1, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 5),
                    $token4 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 9),
                    $token5 = new WordToken('two', 10, '', 'two'),
                    $token6 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 14),
                    $token7 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 17),
                    $token8 = new WordToken('three', 18, '', 'three'),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new LogicalAnd(
                                new Prohibited(
                                    new Term($token2),
                                    $token1
                                ),
                                new Prohibited(
                                    new Term($token5),
                                    $token4
                                ),
                                $token3
                            ),
                            new Prohibited(
                                new Term($token8),
                                $token7
                            ),
                            $token6
                        ),
                    ]
                ),
            ],
            [
                '-(one)',
                [
                    $token1 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 0),
                    $token2 = new GroupBeginToken('(', 1, '(', null),
                    $token3 = new WordToken('one', 2, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 5),
                ],
                new Query(
                    [
                        new Prohibited(
                            new Group(
                                [
                                    new Term($token3),
                                ],
                                $token2,
                                $token4
                            ),
                            $token1
                        ),
                    ]
                ),
            ],
            [
                '(one OR +two three)',
                [
                    $token1 = new GroupBeginToken('(', 0, '(', null),
                    $token2 = new WordToken('one', 1, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 5),
                    $token4 = new Token(Tokenizer::TOKEN_MANDATORY, '+', 8),
                    $token5 = new WordToken('two', 9, '', 'two'),
                    $token6 = new WordToken('three', 13, '', 'three'),
                    $token7 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 18),
                ],
                new Query(
                    [
                        new Group(
                            [
                                new LogicalOr(
                                    new Term($token2),
                                    new Mandatory(
                                        new Term($token5),
                                        $token4
                                    ),
                                    $token3
                                ),
                                new Term($token6),
                            ],
                            $token1,
                            $token7
                        ),
                    ]
                ),
            ],
            [
                '(one OR -two three)',
                [
                    $token1 = new GroupBeginToken('(', 0, '(', null),
                    $token2 = new WordToken('one', 1, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 5),
                    $token4 = new Token(Tokenizer::TOKEN_PROHIBITED, '-', 8),
                    $token5 = new WordToken('two', 9, '', 'two'),
                    $token6 = new WordToken('three', 13, '', 'three'),
                    $token7 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 18),
                ],
                new Query(
                    [
                        new Group(
                            [
                                new LogicalOr(
                                    new Term($token2),
                                    new Prohibited(
                                        new Term($token5),
                                        $token4
                                    ),
                                    $token3
                                ),
                                new Term($token6),
                            ],
                            $token1,
                            $token7
                        ),
                    ]
                ),
            ],
            [
                '((one))',
                [
                    $token1 = new GroupBeginToken('(', 0, '(', null),
                    $token2 = new GroupBeginToken('(', 1, '(', null),
                    $token3 = new WordToken('one', 2, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 5),
                    $token5 = new Token(Tokenizer::TOKEN_GROUP_END, ')', 6),
                ],
                new Query(
                    [
                        new Group(
                            [
                                new Group(
                                    [
                                        new Term($token3),
                                    ],
                                    $token2,
                                    $token4
                                ),
                            ],
                            $token1,
                            $token5
                        ),
                    ]
                ),
            ],
            [
                'NOT NOT one NOT NOT NOT two',
                [
                    $token1 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 0),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 4),
                    $token3 = new WordToken('one', 8, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 12),
                    $token5 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 16),
                    $token6 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 20),
                    $token7 = new WordToken('two', 24, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalNot(
                            new LogicalNot(
                                new Term($token3),
                                $token2
                            ),
                            $token1
                        ),
                        new LogicalNot(
                            new LogicalNot(
                                new LogicalNot(
                                    new Term($token7),
                                    $token6
                                ),
                                $token5
                            ),
                            $token4
                        ),
                    ]
                ),
            ],
            [
                'NOT !one NOT !!two',
                [
                    $token1 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 0),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_NOT_2, '!', 4),
                    $token3 = new WordToken('one', 5, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 9),
                    $token5 = new Token(Tokenizer::TOKEN_LOGICAL_NOT_2, '!', 13),
                    $token6 = new Token(Tokenizer::TOKEN_LOGICAL_NOT_2, '!', 14),
                    $token7 = new WordToken('two', 15, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalNot(
                            new LogicalNot(
                                new Term($token3),
                                $token2
                            ),
                            $token1
                        ),
                        new LogicalNot(
                            new LogicalNot(
                                new LogicalNot(
                                    new Term($token7),
                                    $token6
                                ),
                                $token5
                            ),
                            $token4
                        ),
                    ]
                ),
            ],
            [
                'one AND NOT "two"',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 4),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 8),
                    $token4 = new PhraseToken('"two"', 12, '', '"', 'two'),
                ],
                new Query(
                    [
                        new LogicalAnd(
                            new Term($token1),
                            new LogicalNot(
                                new Term($token4),
                                $token3
                            ),
                            $token2
                        ),
                    ]
                ),
            ],
            [
                'one AND NOT @two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 4),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 8),
                    $token4 = new UserToken('@two', 12, '@', 'two'),
                ],
                new Query(
                    [
                        new LogicalAnd(
                            new Term($token1),
                            new LogicalNot(
                                new Term($token4),
                                $token3
                            ),
                            $token2
                        ),
                    ]
                ),
            ],
            [
                'one AND NOT #two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 4),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 8),
                    $token4 = new TagToken('#two', 12, '#', 'two'),
                ],
                new Query(
                    [
                        new LogicalAnd(
                            new Term($token1),
                            new LogicalNot(
                                new Term($token4),
                                $token3
                            ),
                            $token2
                        ),
                    ]
                ),
            ],
        ];
    }

    public function providerForTestQueryCorrected()
    {
        return [
            [
                'one"',
                'one',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_BAILOUT, '"', 3),
                ],
                new Query(
                    [
                        new Term($token1),
                    ]
                ),
                [
                    new Correction(Parser::CORRECTION_BAILOUT_TOKEN_IGNORED, $token2),
                ],
            ],
            [
                'one AND two AND',
                'one AND two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 4),
                    $token3 = new WordToken('two', 8, '', 'two'),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 12),
                ],
                new Query(
                    [
                        new LogicalAnd(
                            new Term($token1),
                            new Term($token3),
                            $token2
                        ),
                    ]
                ),
                [
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_RIGHT_OPERAND_IGNORED, $token4),
                ],
            ],
            [
                'AND one AND two',
                'one AND two',
                [
                    $token1 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 0),
                    $token2 = new WordToken('one', 4, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 8),
                    $token4 = new WordToken('two', 12, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalAnd(
                            new Term($token2),
                            new Term($token4),
                            $token3
                        ),
                    ]
                ),
                [
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token1),
                ],
            ],
            [
                'AND AND one AND AND two',
                'one two',
                [
                    $token1 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 0),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 4),
                    $token3 = new WordToken('one', 8, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 12),
                    $token5 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 16),
                    $token6 = new WordToken('two', 20, '', 'two'),
                ],
                new Query(
                    [
                        new Term($token3),
                        new Term($token6),
                    ]
                ),
                [
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token1),
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token2),
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED, $token4, $token5),
                ],
            ],
            [
                'OR one OR two',
                'one OR two',
                [
                    $token1 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 0),
                    $token2 = new WordToken('one', 3, '', 'one'),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 7),
                    $token4 = new WordToken('two', 10, '', 'two'),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new Term($token2),
                            new Term($token4),
                            $token3
                        ),
                    ]
                ),
                [
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token1),
                ],
            ],
            [
                'OR OR one OR OR two',
                'one two',
                [
                    $token1 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 0),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 3),
                    $token3 = new WordToken('one', 6, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 10),
                    $token5 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 13),
                    $token6 = new WordToken('two', 16, '', 'two'),
                ],
                new Query(
                    [
                        new Term($token3),
                        new Term($token6),
                    ]
                ),
                [
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token1),
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token2),
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED, $token4, $token5),
                ],
            ],
            [
                'OR OR one OR OR AND two',
                'one two',
                [
                    $token1 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 0),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 3),
                    $token3 = new WordToken('one', 6, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 10),
                    $token5 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 13),
                    $token6 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 16),
                    $token7 = new WordToken('two', 20, '', 'two'),
                ],
                new Query(
                    [
                        new Term($token3),
                        new Term($token7),
                    ]
                ),
                [
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token1),
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token2),
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED, $token4, $token5, $token6),
                ],
            ],
            [
                'one OR two AND OR NOT',
                'one OR two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 4),
                    $token3 = new WordToken('two', 7, '', 'two'),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 11),
                    $token5 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 15),
                    $token6 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 18),
                ],
                new Query(
                    [
                        new LogicalOr(
                            new Term($token1),
                            new Term($token3),
                            $token2
                        ),
                    ]
                ),
                [
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED, $token4, $token5),
                    new Correction(Parser::CORRECTION_UNARY_OPERATOR_MISSING_OPERAND_IGNORED, $token6),
                ],
            ],
            [
                'AND OR one AND OR two AND OR three',
                'one two three',
                [
                    $token1 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 0),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 4),
                    $token3 = new WordToken('one', 7, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 11),
                    $token5 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 15),
                    $token6 = new WordToken('two', 18, '', 'two'),
                    $token7 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 22),
                    $token8 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 26),
                    $token9 = new WordToken('three', 29, '', 'three'),
                ],
                new Query(
                    [
                        new Term($token3),
                        new Term($token6),
                        new Term($token9),
                    ]
                ),
                [
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token1),
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token2),
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED, $token4, $token5),
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED, $token7, $token8),
                ],
            ],
            [
                'OR AND one OR AND two OR AND three',
                'one two three',
                [
                    $token1 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 0),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 3),
                    $token3 = new WordToken('one', 7, '', 'one'),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 11),
                    $token5 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 14),
                    $token6 = new WordToken('two', 18, '', 'two'),
                    $token7 = new Token(Tokenizer::TOKEN_LOGICAL_OR, 'OR', 22),
                    $token8 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 25),
                    $token9 = new WordToken('three', 29, '', 'three'),
                ],
                new Query(
                    [
                        new Term($token3),
                        new Term($token6),
                        new Term($token9),
                    ]
                ),
                [
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token1),
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token2),
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED, $token4, $token5),
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED, $token7, $token8),
                ],
            ],
            [
                'one AND NOT AND two',
                'one two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 4),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 8),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 12),
                    $token5 = new WordToken('two', 16, '', 'two'),
                ],
                new Query(
                    [
                        new Term($token1),
                        new Term($token5),
                    ]
                ),
                [
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED, $token2, $token3, $token4),
                ],
            ],
            [
                'one NOT AND two',
                'one two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 4),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 8),
                    $token4 = new WordToken('two', 12, '', 'two'),
                ],
                new Query(
                    [
                        new Term($token1),
                        new Term($token4),
                    ]
                ),
                [
                    new Correction(Parser::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED, $token2, $token3),
                ],
            ],
            [
                'one NOT AND NOT two',
                'one NOT two',
                [
                    $token1 = new WordToken('one', 0, '', 'one'),
                    $token2 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 4),
                    $token3 = new Token(Tokenizer::TOKEN_LOGICAL_AND, 'AND', 8),
                    $token4 = new Token(Tokenizer::TOKEN_LOGICAL_NOT, 'NOT', 12),
                    $token5 = new WordToken('two', 16, '', 'two'),
                ],
                new Query(
                    [
                        new Term($token1),
             
Download .txt
gitextract_wpaijre9/

├── .gitattributes
├── .github/
│   └── workflows/
│       └── tests.yml
├── .gitignore
├── .php_cs.dist
├── LICENSE
├── README.md
├── composer.json
├── lib/
│   ├── Languages/
│   │   └── Galach/
│   │       ├── Generators/
│   │       │   ├── Common/
│   │       │   │   ├── Aggregate.php
│   │       │   │   └── Visitor.php
│   │       │   ├── ExtendedDisMax.php
│   │       │   ├── Lucene/
│   │       │   │   ├── Common/
│   │       │   │   │   ├── Group.php
│   │       │   │   │   ├── LogicalAnd.php
│   │       │   │   │   ├── LogicalNot.php
│   │       │   │   │   ├── LogicalOr.php
│   │       │   │   │   ├── Mandatory.php
│   │       │   │   │   ├── Phrase.php
│   │       │   │   │   ├── Prohibited.php
│   │       │   │   │   ├── Query.php
│   │       │   │   │   ├── Tag.php
│   │       │   │   │   ├── User.php
│   │       │   │   │   └── WordBase.php
│   │       │   │   ├── ExtendedDisMax/
│   │       │   │   │   └── Word.php
│   │       │   │   └── QueryString/
│   │       │   │       └── Word.php
│   │       │   ├── Native/
│   │       │   │   ├── BinaryOperator.php
│   │       │   │   ├── Group.php
│   │       │   │   ├── Phrase.php
│   │       │   │   ├── Query.php
│   │       │   │   ├── Tag.php
│   │       │   │   ├── UnaryOperator.php
│   │       │   │   ├── User.php
│   │       │   │   └── Word.php
│   │       │   ├── Native.php
│   │       │   └── QueryString.php
│   │       ├── Parser.php
│   │       ├── README.md
│   │       ├── SYNTAX.md
│   │       ├── TokenExtractor/
│   │       │   ├── Full.php
│   │       │   └── Text.php
│   │       ├── TokenExtractor.php
│   │       ├── Tokenizer.php
│   │       └── Values/
│   │           ├── Node/
│   │           │   ├── Group.php
│   │           │   ├── LogicalAnd.php
│   │           │   ├── LogicalNot.php
│   │           │   ├── LogicalOr.php
│   │           │   ├── Mandatory.php
│   │           │   ├── Prohibited.php
│   │           │   ├── Query.php
│   │           │   └── Term.php
│   │           └── Token/
│   │               ├── GroupBegin.php
│   │               ├── Phrase.php
│   │               ├── Tag.php
│   │               ├── User.php
│   │               └── Word.php
│   ├── Parsing.php
│   ├── Tokenizing.php
│   └── Values/
│       ├── Correction.php
│       ├── Node.php
│       ├── SyntaxTree.php
│       ├── Token.php
│       └── TokenSequence.php
├── phpunit.xml
└── tests/
    ├── Galach/
    │   ├── Generators/
    │   │   ├── AggregateVisitorDispatchTest.php
    │   │   ├── ExtendedDisMaxTest.php
    │   │   ├── LuceneVisitorDispatchTest.php
    │   │   ├── NativeVisitorDispatchTest.php
    │   │   └── QueryStringTest.php
    │   ├── IntegrationTest.php
    │   ├── Tokenizer/
    │   │   ├── FullTokenizerTest.php
    │   │   ├── TextTokenizerTest.php
    │   │   └── TokenExtractorTest.php
    │   └── Values/
    │       └── NodeTraversalTest.php
    └── bootstrap.php
Download .txt
SYMBOL INDEX (243 symbols across 61 files)

FILE: lib/Languages/Galach/Generators/Common/Aggregate.php
  class Aggregate (line 11) | final class Aggregate extends Visitor
    method __construct (line 23) | public function __construct(array $visitors = [])
    method addVisitor (line 35) | public function addVisitor(Visitor $visitor)
    method accept (line 40) | public function accept(Node $node)
    method visit (line 45) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Common/Visitor.php
  class Visitor (line 10) | abstract class Visitor
    method accept (line 19) | abstract public function accept(Node $node);
    method visit (line 30) | abstract public function visit(Node $node, Visitor $subVisitor = null,...

FILE: lib/Languages/Galach/Generators/ExtendedDisMax.php
  class ExtendedDisMax (line 13) | final class ExtendedDisMax
    method __construct (line 20) | public function __construct(Visitor $visitor)
    method generate (line 33) | public function generate(SyntaxTree $syntaxTree, $options = null)

FILE: lib/Languages/Galach/Generators/Lucene/Common/Group.php
  class Group (line 14) | final class Group extends Visitor
    method __construct (line 34) | public function __construct(array $domainFieldMap = null, $defaultFiel...
    method accept (line 43) | public function accept(Node $node)
    method visit (line 48) | public function visit(Node $node, Visitor $subVisitor = null, $options...
    method getSolrFieldPrefix (line 79) | private function getSolrFieldPrefix(GroupBegin $token)

FILE: lib/Languages/Galach/Generators/Lucene/Common/LogicalAnd.php
  class LogicalAnd (line 13) | final class LogicalAnd extends Visitor
    method accept (line 15) | public function accept(Node $node)
    method visit (line 20) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Lucene/Common/LogicalNot.php
  class LogicalNot (line 13) | final class LogicalNot extends Visitor
    method accept (line 15) | public function accept(Node $node)
    method visit (line 20) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Lucene/Common/LogicalOr.php
  class LogicalOr (line 13) | final class LogicalOr extends Visitor
    method accept (line 15) | public function accept(Node $node)
    method visit (line 20) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Lucene/Common/Mandatory.php
  class Mandatory (line 13) | final class Mandatory extends Visitor
    method accept (line 15) | public function accept(Node $node)
    method visit (line 20) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Lucene/Common/Phrase.php
  class Phrase (line 14) | final class Phrase extends Visitor
    method __construct (line 34) | public function __construct(array $domainFieldMap = null, $defaultFiel...
    method accept (line 43) | public function accept(Node $node)
    method visit (line 48) | public function visit(Node $node, Visitor $subVisitor = null, $options...
    method getSolrFieldPrefix (line 77) | private function getSolrFieldPrefix(PhraseToken $token)

FILE: lib/Languages/Galach/Generators/Lucene/Common/Prohibited.php
  class Prohibited (line 13) | final class Prohibited extends Visitor
    method accept (line 15) | public function accept(Node $node)
    method visit (line 20) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Lucene/Common/Query.php
  class Query (line 13) | final class Query extends Visitor
    method accept (line 15) | public function accept(Node $node)
    method visit (line 20) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Lucene/Common/Tag.php
  class Tag (line 14) | final class Tag extends Visitor
    method __construct (line 24) | public function __construct($fieldName = null)
    method accept (line 29) | public function accept(Node $node)
    method visit (line 34) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Lucene/Common/User.php
  class User (line 14) | final class User extends Visitor
    method __construct (line 24) | public function __construct($fieldName = null)
    method accept (line 29) | public function accept(Node $node)
    method visit (line 34) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Lucene/Common/WordBase.php
  class WordBase (line 14) | abstract class WordBase extends Visitor
    method __construct (line 34) | public function __construct(array $domainFieldMap = null, $defaultFiel...
    method accept (line 43) | public function accept(Node $node)
    method visit (line 48) | public function visit(Node $node, Visitor $subVisitor = null, $options...
    method escapeWord (line 77) | abstract protected function escapeWord($string);
    method getSolrFieldPrefix (line 86) | private function getSolrFieldPrefix(WordToken $token)

FILE: lib/Languages/Galach/Generators/Lucene/ExtendedDisMax/Word.php
  class Word (line 10) | final class Word extends WordBase
    method escapeWord (line 19) | protected function escapeWord($string)

FILE: lib/Languages/Galach/Generators/Lucene/QueryString/Word.php
  class Word (line 10) | final class Word extends WordBase
    method escapeWord (line 19) | protected function escapeWord($string)

FILE: lib/Languages/Galach/Generators/Native.php
  class Native (line 11) | final class Native
    method __construct (line 18) | public function __construct(Visitor $visitor)
    method generate (line 30) | public function generate(SyntaxTree $syntaxTree)

FILE: lib/Languages/Galach/Generators/Native/BinaryOperator.php
  class BinaryOperator (line 14) | final class BinaryOperator extends Visitor
    method accept (line 16) | public function accept(Node $node)
    method visit (line 21) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Native/Group.php
  class Group (line 13) | final class Group extends Visitor
    method accept (line 15) | public function accept(Node $node)
    method visit (line 20) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Native/Phrase.php
  class Phrase (line 14) | final class Phrase extends Visitor
    method accept (line 16) | public function accept(Node $node)
    method visit (line 21) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Native/Query.php
  class Query (line 13) | final class Query extends Visitor
    method accept (line 15) | public function accept(Node $node)
    method visit (line 20) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Native/Tag.php
  class Tag (line 14) | final class Tag extends Visitor
    method accept (line 16) | public function accept(Node $node)
    method visit (line 21) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Native/UnaryOperator.php
  class UnaryOperator (line 16) | final class UnaryOperator extends Visitor
    method accept (line 18) | public function accept(Node $node)
    method visit (line 23) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Native/User.php
  class User (line 14) | final class User extends Visitor
    method accept (line 16) | public function accept(Node $node)
    method visit (line 21) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/Native/Word.php
  class Word (line 14) | final class Word extends Visitor
    method accept (line 16) | public function accept(Node $node)
    method visit (line 21) | public function visit(Node $node, Visitor $subVisitor = null, $options...

FILE: lib/Languages/Galach/Generators/QueryString.php
  class QueryString (line 13) | final class QueryString
    method __construct (line 20) | public function __construct(Visitor $visitor)
    method generate (line 33) | public function generate(SyntaxTree $syntaxTree, $options = null)

FILE: lib/Languages/Galach/Parser.php
  class Parser (line 24) | final class Parser implements Parsing
    method parse (line 159) | public function parse(TokenSequence $tokenSequence)
    method shift (line 176) | private function shift()
    method reduce (line 184) | private function reduce(Node $node)
    method shiftWhitespace (line 209) | protected function shiftWhitespace()
    method shiftPreference (line 219) | protected function shiftPreference(Token $token)
    method shiftAdjacentUnaryOperator (line 224) | protected function shiftAdjacentUnaryOperator(Token $token, $tokenMask)
    method shiftLogicalNot (line 238) | protected function shiftLogicalNot(Token $token)
    method shiftLogicalNot2 (line 243) | protected function shiftLogicalNot2(Token $token)
    method shiftBinaryOperator (line 250) | protected function shiftBinaryOperator(Token $token)
    method ignoreBinaryOperatorFollowingOperator (line 270) | private function ignoreBinaryOperatorFollowingOperator(Token $token)
    method shiftTerm (line 285) | protected function shiftTerm(Token $token)
    method shiftGroupBegin (line 290) | protected function shiftGroupBegin(Token $token)
    method shiftGroupEnd (line 295) | protected function shiftGroupEnd(Token $token)
    method shiftBailout (line 302) | protected function shiftBailout(Token $token)
    method reducePreference (line 307) | protected function reducePreference(Node $node)
    method reduceLogicalNot (line 322) | protected function reduceLogicalNot(Node $node)
    method ignoreLogicalNotOperatorsPrecedingPreferenceOperator (line 337) | public function ignoreLogicalNotOperatorsPrecedingPreferenceOperator()
    method reduceLogicalAnd (line 349) | protected function reduceLogicalAnd(Node $node)
    method reduceLogicalOr (line 369) | protected function reduceLogicalOr(Node $node, $inGroup = false)
    method reduceGroup (line 393) | protected function reduceGroup(Group $group)
    method collectTopStackNodes (line 422) | private function collectTopStackNodes()
    method ignoreEmptyGroup (line 433) | private function ignoreEmptyGroup(Token $leftDelimiter, Token $rightDe...
    method init (line 453) | private function init(array $tokens)
    method getReduction (line 461) | private function getReduction(Node $node, $reductionIndex)
    method reduceQuery (line 472) | private function reduceQuery()
    method isToken (line 495) | private function isToken($token, $typeMask = null)
    method isTopStackToken (line 508) | private function isTopStackToken($type = null)
    method popWhitespace (line 516) | private function popWhitespace()
    method popTokens (line 530) | private function popTokens($typeMask = null)
    method ignorePrecedingOperators (line 548) | private function ignorePrecedingOperators($type)
    method ignoreFollowingOperators (line 558) | private function ignoreFollowingOperators()
    method reduceRemainingLogicalOr (line 577) | private function reduceRemainingLogicalOr($inGroup = false)
    method cleanupGroupDelimiters (line 592) | private function cleanupGroupDelimiters(array &$tokens)
    method getUnmatchedGroupDelimiterIndexes (line 615) | private function getUnmatchedGroupDelimiterIndexes(array &$tokens)
    method addCorrection (line 640) | private function addCorrection($type, Token ...$tokens)

FILE: lib/Languages/Galach/TokenExtractor.php
  class TokenExtractor (line 14) | abstract class TokenExtractor
    method extract (line 26) | final public function extract($string, $position)
    method getExpressionTypeMap (line 61) | abstract protected function getExpressionTypeMap();
    method createTermToken (line 73) | abstract protected function createTermToken($position, array $data);
    method createToken (line 84) | private function createToken($type, $position, array $data)
    method createGroupBeginToken (line 105) | protected function createGroupBeginToken($position, array $data)
    method getByteOffset (line 120) | private function getByteOffset($string, $position)

FILE: lib/Languages/Galach/TokenExtractor/Full.php
  class Full (line 18) | final class Full extends TokenExtractor
    method getExpressionTypeMap (line 41) | protected function getExpressionTypeMap()
    method createTermToken (line 46) | protected function createTermToken($position, array $data)

FILE: lib/Languages/Galach/TokenExtractor/Text.php
  class Text (line 17) | final class Text extends TokenExtractor
    method getExpressionTypeMap (line 38) | protected function getExpressionTypeMap()
    method createTermToken (line 43) | protected function createTermToken($position, array $data)
    method createGroupBeginToken (line 72) | protected function createGroupBeginToken($position, array $data)

FILE: lib/Languages/Galach/Tokenizer.php
  class Tokenizer (line 11) | final class Tokenizer implements Tokenizing
    method __construct (line 100) | public function __construct(TokenExtractor $tokenExtractor)
    method tokenize (line 105) | public function tokenize($string)

FILE: lib/Languages/Galach/Values/Node/Group.php
  class Group (line 12) | final class Group extends Node
    method __construct (line 34) | public function __construct(
    method getNodes (line 44) | public function getNodes()

FILE: lib/Languages/Galach/Values/Node/LogicalAnd.php
  class LogicalAnd (line 8) | final class LogicalAnd extends Node
    method __construct (line 30) | public function __construct(
    method getNodes (line 40) | public function getNodes()

FILE: lib/Languages/Galach/Values/Node/LogicalNot.php
  class LogicalNot (line 8) | final class LogicalNot extends Node
    method __construct (line 24) | public function __construct(Node $operand = null, Token $token = null)
    method getNodes (line 30) | public function getNodes()

FILE: lib/Languages/Galach/Values/Node/LogicalOr.php
  class LogicalOr (line 8) | final class LogicalOr extends Node
    method __construct (line 30) | public function __construct(
    method getNodes (line 40) | public function getNodes()

FILE: lib/Languages/Galach/Values/Node/Mandatory.php
  class Mandatory (line 8) | final class Mandatory extends Node
    method __construct (line 24) | public function __construct(Node $operand = null, Token $token = null)
    method getNodes (line 30) | public function getNodes()

FILE: lib/Languages/Galach/Values/Node/Prohibited.php
  class Prohibited (line 8) | final class Prohibited extends Node
    method __construct (line 24) | public function __construct(Node $operand = null, Token $token = null)
    method getNodes (line 30) | public function getNodes()

FILE: lib/Languages/Galach/Values/Node/Query.php
  class Query (line 7) | final class Query extends Node
    method __construct (line 17) | public function __construct(array $nodes)
    method getNodes (line 22) | public function getNodes()

FILE: lib/Languages/Galach/Values/Node/Term.php
  class Term (line 8) | final class Term extends Node
    method __construct (line 18) | public function __construct(Token $token)
    method getNodes (line 23) | public function getNodes()

FILE: lib/Languages/Galach/Values/Token/GroupBegin.php
  class GroupBegin (line 11) | final class GroupBegin extends Token
    method __construct (line 33) | public function __construct($lexeme, $position, $delimiter, $domain)

FILE: lib/Languages/Galach/Values/Token/Phrase.php
  class Phrase (line 13) | final class Phrase extends Token
    method __construct (line 39) | public function __construct($lexeme, $position, $domain, $quote, $phrase)

FILE: lib/Languages/Galach/Values/Token/Tag.php
  class Tag (line 13) | final class Tag extends Token
    method __construct (line 31) | public function __construct($lexeme, $position, $marker, $tag)

FILE: lib/Languages/Galach/Values/Token/User.php
  class User (line 13) | final class User extends Token
    method __construct (line 31) | public function __construct($lexeme, $position, $marker, $user)

FILE: lib/Languages/Galach/Values/Token/Word.php
  class Word (line 13) | final class Word extends Token
    method __construct (line 33) | public function __construct($lexeme, $position, $domain, $word)

FILE: lib/Parsing.php
  type Parsing (line 10) | interface Parsing
    method parse (line 19) | public function parse(TokenSequence $tokenSequence);

FILE: lib/Tokenizing.php
  type Tokenizing (line 8) | interface Tokenizing
    method tokenize (line 17) | public function tokenize($string);

FILE: lib/Values/Correction.php
  class Correction (line 11) | class Correction
    method __construct (line 33) | public function __construct($type, Token ...$tokens)

FILE: lib/Values/Node.php
  class Node (line 10) | abstract class Node
    method getNodes (line 17) | abstract public function getNodes();

FILE: lib/Values/SyntaxTree.php
  class SyntaxTree (line 11) | class SyntaxTree
    method __construct (line 39) | public function __construct(Node $rootNode, TokenSequence $tokenSequen...

FILE: lib/Values/Token.php
  class Token (line 8) | class Token
    method __construct (line 39) | public function __construct($type, $lexeme, $position)

FILE: lib/Values/TokenSequence.php
  class TokenSequence (line 11) | class TokenSequence
    method __construct (line 31) | public function __construct(array $tokens, $source)

FILE: tests/Galach/Generators/AggregateVisitorDispatchTest.php
  class AggregateVisitorDispatchTest (line 13) | class AggregateVisitorDispatchTest extends TestCase
    method testAccept (line 15) | public function testAccept()
    method testVisitThrowsException (line 23) | public function testVisitThrowsException()

FILE: tests/Galach/Generators/ExtendedDisMaxTest.php
  class ExtendedDisMaxTest (line 14) | class ExtendedDisMaxTest extends TestCase
    method providerForTestTranslation (line 22) | public function providerForTestTranslation()
    method testTranslation (line 210) | public function testTranslation($string, $expectedTranslatedString)
    method getGenerator (line 227) | protected function getGenerator()

FILE: tests/Galach/Generators/LuceneVisitorDispatchTest.php
  class LuceneVisitorDispatchTest (line 34) | class LuceneVisitorDispatchTest extends TestCase
    method providerForTestVisitThrowsLogicExceptionNode (line 36) | public function providerForTestVisitThrowsLogicExceptionNode()
    method testVisitThrowsLogicExceptionNode (line 111) | public function testVisitThrowsLogicExceptionNode(Visitor $visitor, No...
    method providerForTestVisitThrowsLogicExceptionToken (line 123) | public function providerForTestVisitThrowsLogicExceptionToken()
    method testVisitThrowsLogicExceptionToken (line 165) | public function testVisitThrowsLogicExceptionToken(Visitor $visitor, N...
    method providerForTestVisitThrowsLogicExceptionSubVisitor (line 177) | public function providerForTestVisitThrowsLogicExceptionSubVisitor()
    method testVisitThrowsLogicExceptionSubVisitor (line 217) | public function testVisitThrowsLogicExceptionSubVisitor(Visitor $visit...

FILE: tests/Galach/Generators/NativeVisitorDispatchTest.php
  class NativeVisitorDispatchTest (line 30) | class NativeVisitorDispatchTest extends TestCase
    method providerForTestVisitThrowsLogicExceptionNode (line 32) | public function providerForTestVisitThrowsLogicExceptionNode()
    method testVisitThrowsLogicExceptionNode (line 87) | public function testVisitThrowsLogicExceptionNode(Visitor $visitor, No...
    method providerForTestVisitThrowsLogicExceptionToken (line 99) | public function providerForTestVisitThrowsLogicExceptionToken()
    method testVisitThrowsLogicExceptionToken (line 136) | public function testVisitThrowsLogicExceptionToken(Visitor $visitor, N...
    method providerForTestVisitThrowsLogicExceptionSubVisitor (line 148) | public function providerForTestVisitThrowsLogicExceptionSubVisitor()
    method testVisitThrowsLogicExceptionSubVisitor (line 188) | public function testVisitThrowsLogicExceptionSubVisitor(Visitor $visit...

FILE: tests/Galach/Generators/QueryStringTest.php
  class QueryStringTest (line 11) | class QueryStringTest extends ExtendedDisMaxTest
    method providerForTestTranslation (line 13) | public function providerForTestTranslation()
    method getGenerator (line 37) | protected function getGenerator()

FILE: tests/Galach/IntegrationTest.php
  class IntegrationTest (line 35) | class IntegrationTest extends TestCase
    method providerForTestQuery (line 37) | public function providerForTestQuery()
    method providerForTestQueryCorrected (line 1046) | public function providerForTestQueryCorrected()
    method testQuery (line 3419) | public function testQuery($string, $expectedTokens, $expectedTree)
    method testQueryCorrected (line 3433) | public function testQueryCorrected($string, $correctedString, $expecte...
    method doTestQuery (line 3445) | protected function doTestQuery($string, $expectedCorrectedString, $exp...
    method getNativeGenerator (line 3477) | protected function getNativeGenerator()

FILE: tests/Galach/Tokenizer/FullTokenizerTest.php
  class FullTokenizerTest (line 20) | class FullTokenizerTest extends TestCase
    method providerForTestTokenize (line 22) | public function providerForTestTokenize()
    method testTokenize (line 1240) | public function testTokenize($string, array $expectedTokens)
    method providerForTestTokenizeNotRecognized (line 1252) | public function providerForTestTokenizeNotRecognized()
    method testTokenizeNotRecognized (line 1328) | public function testTokenizeNotRecognized($string, array $expectedTokens)
    method getTokenExtractor (line 1343) | protected function getTokenExtractor()

FILE: tests/Galach/Tokenizer/TextTokenizerTest.php
  class TextTokenizerTest (line 17) | class TextTokenizerTest extends FullTokenizerTest
    method testTokenize (line 30) | public function testTokenize($string, array $expectedTokens)
    method getExpectedFixtureWithOverride (line 42) | protected function getExpectedFixtureWithOverride($string, array $expe...
    method setFixtureOverride (line 53) | protected function setFixtureOverride()
    method getTokenExtractor (line 180) | protected function getTokenExtractor()

FILE: tests/Galach/Tokenizer/TokenExtractorTest.php
  class TokenExtractorTest (line 15) | class TokenExtractorTest extends TestCase
    method testExtractThrowsExceptionPCRE (line 17) | public function testExtractThrowsExceptionPCRE()
    method testFullExtractTermTokenThrowsException (line 38) | public function testFullExtractTermTokenThrowsException()
    method testTextExtractTermTokenThrowsException (line 57) | public function testTextExtractTermTokenThrowsException()

FILE: tests/Galach/Values/NodeTraversalTest.php
  class NodeTraversalTest (line 20) | class NodeTraversalTest extends TestCase
    method testGroupNode (line 22) | public function testGroupNode()
    method testLogicalAndNode (line 33) | public function testLogicalAndNode()
    method testLogicalNotNode (line 44) | public function testLogicalNotNode()
    method testLogicalOrNode (line 53) | public function testLogicalOrNode()
    method testMandatoryNode (line 64) | public function testMandatoryNode()
    method testProhibitedNode (line 73) | public function testProhibitedNode()
    method testQueryNode (line 82) | public function testQueryNode()
    method testTermNode (line 93) | public function testTermNode()
Condensed preview — 72 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (360K chars).
[
  {
    "path": ".gitattributes",
    "chars": 21,
    "preview": "/tests export-ignore\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 2566,
    "preview": "name: Tests\n\non:\n    push:\n        branches: ['master']\n    pull_request:\n\njobs:\n    tests:\n        name: PHP ${{ matrix"
  },
  {
    "path": ".gitignore",
    "chars": 59,
    "preview": "composer.lock\n.php_cs.cache\n.phpunit.result.cache\n/vendor/\n"
  },
  {
    "path": ".php_cs.dist",
    "chars": 955,
    "preview": "<?php\n\n$finder = PhpCsFixer\\Finder::create()\n    ->in(__DIR__)\n    ->exclude([])\n    ->files()->name('*.php')\n;\n\nreturn "
  },
  {
    "path": "LICENSE",
    "chars": 1089,
    "preview": "MIT License\n\nCopyright (c) 2017 Petar Španja <petar@spanja.info>\n\nPermission is hereby granted, free of charge, to any p"
  },
  {
    "path": "README.md",
    "chars": 4401,
    "preview": "# Query Translator\n\n[![Build Status](https://img.shields.io/github/actions/workflow/status/netgen/query-translator/tests"
  },
  {
    "path": "composer.json",
    "chars": 1158,
    "preview": "{\n    \"name\": \"netgen/query-translator\",\n    \"description\": \"Query Translator is a search query translator with AST repr"
  },
  {
    "path": "lib/Languages/Galach/Generators/Common/Aggregate.php",
    "chars": 1353,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Common;\n\nuse QueryTranslator\\Values\\Node;\nuse RuntimeExcept"
  },
  {
    "path": "lib/Languages/Galach/Generators/Common/Visitor.php",
    "chars": 730,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Common;\n\nuse QueryTranslator\\Values\\Node;\n\n/**\n * Common ba"
  },
  {
    "path": "lib/Languages/Galach/Generators/ExtendedDisMax.php",
    "chars": 994,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators;\n\nuse QueryTranslator\\Languages\\Galach\\Generators\\Common\\Vi"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/Common/Group.php",
    "chars": 2365,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\Common;\n\nuse LogicException;\nuse QueryTranslator\\Lan"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/Common/LogicalAnd.php",
    "chars": 1104,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\Common;\n\nuse LogicException;\nuse QueryTranslator\\Lan"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/Common/LogicalNot.php",
    "chars": 987,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\Common;\n\nuse LogicException;\nuse QueryTranslator\\Lan"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/Common/LogicalOr.php",
    "chars": 1096,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\Common;\n\nuse LogicException;\nuse QueryTranslator\\Lan"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/Common/Mandatory.php",
    "chars": 977,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\Common;\n\nuse LogicException;\nuse QueryTranslator\\Lan"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/Common/Phrase.php",
    "chars": 2362,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\Common;\n\nuse LogicException;\nuse QueryTranslator\\Lan"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/Common/Prohibited.php",
    "chars": 984,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\Common;\n\nuse LogicException;\nuse QueryTranslator\\Lan"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/Common/Query.php",
    "chars": 1030,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\Common;\n\nuse LogicException;\nuse QueryTranslator\\Lan"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/Common/Tag.php",
    "chars": 1323,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\Common;\n\nuse LogicException;\nuse QueryTranslator\\Lan"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/Common/User.php",
    "chars": 1330,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\Common;\n\nuse LogicException;\nuse QueryTranslator\\Lan"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/Common/WordBase.php",
    "chars": 2511,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\Common;\n\nuse LogicException;\nuse QueryTranslator\\Lan"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/ExtendedDisMax/Word.php",
    "chars": 742,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\ExtendedDisMax;\n\nuse QueryTranslator\\Languages\\Galac"
  },
  {
    "path": "lib/Languages/Galach/Generators/Lucene/QueryString/Word.php",
    "chars": 751,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Lucene\\QueryString;\n\nuse QueryTranslator\\Languages\\Galach\\G"
  },
  {
    "path": "lib/Languages/Galach/Generators/Native/BinaryOperator.php",
    "chars": 1257,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Native;\n\nuse LogicException;\nuse QueryTranslator\\Languages\\"
  },
  {
    "path": "lib/Languages/Galach/Generators/Native/Group.php",
    "chars": 1222,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Native;\n\nuse LogicException;\nuse QueryTranslator\\Languages\\"
  },
  {
    "path": "lib/Languages/Galach/Generators/Native/Phrase.php",
    "chars": 1246,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Native;\n\nuse LogicException;\nuse QueryTranslator\\Languages\\"
  },
  {
    "path": "lib/Languages/Galach/Generators/Native/Query.php",
    "chars": 1023,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Native;\n\nuse LogicException;\nuse QueryTranslator\\Languages\\"
  },
  {
    "path": "lib/Languages/Galach/Generators/Native/Tag.php",
    "chars": 1027,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Native;\n\nuse LogicException;\nuse QueryTranslator\\Languages\\"
  },
  {
    "path": "lib/Languages/Galach/Generators/Native/UnaryOperator.php",
    "chars": 1427,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Native;\n\nuse LogicException;\nuse QueryTranslator\\Languages\\"
  },
  {
    "path": "lib/Languages/Galach/Generators/Native/User.php",
    "chars": 1034,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Native;\n\nuse LogicException;\nuse QueryTranslator\\Languages\\"
  },
  {
    "path": "lib/Languages/Galach/Generators/Native/Word.php",
    "chars": 1194,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators\\Native;\n\nuse LogicException;\nuse QueryTranslator\\Languages\\"
  },
  {
    "path": "lib/Languages/Galach/Generators/Native.php",
    "chars": 788,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators;\n\nuse QueryTranslator\\Languages\\Galach\\Generators\\Common\\Vi"
  },
  {
    "path": "lib/Languages/Galach/Generators/QueryString.php",
    "chars": 1015,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Generators;\n\nuse QueryTranslator\\Languages\\Galach\\Generators\\Common\\Vi"
  },
  {
    "path": "lib/Languages/Galach/Parser.php",
    "chars": 19274,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach;\n\nuse QueryTranslator\\Languages\\Galach\\Values\\Node\\Group;\nuse QueryTra"
  },
  {
    "path": "lib/Languages/Galach/README.md",
    "chars": 15618,
    "preview": "# Galach query language\n\nTo better understand parts of the language processor described below, run the demo:\n\n1. Create "
  },
  {
    "path": "lib/Languages/Galach/SYNTAX.md",
    "chars": 5982,
    "preview": "# Galach query language syntax\n\n## Terms\n\n1. `Word` term is a string not containing whitespace, unless that whitespace i"
  },
  {
    "path": "lib/Languages/Galach/TokenExtractor/Full.php",
    "chars": 3467,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\TokenExtractor;\n\nuse QueryTranslator\\Languages\\Galach\\TokenExtractor;\n"
  },
  {
    "path": "lib/Languages/Galach/TokenExtractor/Text.php",
    "chars": 2723,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\TokenExtractor;\n\nuse QueryTranslator\\Languages\\Galach\\TokenExtractor;\n"
  },
  {
    "path": "lib/Languages/Galach/TokenExtractor.php",
    "chars": 3773,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach;\n\nuse QueryTranslator\\Languages\\Galach\\Values\\Token\\GroupBegin;\nuse Qu"
  },
  {
    "path": "lib/Languages/Galach/Tokenizer.php",
    "chars": 3274,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach;\n\nuse QueryTranslator\\Tokenizing;\nuse QueryTranslator\\Values\\TokenSequ"
  },
  {
    "path": "lib/Languages/Galach/Values/Node/Group.php",
    "chars": 1083,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Node;\n\nuse QueryTranslator\\Languages\\Galach\\Values\\Token\\GroupB"
  },
  {
    "path": "lib/Languages/Galach/Values/Node/LogicalAnd.php",
    "chars": 997,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Node;\n\nuse QueryTranslator\\Values\\Node;\nuse QueryTranslator\\Val"
  },
  {
    "path": "lib/Languages/Galach/Values/Node/LogicalNot.php",
    "chars": 678,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Node;\n\nuse QueryTranslator\\Values\\Node;\nuse QueryTranslator\\Val"
  },
  {
    "path": "lib/Languages/Galach/Values/Node/LogicalOr.php",
    "chars": 996,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Node;\n\nuse QueryTranslator\\Values\\Node;\nuse QueryTranslator\\Val"
  },
  {
    "path": "lib/Languages/Galach/Values/Node/Mandatory.php",
    "chars": 677,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Node;\n\nuse QueryTranslator\\Values\\Node;\nuse QueryTranslator\\Val"
  },
  {
    "path": "lib/Languages/Galach/Values/Node/Prohibited.php",
    "chars": 678,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Node;\n\nuse QueryTranslator\\Values\\Node;\nuse QueryTranslator\\Val"
  },
  {
    "path": "lib/Languages/Galach/Values/Node/Query.php",
    "chars": 442,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Node;\n\nuse QueryTranslator\\Values\\Node;\n\nfinal class Query exte"
  },
  {
    "path": "lib/Languages/Galach/Values/Node/Term.php",
    "chars": 463,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Node;\n\nuse QueryTranslator\\Values\\Node;\nuse QueryTranslator\\Val"
  },
  {
    "path": "lib/Languages/Galach/Values/Token/GroupBegin.php",
    "chars": 845,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Token;\n\nuse QueryTranslator\\Languages\\Galach\\Tokenizer;\nuse Que"
  },
  {
    "path": "lib/Languages/Galach/Values/Token/Phrase.php",
    "chars": 931,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Token;\n\nuse QueryTranslator\\Languages\\Galach\\Tokenizer;\nuse Que"
  },
  {
    "path": "lib/Languages/Galach/Values/Token/Tag.php",
    "chars": 725,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Token;\n\nuse QueryTranslator\\Languages\\Galach\\Tokenizer;\nuse Que"
  },
  {
    "path": "lib/Languages/Galach/Values/Token/User.php",
    "chars": 732,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Token;\n\nuse QueryTranslator\\Languages\\Galach\\Tokenizer;\nuse Que"
  },
  {
    "path": "lib/Languages/Galach/Values/Token/Word.php",
    "chars": 767,
    "preview": "<?php\n\nnamespace QueryTranslator\\Languages\\Galach\\Values\\Token;\n\nuse QueryTranslator\\Languages\\Galach\\Tokenizer;\nuse Que"
  },
  {
    "path": "lib/Parsing.php",
    "chars": 417,
    "preview": "<?php\n\nnamespace QueryTranslator;\n\nuse QueryTranslator\\Values\\TokenSequence;\n\n/**\n * Interface for parsing a sequence of"
  },
  {
    "path": "lib/Tokenizing.php",
    "chars": 331,
    "preview": "<?php\n\nnamespace QueryTranslator;\n\n/**\n * Interface for tokenizing a string into a sequence of tokens.\n */\ninterface Tok"
  },
  {
    "path": "lib/Values/Correction.php",
    "chars": 747,
    "preview": "<?php\n\nnamespace QueryTranslator\\Values;\n\n/**\n * Represents a correction applied during parsing of the token sequence.\n "
  },
  {
    "path": "lib/Values/Node.php",
    "chars": 323,
    "preview": "<?php\n\nnamespace QueryTranslator\\Values;\n\n/**\n * Node is a basic building element of the syntax tree.\n *\n * @see \\QueryT"
  },
  {
    "path": "lib/Values/SyntaxTree.php",
    "chars": 1160,
    "preview": "<?php\n\nnamespace QueryTranslator\\Values;\n\n/**\n * Syntax tree is an abstract hierarchical representation of the query syn"
  },
  {
    "path": "lib/Values/Token.php",
    "chars": 858,
    "preview": "<?php\n\nnamespace QueryTranslator\\Values;\n\n/**\n * Token represents a sequence of characters which forms a syntactic unit."
  },
  {
    "path": "lib/Values/TokenSequence.php",
    "chars": 722,
    "preview": "<?php\n\nnamespace QueryTranslator\\Values;\n\n/**\n * Token sequence holds an array of tokens extracted from the query string"
  },
  {
    "path": "phpunit.xml",
    "chars": 649,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit bootstrap=\"./tests/bootstrap.php\"\n         colors=\"true\"\n         conver"
  },
  {
    "path": "tests/Galach/Generators/AggregateVisitorDispatchTest.php",
    "chars": 912,
    "preview": "<?php\n\nnamespace QueryTranslator\\Tests\\Galach\\Generators;\n\nuse PHPUnit\\Framework\\TestCase;\nuse QueryTranslator\\Languages"
  },
  {
    "path": "tests/Galach/Generators/ExtendedDisMaxTest.php",
    "chars": 6514,
    "preview": "<?php\n\nnamespace QueryTranslator\\Tests\\Galach\\Generators;\n\nuse PHPUnit\\Framework\\TestCase;\nuse QueryTranslator\\Languages"
  },
  {
    "path": "tests/Galach/Generators/LuceneVisitorDispatchTest.php",
    "chars": 7505,
    "preview": "<?php\n\nnamespace QueryTranslator\\Tests\\Galach\\Generators;\n\nuse LogicException;\nuse PHPUnit\\Framework\\TestCase;\nuse Query"
  },
  {
    "path": "tests/Galach/Generators/NativeVisitorDispatchTest.php",
    "chars": 6359,
    "preview": "<?php\n\nnamespace QueryTranslator\\Tests\\Galach\\Generators;\n\nuse LogicException;\nuse PHPUnit\\Framework\\TestCase;\nuse Query"
  },
  {
    "path": "tests/Galach/Generators/QueryStringTest.php",
    "chars": 2175,
    "preview": "<?php\n\nnamespace QueryTranslator\\Tests\\Galach\\Generators;\n\nuse QueryTranslator\\Languages\\Galach\\Generators;\nuse QueryTra"
  },
  {
    "path": "tests/Galach/IntegrationTest.php",
    "chars": 151810,
    "preview": "<?php\n\nnamespace QueryTranslator\\Tests\\Galach;\n\nuse PHPUnit\\Framework\\TestCase;\nuse QueryTranslator\\Languages\\Galach\\Gen"
  },
  {
    "path": "tests/Galach/Tokenizer/FullTokenizerTest.php",
    "chars": 42694,
    "preview": "<?php\n\nnamespace QueryTranslator\\Tests\\Galach\\Tokenizer;\n\nuse PHPUnit\\Framework\\TestCase;\nuse QueryTranslator\\Languages\\"
  },
  {
    "path": "tests/Galach/Tokenizer/TextTokenizerTest.php",
    "chars": 6854,
    "preview": "<?php\n\nnamespace QueryTranslator\\Tests\\Galach\\Tokenizer;\n\nuse QueryTranslator\\Languages\\Galach\\TokenExtractor;\nuse Query"
  },
  {
    "path": "tests/Galach/Tokenizer/TokenExtractorTest.php",
    "chars": 2480,
    "preview": "<?php\n\nnamespace QueryTranslator\\Tests\\Galach\\Tokenizer;\n\nuse PHPUnit\\Framework\\TestCase;\nuse QueryTranslator\\Languages\\"
  },
  {
    "path": "tests/Galach/Values/NodeTraversalTest.php",
    "chars": 3156,
    "preview": "<?php\n\nnamespace QueryTranslator\\Tests\\Galach\\Tokenizer;\n\nuse PHPUnit\\Framework\\TestCase;\nuse QueryTranslator\\Languages\\"
  },
  {
    "path": "tests/bootstrap.php",
    "chars": 208,
    "preview": "<?php\n\n$autoload = __DIR__ . '/../vendor/autoload.php';\nif (!file_exists($autoload)) {\n    throw new RuntimeException('I"
  }
]

About this extraction

This page contains the full source code of the netgen/query-translator GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 72 files (334.1 KB), approximately 74.2k tokens, and a symbol index with 243 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!