├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── Check.yml │ └── Test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LAST_COMMIT ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── docs ├── .nojekyll ├── 1.0.0 │ ├── README.md │ └── _sidebar.md ├── 404.md ├── CNAME ├── README.md ├── _cover.md ├── _navbar.md ├── _sidebar.md ├── contribute.md ├── getting-started.md ├── index.html ├── reference.md └── tutorials │ ├── README.md │ ├── _sidebar.md │ ├── consuming-until-the-end-of-the-stream.md │ ├── debugging.md │ ├── matching-lists.md │ └── your-first-parser.md ├── index.php ├── package.json ├── parsers └── json.php └── src ├── combinators.php ├── debug.php ├── lib ├── Logger │ └── CLI.php ├── Mapper.php ├── Parser.php ├── Parser │ ├── Debugger.php │ ├── Result.php │ └── Result │ │ ├── Failure.php │ │ ├── Skip.php │ │ └── Success.php ├── Slice.php ├── Stream.php └── Stream │ ├── Char.php │ ├── MultiByteChar.php │ └── Transaction.php ├── mappers.php └── parsers.php /.gitattributes: -------------------------------------------------------------------------------- 1 | benchmarks export-ignore 2 | docker export-ignore 3 | resources export-ignore 4 | tests export-ignore 5 | 6 | benchmarks/ref.xml merge=ours 7 | .php_cs export-ignore 8 | composer.lock merge=ours 9 | docker-compose.yml export-ignore 10 | infection.json export-ignore 11 | LAST_COMMIT export-subst 12 | package-lock.json export-ignore 13 | phpbench.json export-ignore 14 | phpmetrics.json export-ignore 15 | phpstan.neon export-ignore 16 | phpunit.xml export-ignore 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [jubianchi] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "composer" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | assignees: 9 | - "jubianchi" 10 | versioning-strategy: "lockfile-only" 11 | pull-request-branch-name: 12 | separator: "-" 13 | -------------------------------------------------------------------------------- /.github/workflows/Check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | check: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Validate composer.json 16 | run: composer validate --ansi --strict 17 | 18 | - name: Cache Composer packages 19 | id: composer-cache 20 | uses: actions/cache@v2 21 | with: 22 | path: vendor 23 | key: ${{ runner.os }}-node-${{ hashFiles('**/composer.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-node- 26 | 27 | - name: Install dependencies 28 | if: steps.composer-cache.outputs.cache-hit != 'true' 29 | run: composer install --prefer-dist --no-progress --no-suggest 30 | 31 | - name: Coding style 32 | run: mkdir -p tmp && composer run cs 33 | 34 | - name: Static analysis 35 | run: composer run sa 36 | -------------------------------------------------------------------------------- /.github/workflows/Test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Cache Composer packages 19 | id: composer-cache 20 | uses: actions/cache@v2 21 | with: 22 | path: vendor 23 | key: ${{ runner.os }}-node-${{ hashFiles('**/composer.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-node- 26 | 27 | - name: Install dependencies 28 | if: steps.composer-cache.outputs.cache-hit != 'true' 29 | run: composer install --prefer-dist --no-progress --no-suggest 30 | 31 | - name: Unit tests 32 | run: composer ut 33 | 34 | - name: Mutation tests 35 | run: composer mt 36 | 37 | - name: Documentation 38 | run: | 39 | composer run dt 40 | 41 | - name: Benchmarks 42 | run: | 43 | composer run bm 44 | composer run bmc 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ 3 | vendor/ 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@jubianchi.fr. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LAST_COMMIT: -------------------------------------------------------------------------------- 1 | {"sha":"b586ca7", "title": "chore(deps-dev): bump phpunit/phpunit from 9.3.3 to 9.4.0 (#40)", "date": "Wed Oct 7 10:29:31 2020 +0200", "author": "dependabot[bot]"} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2020 Julien Bianchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PPC 2 | 3 | > A parser combinator library for PHP 4 | 5 | Simple to use & extend • Fast & lightweight • Reliable 6 | 7 | ## Introduction 8 | 9 | PPC stands for **P**HP **P**arser **C**ombinator. What an obvious name for such a library! 10 | 11 | As its name tells us, PPC is just another parser combinator library with a clear goal: make writing efficient parsers a 12 | breeze. Writing parser with PPC does not require you to know how parser combinators works internally nor it requires you 13 | to learn a complex object-oriented API. 14 | 15 | PPC is a set of functions which you will love to compose to build complex parsers! 16 | 17 | ## Installation 18 | 19 | PPC requires you to have at least PHP `7.4.0` and the [Multibyte String](https://www.php.net/manual/en/book.mbstring.php) 20 | (`mbstring`) extension enabled. You may want to check if your setup is correct using the following script: 21 | 22 | ```bash 23 | #!/usr/bin/env bash 24 | 25 | echo "PHP version: $(php -v | head -n1 | grep -qE '7.([4-9]|1[0-9]).(0|[1-9][0-9]*)' && echo '✅' || echo '❌')" 26 | echo "Multibyte String extension: $(php -m | grep -qE 'mbstring' && echo '✅' || echo '❌')" 27 | ``` 28 | 29 | Once everything is correct, choose the installation method that best feets your needs: 30 | 31 | ### Composer (CLI) 32 | 33 | ```bash 34 | composer require "jubianchi/ppc" "dev-master" 35 | ``` 36 | 37 | ### Composer (JSON) 38 | 39 | ```json 40 | { 41 | "require": { 42 | "jubianchi/ppc": "dev-master" 43 | } 44 | } 45 | ``` 46 | 47 | ### Git 48 | 49 | ```bash 50 | git clone "https://github.com/jubianchi/ppc.git" 51 | git checkout "master" 52 | ``` 53 | 54 | ## Example parser 55 | 56 | Here is a quick example demonstrating how easy it is to write a parser: 57 | 58 | ```php 59 | =7.4.0", 7 | "ext-json": "*", 8 | "ext-mbstring": "*", 9 | "psr/log": "^1.1" 10 | }, 11 | "require-dev": { 12 | "phpbench/phpbench": "@dev", 13 | "phpunit/phpunit": "^9", 14 | "phpunit/php-invoker": "^3.0", 15 | "friendsofphp/php-cs-fixer": "@dev", 16 | "phpstan/phpstan": "^0.12.32", 17 | "phpstan/phpstan-strict-rules": "^0.12.3", 18 | "phpstan/extension-installer": "^1.0", 19 | "infection/infection": "^0.16.4", 20 | "mathiasverraes/parsica": "@stable", 21 | "mathiasverraes/uptodocs": "dev-main", 22 | "phpmetrics/phpmetrics": "^2.7" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "jubianchi\\PPC\\": ["src/lib"], 27 | "jubianchi\\PPC\\Parsers\\": ["parsers"] 28 | }, 29 | "files": [ 30 | "src/parsers.php", 31 | "src/combinators.php", 32 | "src/debug.php", 33 | "src/mappers.php" 34 | ] 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "jubianchi\\PPC\\Tests\\": ["tests/unit"], 39 | "jubianchi\\PPC\\Tests\\Parsers\\": ["tests/unit/parsers"], 40 | "jubianchi\\PPC\\Tests\\Combinators\\": ["tests/unit/combinators"] 41 | }, 42 | "files": [ 43 | "parsers/json.php" 44 | ] 45 | }, 46 | "scripts": { 47 | "bm": "phpbench run benchmarks --report=aggregate --dump-file=tmp/curr.xml --ansi --tag=current", 48 | "bmc": "phpbench report --report='extends: \"compare\", compare: \"tag\", cols: [\"subject\", \"set\", \"revs\", \"its\"], compare_fields: [\"mem_peak\", \"best\", \"mean\", \"worst\"]' --file=benchmarks/ref.xml --file=tmp/curr.xml --ansi", 49 | "cs": "php-cs-fixer fix --ansi --allow-risky=yes", 50 | "dt": "tests/docs/run.sh", 51 | "mt": "phpdbg -qrr vendor/bin/phpunit --testdox && vendor/bin/infection --coverage=tmp --ansi --skip-initial-tests --no-progress --threads=2", 52 | "sa": "phpstan analyse --ansi --memory-limit=50", 53 | "pm": "phpdbg -qrr vendor/bin/phpunit --testdox && vendor/bin/phpmetrics --config=phpmetrics.json", 54 | "ut": "phpdbg -qrr vendor/bin/phpunit --testdox", 55 | "dev:docs": [ 56 | "Composer\\Config::disableProcessTimeout", 57 | "cd docs && ../node_modules/.bin/docsify serve" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jubianchi/ppc/b586ca71ffec179a209ecf0e37ca502bf74fbd35/docs/.nojekyll -------------------------------------------------------------------------------- /docs/1.0.0/README.md: -------------------------------------------------------------------------------- 1 | # PPC 2 | 3 | > A parser combinator library for PHP 4 | 5 | > [!NOTE] 6 | > Version 1.0.0 has not been release yet. 7 | > 8 | > You can follow the work being done in the milestones, issues or pull requests: 9 | > * [Milestone](https://github.com/jubianchi/ppc/milestones) 10 | > * [Issues](https://github.com/jubianchi/ppc/issues) 11 | > * [Pull requests](https://github.com/jubianchi/ppc/pulls) 12 | > 13 | > Head over to the [master version](/getting-started.md) documentation. 14 | -------------------------------------------------------------------------------- /docs/1.0.0/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **Links** 2 | * [Issues](https://github.com/jubianchi/ppc/issues?q=is%3Aissue+is%3Aopen+milestone%3A1.x) 3 | * [Packagist](https://packagist.org/packages/jubianchi/ppc) 4 | -------------------------------------------------------------------------------- /docs/404.md: -------------------------------------------------------------------------------- 1 | ```php 2 | map(value('Not Found')); 10 | 11 | echo $parser(new Char('404'))->result(); 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | ppc.jubianchi.fr -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # PPC 2 | 3 | > A parser combinator library for PHP 4 | 5 | !> **TODO** write this page 6 | -------------------------------------------------------------------------------- /docs/_cover.md: -------------------------------------------------------------------------------- 1 | ![logo](_media/icon.svg) 2 | 3 | # PPC 1.0.0 4 | 5 | > A parser combinator library for PHP 6 | 7 | * Simple to use & extend 8 | * Fast & lightweight 9 | * Reliable 10 | 11 | [:octocat: GitHub](https://github.com/jubianchi/ppc) 12 | [:checkered_flag: Get Started](#ppc) 13 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | * Versions 2 | * [master](/) 3 | * [1.0.0](/1.0.0/) 4 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [Getting started](/getting-started.md#getting-started) 2 | * [Tutorials](/tutorials/) 3 | * [Reference](/reference.md) 4 | * [Contribute](/contribute.md) 5 | 6 | * **Links** 7 | * [Issues](https://github.com/jubianchi/ppc) 8 | * [Packagist](https://packagist.org/packages/jubianchi/ppc) 9 | -------------------------------------------------------------------------------- /docs/contribute.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | > [!WARNING] 4 | > This is the documentation for the `master` branch. 5 | > Things may change or not be complete. 6 | > 7 | > Head over to the [latest stable version](/1.0.0/) for an up-to-date documentation. 8 | 9 | !> **TODO** write this page 10 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | > [!WARNING] 4 | > This is the documentation for the `master` branch. 5 | > Things may change or not be complete. 6 | > 7 | > Head over to the [latest stable version](/1.0.0/) for an up-to-date documentation. 8 | 9 | ## Introduction 10 | 11 | > In computer programming, a parser combinator is a higher-order function that accepts several parsers as input and 12 | > returns a new parser as its output. In this context, a parser is a function accepting strings as input and returning 13 | > some structure as output [...]. 14 | > 15 | > — [Wikipedia](https://en.wikipedia.org/wiki/Parser_combinator) 16 | 17 | PPC stands for **P**HP **P**arser **C**ombinator. What an obvious name for such a library! 18 | 19 | As its name tells us, PPC is just another parser combinator library with a clear goal: make writing efficient parsers a 20 | breeze. Writing parser with PPC does not require you to know how parser combinators works internally nor it requires you 21 | to learn a complex object-oriented API. 22 | 23 | PPC is a set of functions which you will love to compose to build complex parsers! 24 | 25 | ### Main goals 26 | 27 | 28 | 29 | 30 | 31 | #### Simplicity 32 | 33 | One of the primary goal of the library is to make writing parsers an effortless task: it has to be **simple to use**! As 34 | all libraries, it enforces simplicity through a limited, or might I say, an opinionated API. This API is focused on the 35 | most tasks so at some point you will be something something this is why PPC has to be **simple to extend**. 36 | 37 | 38 | 39 | #### Efficiency 40 | 41 | Parser combinators have many drawbacks. You probably heard they are not be memory-efficient or time-efficient. PPC does 42 | its best to solve these issues and tries to lower its footprint as much as possible. Ensuring good performance is a 43 | prerequiste to every new feature or bugfix. 44 | 45 | 46 | 47 | #### Reliability 48 | 49 | PPC aims at being a very stable library giving you an high level of confidence when writing your parsers. This is 50 | mainly enforced by a strict testing strategy, coding standards and a well-written code which takes advantages of 51 | everything PHP can do fos us. 52 | 53 | 54 | 55 | ## Installation 56 | 57 | PPC requires you to have at least PHP `7.4.0` and the [Multibyte String](https://www.php.net/manual/en/book.mbstring.php) 58 | (`mbstring`) extension enabled. You may want to check if your setup is correct using the following script: 59 | 60 | ```bash 61 | #!/usr/bin/env bash 62 | 63 | echo "PHP version: $(php -v | head -n1 | grep -qE '7.([4-9]|1[0-9]).(0|[1-9][0-9]*)' && echo '✅' || echo '❌')" 64 | echo "Multibyte String extension: $(php -m | grep -qE 'mbstring' && echo '✅' || echo '❌')" 65 | ``` 66 | 67 | Once everything is correct, choose the installation method that best feets your needs: 68 | 69 | 70 | 71 | ### ** Composer (CLI) ** 72 | 73 | ```bash 74 | composer require "jubianchi/ppc" "dev-master" 75 | ``` 76 | 77 | ### ** Composer (JSON) ** 78 | 79 | ```json 80 | { 81 | "require": { 82 | "jubianchi/ppc": "dev-master" 83 | } 84 | } 85 | ``` 86 | 87 | ### ** Git ** 88 | 89 | ```bash 90 | git clone "https://github.com/jubianchi/ppc.git" 91 | git checkout "master" 92 | ``` 93 | 94 | 95 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PPC 6 | 7 | 8 | 10 | 11 | 45 | 46 | 47 |
48 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | > [!WARNING] 4 | > This is the documentation for the `master` branch. 5 | > Things may change or not be complete. 6 | > 7 | > Head over to the [latest stable version](/1.0.0/reference.md) for an up-to-date documentation. 8 | 9 | ## Parsers 10 | 11 | 12 | 13 | 14 | 15 | ### any 16 | 17 | ```phps 18 | function any(): Parser 19 | ``` 20 | 21 | 22 | 23 | The `any` parser matches any character and consumes it from the stream. 24 | 25 | It will always return a `Success` result containing a `Slice` holding the character unless it reaches the end of the 26 | stream. 27 | 28 | If the end of the stream is reached, the parser will return a `Failure` result which can be turned into an exception 29 | when the wrapped value is accessed. 30 | 31 | 32 | 33 | ```php 34 | result() instanceof Slice); 47 | assert((string) $result->result() === 'a'); 48 | 49 | assert($parser($stream) instanceof Result\Failure); 50 | ``` 51 | 52 | 53 | 54 | ### char 55 | 56 | ```phps 57 | function char(string $char): Parser 58 | ``` 59 | 60 | 61 | 62 | The `char` parser matches the given character and consumes it from the stream. 63 | 64 | If the expected character is found, the parser will return a `Success` result containing a `Slice` holding the 65 | character. 66 | 67 | If the expected character is not found, the parser will return a `Failure` result which can be turned into an exception 68 | when the wrapped value is accessed. 69 | 70 | 71 | 72 | ```php 73 | result() instanceof Slice); 87 | assert((string) $result->result() === 'a'); 88 | 89 | assert($failure($stream) instanceof Result\Failure); 90 | ``` 91 | 92 | 93 | 94 | ### eos 95 | 96 | ```phps 97 | function eos(): Parser 98 | ``` 99 | 100 | 101 | The `eos` parser matches the end of the stream. 102 | 103 | It will always return a `Failure` unless it reaches the end of the stream, in which case it will return a `Success` 104 | result. 105 | 106 | > [!TIP] 107 | > Read the [Consuming until the end of the stream](/tutorials/consuming-until-the-end-of-the-stream.md) tutorial for an 108 | > example use case. 109 | 110 | 111 | 112 | ```php 113 | 132 | 133 | ### regex 134 | 135 | ```phps 136 | function regex(string $pattern): Parser 137 | ``` 138 | 139 | 140 | 141 | The `regex` parser matches a single character matching the given regular expression. 142 | 143 | If the current character matches the regular expression, the parser will return a `Success` result containing a 144 | `Slice` holding the character. 145 | 146 | If the expected character does not match, the parser will return a `Failure` result which can be turned into an 147 | exception when the wrapped value is accessed. 148 | 149 | 150 | 151 | ```php 152 | result() instanceof Slice); 165 | assert((string) $result->result() === 'a'); 166 | ``` 167 | 168 | 169 | 170 | > [!NOTE] 171 | > Be carefull when using this parser: 172 | > * whatever the regular expression is, it will only consume a **single** character; 173 | > * if the regular expression allow for `[0, n]` match, the parser will **always succeed**; 174 | > * if the regular expression tries to match more than one character, the parser will **always fail**. 175 | 176 | 177 | 178 | ```php 179 | result() instanceof Slice); 192 | assert((string) $result->result() === '1'); 193 | ``` 194 | 195 | ```php 196 | 210 | 211 | ### word 212 | 213 | ```phps 214 | function word(string $word): Parser 215 | ``` 216 | 217 | 218 | 219 | The `word` parser matches the given _word_ and consumes it from the stream. 220 | 221 | If the expected _word_ is found, the parser will return a `Success` result containing a `Slice` holding the 222 | _word_. 223 | 224 | If the expected _word_ is not found, the parser will return a `Failure` result which can be turned into an exception 225 | when the wrapped value is accessed. 226 | 227 | > [!NOTE] 228 | > Here, a _word_ is a set of consecutive characters: they can be letter, numbers or anything. 229 | 230 | 231 | 232 | ```php 233 | result() instanceof Slice); 247 | assert((string) $result->result() === 'ab3'); 248 | 249 | assert($failure($stream) instanceof Result\Failure); 250 | ``` 251 | 252 | 253 | 254 | ## Combinators 255 | 256 | 257 | 258 | 259 | 260 | ### alt 261 | 262 | ```phps 263 | function alt(Parser $first, Parser $second, Parser ...$parsers): Parser 264 | ``` 265 | 266 | 267 | 268 | The `alt` combinator executes each parser one by one and stops at the first successful one. 269 | 270 | If any of the given parsers matches, the combinator will return the `Success` result it got from the successful parser. 271 | 272 | If none of the given parsers succeeds, the combinator will return the first `Failure` result it got. 273 | 274 | 275 | 276 | ```php 277 | result() instanceof Slice); 292 | assert((string) $result->result() === 'a'); 293 | 294 | assert($failure($stream) instanceof Result\Failure); 295 | ``` 296 | 297 | 298 | 299 | ### enclosed 300 | 301 | ```phps 302 | function enclosed(Parser $before, Parser $parser, ?Parser $after = null): Parser 303 | ``` 304 | 305 | 306 | 307 | The `enclosed` combinator tries to match a parser which is preceded and followed by other parsers. 308 | 309 | If all of the given parsers matches, the combinator will return the `Success` result it got from the second one, which 310 | is the parser matching the enclosed value 311 | 312 | If one of the given parsers fails, the combinator will return its `Failure` result. 313 | 314 | The main goal of this combinator is to ease the process of matching enclosed values. See the example snippets to see 315 | the differences of using — With — (or not using — Without) the `enclosed` combinator. 316 | 317 | > [!TIP] 318 | > Read the [Matching enclosed values]() tutorial for an example use case. 319 | 320 | 321 | 322 | 323 | 324 | ### ** With ** 325 | 326 | ```php 327 | result() instanceof Slice); 342 | assert((string) $result->result() === 'a'); 343 | 344 | assert($failure($stream) instanceof Result\Failure); 345 | ``` 346 | 347 | ### ** Without ** 348 | 349 | ```php 350 | map(skip()), 362 | char('a'), 363 | char('-')->map(skip()) 364 | )->map(first()); 365 | $result = $success($stream); 366 | 367 | assert($result instanceof Result\Success); 368 | assert($result->result() instanceof Slice); 369 | assert((string) $result->result() === 'a'); 370 | ``` 371 | 372 | > [!NOTE] 373 | > In this example, we want you to focus on verbosity and readability. We use mappers here, either ignore them or jump to 374 | > [their documentation](#mappers) if you want to more. 375 | 376 | 377 | 378 | 379 | 380 | 381 | ### many 382 | 383 | ```phps 384 | function many(Parser $parser): Parser> 385 | ``` 386 | 387 | 388 | 389 | The `many` combinator tries to match a given parser one or several times, stopping at the first failure. 390 | 391 | If the given parsers matches at least one time, the combinator will return a `Success` containing an `array` holding 392 | each result. 393 | 394 | If the given parsers never matches, the combinator will return the first `Failure` result it encountered. 395 | 396 | > [!TIP] 397 | > Read the [Matching lists](/tutorials/matching-lists.md) tutorial for an example use case. 398 | 399 | 400 | 401 | ```php 402 | result())); 416 | assert((string) $result->result()[0] === 'a'); 417 | assert((string) $result->result()[1] === 'b'); 418 | 419 | assert($failure($stream) instanceof Result\Failure); 420 | ``` 421 | 422 | 423 | 424 | ### not 425 | 426 | ```phps 427 | function not(Parser $parser, Parser ...$parsers): Parser 428 | ``` 429 | 430 | 431 | 432 | The `not` combinator will be successful when the given parsers fails. 433 | 434 | In such case, it will return a `Success` result containing eqaul to the result of a [`any`](#any) parser. 435 | 436 | If the one of the given parsers succeeds, the `not` combinator will return a `Failure` result. 437 | 438 | > [!TIP] 439 | > The `not` parser may be used to do negative lookaheads. 440 | > 441 | > Read the [Looking ahead]() tutorial for an example use case. 442 | 443 | 444 | 445 | ```php 446 | result() instanceof Slice); 461 | assert((string) $result->result() === 'a'); 462 | 463 | assert($failure($stream) instanceof Result\Failure); 464 | ``` 465 | 466 | 467 | 468 | ### opt 469 | 470 | ```phps 471 | function opt(Parser $parser): Parser 472 | ``` 473 | 474 | 475 | 476 | The `opt` combinator will be always be successful. 477 | 478 | When the given parser succeeds, the `opt` combinator will return its `Success` result. 479 | 480 | If the given parser fails, the `opt` combinator will return a `Success` result holding a null value. 481 | 482 | 483 | 484 | ```php 485 | result() instanceof Slice); 500 | assert((string) $result->result() === 'a'); 501 | 502 | $result = $success($stream); 503 | assert($result instanceof Result\Success); 504 | assert($result->result() === null); 505 | ``` 506 | 507 | 508 | 509 | ### separated 510 | 511 | ```phps 512 | function separated(Parser $separator, Parser $parser): Parser> 513 | ``` 514 | 515 | > [!TIP] 516 | > Read the [Matching lists](/tutorials/matching-lists.md) tutorial for an example use case. 517 | 518 | 519 | 520 | !> **TODO** write this documentation 521 | 522 | 523 | 524 | !> **TODO** write this snippet 525 | 526 | 527 | 528 | ### seq 529 | 530 | ```phps 531 | function seq(Parser $first, Parser $second, Parser ...$parsers): Parser 532 | ``` 533 | 534 | 535 | 536 | The `seq` combinator executes each parser in turn and stops once they are all sucessful. 537 | 538 | If all the given parsers matches, the combinator will return a `Success` result holding each result. 539 | 540 | If any of the given parsers fails, the combinator will return the first `Failure` result it got. 541 | 542 | 543 | 544 | ```php 545 | result())); 559 | assert((string) $result->result()[0] === 'a'); 560 | assert((string) $result->result()[1] === 'b'); 561 | 562 | $result = $success($stream); 563 | assert($result instanceof Result\Failure); 564 | ``` 565 | 566 | 567 | 568 | ### Special combinators 569 | 570 | 571 | 572 | 573 | 574 | #### debug 575 | 576 | ```phps 577 | function debug(Parser $parser): Parser 578 | ``` 579 | 580 | 581 | 582 | The `debug` combinator is an helper to help you enable the debug-mode and get execution trace. 583 | 584 | Given a parser, it will return the exact same parser but with debugging facilities enabled. 585 | 586 | > [!TIP] 587 | > Read the [Debugging](/tutorials/debugging.md) tutorial for an example use case. 588 | 589 | 590 | 591 | 592 | 593 | ### ** Parser ** 594 | 595 | ```php 596 | repeat(2, alt(char(a), char(b))) {"line":1,"column":0,"ops":0} 614 | [info] > alt(char(a), char(b)) {"line":1,"column":0,"ops":0} 615 | [info] > char(a) {"line":1,"column":0,"ops":0} 616 | [info] < char(a) {"line":1,"column":1,"consumed":"a","ops":1,"duration":0.000198} 617 | [info] < alt(char(a), char(b)) {"line":1,"column":1,"consumed":"a","ops":2,"duration":0.000243} 618 | [info] > alt(char(a), char(b)) {"line":1,"column":1,"ops":2} 619 | [info] > char(a) {"line":1,"column":1,"ops":2} 620 | [error] < char(a) {"line":1,"column":1,"ops":3,"duration":0.000104} 621 | [info] > char(b) {"line":1,"column":1,"ops":3} 622 | [info] < char(b) {"line":1,"column":2,"consumed":"b","ops":4,"duration":5.0e-6} 623 | [info] < alt(char(a), char(b)) {"line":1,"column":2,"consumed":"b","ops":5,"duration":0.000149} 624 | [info] < repeat(2, alt(char(a), char(b))) {"line":1,"column":2,"ops":6,"duration":0.000446} 625 | ``` 626 | 627 | 628 | 629 | 630 | 631 | #### recurse 632 | 633 | ```phps 634 | function recurse(?Parser &$parser): Parser 635 | ``` 636 | 637 | 638 | 639 | !> **TODO** write this documentation 640 | 641 | 642 | 643 | !> **TODO** write this snippet 644 | 645 | 646 | 647 | ## Mappers 648 | 649 | 650 | 651 | 652 | 653 | ### otherwise 654 | 655 | ```phps 656 | function otherwise(mixed $value): Mapper 657 | ``` 658 | 659 | 660 | 661 | !> **TODO** write this documentation 662 | 663 | 664 | 665 | !> **TODO** write this snippet 666 | 667 | 668 | 669 | ### concat 670 | 671 | ```phps 672 | function concat(): Mapper 673 | ``` 674 | 675 | 676 | 677 | !> **TODO** write this documentation 678 | 679 | 680 | 681 | !> **TODO** write this snippet 682 | 683 | 684 | 685 | ### structure 686 | 687 | ```phps 688 | function structure(array $mappings): Mapper 689 | ``` 690 | 691 | 692 | 693 | !> **TODO** write this documentation 694 | 695 | 696 | 697 | !> **TODO** write this snippet 698 | 699 | 700 | 701 | ### php 702 | 703 | ```phps 704 | function php(string $name): Mapper 705 | ``` 706 | 707 | 708 | 709 | !> **TODO** write this documentation 710 | 711 | 712 | 713 | !> **TODO** write this snippet 714 | 715 | 716 | 717 | ### skip 718 | 719 | ```phps 720 | function skip(): Mapper 721 | ``` 722 | 723 | 724 | 725 | !> **TODO** write this documentation 726 | 727 | 728 | 729 | !> **TODO** write this snippet 730 | 731 | 732 | 733 | ### nth / first / last 734 | 735 | ```phps 736 | function nth(int $nth): Mapper 737 | function first(): Mapper 738 | function last(): Mapper 739 | ``` 740 | 741 | 742 | 743 | !> **TODO** write this documentation 744 | 745 | 746 | 747 | 748 | 749 | ### ** nth ** 750 | 751 | !> **TODO** write this snippet 752 | 753 | ### ** first ** 754 | 755 | !> **TODO** write this snippet 756 | 757 | ### ** last ** 758 | 759 | !> **TODO** write this snippet 760 | 761 | 762 | 763 | 764 | 765 | ### value 766 | 767 | ```phps 768 | function value(mixed $value): Mapper 769 | ``` 770 | 771 | 772 | 773 | !> **TODO** write this documentation 774 | 775 | 776 | 777 | !> **TODO** write this snippet 778 | 779 | 780 | 781 | ## Internals 782 | 783 | ### Streams 784 | 785 | Streams are the containers from which parsers are going to read data from. They are designed to look like usual PHP 786 | streams. 787 | 788 | They also implement a transaction mechanism allowing parsers to backtrack to previous positions when the need it. 789 | 790 | 791 | 792 | 793 | 794 | #### begin 795 | 796 | ```phps 797 | public function begin(): Transaction 798 | ``` 799 | 800 | 801 | 802 | The `begin` method will start a transaction and hand it to the caller. 803 | 804 | This will actually create a copy of the initial stream on which parser will be able to work without altering the main 805 | stream. 806 | 807 | In the example snippet we use the `*` character to represent the cursor position. 808 | 809 | When the transaction is started, it copy the initial stream and its actual state: both the stream and the transaction 810 | will be at the same position. 811 | 812 | When a parser consumes characters from the transaction it does not alter the initial stream until the transaction is 813 | committed: when this happens, the transaction applies its state to the initial stream. 814 | 815 | 816 | 817 | ```php 818 | consume(); // Stream: [ a*b c d ] 825 | 826 | $transaction = $stream->begin(); // Stream: [ a*b c d ] 827 | // Transaction: [ a*b c d ] 828 | 829 | $transaction->consume(); // Stream: [ a*b c d ] 830 | // Transaction: [ a b*c d ] 831 | 832 | $transaction->consume(); // Stream: [ a*b c d ] 833 | // Transaction: [ a b c*d ] 834 | 835 | $transaction->commit(); // Stream: [ a b c*d ] 836 | // Transaction: [ a b c*d ] 837 | ``` 838 | 839 | 840 | 841 | #### consume 842 | 843 | ```phps 844 | public function consume(): Slice 845 | ``` 846 | 847 | 848 | 849 | !> **TODO** write this documentation 850 | 851 | 852 | 853 | !> **TODO** write this snippet 854 | 855 | 856 | 857 | #### current 858 | 859 | ```phps 860 | public function current(): string 861 | ``` 862 | 863 | 864 | 865 | !> **TODO** write this documentation 866 | 867 | 868 | 869 | !> **TODO** write this snippet 870 | 871 | 872 | 873 | #### cut 874 | 875 | ```phps 876 | public function cut(int $offset, ?int $length = null): string 877 | ``` 878 | 879 | 880 | 881 | !> **TODO** write this documentation 882 | 883 | 884 | 885 | !> **TODO** write this snippet 886 | 887 | 888 | 889 | #### eos 890 | 891 | ```phps 892 | public function eos(): bool 893 | ``` 894 | 895 | 896 | 897 | !> **TODO** write this documentation 898 | 899 | 900 | 901 | !> **TODO** write this snippet 902 | 903 | 904 | 905 | #### seek 906 | 907 | ```phps 908 | public function seek(int $offset): bool 909 | ``` 910 | 911 | 912 | 913 | !> **TODO** write this documentation 914 | 915 | 916 | 917 | !> **TODO** write this snippet 918 | 919 | 920 | 921 | #### tell 922 | 923 | ```phps 924 | public function tell(): int 925 | ``` 926 | 927 | 928 | 929 | !> **TODO** write this documentation 930 | 931 | 932 | 933 | !> **TODO** write this snippet 934 | 935 | 936 | -------------------------------------------------------------------------------- /docs/tutorials/README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > This is the documentation for the `master` branch. 3 | > Things may change or not be complete. 4 | > 5 | > Head over to the [latest stable version](/1.0.0/) for an up-to-date documentation. 6 | 7 | 8 | 9 | 10 | 11 | ## [Your first parser](/tutorials/your-first-parser.md) {docsify-ignore} 12 | 13 | In this tutorial you will learn how to build a simple parser walking through some standard steps. You will discover 14 | some key elements and features of PPC and how to use them in a basic way. 15 | 16 | 17 | 18 | ## [Consuming until the end of the stream](/tutorials/consuming-until-the-end-of-the-stream.md) {docsify-ignore} 19 | 20 | In this tutorial, you will learn how to ensure your parser is able to consume an input until its end. This is 21 | particularly useful when you want to be sure there is nothing left to consume meaning your parser is complete. 22 | 23 | 24 | 25 | ## Matching enclosed values {docsify-ignore} 26 | 27 | !> **TODO** write this tutorial 28 | 29 | 30 | 31 | ## [Matching lists](/tutorials/matching-lists.md) {docsify-ignore} 32 | 33 | In this tutorial you will walk through some simple examples showing how to easily parse lists and extend this logic to 34 | allow variant on the list. 35 | 36 | 37 | 38 | ## [Debugging](/tutorials/debugging.md) {docsify-ignore} 39 | 40 | In this tutorial, you will learn how to use the [`debug`](/reference.md#debug) combinator to get a clear view of what's 41 | happening in your parsers. 42 | 43 | 44 | 45 | ## Looking ahead {docsify-ignore} 46 | 47 | !> **TODO** write this tutorial 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/tutorials/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [Getting started](/getting-started.md#getting-started) 2 | * [Tutorials](/tutorials/) 3 | * [Your first parser](/tutorials/your-first-parser.md) 4 | * [Consuming until the end of the stream](/tutorials/consuming-until-the-end-of-the-stream.md) 5 | * [Matching enclosed values](/tutorials/matching-enclosed-values.md) 6 | * [Matching lists](/tutorials/matching-lists.md) 7 | * [Debugging](/tutorials/debugging.md) 8 | * [Looking ahead](/tutorials/looking-ahead.md) 9 | * [Reference](/reference.md) 10 | * [Contribute](/contribute.md) 11 | 12 | * **Links** 13 | * [Issues](https://github.com/jubianchi/ppc) 14 | * [Packagist](https://packagist.org/packages/jubianchi/ppc) 15 | -------------------------------------------------------------------------------- /docs/tutorials/consuming-until-the-end-of-the-stream.md: -------------------------------------------------------------------------------- 1 | # Consuming until the end of the stream 2 | 3 | In this tutorial, you will learn how to ensure your parser is able to consume an input until its end. 4 | 5 | Sometimes it is important to make sure the parser consumes the whole input. Let's take a look at the 6 | [previous tutorial](/tutorials/your-first-parser.md) example where you built a working date and time parser. This parser 7 | is able to parser a ISO8601 compliant date until its second component. It does not know how to handle the timezone part. 8 | 9 | What you want here is to be sure that the parser consumed the whole input or produced a `Failure`. 10 | 11 | Let's go! 12 | 13 | ## The parser 14 | 15 | ```php 16 | map(concat()); 23 | $fourDigits = repeat(4, regex('/[0-9]/'))->map(concat()); 24 | 25 | $year = $fourDigits; 26 | $month = $twoDigits; 27 | $day = $twoDigits; 28 | $hour = $twoDigits; 29 | $minute = $twoDigits; 30 | $second = $twoDigits; 31 | $dash = char('-')->map(skip()); 32 | $colon = char(':')->map(skip()); 33 | $t = char('T')->map(skip()); 34 | $z = char('Z')->map(skip()); 35 | 36 | $dateTime = seq($year, $dash, $month, $dash, $day, $t, $hour, $colon, $minute, $colon, $second) 37 | ->map(structure(['year', 'month', 'day', 'hour', 'minute', 'second'])); 38 | ``` 39 | 40 | As you may know, a ISO8601 compliant date ends with a `Z` meaning the date and time are UTC based. Let's see if the 41 | parser takes care of this: 42 | 43 | ```phps 44 | valid()); // true 53 | ``` 54 | 55 | The stream is still valid after the parser consumed it: this means there is characters left to consume or, in other 56 | words, the parser did not reach the end of the stream (EOS). 57 | 58 | ### EOS 59 | 60 | PPC provides a dedicated parser, [`eos`](/reference.md#eos) which goal is to match the end of the stream. Fixing our date and time parser should be 61 | easy: 62 | 63 | ```phps 64 | map(skip()); 70 | 71 | $dateTime = seq($year, $dash, $month, $dash, $day, $t, $hour, $colon, $minute, $colon, $second, $eos) 72 | ->map(structure(['year', 'month', 'day', 'hour', 'minute', 'second'])); 73 | ``` 74 | 75 | EZPZ, right? 76 | 77 | Let's see if the parser is now failing correctly: 78 | 79 | ```phps 80 | map(concat()); 109 | $fourDigits = repeat(4, regex('/[0-9]/'))->map(concat()); 110 | 111 | $year = $fourDigits; 112 | $month = $twoDigits; 113 | $day = $twoDigits; 114 | $hour = $twoDigits; 115 | $minute = $twoDigits; 116 | $second = $twoDigits; 117 | $dash = char('-')->map(skip()); 118 | $colon = char(':')->map(skip()); 119 | $t = char('T')->map(skip()); 120 | $z = char('Z')->map(skip()); 121 | $eos = eos()->map(skip()); 122 | 123 | $dateTime = seq($year, $dash, $month, $dash, $day, $t, $hour, $colon, $minute, $colon, $second, $z, $eos) 124 | ->map(structure(['year', 'month', 'day', 'hour', 'minute', 'second'])); 125 | 126 | $result = $dateTime(new Char('2020-07-21T17:35:00Z')); 127 | var_dump($result->result()); 128 | ``` 129 | 130 | Again, this parser is not capable of parsing the time offset described in ISO8601: this is left as an exercice for you. 131 | -------------------------------------------------------------------------------- /docs/tutorials/debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | In this tutorial, you will learn how to use the [`debug`](/reference.md#debug) combinator to get a clear view of what's 4 | happening in your parsers. 5 | 6 | AS an example, we will use a snippet from the [Your first parser](/tutorials/your-first-parser.md) tutorial. 7 | 8 | ## The example 9 | 10 | In the previous previous tutorial we wrote a date and time parser. One of the first steps was to identify what we called 11 | _tokens_ ans then try to refactor the definitions to write less code. We started with: 12 | 13 | ```php 14 | 77 | 78 | 79 | 80 | ### Good 81 | 82 | 83 | 84 | ### ** Parser ** 85 | 86 | ```php 87 | repeat(4, regex(/[0-9]/)) {"line":1,"column":0,"ops":0} 104 | [info] > regex(/[0-9]/) {"line":1,"column":0,"ops":0} 105 | [info] < regex(/[0-9]/) {"line":1,"column":1,"consumed":"2","ops":1,"duration":0.000674} 106 | [info] > regex(/[0-9]/) {"line":1,"column":1,"ops":1} 107 | [info] < regex(/[0-9]/) {"line":1,"column":2,"consumed":"0","ops":2,"duration":7.0e-6} 108 | [info] > regex(/[0-9]/) {"line":1,"column":2,"ops":2} 109 | [info] < regex(/[0-9]/) {"line":1,"column":3,"consumed":"2","ops":3,"duration":4.0e-6} 110 | [info] > regex(/[0-9]/) {"line":1,"column":3,"ops":3} 111 | [info] < regex(/[0-9]/) {"line":1,"column":4,"consumed":"0","ops":4,"duration":3.0e-6} 112 | [info] < repeat(4, regex(/[0-9]/)) {"line":1,"column":4,"ops":5,"duration":0.000845} 113 | ``` 114 | 115 | 116 | 117 | 118 | 119 | ### Bad 120 | 121 | 122 | 123 | ### ** Parser ** 124 | 125 | ```php 126 | repeat(2, repeat(2, regex(/[0-9]/))) {"line":1,"column":0,"ops":0} 143 | [info] > repeat(2, regex(/[0-9]/)) {"line":1,"column":0,"ops":0} 144 | [info] > regex(/[0-9]/) {"line":1,"column":0,"ops":0} 145 | [info] < regex(/[0-9]/) {"line":1,"column":1,"consumed":"2","ops":1,"duration":0.000298} 146 | [info] > regex(/[0-9]/) {"line":1,"column":1,"ops":1} 147 | [info] < regex(/[0-9]/) {"line":1,"column":2,"consumed":"0","ops":2,"duration":4.0e-6} 148 | [info] < repeat(2, regex(/[0-9]/)) {"line":1,"column":2,"ops":3,"duration":0.000371} 149 | [info] > repeat(2, regex(/[0-9]/)) {"line":1,"column":2,"ops":3} 150 | [info] > regex(/[0-9]/) {"line":1,"column":2,"ops":3} 151 | [info] < regex(/[0-9]/) {"line":1,"column":3,"consumed":"2","ops":4,"duration":3.0e-6} 152 | [info] > regex(/[0-9]/) {"line":1,"column":3,"ops":4} 153 | [info] < regex(/[0-9]/) {"line":1,"column":4,"consumed":"0","ops":5,"duration":2.0e-6} 154 | [info] < repeat(2, regex(/[0-9]/)) {"line":1,"column":4,"ops":6,"duration":5.7e-5} 155 | [info] < repeat(2, repeat(2, regex(/[0-9]/))) {"line":1,"column":4,"ops":7,"duration":0.00048} 156 | ``` 157 | 158 | 159 | 160 | 161 | 162 | Before diving into the results, let's see how the debugger's output should be read: 163 | * The first element is the log level, it can be `info` or `error`; 164 | * The second element is either a `>` character, meaning we entered a parser or combinator, or a `<` meaning we exited 165 | from a parser or combinator; 166 | * The third element is the parser or combinator label; 167 | * The last element is the context, it contains: 168 | * The actual position in the stream in `line`s (starting at `1`) and `column`s (starting at `0`); 169 | * The `ops` count which is the actual number of parsers or combinators that have been executed; 170 | * The `duration` which is the time it took for the the parser to execute in seconds; 171 | * In case of a parser or combinator returning a `Slice`, the `consumed` characters. 172 | 173 | ### Reading the actual result 174 | 175 | Now that we know how to read the debugger output, what can we see in our previous examples? 176 | 177 | With the second snippet, the "bad" one, wa have more `ops` than with the good snippet. In our example, we do not see 178 | the impact of such a difference because we are parsing a tiny input but trust me, with large inputs, 2 less `ops` can 179 | make a real difference! 180 | 181 | Also, we see that with the good snippet, the output is flat compared to the "bad" one where we have two levels of 182 | [`repeat`](/reference.md#repeat). This can also make a difference when you will have to debug complex parsers. 183 | 184 | Talking about complex parsers, you may think that the actual output produced by the debugger might not be very 185 | comfortable. Let's see what we can do about that. 186 | 187 | ### Prettifying the output 188 | 189 | In our example, we use the `$twoDigits` and `$fourDigits` parsers as our base building blocks for 6 other parsers. When 190 | running the debugger against such a parser the output will always tell you we entered the `repeat` and `regex` parsers 191 | and combinators. But how do you know exactly which one was executed? Is it `$year` or `$month`? 192 | 193 | To make this easier, each parser can be given a label which will be used by the debugger: 194 | 195 | ```php 196 | label('year'); 205 | $month = $twoDigits->label('month'); 206 | $day = $twoDigits->label('day'); 207 | $hour = $twoDigits->label('hour'); 208 | $minute = $twoDigits->label('minute'); 209 | $second = $twoDigits->label('second'); 210 | 211 | ``` 212 | 213 | We should now have a very explicit output: 214 | 215 | ``` 216 | ... 217 | [info] > year•repeat(4, regex(/[0-9]/)) {"line":1,"column":0,"ops":0} 218 | [info] > regex(/[0-9]/) {"line":1,"column":0,"ops":0} 219 | [info] < regex(/[0-9]/) {"line":1,"column":1,"duration":0.003302,"consumed":"2","ops":1} 220 | [info] > regex(/[0-9]/) {"line":1,"column":1,"ops":1} 221 | [info] < regex(/[0-9]/) {"line":1,"column":2,"duration":4.0e-6,"consumed":"0","ops":2} 222 | [info] > regex(/[0-9]/) {"line":1,"column":2,"ops":2} 223 | [info] < regex(/[0-9]/) {"line":1,"column":3,"duration":2.0e-6,"consumed":"2","ops":3} 224 | [info] > regex(/[0-9]/) {"line":1,"column":3,"ops":3} 225 | [info] < regex(/[0-9]/) {"line":1,"column":4,"duration":2.0e-6,"consumed":"0","ops":4} 226 | [info] < year•repeat(4, regex(/[0-9]/)) {"line":1,"column":4,"duration":0.003373,"ops":5} 227 | [info] > char(-) {"line":1,"column":4,"ops":5} 228 | [info] < char(-) {"line":1,"column":5,"duration":6.0e-6,"consumed":"-","ops":6} 229 | [info] > month•repeat(2, regex(/[0-9]/)) {"line":1,"column":5,"ops":6} 230 | [info] > regex(/[0-9]/) {"line":1,"column":5,"ops":6} 231 | [info] < regex(/[0-9]/) {"line":1,"column":6,"duration":4.0e-6,"consumed":"0","ops":7} 232 | [info] > regex(/[0-9]/) {"line":1,"column":6,"ops":7} 233 | [info] < regex(/[0-9]/) {"line":1,"column":7,"duration":2.0e-6,"consumed":"7","ops":8} 234 | [info] < month•repeat(2, regex(/[0-9]/)) {"line":1,"column":7,"duration":2.3e-5,"ops":9} 235 | ... 236 | ``` 237 | 238 | See how the label changed? Now, for each output line we have the parser's label. 239 | -------------------------------------------------------------------------------- /docs/tutorials/matching-lists.md: -------------------------------------------------------------------------------- 1 | # Matching lists 2 | 3 | In this short tutorial, you will learn how to easily parse a separated list of items. 4 | 5 | The goal is to highlight the composability of parsers and how it can help you build powerful things that you can easily 6 | change to handle more cases. 7 | 8 | ## The list 9 | 10 | For this tutorial, let's take a list which looks like JavaScript arrays: `[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`. For the sake 11 | of simplicity here we will only consider a list of single-digits from 0 to 9. 12 | 13 | ### The simplest form 14 | 15 | For the first example, let's try to parse the list as is: 16 | 17 | ```php 18 | [!WARNING] 99 | > You may be tempted to also change the `$fourDigits` parser to something like `repeat(2, $twoDigits)` but this is a bad 100 | > idea. 101 | > 102 | > Do not fall into premature optimization: the optimization topic will be covered in another tutorial. 103 | 104 | Before you continue, there is an interesting thing to note here: as soon as you have your first parsers defined, you can 105 | start testing. In fact, each parser can work on its own: 106 | 107 | ```phps 108 | assert($year(new Stream('2020')) instanceof Result\Success); 109 | assert($year(new Stream('20')) instanceof Result\Failure); 110 | ``` 111 | 112 | Testing your parsers as you write them will help you ensure they are correct before you start combining them. It's 113 | easier to reason about small units (think about unit tests). 114 | 115 | ### Combining the parsers 116 | 117 | Now you have defined the _tokens_, let's try to combine them to produce an effective parser. The goal is to build a 118 | parser able to match a series of these _tokens_. To do that you can use the [`seq`](/reference.md#seq) combinator: 119 | 120 | ````phps 121 | 149 | 150 | 151 | 152 | ```phps 153 | $result = $dateTime(new Stream('2020-07-21T17:35:00Z')); 154 | 155 | var_dump($result); 156 | ``` 157 | 158 | 159 | 160 | ``` 161 | Success { 162 | array { 163 | Success { 164 | Success 165 | Success 166 | Success 167 | Success 168 | } 169 | Success 170 | Success { 171 | Success 172 | Success 173 | } 174 | Success 175 | Success { 176 | Success 177 | Success 178 | } 179 | ... 180 | } 181 | } 182 | ``` 183 | 184 | 185 | 186 | This result is hardly readable and moreover hard to use. We'll need to transform parsers' results into something better, 187 | this is what mappers are made for. 188 | 189 | ### Shaping the result 190 | 191 | Let's take for example the year parser: its results contains an array of four `Slice`s. What if you were able to reduce 192 | this number to only one? Better, what if you could directly get a string out of the `Slice`s? 193 | 194 | ```phps 195 | map(concat()); 202 | $year = $fourDigits; 203 | ``` 204 | 205 | Basically, the [`concat`](/reference.md#concat) will turn a `Result` containg an array of `Slice`s to a `Result` 206 | containing a single string: 207 | 208 | 209 | 210 | 211 | 212 | ``` 213 | Success { 214 | array { 215 | Slice(2) 216 | Slice(0) 217 | Slice(2) 218 | Slice(0) 219 | } 220 | } 221 | ``` 222 | 223 | 224 | 225 | ``` 226 | Success { 227 | "2020" 228 | } 229 | ``` 230 | 231 | 232 | 233 | Way better, there is still room for improvment! Results for the several separators (`-`, `:`, ...) are not really 234 | useful. Let's exclude them. This is what the [`skip`](/reference.md#skip) mapper is for: 235 | 236 | ```phps 237 | use function jubianchi\PPC\Mappers\skip; 238 | use function jubianchi\PPC\Parsers\char; 239 | 240 | $dash = char('-')->map(skip()); 241 | $colon = char(':')->map(skip()); 242 | $t = char('T')->map(skip()); 243 | $z = char('Z')->map(skip()); 244 | ``` 245 | 246 | We should now be able to read the raw result easily: 247 | 248 | 249 | 250 | 251 | 252 | ```phps 253 | $result = $dateTime(new Stream('2020-07-21T17:35:00Z')); 254 | 255 | var_dump($result); 256 | ``` 257 | 258 | 259 | 260 | ``` 261 | Success { 262 | array { 263 | "2020" 264 | "07" 265 | "21" 266 | "17" 267 | "35" 268 | "00" 269 | } 270 | } 271 | ``` 272 | 273 | 274 | 275 | One last thing you can do is make the resulting array a bit more self-descriptive. Let's turn the `array` 276 | into a `array` with keys being the names of the date and time parts: 277 | 278 | 279 | 280 | 281 | 282 | ```phps 283 | map(\jubianchi\PPC\Mappers\structure([ 289 | 'year', 290 | 'month', 291 | 'day', 292 | 'hour', 293 | 'minute', 294 | 'second', 295 | ])); 296 | 297 | $result = $dateTimeStructured(new Stream('2020-07-21T17:35:00Z')); 298 | 299 | var_dump($result->result()); 300 | ``` 301 | 302 | 303 | 304 | ``` 305 | array { 306 | "year" => 2020" 307 | "month" => "07" 308 | "day" => "21" 309 | "hour" => "17" 310 | "minute" => "35" 311 | "second" => "00" 312 | } 313 | ``` 314 | 315 | 316 | 317 | ## Wrapping everything together 318 | 319 | Now that you went through the process of writing a parser, let's wrap everything together and look at the code: 320 | 321 | ```php 322 | map(concat()); 330 | $fourDigits = repeat(4, regex('/[0-9]/'))->map(concat()); 331 | 332 | $year = $fourDigits; 333 | $month = $twoDigits; 334 | $day = $twoDigits; 335 | $hour = $twoDigits; 336 | $minute = $twoDigits; 337 | $second = $twoDigits; 338 | $dash = char('-')->map(skip()); 339 | $colon = char(':')->map(skip()); 340 | $t = char('T')->map(skip()); 341 | $z = char('Z')->map(skip()); 342 | 343 | $dateTime = seq($year, $dash, $month, $dash, $day, $t, $hour, $colon, $minute, $colon, $second) 344 | ->map(structure(['year', 'month', 'day', 'hour', 'minute', 'second'])); 345 | 346 | $result = $dateTime(new Char('2020-07-21T17:35:00Z')); 347 | var_dump($result->result()); 348 | ``` 349 | 350 | As you may have seen this parser is not capable of parsing the time offset described in ISO8601: this is left as an 351 | exercice for you. 352 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC; 14 | 15 | use jubianchi\PPC\Stream\Char; 16 | use jubianchi\PPC\Stream\File; 17 | use function jubianchi\PPC\Combinators\debug; 18 | use function jubianchi\PPC\Parsers\json; 19 | 20 | require_once __DIR__.'/vendor/autoload.php'; 21 | require_once __DIR__.'/parsers/json.php'; 22 | 23 | $stream = new Char(file_get_contents(__DIR__.'/resources/composer.json')); 24 | //$stream = new File(__DIR__.'/resources/composer.json'); 25 | //$stream = new CharStream('"foo": "bar", "bar": "baz", "boo": false'); 26 | //$stream = new CharStream('{"foo": false, "bar": "baz", "boo": false}'); 27 | //$stream = new CharStream('["foo", true, false]'); 28 | //$stream = new CharStream('{"foo": false, "bar": "baz", "boo": ["foo", true, false], "bee": null, "bii": ""}'); 29 | //$stream = new CharStream('"foo"'); 30 | $parser = json(); 31 | var_dump($parser($stream)->result()); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ppc", 3 | "description": "A parser combinator library for PHP", 4 | "directories": { 5 | "doc": "docs" 6 | }, 7 | "dependencies": { 8 | "docsify-cli": "^4.4.1", 9 | "dot-prop": ">=5.1.1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/jubianchi/ppc.git" 14 | }, 15 | "author": { 16 | "name": "Julien Bianchi", 17 | "email": "contact@jubianchi.fr" 18 | }, 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/jubianchi/ppc/issues" 22 | }, 23 | "homepage": "https://jubianchi.github.io/ppc/" 24 | } 25 | -------------------------------------------------------------------------------- /parsers/json.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Parsers; 14 | 15 | use function jubianchi\PPC\Combinators\alt; 16 | use function jubianchi\PPC\Combinators\enclosed; 17 | use function jubianchi\PPC\Combinators\many; 18 | use function jubianchi\PPC\Combinators\not; 19 | use function jubianchi\PPC\Combinators\opt; 20 | use function jubianchi\PPC\Combinators\recurse; 21 | use function jubianchi\PPC\Combinators\separated; 22 | use function jubianchi\PPC\Combinators\seq; 23 | use function jubianchi\PPC\Mappers\concat; 24 | use function jubianchi\PPC\Mappers\first; 25 | use function jubianchi\PPC\Mappers\otherwise; 26 | use function jubianchi\PPC\Mappers\php; 27 | use function jubianchi\PPC\Mappers\skip; 28 | use function jubianchi\PPC\Mappers\structure; 29 | use function jubianchi\PPC\Mappers\value; 30 | use jubianchi\PPC\Parser; 31 | use jubianchi\PPC\Parser\Result; 32 | use jubianchi\PPC\Parser\Result\Success; 33 | 34 | function json(): Parser 35 | { 36 | $space = regex('/\s/')->label('space'); 37 | $spaces = opt(many($space))->label('spaces')->map(skip())->stringify(fn (string $label): string => $label); 38 | $comma = seq($spaces, char(','), $spaces)->label('comma')->map(skip())->stringify(fn (string $label): string => $label); 39 | $colon = seq($spaces, char(':'), $spaces)->label('colon')->map(skip())->stringify(fn (string $label): string => $label); 40 | 41 | $string = enclosed( 42 | char('"'), 43 | opt( 44 | many( 45 | alt( 46 | word('\"'), 47 | word('\\\\'), 48 | not(char('"')), 49 | ), 50 | )->map(concat()->then(php('stripcslashes'))), 51 | )->map(otherwise('')), 52 | ) 53 | ->label('string'); 54 | 55 | $boolean = alt( 56 | word('true')->map(value(true)), 57 | word('false')->map(value(false)), 58 | ) 59 | ->label('boolean'); 60 | 61 | $null = word('null') 62 | ->map(fn (Result $result) => new Success(null)) 63 | ->label('null'); 64 | 65 | $numeric = alt( 66 | char('0'), 67 | seq( 68 | regex('/[1-9]/'), 69 | opt(many(regex('/[0-9]/'))), 70 | opt( 71 | seq( 72 | char('.'), 73 | regex('/[0-9]/'), 74 | opt(many(regex('/[0-9]/'))), 75 | ) 76 | ) 77 | ) 78 | ) 79 | ->label('numeric'); 80 | 81 | $pair = seq( 82 | recurse($string), 83 | $colon, 84 | recurse($value), 85 | ) 86 | ->label('pair') 87 | ->map(structure(['key', 'value'])); 88 | 89 | $members = separated($comma, $pair) 90 | ->label('members'); 91 | 92 | $object = enclosed( 93 | seq(char('{'), $spaces), 94 | opt($members), 95 | seq($spaces, char('}')), 96 | ) 97 | ->label('object') 98 | ->map(function (Result $result): Result { 99 | $object = new \StdClass(); 100 | 101 | foreach ($result->result() as $pair) { 102 | ['key' => $key, 'value' => $value] = $pair; 103 | 104 | $object->{$key} = $value; 105 | } 106 | 107 | return new Success($object); 108 | }); 109 | 110 | $items = separated( 111 | seq($spaces, $comma = char(','), $spaces)->stringify(fn (): string => (string) $comma), 112 | recurse($value) 113 | ) 114 | ->label('items'); 115 | 116 | $array = enclosed( 117 | seq($open = char('['), $spaces)->stringify(fn (): string => (string) $open), 118 | opt($items), 119 | seq($spaces, $close = char(']'))->stringify(fn (): string => (string) $close), 120 | ) 121 | ->label('array'); 122 | 123 | $value = alt($object, $array, $string, $boolean, $null, $numeric) 124 | ->label('value') 125 | ->stringify(fn (string $label): string => $label); 126 | 127 | return seq( 128 | $value, 129 | $spaces, 130 | eos()->map(skip()), 131 | ) 132 | ->label('json') 133 | ->stringify(fn (string $label): string => $label) 134 | ->map(first()); 135 | } 136 | -------------------------------------------------------------------------------- /src/combinators.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Combinators; 14 | 15 | use Exception; 16 | use jubianchi\PPC\Parser; 17 | use jubianchi\PPC\Parser\Debugger; 18 | use jubianchi\PPC\Parser\Result; 19 | use jubianchi\PPC\Parser\Result\Failure; 20 | use jubianchi\PPC\Parser\Result\Skip; 21 | use jubianchi\PPC\Parser\Result\Success; 22 | use function jubianchi\PPC\Parsers\any; 23 | use jubianchi\PPC\Stream; 24 | 25 | function alt(Parser $first, Parser $second, Parser ...$parsers): Parser 26 | { 27 | array_unshift($parsers, $first, $second); 28 | 29 | return (new Parser('alt', function (Stream $stream, string $label, ?Debugger $debugger = null) use ($parsers): Result { 30 | $failure = null; 31 | 32 | foreach ($parsers as $parser) { 33 | $transaction = $stream->begin(); 34 | $result = $parser($transaction, $debugger); 35 | 36 | if ($result->isSuccess()) { 37 | $transaction->commit(); 38 | 39 | return $result; 40 | } 41 | 42 | if (null === $failure) { 43 | $failure = $result; 44 | } 45 | } 46 | 47 | return $failure; 48 | })) 49 | ->stringify(fn (string $label): string => $label.'('.implode(', ', $parsers).')'); 50 | } 51 | 52 | function seq(Parser $first, Parser $second, Parser ...$parsers): Parser 53 | { 54 | array_unshift($parsers, $first, $second); 55 | 56 | return (new Parser('seq', function (Stream $stream, string $label, ?Debugger $debugger = null) use ($parsers): Result { 57 | $results = []; 58 | $transaction = $stream->begin(); 59 | 60 | foreach ($parsers as $parser) { 61 | $result = $parser($transaction, $debugger); 62 | 63 | if ($result->isFailure()) { 64 | return $result; 65 | } 66 | 67 | if (!($result instanceof Skip)) { 68 | $results[] = $result->result(); 69 | } 70 | } 71 | 72 | $transaction->commit(); 73 | 74 | return new Success($results); 75 | }))->stringify(fn (string $label): string => $label.'('.implode(', ', $parsers).')'); 76 | } 77 | 78 | function opt(Parser $parser): Parser 79 | { 80 | return (new Parser('opt', function (Stream $stream, string $label, ?Debugger $debugger = null) use ($parser): Result { 81 | $transaction = $stream->begin(); 82 | $result = $parser($transaction, $debugger); 83 | 84 | if ($result->isSuccess()) { 85 | $transaction->commit(); 86 | 87 | return $result; 88 | } 89 | 90 | return new Success(null); 91 | })) 92 | ->stringify(fn (string $label): string => $label.'('.$parser.')'); 93 | } 94 | 95 | function many(Parser $parser): Parser 96 | { 97 | return (new Parser('many', function (Stream $stream, string $label, ?Debugger $debugger = null) use ($parser): Result { 98 | $count = 0; 99 | $results = []; 100 | 101 | while (true) { 102 | $transaction = $stream->begin(); 103 | $result = $parser($transaction, $debugger); 104 | 105 | if ($result->isFailure()) { 106 | if (0 === $count) { 107 | return $result; 108 | } 109 | 110 | break; 111 | } 112 | 113 | ++$count; 114 | 115 | if (!($result instanceof Skip)) { 116 | $results[] = $result->result(); 117 | } 118 | 119 | $transaction->commit(); 120 | } 121 | 122 | return new Success($results); 123 | })) 124 | ->stringify(fn (string $label): string => $label.'('.$parser.')'); 125 | } 126 | 127 | function repeat(int $times, Parser $parser): Parser 128 | { 129 | return (new Parser('repeat', function (Stream $stream, string $label, ?Debugger $debugger = null) use ($parser, $times): Result { 130 | $results = []; 131 | $transaction = $stream->begin(); 132 | 133 | while ($times-- > 0) { 134 | $result = $parser($transaction, $debugger); 135 | 136 | if ($result->isFailure()) { 137 | return $result; 138 | } 139 | 140 | if (!($result instanceof Skip)) { 141 | $results[] = $result->result(); 142 | } 143 | } 144 | 145 | $transaction->commit(); 146 | 147 | return new Success($results); 148 | })) 149 | ->stringify(fn (string $label): string => $label.'('.$times.', '.$parser.')'); 150 | } 151 | 152 | function not(Parser $parser, Parser ...$parsers): Parser 153 | { 154 | array_unshift($parsers, $parser); 155 | 156 | return (new Parser('not', function (Stream $stream, string $label, ?Debugger $debugger = null) use ($parsers): Result { 157 | foreach ($parsers as $parser) { 158 | $transaction = $stream->begin(); 159 | $result = $parser($transaction, $debugger); 160 | 161 | if ($result->isSuccess()) { 162 | return new Failure( 163 | $label, 164 | sprintf( 165 | 'Expected "%s" not to match, got "%s" at line %s offset %d', 166 | $parser, 167 | $stream->current(), 168 | $stream->position()['line'], 169 | $stream->tell() 170 | ) 171 | ); 172 | } 173 | } 174 | 175 | $result = any()($stream, $debugger); 176 | 177 | return $result; 178 | })) 179 | ->stringify(fn (string $label): string => $label.'('.implode(', ', $parsers).')'); 180 | } 181 | 182 | function recurse(?Parser &$parser): Parser 183 | { 184 | return (new Parser('recurse', function (Stream $stream, string $label, ?Debugger $debugger = null) use (&$parser): Result { 185 | if (null === $parser) { 186 | throw new Exception('Could not call parser'); 187 | } 188 | 189 | return $parser($stream, $debugger); 190 | })) 191 | ->stringify(function (string $label) use (&$parser): string { 192 | if (null === $parser) { 193 | throw new Exception('Could not call parser'); 194 | } 195 | 196 | return $label.'('.$parser->stringify(fn (string $label): string => $label).')'; 197 | }); 198 | } 199 | 200 | function enclosed(Parser $before, Parser $parser, ?Parser $after = null): Parser 201 | { 202 | $after = $after ?? $before; 203 | 204 | return (new Parser('enclosed', function (Stream $stream, string $label, ?Debugger $debugger = null) use ($before, $parser, $after): Result { 205 | $transaction = $stream->begin(); 206 | $beforeResult = $before($transaction, $debugger); 207 | 208 | if ($beforeResult->isFailure()) { 209 | return $beforeResult; 210 | } 211 | 212 | $result = $parser($transaction, $debugger); 213 | 214 | if ($result->isFailure()) { 215 | return $result; 216 | } 217 | 218 | $afterResult = $after($transaction, $debugger); 219 | 220 | if ($afterResult->isFailure()) { 221 | return $afterResult; 222 | } 223 | 224 | $transaction->commit(); 225 | 226 | return $result; 227 | })) 228 | ->stringify(fn (string $label): string => $label.'('.$before.', '.$parser.', '.$after.')'); 229 | } 230 | 231 | function separated(Parser $separator, Parser $parser): Parser 232 | { 233 | return (new Parser('separated', function (Stream $stream, string $label, ?Debugger $debugger = null) use ($separator, $parser): Result { 234 | $results = []; 235 | 236 | while (true) { 237 | $transaction = $stream->begin(); 238 | 239 | if (count($results) > 0) { 240 | $result = $separator($transaction, $debugger); 241 | 242 | if ($result->isFailure()) { 243 | break; 244 | } 245 | } 246 | 247 | $result = $parser($transaction, $debugger); 248 | 249 | if ($result->isFailure()) { 250 | if (0 === count($results)) { 251 | return $result; 252 | } 253 | 254 | break; 255 | } 256 | 257 | $transaction->commit(); 258 | $results[] = $result->result(); 259 | } 260 | 261 | return new Success($results); 262 | })) 263 | ->stringify(fn (string $label): string => $label.'('.$separator.', '.$parser.')'); 264 | } 265 | -------------------------------------------------------------------------------- /src/debug.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Combinators; 14 | 15 | use jubianchi\PPC\Logger\CLI; 16 | use jubianchi\PPC\Parser; 17 | use jubianchi\PPC\Parser\Debugger; 18 | use jubianchi\PPC\Parser\Result; 19 | use jubianchi\PPC\Stream; 20 | 21 | function debug(Parser $parser): Parser 22 | { 23 | $logger = new CLI(); 24 | 25 | return new Parser('debug', fn (Stream $stream, string $label, ?Debugger $debugger = null): Result => $parser( 26 | $stream, 27 | $debugger ?? new Parser\Debugger($logger) 28 | )); 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/Logger/CLI.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Logger; 14 | 15 | use Psr\Log\AbstractLogger; 16 | use Psr\Log\LogLevel; 17 | 18 | class CLI extends AbstractLogger 19 | { 20 | public function log($level, $message, array $context = []): void 21 | { 22 | $context = json_encode($context); 23 | $label = str_pad('['.$level.']', 9, ' '); 24 | 25 | $format = function (string $level, string $message): string { 26 | switch ($level) { 27 | case LogLevel::INFO: 28 | return "\033[38m".$message."\033[0m"; 29 | 30 | case LogLevel::ERROR: 31 | return "\033[1;31m".$message."\033[0m"; 32 | 33 | case LogLevel::WARNING: 34 | return "\033[1;33m".$message."\033[0m"; 35 | 36 | default: 37 | return $message; 38 | } 39 | }; 40 | 41 | echo $format($level, $label."\t".$message.' '.$context).PHP_EOL; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/Mapper.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC; 14 | 15 | use jubianchi\PPC\Parser\Result; 16 | 17 | class Mapper 18 | { 19 | /** 20 | * @var callable(Result): Result 21 | */ 22 | private $mapper; 23 | 24 | /** 25 | * @var ?callable(Result): Result 26 | */ 27 | private $next = null; 28 | 29 | public function __construct(callable $mapper) 30 | { 31 | $this->mapper = $mapper; 32 | } 33 | 34 | public function __invoke(Result $result): Result 35 | { 36 | if (null === $this->next) { 37 | return ($this->mapper)($result); 38 | } 39 | 40 | return ($this->next)(($this->mapper)($result)); 41 | } 42 | 43 | /** 44 | * @param callable(Result): Result $next 45 | * 46 | * @return $this 47 | */ 48 | public function then(callable $next): self 49 | { 50 | $this->next = $next; 51 | 52 | return $this; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/Parser.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC; 14 | 15 | use Exception; 16 | use jubianchi\PPC\Parser\Debugger; 17 | use jubianchi\PPC\Parser\Result; 18 | use RuntimeException; 19 | 20 | class Parser 21 | { 22 | /** 23 | * @var callable(Stream, string, ?Debugger): Result 24 | */ 25 | private $parser; 26 | 27 | /** 28 | * @var callable(Result): Result 29 | */ 30 | private $mapper; 31 | 32 | /** 33 | * @var callable(string): string 34 | */ 35 | private $stringify; 36 | 37 | private string $label; 38 | private string $originalLabel; 39 | 40 | /** 41 | * @var callable(Result): mixed 42 | */ 43 | private $action; 44 | 45 | /** 46 | * @param callable(Stream, string, ?Debugger): Result $parser 47 | */ 48 | public function __construct(string $label, callable $parser) 49 | { 50 | $this->originalLabel = $this->label = $label; 51 | $this->parser = $parser; 52 | $this->mapper = fn (Result $result): Result => $result; 53 | $this->stringify = fn (string $label): string => $label; 54 | } 55 | 56 | public function __toString(): string 57 | { 58 | return ($this->stringify)($this->label); 59 | } 60 | 61 | /** 62 | * @throws Exception 63 | */ 64 | public function __invoke(Stream $stream, ?Debugger $debugger = null): Result 65 | { 66 | $debugger and $debugger->enter($this, $stream); 67 | 68 | try { 69 | $result = ($this->parser)($stream, $this->label, $debugger); 70 | 71 | $debugger and $debugger->exit($this, $stream, $result); 72 | 73 | if ($result->isFailure()) { 74 | return $result; 75 | } 76 | 77 | $mapped = ($this->mapper)($result); 78 | 79 | if (null !== $this->action) { 80 | ($this->action)($mapped); 81 | } 82 | 83 | return $mapped; 84 | } catch (Exception $exception) { 85 | throw new RuntimeException($this->label.': '.$exception->getMessage(), $exception->getCode(), $exception); 86 | } 87 | } 88 | 89 | public function label(string $label): self 90 | { 91 | $parser = clone $this; 92 | $parser->label = $label.'•'.$this->originalLabel; 93 | 94 | return $parser; 95 | } 96 | 97 | /** 98 | * @param callable(Result): Result $mapper 99 | * 100 | * @return $this 101 | */ 102 | public function map(callable $mapper): self 103 | { 104 | $parser = clone $this; 105 | $parser->mapper = $mapper; 106 | 107 | return $parser; 108 | } 109 | 110 | /** 111 | * @param callable(Result): mixed $action 112 | * 113 | * @return $this 114 | */ 115 | public function do(callable $action): self 116 | { 117 | $parser = clone $this; 118 | $parser->action = $action; 119 | 120 | return $parser; 121 | } 122 | 123 | /** 124 | * @param callable(string): string $stringify 125 | * 126 | * @return $this 127 | */ 128 | public function stringify(callable $stringify): self 129 | { 130 | $parser = clone $this; 131 | $parser->stringify = $stringify; 132 | 133 | return $parser; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/lib/Parser/Debugger.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Parser; 14 | 15 | use jubianchi\PPC\Parser; 16 | use jubianchi\PPC\Slice; 17 | use jubianchi\PPC\Stream; 18 | use Psr\Log\LoggerInterface; 19 | use SplObjectStorage; 20 | 21 | class Debugger 22 | { 23 | private int $padding = 0; 24 | private int $ops = 0; 25 | private LoggerInterface $logger; 26 | private SplObjectStorage $starts; 27 | 28 | public function __construct(LoggerInterface $logger) 29 | { 30 | $this->logger = $logger; 31 | $this->starts = new SplObjectStorage(); 32 | } 33 | 34 | public function enter(Parser $parser, Stream $stream): self 35 | { 36 | $context = $stream->position(); 37 | $context = $context + ['stream' => get_class($stream).'#'.spl_object_id($stream)]; 38 | 39 | $this->info('> '.$parser, $context); 40 | 41 | ++$this->padding; 42 | 43 | $this->starts->attach($parser, microtime(true)); 44 | 45 | return $this; 46 | } 47 | 48 | public function exit(Parser $parser, Stream $stream, Result $result): self 49 | { 50 | $context = $stream->position(); 51 | $context = $context + ['stream' => get_class($stream).'#'.spl_object_id($stream)]; 52 | 53 | if ($this->starts->contains($parser)) { 54 | $context = $context + ['duration' => round(microtime(true) - $this->starts[$parser], 6)]; 55 | $this->starts->detach($parser); 56 | } 57 | 58 | ++$this->ops; 59 | --$this->padding; 60 | 61 | if ($result instanceof Result\Failure) { 62 | $this->error('< '.$parser, $context); 63 | } else { 64 | $context = $context + ($result->result() instanceof Slice ? ['consumed' => (string) $result->result()] : []); 65 | 66 | $this->info('< '.$parser, $context); 67 | } 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * @param string $message 74 | * @param array $context 75 | * 76 | * @return $this 77 | */ 78 | public function error($message, array $context = []): self 79 | { 80 | $this->logger->error($this->format($message), $this->build($context)); 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * @param string $message 87 | * @param array $context 88 | * 89 | * @return $this 90 | */ 91 | public function info($message, array $context = []): self 92 | { 93 | $this->logger->info($this->format($message), $this->build($context)); 94 | 95 | return $this; 96 | } 97 | 98 | private function format(string $message): string 99 | { 100 | $padding = $this->padding > 0 ? str_repeat(' ', $this->padding) : ''; 101 | 102 | return $padding.$message; 103 | } 104 | 105 | /** 106 | * @param array $context 107 | * 108 | * @return array 109 | */ 110 | private function build(array $context): array 111 | { 112 | $context = $context + ['ops' => $this->ops]; 113 | 114 | return $context; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/lib/Parser/Result.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Parser; 14 | 15 | use Exception; 16 | 17 | interface Result 18 | { 19 | /** 20 | * @throws Exception 21 | * 22 | * @return mixed 23 | */ 24 | public function result(); 25 | 26 | public function isSuccess(): bool; 27 | 28 | public function isFailure(): bool; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/Parser/Result/Failure.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Parser\Result; 14 | 15 | use Exception; 16 | use jubianchi\PPC\Parser\Result; 17 | use jubianchi\PPC\Stream; 18 | 19 | class Failure extends Exception implements Result 20 | { 21 | public function __construct(string $label, string $message) 22 | { 23 | parent::__construct(sprintf('%s: %s', $label, $message)); 24 | } 25 | 26 | public function isSuccess(): bool 27 | { 28 | return false; 29 | } 30 | 31 | public function isFailure(): bool 32 | { 33 | return true; 34 | } 35 | 36 | /** 37 | * @throws $this 38 | */ 39 | public function result() 40 | { 41 | throw $this; 42 | } 43 | 44 | public static function create(string $label, string $expected, string $actual, Stream $stream): self 45 | { 46 | ['line' => $line, 'column' => $column] = $stream->position(); 47 | 48 | return new self($label, sprintf( 49 | 'Expected "%s", got "%s" at line %d offset %d', 50 | $expected, 51 | $actual, 52 | $line, 53 | $column, 54 | )); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/lib/Parser/Result/Skip.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Parser\Result; 14 | 15 | class Skip extends Success 16 | { 17 | public function __construct($result = null) 18 | { 19 | parent::__construct($result); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/Parser/Result/Success.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Parser\Result; 14 | 15 | use jubianchi\PPC\Parser\Result; 16 | 17 | class Success implements Result 18 | { 19 | /** 20 | * @var mixed 21 | */ 22 | private $result; 23 | 24 | /** 25 | * @param mixed $result 26 | */ 27 | public function __construct($result) 28 | { 29 | $this->result = $result; 30 | } 31 | 32 | public function isSuccess(): bool 33 | { 34 | return true; 35 | } 36 | 37 | public function isFailure(): bool 38 | { 39 | return false; 40 | } 41 | 42 | public function result() 43 | { 44 | return $this->result; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/Slice.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC; 14 | 15 | class Slice 16 | { 17 | private Stream $stream; 18 | private int $offset; 19 | private int $length; 20 | 21 | public function __construct(Stream $stream, int $offset, int $length) 22 | { 23 | $this->stream = $stream; 24 | $this->offset = $offset; 25 | $this->length = $length; 26 | } 27 | 28 | public function __toString(): string 29 | { 30 | return $this->stream->cut($this->offset, $this->length); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/Stream.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC; 14 | 15 | use jubianchi\PPC\Stream\Transaction; 16 | 17 | interface Stream 18 | { 19 | const EOS = __CLASS__.'::EOS'; 20 | const FIRST_LINE = 1; 21 | const FIRST_COLUMN = 0; 22 | 23 | public function begin(): Transaction; 24 | 25 | public function consume(): Slice; 26 | 27 | public function current(): string; 28 | 29 | public function cut(int $offset, ?int $length = null): string; 30 | 31 | public function eos(): bool; 32 | 33 | public function seek(int $offset): void; 34 | 35 | public function tell(): int; 36 | 37 | /** 38 | * @return array 39 | */ 40 | public function position(): array; 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/Stream/Char.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Stream; 14 | 15 | use jubianchi\PPC\Slice; 16 | use jubianchi\PPC\Stream; 17 | use OutOfBoundsException; 18 | 19 | class Char implements Stream 20 | { 21 | protected int $offset = 0; 22 | protected string $current; 23 | protected int $line = self::FIRST_LINE; 24 | protected int $column = self::FIRST_COLUMN; 25 | 26 | private string $contents; 27 | private int $length; 28 | 29 | public function __construct(string $contents) 30 | { 31 | $this->contents = $contents; 32 | $this->length = strlen($this->contents); 33 | 34 | if ($this->offset >= $this->length) { 35 | $this->current = self::EOS; 36 | } else { 37 | $this->current = substr($this->contents, $this->offset, 1); 38 | } 39 | } 40 | 41 | public function seek(int $offset): void 42 | { 43 | if (0 > $offset) { 44 | throw new OutOfBoundsException(); 45 | } 46 | 47 | if ($offset > $this->length) { 48 | throw new OutOfBoundsException(); 49 | } 50 | 51 | $part = substr($this->contents, 0, $offset); 52 | $newlines = substr_count($part, "\n"); 53 | 54 | $this->offset = $offset; 55 | $this->line = self::FIRST_LINE + $newlines; 56 | $this->column = $offset; 57 | 58 | if ($newlines > 0) { 59 | /* @phpstan-ignore-next-line */ 60 | $this->column = $offset - strripos($part, "\n") - 1; 61 | } 62 | 63 | if ($this->offset === $this->length) { 64 | $this->current = self::EOS; 65 | } else { 66 | $this->current = substr($this->contents, $this->offset, 1); 67 | } 68 | } 69 | 70 | public function current(): string 71 | { 72 | return $this->current; 73 | } 74 | 75 | public function consume(): Slice 76 | { 77 | if ($this->offset === $this->length) { 78 | throw new OutOfBoundsException(); 79 | } 80 | 81 | $slice = new Slice($this, $this->offset, 1); 82 | 83 | ++$this->offset; 84 | ++$this->column; 85 | 86 | if ("\n" === $this->current) { 87 | ++$this->line; 88 | $this->column = self::FIRST_COLUMN; 89 | } 90 | 91 | if ($this->offset === $this->length) { 92 | $this->current = self::EOS; 93 | } else { 94 | $this->current = substr($this->contents, $this->offset, 1); 95 | } 96 | 97 | return $slice; 98 | } 99 | 100 | public function tell(): int 101 | { 102 | return $this->offset; 103 | } 104 | 105 | public function eos(): bool 106 | { 107 | return $this->offset >= $this->length; 108 | } 109 | 110 | public function cut(int $offset, ?int $length = null): string 111 | { 112 | $length = $length ?? $this->length - $offset; 113 | 114 | if (0 > $offset) { 115 | throw new OutOfBoundsException(); 116 | } 117 | 118 | if ($offset + $length > $this->length) { 119 | throw new OutOfBoundsException(); 120 | } 121 | 122 | return substr($this->contents, $offset, $length); 123 | } 124 | 125 | public function begin(): Transaction 126 | { 127 | return new Transaction($this); 128 | } 129 | 130 | public function position(): array 131 | { 132 | return [ 133 | 'line' => $this->line, 134 | 'column' => $this->column, 135 | ]; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/lib/Stream/MultiByteChar.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Stream; 14 | 15 | use jubianchi\PPC\Slice; 16 | use jubianchi\PPC\Stream; 17 | use OutOfBoundsException; 18 | 19 | class MultiByteChar implements Stream 20 | { 21 | protected int $offset = 0; 22 | protected string $current; 23 | protected int $line = self::FIRST_LINE; 24 | protected int $column = self::FIRST_COLUMN; 25 | 26 | private string $contents; 27 | private int $length; 28 | 29 | public function __construct(string $contents) 30 | { 31 | $this->contents = $contents; 32 | $this->length = mb_strlen($this->contents); 33 | 34 | if ($this->offset >= $this->length) { 35 | $this->current = self::EOS; 36 | } else { 37 | $this->current = mb_substr($this->contents, $this->offset, 1); 38 | } 39 | } 40 | 41 | public function seek(int $offset): void 42 | { 43 | if (0 > $offset) { 44 | throw new OutOfBoundsException(); 45 | } 46 | 47 | if ($offset > $this->length) { 48 | throw new OutOfBoundsException(); 49 | } 50 | 51 | $part = mb_substr($this->contents, 0, $offset); 52 | $newlines = mb_substr_count($part, "\n"); 53 | 54 | $this->offset = $offset; 55 | $this->line = self::FIRST_LINE + $newlines; 56 | $this->column = $offset; 57 | 58 | if ($newlines > 0) { 59 | /* @phpstan-ignore-next-line */ 60 | $this->column = $offset - mb_strripos($part, "\n") - 1; 61 | } 62 | 63 | if ($this->offset === $this->length) { 64 | $this->current = self::EOS; 65 | } else { 66 | $this->current = mb_substr($this->contents, $this->offset, 1); 67 | } 68 | } 69 | 70 | public function current(): string 71 | { 72 | return $this->current; 73 | } 74 | 75 | public function consume(): Slice 76 | { 77 | if ($this->offset === $this->length) { 78 | throw new OutOfBoundsException(); 79 | } 80 | 81 | ++$this->offset; 82 | ++$this->column; 83 | 84 | if ("\n" === $this->current) { 85 | ++$this->line; 86 | $this->column = self::FIRST_COLUMN; 87 | } 88 | 89 | if ($this->offset === $this->length) { 90 | $this->current = self::EOS; 91 | } else { 92 | $this->current = mb_substr($this->contents, $this->offset, 1); 93 | } 94 | 95 | return new Slice($this, $this->offset, 1); 96 | } 97 | 98 | public function tell(): int 99 | { 100 | return $this->offset; 101 | } 102 | 103 | public function eos(): bool 104 | { 105 | return $this->offset >= $this->length; 106 | } 107 | 108 | public function cut(int $offset, ?int $length = null): string 109 | { 110 | $length = $length ?? $this->length - $offset; 111 | 112 | if (0 > $offset) { 113 | throw new OutOfBoundsException(); 114 | } 115 | 116 | if ($offset + $length > $this->length) { 117 | throw new OutOfBoundsException(); 118 | } 119 | 120 | return mb_substr($this->contents, $offset, $length); 121 | } 122 | 123 | public function begin(): Transaction 124 | { 125 | return new Transaction($this); 126 | } 127 | 128 | public function position(): array 129 | { 130 | return [ 131 | 'line' => $this->line, 132 | 'column' => $this->column, 133 | ]; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/lib/Stream/Transaction.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Stream; 14 | 15 | use jubianchi\PPC\Slice; 16 | use jubianchi\PPC\Stream; 17 | 18 | class Transaction implements Stream 19 | { 20 | private Stream $stream; 21 | private Stream $transaction; 22 | 23 | public function __construct(Stream $stream) 24 | { 25 | $this->stream = $stream; 26 | $this->transaction = clone $stream; 27 | } 28 | 29 | public function seek(int $offset): void 30 | { 31 | $this->transaction->seek($offset); 32 | } 33 | 34 | public function begin(): Transaction 35 | { 36 | return new Transaction($this->transaction); 37 | } 38 | 39 | public function commit(): void 40 | { 41 | $this->stream->seek($this->tell()); 42 | } 43 | 44 | public function current(): string 45 | { 46 | return $this->transaction->current(); 47 | } 48 | 49 | public function consume(): Slice 50 | { 51 | return $this->transaction->consume(); 52 | } 53 | 54 | public function tell(): int 55 | { 56 | return $this->transaction->tell(); 57 | } 58 | 59 | public function eos(): bool 60 | { 61 | return $this->transaction->eos(); 62 | } 63 | 64 | public function cut(int $offset, ?int $length = null): string 65 | { 66 | return $this->transaction->cut($offset, $length); 67 | } 68 | 69 | public function position(): array 70 | { 71 | return $this->transaction->position(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/mappers.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Mappers; 14 | 15 | use InvalidArgumentException; 16 | use jubianchi\PPC\Mapper; 17 | use jubianchi\PPC\Parser\Result; 18 | use jubianchi\PPC\Parser\Result\Skip; 19 | use jubianchi\PPC\Parser\Result\Success; 20 | use jubianchi\PPC\Slice; 21 | 22 | /** 23 | * @param mixed $default 24 | */ 25 | function otherwise($default): Mapper 26 | { 27 | return new Mapper(fn (Result $result): Result => null !== $result->result() ? $result : new Success($default)); 28 | } 29 | 30 | function concat(): Mapper 31 | { 32 | $reduce = fn (Slice ...$slices): string => array_reduce( 33 | $slices, 34 | fn ($prev, $current): string => $prev.$current, 35 | '' 36 | ); 37 | 38 | return new Mapper(fn (Result $result): Result => new Success( 39 | $reduce(...($result->result() ?? [])) 40 | )); 41 | } 42 | 43 | /** 44 | * @param array $mappings 45 | */ 46 | function structure(array $mappings): Mapper 47 | { 48 | return new Mapper(fn (Result $result): Result => new Success( 49 | (array) array_reduce( 50 | array_keys($mappings), 51 | fn ($prev, $current) => $prev + [$mappings[$current] => $result->result()[$current]], 52 | [] 53 | ) 54 | )); 55 | } 56 | 57 | /** 58 | * @throws InvalidArgumentException 59 | */ 60 | function php(string $name): Mapper 61 | { 62 | if (!is_callable($name)) { 63 | throw new InvalidArgumentException(sprintf('"%s" is not callable', $name)); 64 | } 65 | 66 | return new Mapper(fn (Result $result) => new Success($name($result->result()))); 67 | } 68 | 69 | function skip(): Mapper 70 | { 71 | return new Mapper(fn (Result $result) => new Skip($result->result())); 72 | } 73 | 74 | function nth(int $nth): Mapper 75 | { 76 | return new Mapper(fn (Result $result): Result => new Success($result->result()[$nth])); 77 | } 78 | 79 | function first(): Mapper 80 | { 81 | return nth(0); 82 | } 83 | 84 | function last(): Mapper 85 | { 86 | return new Mapper(fn (Result $result): Result => nth(count($result->result()) - 1)($result)); 87 | } 88 | 89 | /** 90 | * @param mixed $value 91 | */ 92 | function value($value): Mapper 93 | { 94 | return new Mapper(fn (): Result => new Success($value)); 95 | } 96 | -------------------------------------------------------------------------------- /src/parsers.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace jubianchi\PPC\Parsers; 14 | 15 | use jubianchi\PPC\Parser; 16 | use jubianchi\PPC\Parser\Result; 17 | use jubianchi\PPC\Parser\Result\Failure; 18 | use jubianchi\PPC\Parser\Result\Success; 19 | use jubianchi\PPC\Slice; 20 | use jubianchi\PPC\Stream; 21 | use OutOfBoundsException; 22 | 23 | function char(string $char): Parser 24 | { 25 | $format = fn (string $char): string => str_replace(["\r", "\n", "\t"], ['\r', '\n', '\t'], $char); 26 | 27 | return (new Parser('char', function (Stream $stream, string $label) use ($char, $format): Result { 28 | if ($stream->eos()) { 29 | return Failure::create( 30 | $label, 31 | $char, 32 | Stream::EOS, 33 | $stream, 34 | ); 35 | } 36 | 37 | $current = $stream->current(); 38 | 39 | if ($current !== $char) { 40 | return Failure::create( 41 | $label, 42 | $format($char), 43 | $current, 44 | $stream, 45 | ); 46 | } 47 | 48 | return new Success($stream->consume()); 49 | })) 50 | ->stringify(fn (string $label): string => sprintf('%s(%s)', $label, $format($char))); 51 | } 52 | 53 | function regex(string $pattern): Parser 54 | { 55 | return (new Parser('regex', function (Stream $stream, string $label) use ($pattern): Result { 56 | if ($stream->eos()) { 57 | return Failure::create( 58 | $label, 59 | $pattern, 60 | Stream::EOS, 61 | $stream, 62 | ); 63 | } 64 | 65 | $current = $stream->current(); 66 | 67 | if (0 === preg_match($pattern, $current)) { 68 | return Failure::create( 69 | $label, 70 | $pattern, 71 | $current, 72 | $stream, 73 | ); 74 | } 75 | 76 | return new Success($stream->consume()); 77 | })) 78 | ->stringify(fn (string $label): string => sprintf('%s(%s)', $label, $pattern)); 79 | } 80 | 81 | function word(string $word): Parser 82 | { 83 | $format = fn (string $char): string => str_replace(["\r", "\n", "\t"], ['\r', '\n', '\t'], $char); 84 | 85 | return (new Parser('word', function (Stream $stream, string $label) use ($word, $format): Result { 86 | if ($stream->eos()) { 87 | return Failure::create( 88 | $label, 89 | $format($word), 90 | Stream::EOS, 91 | $stream, 92 | ); 93 | } 94 | 95 | $length = mb_strlen($word); 96 | 97 | try { 98 | $actual = $stream->cut($stream->tell(), $length); 99 | } catch (OutOfBoundsException $exception) { 100 | return Failure::create( 101 | $label, 102 | $word, 103 | $stream->cut($stream->tell()).' . '.Stream::EOS, 104 | $stream, 105 | ); 106 | } 107 | 108 | if ($actual !== $word) { 109 | return Failure::create( 110 | $label, 111 | $format($word), 112 | $actual, 113 | $stream, 114 | ); 115 | } 116 | 117 | $slice = new Slice($stream, $stream->tell(), $length); 118 | 119 | $stream->seek($stream->tell() + $length); 120 | 121 | return new Success($slice); 122 | })) 123 | ->stringify(fn (string $label): string => sprintf('%s(%s)', $label, $format($word))); 124 | } 125 | 126 | function any(): Parser 127 | { 128 | return new Parser('any', function (Stream $stream, string $label): Result { 129 | if ($stream->eos()) { 130 | return Failure::create( 131 | $label, 132 | 'any', 133 | Stream::EOS, 134 | $stream, 135 | ); 136 | } 137 | 138 | return new Success($stream->consume()); 139 | }); 140 | } 141 | 142 | function eos(): Parser 143 | { 144 | return new Parser('eos', function (Stream $stream, string $label): Result { 145 | if (!$stream->eos()) { 146 | return Failure::create( 147 | $label, 148 | Stream::EOS, 149 | $stream->current(), 150 | $stream, 151 | ); 152 | } 153 | 154 | return new Success(null); 155 | }); 156 | } 157 | --------------------------------------------------------------------------------