├── .eslintrc.json ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .markdownlint-cli2.jsonc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── docs.css ├── deno ├── LICENSE ├── README.md ├── char.ts ├── core.ts ├── mod.ts └── util.ts ├── docs ├── .nojekyll ├── assets │ ├── custom.css │ ├── highlight.css │ ├── main.js │ ├── search.js │ └── style.css ├── functions │ ├── char.char.html │ ├── char.charTest.html │ ├── char.concat.html │ ├── char.match.html │ ├── char.noneOf.html │ ├── char.oneOf.html │ ├── char.parse.html │ ├── char.parserPosition.html │ ├── char.str.html │ ├── char.tryParse.html │ ├── core.ab.html │ ├── core.abc.html │ ├── core.action.html │ ├── core.ahead.html │ ├── core.all.html │ ├── core.any.html │ ├── core.chain.html │ ├── core.chainReduce.html │ ├── core.choice.html │ ├── core.condition.html │ ├── core.decide.html │ ├── core.emit.html │ ├── core.end.html │ ├── core.error.html │ ├── core.fail.html │ ├── core.flatten.html │ ├── core.flatten1.html │ ├── core.left.html │ ├── core.leftAssoc1.html │ ├── core.leftAssoc2.html │ ├── core.longest.html │ ├── core.make.html │ ├── core.many.html │ ├── core.many1.html │ ├── core.map.html │ ├── core.map1.html │ ├── core.match-1.html │ ├── core.middle.html │ ├── core.not.html │ ├── core.option.html │ ├── core.otherwise.html │ ├── core.parse.html │ ├── core.parserPosition.html │ ├── core.peek.html │ ├── core.recursive.html │ ├── core.reduceLeft.html │ ├── core.reduceRight.html │ ├── core.remainingTokensNumber.html │ ├── core.right.html │ ├── core.rightAssoc1.html │ ├── core.rightAssoc2.html │ ├── core.satisfy.html │ ├── core.sepBy.html │ ├── core.sepBy1.html │ ├── core.skip.html │ ├── core.start.html │ ├── core.takeUntil.html │ ├── core.takeUntilP.html │ ├── core.takeWhile.html │ ├── core.takeWhileP.html │ ├── core.token.html │ └── core.tryParse.html ├── index.html ├── modules.html ├── modules │ ├── char.html │ └── core.html └── types │ ├── core.Data.html │ ├── core.Match.html │ ├── core.Matcher.html │ ├── core.NonMatch.html │ ├── core.Parser.html │ └── core.Result.html ├── examples ├── bf1.ts ├── bf2.ts ├── calc.ts ├── csv.ts ├── hexColor.ts ├── json.ts └── nonDec.ts ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── char.ts ├── core.ts └── util.ts ├── test-types └── core.ts ├── test ├── char.ts ├── core.ts ├── examples.ts └── snapshots │ ├── examples.ts.md │ └── examples.ts.snap ├── tsconfig.eslint.json ├── tsconfig.json ├── tsconfig.tsc.json ├── tsdoc.json └── typedoc.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended" 8 | ], 9 | "ignorePatterns": [ 10 | ".vscode", 11 | "coverage", 12 | "deno", 13 | "docs", 14 | "lib", 15 | "node_modules", 16 | "__*" 17 | ], 18 | "parserOptions": { 19 | "sourceType": "module", 20 | "ecmaVersion": 2020 21 | }, 22 | "rules": { 23 | "eol-last": "error", 24 | "indent": ["error", 2, { "SwitchCase": 1 }], 25 | "no-trailing-spaces": "error", 26 | "no-var": "error", 27 | "no-warning-comments": "warn", 28 | "prefer-const": "error", 29 | "quote-props": ["error", "consistent"], 30 | "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], 31 | "semi": ["error", "always"] 32 | }, 33 | "overrides": [ 34 | { 35 | "files": ["**/*.ts"], 36 | "parser": "@typescript-eslint/parser", 37 | "parserOptions": { 38 | "project": ["./tsconfig.eslint.json"], 39 | "sourceType": "module", 40 | "ecmaVersion": 2020 41 | }, 42 | "extends": [ 43 | "eslint:recommended", 44 | "plugin:@typescript-eslint/recommended", 45 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 46 | ], 47 | "plugins": [ 48 | "@typescript-eslint", 49 | "eslint-plugin-tsdoc" 50 | ], 51 | "rules": { 52 | "object-curly-newline": ["error", { "ObjectExpression": { "minProperties": 3 } }], 53 | "tsdoc/syntax": "error", 54 | "indent": "off", 55 | "@typescript-eslint/indent": ["error", 2, { "SwitchCase": 1 }] 56 | } 57 | }, 58 | { 59 | "files": ["*.json", "*.json5", "*.jsonc"], 60 | "parser": "jsonc-eslint-parser", 61 | "plugins": [ 62 | "jsonc" 63 | ], 64 | "extends": [ 65 | "eslint:recommended", 66 | "plugin:jsonc/auto-config", 67 | "plugin:jsonc/recommended-with-jsonc" 68 | ] 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: killymxi -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "monthly" 11 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | name: Run linters 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Use Node.js 14.x 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 14.x 18 | cache: 'npm' 19 | 20 | - run: npm ci 21 | 22 | - run: npm run lint 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Run tests 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.x, 16.x] 12 | os: [ubuntu-latest, windows-latest] 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | 25 | - run: npm ci 26 | 27 | - run: npm test 28 | 29 | - name: Generate coverage report 30 | if: ${{ matrix.os == 'ubuntu-latest' && matrix.node-version == '16.x' }} 31 | run: npm run cover 32 | 33 | - name: Upload coverage report to Codecov 34 | if: ${{ matrix.os == 'ubuntu-latest' && matrix.node-version == '16.x' }} 35 | uses: codecov/codecov-action@v3 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | file: ./coverage/lcov.info 39 | flags: unit,${{ matrix.node-version }},${{ matrix.os }} 40 | fail_ci_if_error: true 41 | verbose: true 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /lib 3 | /node_modules 4 | __* 5 | -------------------------------------------------------------------------------- /.markdownlint-cli2.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "default": true, 4 | "emphasis-style": false, 5 | "first-line-heading": false, 6 | "line-length": false, 7 | "no-duplicate-header": { "allow_different_nesting": true }, 8 | "no-inline-html": { "allowed_elements": ["details", "summary", "div"] }, 9 | "no-multiple-blanks": { "maximum": 2 } 10 | }, 11 | "globs": [ "**/*.md" ], 12 | "ignores": [ "node_modules" ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.detectIndentation": false, 4 | "files.exclude": { 5 | "deno/": true, 6 | "node_modules/": true, 7 | "package-lock.json":true 8 | }, 9 | "eslint.validate": [ 10 | "javascript", 11 | "javascriptreact", 12 | "typescript", 13 | "typescriptreact", 14 | "json", 15 | "jsonc", 16 | "json5" 17 | ] 18 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 0.9.0 4 | 5 | - many functions got overloads for `Matcher` type propagation in less common scenarios; 6 | - `condition` function now accepts Parsers/Matchers with different value types, result value type is the union of the two; 7 | - added type tests for overloads using [expect-type](https://github.com/mmkal/expect-type). 8 | 9 | ## Version 0.8.0 10 | 11 | - Targeting Node.js version 14 and ES2020; 12 | - Now should be discoverable with [denoify](https://github.com/garronej/denoify). 13 | 14 | ## Version 0.7.0 15 | 16 | - `otherwise` function now has two overloads - `Parser * Matcher -> Matcher` and `Parser * Parser -> Parser`; 17 | - `otherwise` function now accepts Parsers/Matchers with different value types, result value type is the union of the two; 18 | - `otherwise` function now has an alias called `eitherOr` which might be more natural for combining parsers. 19 | 20 | ## Version 0.6.0 21 | 22 | - ensure local imports have file extensions - fix "./core module cannot be found" issue. 23 | 24 | ## Version 0.5.4 25 | 26 | - remove terser, source-map files; 27 | - use only `rollup-plugin-cleanup` to condition published files. 28 | 29 | ## Version 0.5.3 30 | 31 | - source-map files; 32 | - minor documentation update. 33 | 34 | ## Version 0.5.2 35 | 36 | - `peek` function keeps Parser/Matcher distinction; 37 | 38 | ## Version 0.5.1 39 | 40 | - documentation updates; 41 | - package marked as free of side effects for tree shaking. 42 | 43 | ## Version 0.5.0 44 | 45 | - Initial release; 46 | - Aiming at Node.js version 12 and up. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 KillyMXI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # peberminta 2 | 3 | ![lint status badge](https://github.com/mxxii/peberminta/workflows/lint/badge.svg) 4 | ![test status badge](https://github.com/mxxii/peberminta/workflows/test/badge.svg) 5 | [![codecov](https://codecov.io/gh/mxxii/peberminta/branch/main/graph/badge.svg?token=TYwVNcTQJd)](https://codecov.io/gh/mxxii/peberminta) 6 | [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/mxxii/peberminta/blob/main/LICENSE) 7 | [![npm](https://img.shields.io/npm/v/peberminta?logo=npm)](https://www.npmjs.com/package/peberminta) 8 | [![npm](https://img.shields.io/npm/dw/peberminta?color=informational&logo=npm)](https://www.npmjs.com/package/peberminta) 9 | [![deno](https://img.shields.io/badge/deno.land%2Fx%2F-peberminta-informational?logo=deno)](https://deno.land/x/peberminta) 10 | 11 | Simple, transparent parser combinators toolkit that supports tokens of any type. 12 | 13 | For when you wanna do weird things with parsers. 14 | 15 | 16 | ## Features 17 | 18 | - **Well typed** - written in TypeScript and with a lot of attention to keep types well defined. 19 | 20 | - **Highly generic** - no constraints on tokens, options (additional state data) and output types. Core module has not a single mention of strings as a part of normal flow. Some string-specific building blocks can be loaded from a separate module in case you need them. 21 | 22 | - **Transparent**. Built on a very simple base idea - just a few type aliases. Whole parser state is accessible at any time. 23 | 24 | - **Lightweight**. Zero dependencies. Just type aliases and functions. 25 | 26 | - **Batteries included** - comes with a pretty big set of building blocks. 27 | 28 | - **Easy to extend** - just follow the convention defined by type aliases when making your own building blocks. *(And maybe let me know what you think can be universally useful to be included in the package itself.)* 29 | 30 | - **Easy to make configurable parsers**. Rather than dynamically composing parsers based on options or manually weaving options into a dynamic parser state, this package offers a standard way to treat options as a part of static data and access them at any moment for course correction. 31 | 32 | - **Well tested** - comes with tests for everything including examples. 33 | 34 | - **Practicality over "purity"**. To be understandable and self-consistent is more important than to follow an established encoding of abstract ideas. More on this below. 35 | 36 | - **No streaming** - accepts a fixed array of tokens. It is simple, whole input can be accessed at any time if needed. More on this below. 37 | 38 | - **Bring your own lexer/tokenizer** - if you need it. It doesn't matter how tokens are made - this package can consume anything you can type. I have a lexer as well, called [leac](https://github.com/mxxii/leac), and it is used in some examples, but there is nothing special about it to make it the best match (well, maybe the fact it is written in TypeScript, has equal level of maintenance and is made with arrays instead of iterators in mind as well). 39 | 40 | 41 | ## Changelog 42 | 43 | Available here: [CHANGELOG.md](https://github.com/mxxii/peberminta/blob/main/CHANGELOG.md) 44 | 45 | 46 | ## Install 47 | 48 | ### Node 49 | 50 | ```shell 51 | > npm i peberminta 52 | ``` 53 | 54 | ```ts 55 | import * as p from 'peberminta'; 56 | import * as pc from 'peberminta/char'; 57 | ``` 58 | 59 | ### Deno 60 | 61 | ```ts 62 | import * as p from 'https://deno.land/x/peberminta@.../core.ts'; 63 | import * as pc from 'https://deno.land/x/peberminta@.../char.ts'; 64 | ``` 65 | 66 | 67 | ## Examples 68 | 69 | - [JSON](https://github.com/mxxii/peberminta/blob/main/examples/json.ts); 70 | - [CSV](https://github.com/mxxii/peberminta/blob/main/examples/csv.ts); 71 | - [Hex Color](https://github.com/mxxii/peberminta/blob/main/examples/hexColor.ts); 72 | - [Calc](https://github.com/mxxii/peberminta/blob/main/examples/calc.ts); 73 | - [Brainfuck](https://github.com/mxxii/peberminta/blob/main/examples/bf1.ts) (and [another implementation](https://github.com/mxxii/peberminta/blob/main/examples/bf2.ts)); 74 | - [Non-decreasing sequences](https://github.com/mxxii/peberminta/blob/main/examples/nonDec.ts); 75 | - *feel free to PR or request interesting compact grammar examples.* 76 | 77 | ### Published packages using `peberminta` 78 | 79 | - [aspargvs](https://github.com/mxxii/aspargvs) - arg parser, CLI wrapper 80 | - [parseley](https://github.com/mxxii/parseley) - CSS selectors parser 81 | 82 | 83 | ## API 84 | 85 | Detailed API documentation with navigation and search: 86 | 87 | - [core module](https://mxxii.github.io/peberminta/modules/core.html); 88 | - [char module](https://mxxii.github.io/peberminta/modules/char.html). 89 | 90 | ### Convention 91 | 92 | Whole package is built around these type aliases: 93 | 94 | ```typescript 95 | export type Data = { 96 | tokens: TToken[], 97 | options: TOptions 98 | }; 99 | 100 | export type Parser = 101 | (data: Data, i: number) => Result; 102 | 103 | export type Matcher = 104 | (data: Data, i: number) => Match; 105 | 106 | export type Result = Match | NonMatch; 107 | 108 | export type Match = { 109 | matched: true, 110 | position: number, 111 | value: TValue 112 | }; 113 | 114 | export type NonMatch = { 115 | matched: false 116 | }; 117 | ``` 118 | 119 | - **Data** object holds tokens array and possibly an options object - it's just a container for all static data used by a parser. Parser position, on the other hand, has it's own life cycle and passed around separately. 120 | 121 | - A **Parser** is a function that accepts Data object and a parser position, looks into the tokens array at the given position and returns either a Match with a parsed value (use `null` if there is no value) and a new position or a NonMatch. 122 | 123 | - A **Matcher** is a special case of Parser that never fails and always returns a Match. 124 | 125 | - **Result** object from a Parser can be either a Match or a NonMatch. 126 | 127 | - **Match** is a result of successful parsing - it contains a parsed value and a new parser position. 128 | 129 | - **NonMatch** is a result of unsuccessful parsing. It doesn't have any data attached to it. 130 | 131 | - **TToken** can be any type. 132 | 133 | - **TOptions** can be any type. Use it to make your parser customizable. Or set it as `undefined` and type as `unknown` if not needed. 134 | 135 | ### Building blocks 136 | 137 | #### Core blocks 138 | 139 |
140 | 141 | | | | | 142 | | -------- | -------- | -------- | -------- 143 | | [ab](https://mxxii.github.io/peberminta/modules/core.html#ab) | [abc](https://mxxii.github.io/peberminta/modules/core.html#abc) | [action](https://mxxii.github.io/peberminta/modules/core.html#action) | [ahead](https://mxxii.github.io/peberminta/modules/core.html#ahead) 144 | | [all](https://mxxii.github.io/peberminta/modules/core.html#all) | _[and](https://mxxii.github.io/peberminta/modules/core.html#and)_ | [any](https://mxxii.github.io/peberminta/modules/core.html#any) | [chain](https://mxxii.github.io/peberminta/modules/core.html#chain) 145 | | [chainReduce](https://mxxii.github.io/peberminta/modules/core.html#chainReduce) | [choice](https://mxxii.github.io/peberminta/modules/core.html#choice) | [condition](https://mxxii.github.io/peberminta/modules/core.html#condition) | [decide](https://mxxii.github.io/peberminta/modules/core.html#decide) 146 | | _[discard](https://mxxii.github.io/peberminta/modules/core.html#discard)_ | _[eitherOr](https://mxxii.github.io/peberminta/modules/core.html#eitherOr)_ | [emit](https://mxxii.github.io/peberminta/modules/core.html#emit) | [end](https://mxxii.github.io/peberminta/modules/core.html#end) 147 | | _[eof](https://mxxii.github.io/peberminta/modules/core.html#eof)_ | [error](https://mxxii.github.io/peberminta/modules/core.html#error) | [fail](https://mxxii.github.io/peberminta/modules/core.html#fail) | [flatten](https://mxxii.github.io/peberminta/modules/core.html#flatten) 148 | | [flatten1](https://mxxii.github.io/peberminta/modules/core.html#flatten1) | [left](https://mxxii.github.io/peberminta/modules/core.html#left) | [leftAssoc1](https://mxxii.github.io/peberminta/modules/core.html#leftAssoc1) | [leftAssoc2](https://mxxii.github.io/peberminta/modules/core.html#leftAssoc2) 149 | | [longest](https://mxxii.github.io/peberminta/modules/core.html#longest) | _[lookAhead](https://mxxii.github.io/peberminta/modules/core.html#lookAhead)_ | [make](https://mxxii.github.io/peberminta/modules/core.html#make) | [many](https://mxxii.github.io/peberminta/modules/core.html#many) 150 | | [many1](https://mxxii.github.io/peberminta/modules/core.html#many1) | [map](https://mxxii.github.io/peberminta/modules/core.html#map) | [map1](https://mxxii.github.io/peberminta/modules/core.html#map1) | [middle](https://mxxii.github.io/peberminta/modules/core.html#middle) 151 | | [not](https://mxxii.github.io/peberminta/modules/core.html#not) | _[of](https://mxxii.github.io/peberminta/modules/core.html#of)_ | [option](https://mxxii.github.io/peberminta/modules/core.html#option) | _[or](https://mxxii.github.io/peberminta/modules/core.html#or)_ 152 | | [otherwise](https://mxxii.github.io/peberminta/modules/core.html#otherwise) | [peek](https://mxxii.github.io/peberminta/modules/core.html#peek) | [recursive](https://mxxii.github.io/peberminta/modules/core.html#recursive) | [reduceLeft](https://mxxii.github.io/peberminta/modules/core.html#reduceLeft) 153 | | [reduceRight](https://mxxii.github.io/peberminta/modules/core.html#reduceRight) | [right](https://mxxii.github.io/peberminta/modules/core.html#right) | [rightAssoc1](https://mxxii.github.io/peberminta/modules/core.html#rightAssoc1) | [rightAssoc2](https://mxxii.github.io/peberminta/modules/core.html#rightAssoc2) 154 | | [satisfy](https://mxxii.github.io/peberminta/modules/core.html#satisfy) | [sepBy](https://mxxii.github.io/peberminta/modules/core.html#sepBy) | [sepBy1](https://mxxii.github.io/peberminta/modules/core.html#sepBy1) | [skip](https://mxxii.github.io/peberminta/modules/core.html#skip) 155 | | _[some](https://mxxii.github.io/peberminta/modules/core.html#some)_ | [start](https://mxxii.github.io/peberminta/modules/core.html#start) | [takeUntil](https://mxxii.github.io/peberminta/modules/core.html#takeUntil) | [takeUntilP](https://mxxii.github.io/peberminta/modules/core.html#takeUntilP) 156 | | [takeWhile](https://mxxii.github.io/peberminta/modules/core.html#takeWhile) | [takeWhileP](https://mxxii.github.io/peberminta/modules/core.html#takeWhileP) | [token](https://mxxii.github.io/peberminta/modules/core.html#token) 157 | 158 |
159 | 160 | #### Core utilities 161 | 162 |
163 | 164 | | | | | 165 | | -------- | -------- | -------- | -------- 166 | | [match](https://mxxii.github.io/peberminta/modules/core.html#match) | [parse](https://mxxii.github.io/peberminta/modules/core.html#parse) | [parserPosition](https://mxxii.github.io/peberminta/modules/core.html#parserPosition) | [remainingTokensNumber](https://mxxii.github.io/peberminta/modules/core.html#remainingTokensNumber) 167 | | [tryParse](https://mxxii.github.io/peberminta/modules/char.html#tryParse) 168 | 169 |
170 | 171 | #### Char blocks 172 | 173 |
174 | 175 | | | | | 176 | | -------- | -------- | -------- | -------- 177 | | _[anyOf](https://mxxii.github.io/peberminta/modules/char.html#anyOf)_ | [char](https://mxxii.github.io/peberminta/modules/char.html#char) | [charTest](https://mxxii.github.io/peberminta/modules/char.html#charTest) | [concat](https://mxxii.github.io/peberminta/modules/char.html#concat) 178 | | [noneOf](https://mxxii.github.io/peberminta/modules/char.html#noneOf) | [oneOf](https://mxxii.github.io/peberminta/modules/char.html#oneOf) | [str](https://mxxii.github.io/peberminta/modules/char.html#str) 179 | 180 |
181 | 182 | #### Char utilities 183 | 184 |
185 | 186 | | | | | 187 | | -------- | -------- | -------- | -------- 188 | | [match](https://mxxii.github.io/peberminta/modules/char.html#match) | [parse](https://mxxii.github.io/peberminta/modules/char.html#parse) | [parserPosition](https://mxxii.github.io/peberminta/modules/char.html#parserPosition) | [tryParse](https://mxxii.github.io/peberminta/modules/char.html#tryParse) 189 | 190 |
191 | 192 | 193 | ## What about ...? 194 | 195 | - performance - The code is very simple but I won't put any unverified assumptions here. I'd be grateful to anyone who can set up a good benchmark project to compare different parser combinators. 196 | 197 | - stable release - Current release is well thought out and tested. I leave a chance that some supplied functions may need an incompatible change. Before version 1.0.0 this will be done without a deprecation cycle. 198 | 199 | - streams/iterators - Maybe some day, if the need to parse a stream of non-string data arise. For now I don't have a task that would force me to think well on how to design it. It would require a significant trade off and may end up being a separate module (like `char`) at best or even a separate package. 200 | 201 | - Fantasy Land - You can find some familiar ideas here, especially when compared to Static Land. But I'm not concerned about compatibility with that spec - see "Practicality over "purity"" entry above. What I think might make sense is to add separate tests for laws applicable in context of this package. Low priority though. 202 | 203 | 204 | ## Some other parser combinator packages 205 | 206 | - [arcsecond](https://github.com/francisrstokes/arcsecond); 207 | - [parsimmon](https://github.com/jneen/parsimmon); 208 | - [chevrotain](https://github.com/Chevrotain/chevrotain); 209 | - [prsc.js](https://github.com/bwrrp/prsc.js); 210 | - [lop](https://github.com/mwilliamson/lop); 211 | - [parser-lang](https://github.com/disnet/parser-lang); 212 | - *and more, with varied level of maintenance.* 213 | -------------------------------------------------------------------------------- /assets/docs.css: -------------------------------------------------------------------------------- 1 | /* CSS to customize Typedoc's default theme. */ 2 | 3 | :root { 4 | --dark-color-background-secondary: #1B2B34; 5 | --dark-color-background: #132026; 6 | --dark-color-icon-background: var(--dark-color-background); 7 | --dark-color-accent: #ffffff33; 8 | --dark-color-text: #ffffffbb; 9 | --dark-color-text-aside: #778692; 10 | --dark-color-link: #6699cc; 11 | --dark-color-ts: #c594c5; 12 | --dark-color-ts-interface: #99c794; 13 | --dark-color-ts-enum: #f99157; 14 | --dark-color-ts-class: #fac863; 15 | --dark-color-ts-function: #9772ff; 16 | --dark-color-ts-namespace: #e14dff; 17 | --dark-color-ts-private: #cdd3de; 18 | --dark-color-ts-variable: #4d68ff; 19 | --dark-code-background: #0c1418; 20 | 21 | --dark-color-tag: #5fb3b3; 22 | --dark-color-tag-text: #132026; 23 | } 24 | 25 | /* Extra space between overloads */ 26 | 27 | .tsd-signature ~ .tsd-signature { 28 | margin-top: 4em; 29 | } 30 | 31 | /* Monospace parameter names */ 32 | 33 | .tsd-parameter-list > li > h5 { 34 | font-weight: normal; 35 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 36 | } 37 | 38 | /* Enclose whole parameter signatures rather than their type */ 39 | 40 | .tsd-parameter-signature { 41 | border: 1px solid var(--color-accent); 42 | list-style: none; 43 | margin-inline-start: -40px; 44 | } 45 | 46 | .tsd-parameter-signature > ul > .tsd-signature { 47 | border: none; 48 | padding: 1rem 0.5rem 0.5rem 0.5rem; 49 | } 50 | 51 | .tsd-parameter-signature > ul > .tsd-description { 52 | margin: 1em; 53 | } 54 | 55 | /* Type parameters have no additional information on them */ 56 | 57 | .tsd-type-parameters-title, 58 | .tsd-type-parameters { 59 | display: none; 60 | } 61 | 62 | /* Parameter flags - rest, optional */ 63 | 64 | @media (prefers-color-scheme: dark) { 65 | .tsd-tag { 66 | border-radius: 4px; 67 | color: var(--dark-color-tag-text); 68 | background-color: var(--dark-color-tag); 69 | } 70 | } 71 | 72 | /* Signature color highlighting */ 73 | 74 | .tsd-signature-type { 75 | color: var(--color-ts-class); 76 | } 77 | 78 | .tsd-signature-type[data-tsd-kind] { 79 | color: var(--color-ts); 80 | } 81 | 82 | .tsd-signature-type[data-tsd-kind="Type parameter"] { 83 | color: var(--color-ts-interface); 84 | } 85 | 86 | /* Search */ 87 | 88 | #tsd-search.has-focus { 89 | background-color: var(--code-background); 90 | } 91 | 92 | #tsd-search > .results { 93 | background-color: var(--color-background-secondary); 94 | } 95 | 96 | /* Unnecessary settings */ 97 | 98 | .tsd-filter-visibility { 99 | display: none; 100 | } 101 | 102 | /* Current item */ 103 | 104 | .tsd-navigation.secondary li.current > a { 105 | font-weight: bold; 106 | } 107 | 108 | .tsd-navigation.secondary li.current > a::after { 109 | content: "\00a0←"; 110 | } 111 | 112 | /* Tables */ 113 | 114 | .headlessTable > table > thead { 115 | display: none; 116 | } 117 | 118 | .tsd-panel table { 119 | width: 100%; 120 | margin: 10px 0; 121 | border-collapse: collapse; 122 | } 123 | 124 | .tsd-panel table th, 125 | .tsd-panel table td { 126 | padding: 6px 13px; 127 | border: 1px solid var(--color-accent); 128 | } 129 | 130 | /* Code blocks don't need extra decorations */ 131 | 132 | code, pre { 133 | border-radius: 0; 134 | } 135 | 136 | pre { 137 | border: none; 138 | } 139 | -------------------------------------------------------------------------------- /deno/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 KillyMXI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /deno/README.md: -------------------------------------------------------------------------------- 1 | # peberminta 2 | 3 | ![lint status badge](https://github.com/mxxii/peberminta/workflows/lint/badge.svg) 4 | ![test status badge](https://github.com/mxxii/peberminta/workflows/test/badge.svg) 5 | [![codecov](https://codecov.io/gh/mxxii/peberminta/branch/main/graph/badge.svg?token=TYwVNcTQJd)](https://codecov.io/gh/mxxii/peberminta) 6 | [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/mxxii/peberminta/blob/main/LICENSE) 7 | [![npm](https://img.shields.io/npm/v/peberminta?logo=npm)](https://www.npmjs.com/package/peberminta) 8 | [![npm](https://img.shields.io/npm/dw/peberminta?color=informational&logo=npm)](https://www.npmjs.com/package/peberminta) 9 | [![deno](https://img.shields.io/badge/deno.land%2Fx%2F-peberminta-informational?logo=deno)](https://deno.land/x/peberminta) 10 | 11 | Simple, transparent parser combinators toolkit that supports tokens of any type. 12 | 13 | For when you wanna do weird things with parsers. 14 | 15 | 16 | ## Features 17 | 18 | - **Well typed** - written in TypeScript and with a lot of attention to keep types well defined. 19 | 20 | - **Highly generic** - no constraints on tokens, options (additional state data) and output types. Core module has not a single mention of strings as a part of normal flow. Some string-specific building blocks can be loaded from a separate module in case you need them. 21 | 22 | - **Transparent**. Built on a very simple base idea - just a few type aliases. Whole parser state is accessible at any time. 23 | 24 | - **Lightweight**. Zero dependencies. Just type aliases and functions. 25 | 26 | - **Batteries included** - comes with a pretty big set of building blocks. 27 | 28 | - **Easy to extend** - just follow the convention defined by type aliases when making your own building blocks. *(And maybe let me know what you think can be universally useful to be included in the package itself.)* 29 | 30 | - **Easy to make configurable parsers**. Rather than dynamically composing parsers based on options or manually weaving options into a dynamic parser state, this package offers a standard way to treat options as a part of static data and access them at any moment for course correction. 31 | 32 | - **Well tested** - comes with tests for everything including examples. 33 | 34 | - **Practicality over "purity"**. To be understandable and self-consistent is more important than to follow an established encoding of abstract ideas. More on this below. 35 | 36 | - **No streaming** - accepts a fixed array of tokens. It is simple, whole input can be accessed at any time if needed. More on this below. 37 | 38 | - **Bring your own lexer/tokenizer** - if you need it. It doesn't matter how tokens are made - this package can consume anything you can type. I have a lexer as well, called [leac](https://github.com/mxxii/leac), and it is used in some examples, but there is nothing special about it to make it the best match (well, maybe the fact it is written in TypeScript, has equal level of maintenance and is made with arrays instead of iterators in mind as well). 39 | 40 | 41 | ## Changelog 42 | 43 | Available here: [CHANGELOG.md](https://github.com/mxxii/peberminta/blob/main/CHANGELOG.md) 44 | 45 | 46 | ## Install 47 | 48 | ### Node 49 | 50 | ```shell 51 | > npm i peberminta 52 | ``` 53 | 54 | ```ts 55 | import * as p from 'peberminta'; 56 | import * as pc from 'peberminta/char'; 57 | ``` 58 | 59 | ### Deno 60 | 61 | ```ts 62 | import * as p from 'https://deno.land/x/peberminta@.../core.ts'; 63 | import * as pc from 'https://deno.land/x/peberminta@.../char.ts'; 64 | ``` 65 | 66 | 67 | ## Examples 68 | 69 | - [JSON](https://github.com/mxxii/peberminta/blob/main/examples/json.ts); 70 | - [CSV](https://github.com/mxxii/peberminta/blob/main/examples/csv.ts); 71 | - [Hex Color](https://github.com/mxxii/peberminta/blob/main/examples/hexColor.ts); 72 | - [Calc](https://github.com/mxxii/peberminta/blob/main/examples/calc.ts); 73 | - [Brainfuck](https://github.com/mxxii/peberminta/blob/main/examples/bf1.ts) (and [another implementation](https://github.com/mxxii/peberminta/blob/main/examples/bf2.ts)); 74 | - [Non-decreasing sequences](https://github.com/mxxii/peberminta/blob/main/examples/nonDec.ts); 75 | - *feel free to PR or request interesting compact grammar examples.* 76 | 77 | ### Published packages using `peberminta` 78 | 79 | - [aspargvs](https://github.com/mxxii/aspargvs) - arg parser, CLI wrapper 80 | - [parseley](https://github.com/mxxii/parseley) - CSS selectors parser 81 | 82 | 83 | ## API 84 | 85 | Detailed API documentation with navigation and search: 86 | 87 | - [core module](https://mxxii.github.io/peberminta/modules/core.html); 88 | - [char module](https://mxxii.github.io/peberminta/modules/char.html). 89 | 90 | ### Convention 91 | 92 | Whole package is built around these type aliases: 93 | 94 | ```typescript 95 | export type Data = { 96 | tokens: TToken[], 97 | options: TOptions 98 | }; 99 | 100 | export type Parser = 101 | (data: Data, i: number) => Result; 102 | 103 | export type Matcher = 104 | (data: Data, i: number) => Match; 105 | 106 | export type Result = Match | NonMatch; 107 | 108 | export type Match = { 109 | matched: true, 110 | position: number, 111 | value: TValue 112 | }; 113 | 114 | export type NonMatch = { 115 | matched: false 116 | }; 117 | ``` 118 | 119 | - **Data** object holds tokens array and possibly an options object - it's just a container for all static data used by a parser. Parser position, on the other hand, has it's own life cycle and passed around separately. 120 | 121 | - A **Parser** is a function that accepts Data object and a parser position, looks into the tokens array at the given position and returns either a Match with a parsed value (use `null` if there is no value) and a new position or a NonMatch. 122 | 123 | - A **Matcher** is a special case of Parser that never fails and always returns a Match. 124 | 125 | - **Result** object from a Parser can be either a Match or a NonMatch. 126 | 127 | - **Match** is a result of successful parsing - it contains a parsed value and a new parser position. 128 | 129 | - **NonMatch** is a result of unsuccessful parsing. It doesn't have any data attached to it. 130 | 131 | - **TToken** can be any type. 132 | 133 | - **TOptions** can be any type. Use it to make your parser customizable. Or set it as `undefined` and type as `unknown` if not needed. 134 | 135 | ### Building blocks 136 | 137 | #### Core blocks 138 | 139 |
140 | 141 | | | | | 142 | | -------- | -------- | -------- | -------- 143 | | [ab](https://mxxii.github.io/peberminta/modules/core.html#ab) | [abc](https://mxxii.github.io/peberminta/modules/core.html#abc) | [action](https://mxxii.github.io/peberminta/modules/core.html#action) | [ahead](https://mxxii.github.io/peberminta/modules/core.html#ahead) 144 | | [all](https://mxxii.github.io/peberminta/modules/core.html#all) | _[and](https://mxxii.github.io/peberminta/modules/core.html#and)_ | [any](https://mxxii.github.io/peberminta/modules/core.html#any) | [chain](https://mxxii.github.io/peberminta/modules/core.html#chain) 145 | | [chainReduce](https://mxxii.github.io/peberminta/modules/core.html#chainReduce) | [choice](https://mxxii.github.io/peberminta/modules/core.html#choice) | [condition](https://mxxii.github.io/peberminta/modules/core.html#condition) | [decide](https://mxxii.github.io/peberminta/modules/core.html#decide) 146 | | _[discard](https://mxxii.github.io/peberminta/modules/core.html#discard)_ | _[eitherOr](https://mxxii.github.io/peberminta/modules/core.html#eitherOr)_ | [emit](https://mxxii.github.io/peberminta/modules/core.html#emit) | [end](https://mxxii.github.io/peberminta/modules/core.html#end) 147 | | _[eof](https://mxxii.github.io/peberminta/modules/core.html#eof)_ | [error](https://mxxii.github.io/peberminta/modules/core.html#error) | [fail](https://mxxii.github.io/peberminta/modules/core.html#fail) | [flatten](https://mxxii.github.io/peberminta/modules/core.html#flatten) 148 | | [flatten1](https://mxxii.github.io/peberminta/modules/core.html#flatten1) | [left](https://mxxii.github.io/peberminta/modules/core.html#left) | [leftAssoc1](https://mxxii.github.io/peberminta/modules/core.html#leftAssoc1) | [leftAssoc2](https://mxxii.github.io/peberminta/modules/core.html#leftAssoc2) 149 | | [longest](https://mxxii.github.io/peberminta/modules/core.html#longest) | _[lookAhead](https://mxxii.github.io/peberminta/modules/core.html#lookAhead)_ | [make](https://mxxii.github.io/peberminta/modules/core.html#make) | [many](https://mxxii.github.io/peberminta/modules/core.html#many) 150 | | [many1](https://mxxii.github.io/peberminta/modules/core.html#many1) | [map](https://mxxii.github.io/peberminta/modules/core.html#map) | [map1](https://mxxii.github.io/peberminta/modules/core.html#map1) | [middle](https://mxxii.github.io/peberminta/modules/core.html#middle) 151 | | [not](https://mxxii.github.io/peberminta/modules/core.html#not) | _[of](https://mxxii.github.io/peberminta/modules/core.html#of)_ | [option](https://mxxii.github.io/peberminta/modules/core.html#option) | _[or](https://mxxii.github.io/peberminta/modules/core.html#or)_ 152 | | [otherwise](https://mxxii.github.io/peberminta/modules/core.html#otherwise) | [peek](https://mxxii.github.io/peberminta/modules/core.html#peek) | [recursive](https://mxxii.github.io/peberminta/modules/core.html#recursive) | [reduceLeft](https://mxxii.github.io/peberminta/modules/core.html#reduceLeft) 153 | | [reduceRight](https://mxxii.github.io/peberminta/modules/core.html#reduceRight) | [right](https://mxxii.github.io/peberminta/modules/core.html#right) | [rightAssoc1](https://mxxii.github.io/peberminta/modules/core.html#rightAssoc1) | [rightAssoc2](https://mxxii.github.io/peberminta/modules/core.html#rightAssoc2) 154 | | [satisfy](https://mxxii.github.io/peberminta/modules/core.html#satisfy) | [sepBy](https://mxxii.github.io/peberminta/modules/core.html#sepBy) | [sepBy1](https://mxxii.github.io/peberminta/modules/core.html#sepBy1) | [skip](https://mxxii.github.io/peberminta/modules/core.html#skip) 155 | | _[some](https://mxxii.github.io/peberminta/modules/core.html#some)_ | [start](https://mxxii.github.io/peberminta/modules/core.html#start) | [takeUntil](https://mxxii.github.io/peberminta/modules/core.html#takeUntil) | [takeUntilP](https://mxxii.github.io/peberminta/modules/core.html#takeUntilP) 156 | | [takeWhile](https://mxxii.github.io/peberminta/modules/core.html#takeWhile) | [takeWhileP](https://mxxii.github.io/peberminta/modules/core.html#takeWhileP) | [token](https://mxxii.github.io/peberminta/modules/core.html#token) 157 | 158 |
159 | 160 | #### Core utilities 161 | 162 |
163 | 164 | | | | | 165 | | -------- | -------- | -------- | -------- 166 | | [match](https://mxxii.github.io/peberminta/modules/core.html#match) | [parse](https://mxxii.github.io/peberminta/modules/core.html#parse) | [parserPosition](https://mxxii.github.io/peberminta/modules/core.html#parserPosition) | [remainingTokensNumber](https://mxxii.github.io/peberminta/modules/core.html#remainingTokensNumber) 167 | | [tryParse](https://mxxii.github.io/peberminta/modules/char.html#tryParse) 168 | 169 |
170 | 171 | #### Char blocks 172 | 173 |
174 | 175 | | | | | 176 | | -------- | -------- | -------- | -------- 177 | | _[anyOf](https://mxxii.github.io/peberminta/modules/char.html#anyOf)_ | [char](https://mxxii.github.io/peberminta/modules/char.html#char) | [charTest](https://mxxii.github.io/peberminta/modules/char.html#charTest) | [concat](https://mxxii.github.io/peberminta/modules/char.html#concat) 178 | | [noneOf](https://mxxii.github.io/peberminta/modules/char.html#noneOf) | [oneOf](https://mxxii.github.io/peberminta/modules/char.html#oneOf) | [str](https://mxxii.github.io/peberminta/modules/char.html#str) 179 | 180 |
181 | 182 | #### Char utilities 183 | 184 |
185 | 186 | | | | | 187 | | -------- | -------- | -------- | -------- 188 | | [match](https://mxxii.github.io/peberminta/modules/char.html#match) | [parse](https://mxxii.github.io/peberminta/modules/char.html#parse) | [parserPosition](https://mxxii.github.io/peberminta/modules/char.html#parserPosition) | [tryParse](https://mxxii.github.io/peberminta/modules/char.html#tryParse) 189 | 190 |
191 | 192 | 193 | ## What about ...? 194 | 195 | - performance - The code is very simple but I won't put any unverified assumptions here. I'd be grateful to anyone who can set up a good benchmark project to compare different parser combinators. 196 | 197 | - stable release - Current release is well thought out and tested. I leave a chance that some supplied functions may need an incompatible change. Before version 1.0.0 this will be done without a deprecation cycle. 198 | 199 | - streams/iterators - Maybe some day, if the need to parse a stream of non-string data arise. For now I don't have a task that would force me to think well on how to design it. It would require a significant trade off and may end up being a separate module (like `char`) at best or even a separate package. 200 | 201 | - Fantasy Land - You can find some familiar ideas here, especially when compared to Static Land. But I'm not concerned about compatibility with that spec - see "Practicality over "purity"" entry above. What I think might make sense is to add separate tests for laws applicable in context of this package. Low priority though. 202 | 203 | 204 | ## Some other parser combinator packages 205 | 206 | - [arcsecond](https://github.com/francisrstokes/arcsecond); 207 | - [parsimmon](https://github.com/jneen/parsimmon); 208 | - [chevrotain](https://github.com/Chevrotain/chevrotain); 209 | - [prsc.js](https://github.com/bwrrp/prsc.js); 210 | - [lop](https://github.com/mwilliamson/lop); 211 | - [parser-lang](https://github.com/disnet/parser-lang); 212 | - *and more, with varied level of maintenance.* 213 | -------------------------------------------------------------------------------- /deno/char.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an additional module specifically for string parsers. 3 | * 4 | * It contains parsers with token type bound to be `string` 5 | * and expected to work with individual characters. 6 | * 7 | * It should work even if you have a custom way to split 8 | * a string into symbols such as graphemes. 9 | * 10 | * Node: 11 | * ```ts 12 | * import * as pc from 'peberminta/char'; 13 | * ``` 14 | * 15 | * Deno: 16 | * ```ts 17 | * import * as p from 'https://deno.land/x/peberminta@.../char.ts'; 18 | * ``` 19 | * 20 | * @packageDocumentation 21 | */ 22 | 23 | import { 24 | Parser, Matcher, Data, 25 | token, map, flatten, remainingTokensNumber, 26 | tryParse as tryParseCore, match as matchCore, 27 | parserPosition as parserPositionCore 28 | } from './core.ts'; 29 | import { clamp, escapeWhitespace } from './util.ts'; 30 | 31 | /** 32 | * Make a parser that looks for the exact match for a given character 33 | * and returns a match with that character. 34 | * 35 | * Tokens expected to be individual characters/graphemes. 36 | * 37 | * @param char - A character to look for. 38 | */ 39 | export function char ( 40 | char: string 41 | ): Parser { 42 | return token((c) => (c === char) ? c : undefined); 43 | } 44 | 45 | /** 46 | * Make a parser that matches and returns a character 47 | * if it is present in a given character samples string/array. 48 | * 49 | * Tokens expected to be individual characters/graphemes. 50 | * 51 | * @param chars - An array (or a string) of all acceptable characters. 52 | */ 53 | export function oneOf ( 54 | chars: string | string[] 55 | ): Parser { 56 | return token((c) => (chars.includes(c)) ? c : undefined); 57 | } 58 | 59 | export { oneOf as anyOf }; 60 | 61 | /** 62 | * Make a parser that matches and returns a character 63 | * if it is absent in a given character samples string/array. 64 | * 65 | * Tokens expected to be individual characters/graphemes. 66 | * 67 | * @param chars - An array (or a string) of all characters that are not acceptable. 68 | */ 69 | export function noneOf ( 70 | chars: string | string[] 71 | ): Parser { 72 | return token((c) => (chars.includes(c)) ? undefined : c); 73 | } 74 | 75 | /** 76 | * Make a parser that matches input character against given regular expression. 77 | * 78 | * Use this to match characters belonging to a certain range 79 | * or having a certain [unicode property](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes). 80 | * 81 | * Use `satisfy` from core module instead if you need a predicate. 82 | * 83 | * Tokens expected to be individual characters/graphemes. 84 | * 85 | * @param regex - Tester regular expression. 86 | */ 87 | export function charTest ( 88 | regex: RegExp 89 | ): Parser { 90 | return token((c) => regex.test(c) ? c : undefined); 91 | } 92 | 93 | /** 94 | * Make a parser that looks for the exact match for a given string, 95 | * returns a match with that string and consumes an according number of tokens. 96 | * 97 | * Empty string matches without consuming input. 98 | * 99 | * Tokens expected to be individual characters/graphemes. 100 | * 101 | * @param str - A string to look for. 102 | */ 103 | export function str ( 104 | str: string 105 | ): Parser { 106 | const len = str.length; 107 | return (data, i) => { 108 | // Keep working even if tokens are 109 | // not single chars but multi-char graphemes etc. 110 | // Can't just take a slice of len from tokens. 111 | const tokensNumber = remainingTokensNumber(data, i); 112 | let substr = ''; 113 | let j = 0; 114 | while (j < tokensNumber && substr.length < len) { 115 | substr += data.tokens[i+j]; 116 | j++; 117 | } 118 | return (substr === str) 119 | ? { 120 | matched: true, 121 | position: i + j, 122 | value: str 123 | } 124 | : { matched: false }; 125 | }; 126 | } 127 | 128 | /** 129 | * Make a parser that concatenates characters/strings 130 | * from all provided parsers into a single string. 131 | * 132 | * Nonmatch is returned if any of parsers didn't match. 133 | * 134 | * @param ps - Parsers sequence. 135 | * Each parser can return a string or an array of strings. 136 | */ 137 | export function concat ( 138 | ...ps: Parser[] 139 | ): Parser { 140 | return map( 141 | flatten(...ps), 142 | (vs) => vs.join('') 143 | ); 144 | } 145 | 146 | 147 | //------------------------------------------------------------ 148 | 149 | 150 | /** 151 | * Utility function to render a given parser position 152 | * for error reporting and debug purposes. 153 | * 154 | * This is a version specific for char parsers. 155 | * 156 | * Note: it will fall back to core version (one token per line) 157 | * in case any multicharacter tokens are present. 158 | * 159 | * @param data - Data object (tokens and options). 160 | * @param i - Parser position in the tokens array. 161 | * @param contextTokens - How many tokens (characters) around the current one to render. 162 | * @returns A multiline string. 163 | * 164 | * @category Utility functions 165 | */ 166 | export function parserPosition ( 167 | data: Data, 168 | i: number, 169 | contextTokens = 11 170 | ): string { 171 | const len = data.tokens.length; 172 | const lowIndex = clamp(0, i - contextTokens, len - contextTokens); 173 | const highIndex = clamp(contextTokens, i + 1 + contextTokens, len); 174 | const tokensSlice = data.tokens.slice(lowIndex, highIndex); 175 | if (tokensSlice.some((t) => t.length !== 1)) { 176 | return parserPositionCore(data, i, (t) => t); 177 | } 178 | let line = ''; 179 | let offset = 0; 180 | let markerLen = 1; 181 | if (i < 0) { line += ' '; } 182 | if (0 < lowIndex) { line += '...'; } 183 | for (let j = 0; j < tokensSlice.length; j++) { 184 | const token = escapeWhitespace(tokensSlice[j]); 185 | if (lowIndex + j === i) { 186 | offset = line.length; 187 | markerLen = token.length; 188 | } 189 | line += token; 190 | } 191 | if (highIndex < len) { line += '...'; } 192 | if (len <= i) { offset = line.length; } 193 | return `${''.padEnd(offset)}${i}\n${line}\n${''.padEnd(offset)}${'^'.repeat(markerLen)}`; 194 | } 195 | 196 | /** 197 | * Utility function that provides a bit cleaner interface for running a parser. 198 | * 199 | * This one throws an error in case parser didn't match 200 | * OR the match is incomplete (some part of input string left unparsed). 201 | * 202 | * Input string is broken down to characters as `[...str]` 203 | * unless you provide a pre-split array. 204 | * 205 | * @param parser - A parser to run. 206 | * @param str - Input string or an array of graphemes. 207 | * @param options - Parser options. 208 | * @returns A matched value. 209 | * 210 | * @category Utility functions 211 | */ 212 | export function parse ( 213 | parser: Parser, 214 | str: string | string[], 215 | options: TOptions 216 | ): TValue { 217 | const data: Data = { tokens: [...str], options: options }; 218 | const result = parser(data, 0); 219 | if (!result.matched) { 220 | throw new Error('No match'); 221 | } 222 | if (result.position < data.tokens.length) { 223 | throw new Error( 224 | `Partial match. Parsing stopped at:\n${parserPosition(data, result.position)}` 225 | ); 226 | } 227 | return result.value; 228 | } 229 | 230 | /** 231 | * Utility function that provides a bit cleaner interface 232 | * for running a parser over a string. 233 | * Returns `undefined` in case parser did not match. 234 | * 235 | * Input string is broken down to characters as `[...str]` 236 | * unless you provide a pre-split array. 237 | * 238 | * Note: this doesn't capture errors thrown during parsing. 239 | * Nonmatch is considered a part or normal flow. 240 | * Errors mean unrecoverable state and it's up to client code to decide 241 | * where to throw errors and how to get back to safe state. 242 | * 243 | * @param parser - A parser to run. 244 | * @param str - Input string or an array of graphemes. 245 | * @param options - Parser options. 246 | * @returns A matched value or `undefined` in case of nonmatch. 247 | * 248 | * @category Utility functions 249 | */ 250 | export function tryParse ( 251 | parser: Parser, 252 | str: string | string[], 253 | options: TOptions 254 | ): TValue | undefined { 255 | return tryParseCore(parser, [...str], options); 256 | } 257 | 258 | /** 259 | * Utility function that provides a bit cleaner interface 260 | * for running a {@link Matcher} over a string. 261 | * 262 | * Input string is broken down to characters as `[...str]` 263 | * unless you provide a pre-split array. 264 | * 265 | * @param matcher - A matcher to run. 266 | * @param str - Input string or an array of graphemes. 267 | * @param options - Parser options. 268 | * @returns A matched value. 269 | * 270 | * @category Utility functions 271 | */ 272 | export function match ( 273 | matcher: Matcher, 274 | str: string | string[], 275 | options: TOptions 276 | ): TValue { 277 | return matchCore(matcher, [...str], options); 278 | } 279 | -------------------------------------------------------------------------------- /deno/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./core.ts"; -------------------------------------------------------------------------------- /deno/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Clamp `x` between `left` and `right`. 3 | * 4 | * `left` is expected to be less then `right`. 5 | */ 6 | export function clamp (left: number, x: number, right: number): number { 7 | return Math.max(left, Math.min(x, right)); 8 | } 9 | 10 | /** 11 | * Escape \\r, \\n, \\t characters in a string. 12 | */ 13 | export function escapeWhitespace (str: string): string { 14 | return str.replace(/(\t)|(\r)|(\n)/g, (m,t,r) => t ? '\\t' : r ? '\\r' : '\\n'); 15 | } 16 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/custom.css: -------------------------------------------------------------------------------- 1 | /* CSS to customize Typedoc's default theme. */ 2 | 3 | :root { 4 | --dark-color-background-secondary: #1B2B34; 5 | --dark-color-background: #132026; 6 | --dark-color-icon-background: var(--dark-color-background); 7 | --dark-color-accent: #ffffff33; 8 | --dark-color-text: #ffffffbb; 9 | --dark-color-text-aside: #778692; 10 | --dark-color-link: #6699cc; 11 | --dark-color-ts: #c594c5; 12 | --dark-color-ts-interface: #99c794; 13 | --dark-color-ts-enum: #f99157; 14 | --dark-color-ts-class: #fac863; 15 | --dark-color-ts-function: #9772ff; 16 | --dark-color-ts-namespace: #e14dff; 17 | --dark-color-ts-private: #cdd3de; 18 | --dark-color-ts-variable: #4d68ff; 19 | --dark-code-background: #0c1418; 20 | 21 | --dark-color-tag: #5fb3b3; 22 | --dark-color-tag-text: #132026; 23 | } 24 | 25 | /* Extra space between overloads */ 26 | 27 | .tsd-signature ~ .tsd-signature { 28 | margin-top: 4em; 29 | } 30 | 31 | /* Monospace parameter names */ 32 | 33 | .tsd-parameter-list > li > h5 { 34 | font-weight: normal; 35 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 36 | } 37 | 38 | /* Enclose whole parameter signatures rather than their type */ 39 | 40 | .tsd-parameter-signature { 41 | border: 1px solid var(--color-accent); 42 | list-style: none; 43 | margin-inline-start: -40px; 44 | } 45 | 46 | .tsd-parameter-signature > ul > .tsd-signature { 47 | border: none; 48 | padding: 1rem 0.5rem 0.5rem 0.5rem; 49 | } 50 | 51 | .tsd-parameter-signature > ul > .tsd-description { 52 | margin: 1em; 53 | } 54 | 55 | /* Type parameters have no additional information on them */ 56 | 57 | .tsd-type-parameters-title, 58 | .tsd-type-parameters { 59 | display: none; 60 | } 61 | 62 | /* Parameter flags - rest, optional */ 63 | 64 | @media (prefers-color-scheme: dark) { 65 | .tsd-tag { 66 | border-radius: 4px; 67 | color: var(--dark-color-tag-text); 68 | background-color: var(--dark-color-tag); 69 | } 70 | } 71 | 72 | /* Signature color highlighting */ 73 | 74 | .tsd-signature-type { 75 | color: var(--color-ts-class); 76 | } 77 | 78 | .tsd-signature-type[data-tsd-kind] { 79 | color: var(--color-ts); 80 | } 81 | 82 | .tsd-signature-type[data-tsd-kind="Type parameter"] { 83 | color: var(--color-ts-interface); 84 | } 85 | 86 | /* Search */ 87 | 88 | #tsd-search.has-focus { 89 | background-color: var(--code-background); 90 | } 91 | 92 | #tsd-search > .results { 93 | background-color: var(--color-background-secondary); 94 | } 95 | 96 | /* Unnecessary settings */ 97 | 98 | .tsd-filter-visibility { 99 | display: none; 100 | } 101 | 102 | /* Current item */ 103 | 104 | .tsd-navigation.secondary li.current > a { 105 | font-weight: bold; 106 | } 107 | 108 | .tsd-navigation.secondary li.current > a::after { 109 | content: "\00a0←"; 110 | } 111 | 112 | /* Tables */ 113 | 114 | .headlessTable > table > thead { 115 | display: none; 116 | } 117 | 118 | .tsd-panel table { 119 | width: 100%; 120 | margin: 10px 0; 121 | border-collapse: collapse; 122 | } 123 | 124 | .tsd-panel table th, 125 | .tsd-panel table td { 126 | padding: 6px 13px; 127 | border: 1px solid var(--color-accent); 128 | } 129 | 130 | /* Code blocks don't need extra decorations */ 131 | 132 | code, pre { 133 | border-radius: 0; 134 | } 135 | 136 | pre { 137 | border: none; 138 | } 139 | -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #000000; 3 | --dark-hl-0: #D4D4D4; 4 | --light-hl-1: #AF00DB; 5 | --dark-hl-1: #C586C0; 6 | --light-hl-2: #0000FF; 7 | --dark-hl-2: #569CD6; 8 | --light-hl-3: #001080; 9 | --dark-hl-3: #9CDCFE; 10 | --light-hl-4: #A31515; 11 | --dark-hl-4: #CE9178; 12 | --light-hl-5: #267F99; 13 | --dark-hl-5: #4EC9B0; 14 | --light-code-background: #FFFFFF; 15 | --dark-code-background: #1E1E1E; 16 | } 17 | 18 | @media (prefers-color-scheme: light) { :root { 19 | --hl-0: var(--light-hl-0); 20 | --hl-1: var(--light-hl-1); 21 | --hl-2: var(--light-hl-2); 22 | --hl-3: var(--light-hl-3); 23 | --hl-4: var(--light-hl-4); 24 | --hl-5: var(--light-hl-5); 25 | --code-background: var(--light-code-background); 26 | } } 27 | 28 | @media (prefers-color-scheme: dark) { :root { 29 | --hl-0: var(--dark-hl-0); 30 | --hl-1: var(--dark-hl-1); 31 | --hl-2: var(--dark-hl-2); 32 | --hl-3: var(--dark-hl-3); 33 | --hl-4: var(--dark-hl-4); 34 | --hl-5: var(--dark-hl-5); 35 | --code-background: var(--dark-code-background); 36 | } } 37 | 38 | :root[data-theme='light'] { 39 | --hl-0: var(--light-hl-0); 40 | --hl-1: var(--light-hl-1); 41 | --hl-2: var(--light-hl-2); 42 | --hl-3: var(--light-hl-3); 43 | --hl-4: var(--light-hl-4); 44 | --hl-5: var(--light-hl-5); 45 | --code-background: var(--light-code-background); 46 | } 47 | 48 | :root[data-theme='dark'] { 49 | --hl-0: var(--dark-hl-0); 50 | --hl-1: var(--dark-hl-1); 51 | --hl-2: var(--dark-hl-2); 52 | --hl-3: var(--dark-hl-3); 53 | --hl-4: var(--dark-hl-4); 54 | --hl-5: var(--dark-hl-5); 55 | --code-background: var(--dark-code-background); 56 | } 57 | 58 | .hl-0 { color: var(--hl-0); } 59 | .hl-1 { color: var(--hl-1); } 60 | .hl-2 { color: var(--hl-2); } 61 | .hl-3 { color: var(--hl-3); } 62 | .hl-4 { color: var(--hl-4); } 63 | .hl-5 { color: var(--hl-5); } 64 | pre, code { background: var(--code-background); } 65 | -------------------------------------------------------------------------------- /docs/functions/char.char.html: -------------------------------------------------------------------------------- 1 | char | peberminta - v0.9.0
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function char

19 |
20 |
    21 | 22 |
  • 23 |

    Make a parser that looks for the exact match for a given character 24 | and returns a match with that character.

    25 |

    Tokens expected to be individual characters/graphemes.

    26 |
    27 |
    28 |

    Type Parameters

    29 |
      30 |
    • 31 |

      TOptions

    32 |
    33 |

    Parameters

    34 |
      35 |
    • 36 |
      char: string
      37 |

      A character to look for.

      38 |
    39 |

    Returns Parser<string, TOptions, string>

40 |
76 |
77 |

Generated using TypeDoc

78 |
-------------------------------------------------------------------------------- /docs/functions/char.charTest.html: -------------------------------------------------------------------------------- 1 | charTest | peberminta - v0.9.0
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function charTest

19 |
20 |
    21 | 22 |
  • 23 |

    Make a parser that matches input character against given regular expression.

    24 |

    Use this to match characters belonging to a certain range 25 | or having a certain unicode property.

    26 |

    Use satisfy from core module instead if you need a predicate.

    27 |

    Tokens expected to be individual characters/graphemes.

    28 |
    29 |
    30 |

    Type Parameters

    31 |
      32 |
    • 33 |

      TOptions

    34 |
    35 |

    Parameters

    36 |
      37 |
    • 38 |
      regex: RegExp
      39 |

      Tester regular expression.

      40 |
    41 |

    Returns Parser<string, TOptions, string>

42 |
78 |
79 |

Generated using TypeDoc

80 |
-------------------------------------------------------------------------------- /docs/functions/char.match.html: -------------------------------------------------------------------------------- 1 | match | peberminta - v0.9.0
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function match

19 |
20 |
    21 | 22 |
  • 23 |

    Utility function that provides a bit cleaner interface 24 | for running a Matcher over a string.

    25 |

    Input string is broken down to characters as [...str] 26 | unless you provide a pre-split array.

    27 | 28 |

    Returns

    A matched value.

    29 |
    30 |
    31 |

    Type Parameters

    32 |
      33 |
    • 34 |

      TOptions

    • 35 |
    • 36 |

      TValue

    37 |
    38 |

    Parameters

    39 |
      40 |
    • 41 |
      matcher: Matcher<string, TOptions, TValue>
      42 |

      A matcher to run.

      43 |
    • 44 |
    • 45 |
      str: string | string[]
      46 |

      Input string or an array of graphemes.

      47 |
    • 48 |
    • 49 |
      options: TOptions
      50 |

      Parser options.

      51 |
    52 |

    Returns TValue

53 |
89 |
90 |

Generated using TypeDoc

91 |
-------------------------------------------------------------------------------- /docs/functions/char.noneOf.html: -------------------------------------------------------------------------------- 1 | noneOf | peberminta - v0.9.0
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function noneOf

19 |
20 |
    21 | 22 |
  • 23 |

    Make a parser that matches and returns a character 24 | if it is absent in a given character samples string/array.

    25 |

    Tokens expected to be individual characters/graphemes.

    26 |
    27 |
    28 |

    Type Parameters

    29 |
      30 |
    • 31 |

      TOptions

    32 |
    33 |

    Parameters

    34 |
      35 |
    • 36 |
      chars: string | string[]
      37 |

      An array (or a string) of all characters that are not acceptable.

      38 |
    39 |

    Returns Parser<string, TOptions, string>

40 |
76 |
77 |

Generated using TypeDoc

78 |
-------------------------------------------------------------------------------- /docs/functions/char.oneOf.html: -------------------------------------------------------------------------------- 1 | oneOf | peberminta - v0.9.0
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function oneOf

19 |
20 |
    21 | 22 |
  • 23 |

    Make a parser that matches and returns a character 24 | if it is present in a given character samples string/array.

    25 |

    Tokens expected to be individual characters/graphemes.

    26 |
    27 |
    28 |

    Type Parameters

    29 |
      30 |
    • 31 |

      TOptions

    32 |
    33 |

    Parameters

    34 |
      35 |
    • 36 |
      chars: string | string[]
      37 |

      An array (or a string) of all acceptable characters.

      38 |
    39 |

    Returns Parser<string, TOptions, string>

40 |
76 |
77 |

Generated using TypeDoc

78 |
-------------------------------------------------------------------------------- /docs/functions/char.parserPosition.html: -------------------------------------------------------------------------------- 1 | parserPosition | peberminta - v0.9.0
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function parserPosition

19 |
20 |
    21 | 22 |
  • 23 |

    Utility function to render a given parser position 24 | for error reporting and debug purposes.

    25 |

    This is a version specific for char parsers.

    26 |

    Note: it will fall back to core version (one token per line) 27 | in case any multicharacter tokens are present.

    28 | 29 |

    Returns

    A multiline string.

    30 |
    31 |
    32 |

    Parameters

    33 |
      34 |
    • 35 |
      data: Data<string, unknown>
      36 |

      Data object (tokens and options).

      37 |
    • 38 |
    • 39 |
      i: number
      40 |

      Parser position in the tokens array.

      41 |
    • 42 |
    • 43 |
      contextTokens: number = 11
      44 |

      How many tokens (characters) around the current one to render.

      45 |
    46 |

    Returns string

47 |
83 |
84 |

Generated using TypeDoc

85 |
-------------------------------------------------------------------------------- /docs/functions/char.str.html: -------------------------------------------------------------------------------- 1 | str | peberminta - v0.9.0
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function str

19 |
20 |
    21 | 22 |
  • 23 |

    Make a parser that looks for the exact match for a given string, 24 | returns a match with that string and consumes an according number of tokens.

    25 |

    Empty string matches without consuming input.

    26 |

    Tokens expected to be individual characters/graphemes.

    27 |
    28 |
    29 |

    Type Parameters

    30 |
      31 |
    • 32 |

      TOptions

    33 |
    34 |

    Parameters

    35 |
      36 |
    • 37 |
      str: string
      38 |

      A string to look for.

      39 |
    40 |

    Returns Parser<string, TOptions, string>

41 |
77 |
78 |

Generated using TypeDoc

79 |
-------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | peberminta - v0.9.0
2 |
3 | 10 |
11 |
12 |
13 |
14 |

peberminta - v0.9.0

15 |
16 |
17 |

Index

18 |
19 |

Modules

20 |
char 21 | core 22 |
23 |
46 |
47 |

Generated using TypeDoc

48 |
-------------------------------------------------------------------------------- /examples/bf1.ts: -------------------------------------------------------------------------------- 1 | 2 | // This implementation "compiles" a Brainfuck code into a runnable function. 3 | 4 | import * as p from '../src/core'; 5 | import * as pc from '../src/char'; 6 | 7 | 8 | // Types 9 | 10 | type Options = { 11 | maxLoops: number // max number of loops before terminating the computation 12 | } 13 | 14 | type State = { 15 | pointer: number, // points at a memory cell 16 | memory: number[], // memory tape 17 | input: number[], // input buffer for the read op 18 | output: number[], // output buffer for the write op 19 | }; 20 | 21 | type Op = (state: State) => void; // A function that modifies state in-place. 22 | 23 | 24 | // Build up the parser / compiler 25 | 26 | function getValue (state: State) { 27 | return state.memory[state.pointer] || 0; 28 | } 29 | 30 | function setValue (state: State, value: number) { 31 | state.memory[state.pointer] = value % 256; 32 | } 33 | 34 | function makeOpParser (char: string, op: Op): p.Parser { 35 | return p.right( 36 | pc.char(char), 37 | p.emit(op) 38 | ); 39 | } 40 | 41 | function makeSequence (ops: Op[]): Op { 42 | return (state: State) => { 43 | for (const op of ops) { 44 | op(state); 45 | } 46 | }; 47 | } 48 | 49 | const next = makeOpParser('>', (st) => { st.pointer++; }); 50 | const prev = makeOpParser('<', (st) => { st.pointer--; }); 51 | 52 | const inc = makeOpParser('+', (st) => { setValue(st, getValue(st) + 1); }); 53 | const dec = makeOpParser('-', (st) => { setValue(st, getValue(st) - 1); }); 54 | 55 | const write = makeOpParser('.', (st) => { st.output.push(getValue(st)); }); 56 | const read = makeOpParser(',', (st) => { setValue(st, st.input.shift() || 0); }); 57 | 58 | const parseAnyOp = p.choice( 59 | next, prev, 60 | inc, dec, 61 | write, read, 62 | p.recursive(() => loop) 63 | ); 64 | 65 | const loop: p.Parser = p.middle( 66 | pc.char('['), 67 | p.chain( 68 | p.many( parseAnyOp ), 69 | (ops, data) => { 70 | const seq = makeSequence(ops); 71 | const maxLoops = data.options.maxLoops; 72 | return p.emit( 73 | (state: State) => { 74 | let loopCounter = maxLoops; 75 | while (getValue(state)) { 76 | if (--loopCounter < 0) { 77 | throw new Error('Loop counter exhausted!'); 78 | } 79 | seq(state); 80 | } 81 | } 82 | ); 83 | } 84 | ), 85 | p.token( 86 | (c) => (c === ']') ? c : undefined, 87 | () => { throw new Error('Missing closing bracket!'); } 88 | ) 89 | ); 90 | 91 | const unexpectedBracket = p.right( 92 | pc.char(']'), 93 | p.error('Unexpected closing bracket!') 94 | ); 95 | 96 | const parseAll = p.map( 97 | p.many(p.otherwise( 98 | parseAnyOp, 99 | unexpectedBracket 100 | )), 101 | makeSequence 102 | ); 103 | 104 | 105 | // Wrap it up 106 | 107 | function compile (program: string, maxLoops = 1_000) { 108 | const op = pc.match( 109 | parseAll, 110 | program.replace(/[^-+<>.,[\]]/g, ''), 111 | { maxLoops: maxLoops } 112 | ); 113 | return (input: string) => { 114 | const state: State = { 115 | pointer: 0, 116 | memory: [], 117 | input: [...input].map(c => c.charCodeAt(0)), 118 | output: [] 119 | }; 120 | op(state); 121 | return state.output 122 | .map(c => String.fromCharCode(c)) 123 | .join(''); 124 | }; 125 | } 126 | 127 | 128 | // Usage 129 | 130 | const cat = compile(',[.,]'); // Copy input to output and stop. 131 | 132 | const helloWorld = compile(` 133 | [ This program prints "Hello World!" and a newline to the screen. 134 | 135 | This loop is an "initial comment loop", a simple way of adding a comment 136 | to a BF program such that you don't have to worry about any command 137 | characters. Any ".", ",", "+", "-", "<" and ">" characters are simply 138 | ignored, the "[" and "]" characters just have to be balanced. This 139 | loop and the commands it contains are ignored because the current cell 140 | defaults to a value of 0; the 0 value causes this loop to be skipped. 141 | ] 142 | +++++ +++++ initialize counter (cell #0) to 10 143 | [ use loop to set the next four cells to 70/100/30/10 144 | > +++++ ++ add 7 to cell #1 145 | > +++++ +++++ add 10 to cell #2 146 | > +++ add 3 to cell #3 147 | > + add 1 to cell #4 148 | <<<< - decrement counter (cell #0) 149 | ] 150 | > ++ . print 'H' ( 72) 151 | > + . print 'e' (101) 152 | +++++ ++ . print 'l' (108) 153 | . print 'l' (108) 154 | +++ . print 'o' (111) 155 | > ++ . print ' ' ( 32) 156 | << +++++ +++++ +++++ . print 'W' ( 87) 157 | > . print 'o' (111) 158 | +++ . print 'r' (114) 159 | ----- - . print 'l' (108) 160 | ----- --- . print 'd' (100) 161 | > + . print '!' ( 33) 162 | > . print '\n'( 10) 163 | `); 164 | 165 | console.log(cat('Meow!')); 166 | console.log(helloWorld('')); 167 | 168 | 169 | // Output 170 | 171 | // Meow! 172 | // Hello World! 173 | // 174 | -------------------------------------------------------------------------------- /examples/bf2.ts: -------------------------------------------------------------------------------- 1 | 2 | // This implementation abuses the openness of the parser convention 3 | // by "parsing" in non-linear fashion. 4 | // Parser position is being manipulated by the parsed code, 5 | // making it to behave exactly like an instruction pointer of an interpreting machine. 6 | 7 | import * as p from '../src/core'; 8 | import * as pc from '../src/char'; 9 | 10 | 11 | // Types 12 | 13 | type State = { 14 | pointer: number, // points at a memory cell 15 | memory: number[], // memory tape 16 | input: number[], // input buffer for the read op 17 | output: number[], // output buffer for the write op 18 | jumps: number // max number of jumps before terminating the computation 19 | }; 20 | 21 | type OpToken = '<' | '>' | '+' | '-' | '.' | ','; 22 | type Token = OpToken | number; 23 | 24 | 25 | // First round parser - replace brackets with indices 26 | // It does a bit more that a lexer/tokenizer, or is it? 27 | // Let's call it translator since it translates the input Brainfuck program 28 | // into a new language that is almost the same except there are 29 | // jump offsets in place of brackets. 30 | 31 | const translate = p.flatten1( 32 | p.many( 33 | p.eitherOr( 34 | pc.oneOf('<>+-.,') as p.Parser, 35 | p.recursive(() => brackets) as p.Parser 36 | ) 37 | ) 38 | ); 39 | 40 | const brackets: p.Parser = p.abc( 41 | pc.char('['), 42 | translate, 43 | p.token( 44 | (c) => (c === ']') ? c : undefined, 45 | () => { throw new Error('Missing closing bracket!'); } 46 | ), 47 | ((lbr,ops) => [ops.length + 1, ...ops, -ops.length - 1]) 48 | ); 49 | 50 | 51 | // Second round parser - interpreter 52 | 53 | function op ( 54 | op: OpToken, 55 | f: (state: State) => void 56 | ): p.Parser { 57 | return p.middle( 58 | p.satisfy((t) => t === op), 59 | p.emit(true), 60 | p.action((data) => { f(data.options); }) 61 | ); 62 | } 63 | 64 | function getValue (state: State) { 65 | return state.memory[state.pointer] || 0; 66 | } 67 | 68 | function setValue (state: State, value: number) { 69 | state.memory[state.pointer] = value % 256; 70 | } 71 | 72 | const next = op('>', (state) => { state.pointer++; }); 73 | const prev = op('<', (state) => { state.pointer--; }); 74 | 75 | const inc = op('+', (state) => { setValue(state, getValue(state) + 1); }); 76 | const dec = op('-', (state) => { setValue(state, getValue(state) - 1); }); 77 | 78 | const write = op('.', (state) => { state.output.push(getValue(state)); }); 79 | const read = op(',', (state) => { setValue(state, state.input.shift() || 0); }); 80 | 81 | function goto (position: number): p.Match { 82 | return { 83 | matched: true, 84 | position: position, 85 | value: true 86 | }; 87 | } 88 | 89 | const jump: p.Parser = (data, i) => { 90 | if (--data.options.jumps < 0) { 91 | throw new Error('Jumps counter exhausted!'); 92 | } 93 | const t = data.tokens[i]; 94 | if (typeof t !== 'number') { return { matched: false }; } 95 | const v = getValue(data.options); 96 | const isForward = (t > 0); 97 | const isLoop = (v !== 0); 98 | return (isForward === isLoop) 99 | ? goto(i + 1) // no jump 100 | : goto(i + 1 + t); // jump after the matching bracket 101 | }; 102 | 103 | const anyOp = p.choice( 104 | next, prev, 105 | inc, dec, 106 | write, read, 107 | jump 108 | ); 109 | 110 | const interpret = p.many(anyOp); 111 | 112 | 113 | // Complete interpreter 114 | 115 | function run ( 116 | bfProgram: string, 117 | input: string, 118 | jumps = 10_000 119 | ): string { 120 | const translatedProgram = pc.match(translate, bfProgram.replace(/[^-+<>.,[\]]/g, ''), {}); 121 | const state: State = { 122 | pointer: 0, 123 | memory: [], 124 | input: [...input].map(c => c.charCodeAt(0)), 125 | output: [], 126 | jumps: jumps 127 | }; 128 | p.match(interpret, translatedProgram, state); 129 | return state.output 130 | .map(c => String.fromCharCode(c)) 131 | .join(''); 132 | } 133 | 134 | 135 | // Usage 136 | 137 | const cat = ',[.,]'; // Copy input to output and stop. 138 | 139 | const xmastree = ` 140 | [xmastree.b -- print Christmas tree 141 | (c) 2016 Daniel B. Cristofani 142 | http://brainfuck.org/] 143 | 144 | >>>--------<,[<[>++++++++++<-]>>[<------>>-<+],]++>>++<--[<++[+>]>+<<+++<]< 145 | <[>>+[[>>+<<-]<<]>>>>[[<<+>.>-]>>]<.<<<+<<-]>>[<.>--]>.>>. 146 | `; 147 | 148 | console.log(run(cat, 'Meow!')); 149 | console.log(run(xmastree, '11')); 150 | 151 | 152 | // Output 153 | 154 | // Meow! 155 | // * 156 | // *** 157 | // ***** 158 | // ******* 159 | // ********* 160 | // *********** 161 | // ************* 162 | // *************** 163 | // ***************** 164 | // ******************* 165 | // ********************* 166 | // * 167 | -------------------------------------------------------------------------------- /examples/calc.ts: -------------------------------------------------------------------------------- 1 | 2 | // Calculator that supports metric and binary unit prefixes. 3 | // (No actual units - that'd be quite a bit more work.) 4 | 5 | // https://en.wikipedia.org/wiki/Unit_prefix 6 | // https://en.wikipedia.org/wiki/Metric_prefix 7 | 8 | // Good calculator might need a BigNumber support. 9 | // This is just an example and such detail is irrelevant from the parsing side. 10 | 11 | // On the other hand, challenging grammar is relevant to the example. 12 | // In addition to unit prefixes this calculator also supports: 13 | // - math constants; 14 | // - functions of 1 or 2 arguments; 15 | // - exponent after a function name as in written math expressions; 16 | // - right associative exponent and unary minus at the same level of precedence; 17 | // - factorial. 18 | 19 | // Precedence levels and associativity: 20 | // 1. atom_ parentheses 21 | // 2. factorial_ [!] 22 | // 3. signExp_ right [-^] 23 | // 4. prod_ left [*/%] 24 | // 5. sum_ left [+-] 25 | 26 | import { createLexer, Token } from 'leac'; 27 | import * as p from '../src/core'; 28 | 29 | 30 | const multipliers: Record = { 31 | y: 10 ** -24, 32 | z: 10 ** -21, 33 | a: 10 ** -18, 34 | f: 10 ** -15, 35 | p: 10 ** -12, 36 | n: 10 ** -9, 37 | u: 10 ** -6, 38 | m: 10 ** -3, 39 | c: 10 ** -2, 40 | d: 10 ** -1, 41 | da: 10, 42 | h: 100, 43 | k: 10 ** 3, 44 | M: 10 ** 6, 45 | G: 10 ** 9, 46 | T: 10 ** 12, 47 | P: 10 ** 15, 48 | E: 10 ** 18, 49 | Z: 10 ** 21, 50 | Y: 10 ** 24, 51 | ki: 2 ** 10, 52 | Mi: 2 ** 20, 53 | Gi: 2 ** 30, 54 | Ti: 2 ** 40, 55 | Pi: 2 ** 50, 56 | Ei: 2 ** 60, 57 | Zi: 2 ** 70, 58 | Yi: 2 ** 80 59 | }; 60 | 61 | 62 | // Lexer / tokenizer 63 | 64 | const lex = createLexer([ 65 | { name: '(' }, 66 | { name: ')' }, 67 | { name: ',' }, 68 | { name: '^' }, 69 | { name: '!' }, 70 | { name: '-' }, 71 | { name: '+' }, 72 | { name: '*' }, 73 | { name: '/' }, 74 | { name: '%' }, 75 | { 76 | name: 'space', 77 | regex: /\s+/, 78 | discard: true 79 | }, 80 | { name: 'number', regex: /(?:[0-9]|[1-9][0-9]+)(?:\.[0-9]+)?(?:[eE][-+]?[0-9]+)?(?![0-9])/ }, 81 | { name: 'unitPrefix', regex: /(?:[yzafpnumcdh]|da|[kMGTPEZY]i?)\b/ }, 82 | { name: 'unaryFun', regex: /a?sin|a?cos|a?tan|ln|lg\b/ }, 83 | { name: 'biFun', regex: /log\b/ }, 84 | { name: 'constant', regex: /e|pi|tau\b/ }, 85 | ]); 86 | 87 | 88 | // Build up the calculator from smaller, simpler parts 89 | 90 | function literal (type: string): p.Parser { 91 | return p.token((t) => t.name === type ? true : undefined); 92 | } 93 | 94 | const number_: p.Parser 95 | = p.token((t) => t.name === 'number' ? parseFloat(t.text) : undefined); 96 | 97 | const unitPrefix_: p.Parser 98 | = p.token((t) => t.name === 'unitPrefix' ? multipliers[t.text] : undefined ); 99 | 100 | const scaledNumber_: p.Parser = p.ab( 101 | number_, 102 | p.option(unitPrefix_, 1), 103 | (value, multiplier) => value * multiplier 104 | ); 105 | 106 | const constant_: p.Parser = p.token((t) => { 107 | if (t.name !== 'constant') { return undefined; } 108 | switch (t.text) { 109 | case 'e': 110 | return Math.E; 111 | case 'pi': 112 | return Math.PI; 113 | case 'tau': 114 | return Math.PI * 2; 115 | default: 116 | throw new Error(`Unexpected constant name: "${t.text}"`); 117 | } 118 | }); 119 | 120 | const unaryFun_: p.Parser number> = p.ab( 121 | p.token((t) => { 122 | if (t.name !== 'unaryFun') { return undefined; } 123 | switch (t.text) { 124 | case 'sin': 125 | return (x) => Math.sin(x); 126 | case 'cos': 127 | return (x) => Math.cos(x); 128 | case 'tan': 129 | return (x) => Math.tan(x); 130 | case 'asin': 131 | return (x) => Math.asin(x); 132 | case 'acos': 133 | return (x) => Math.acos(x); 134 | case 'atan': 135 | return (x) => Math.atan(x); 136 | case 'ln': 137 | return (x) => Math.log(x); 138 | case 'lg': 139 | return (x) => Math.log10(x); 140 | default: 141 | throw new Error(`Unexpected function name: "${t.text}"`); 142 | } 143 | }) as p.Parser number>, 144 | p.option(p.right( 145 | literal('^'), 146 | p.recursive(() => signExp_) 147 | ), 1), 148 | (f, pow: number) => (pow === 1) ? f : (x) => f(x) ** pow 149 | ); 150 | 151 | const biFun_: p.Parser number> = p.ab( 152 | p.token((t) => { 153 | if (t.name !== 'biFun') { return undefined; } 154 | switch (t.text) { 155 | case 'log': 156 | return (x, y) => Math.log(x) / Math.log(y); 157 | default: 158 | throw new Error(`Unexpected function name: "${t.text}"`); 159 | } 160 | }) as p.Parser number>, 161 | p.option(p.right( 162 | literal('^'), 163 | p.recursive(() => signExp_) 164 | ), 1), 165 | (f, pow: number) => (pow === 1) ? f : (x, y) => f(x, y) ** pow 166 | ); 167 | 168 | const expression_: p.Parser = p.recursive( 169 | () => sum_ 170 | ); 171 | 172 | const paren_ = p.middle( 173 | literal('('), 174 | expression_, 175 | literal(')') 176 | ); 177 | 178 | const unaryFunWithParen_ = p.ab( 179 | unaryFun_, 180 | paren_, 181 | (f, x) => f(x) 182 | ); 183 | 184 | const biFunWithParen_ = p.abc( 185 | biFun_, 186 | p.middle( 187 | literal('('), 188 | expression_, 189 | literal(',') 190 | ), 191 | p.left( 192 | expression_, 193 | literal(')') 194 | ), 195 | (f, x, y) => f(x, y) 196 | ); 197 | 198 | const atom_ = p.choice( 199 | paren_, 200 | unaryFunWithParen_, 201 | biFunWithParen_, 202 | scaledNumber_, 203 | constant_ 204 | ); 205 | 206 | const factorialCache = [1, 1]; 207 | function factorial (n: number) 208 | { 209 | if (n < 0) { 210 | throw new Error(`Trying to take factorial of ${n}, a negative value!`); 211 | } 212 | if (!Number.isSafeInteger(n)) { 213 | throw new Error(`Trying to take factorial of ${n}, not a safe integer!`); 214 | } 215 | if (typeof factorialCache[n] != 'undefined') { return factorialCache[n]; } 216 | let i = factorialCache.length; 217 | let result = factorialCache[i-1]; 218 | for (; i <= n; i++) { 219 | factorialCache[i] = result = result * i; 220 | } 221 | return result; 222 | } 223 | 224 | const factorial_ = p.leftAssoc1( 225 | atom_, 226 | p.map( 227 | literal('!'), 228 | () => (x: number) => factorial(x) 229 | ) 230 | ); 231 | 232 | const signExp_ = p.rightAssoc1( 233 | p.choice( 234 | p.map( 235 | literal('-'), 236 | () => (y) => -y 237 | ), 238 | p.ab( 239 | factorial_, 240 | literal('^'), 241 | (x) => (y) => x ** y 242 | ) 243 | ), 244 | factorial_ 245 | ); 246 | 247 | const prod_ = p.leftAssoc2( 248 | signExp_, 249 | p.token((t) => { 250 | switch (t.name) { 251 | case '*': 252 | return (x, y) => x * y; 253 | case '/': 254 | return (x, y) => x / y; 255 | case '%': 256 | return (x, y) => x % y; 257 | default: 258 | return undefined; 259 | } 260 | }), 261 | signExp_ 262 | ); 263 | 264 | const sum_ = p.leftAssoc2( 265 | prod_, 266 | p.token((t) => { 267 | switch (t.name) { 268 | case '+': 269 | return (x, y) => x + y; 270 | case '-': 271 | return (x, y) => x - y; 272 | default: 273 | return undefined; 274 | } 275 | }), 276 | prod_ 277 | ); 278 | 279 | 280 | // Complete calculator 281 | 282 | function calc(expr: string): number { 283 | const lexerResult = lex(expr); 284 | if (!lexerResult.complete) { 285 | console.warn( 286 | `Input string was only partially tokenized, stopped at offset ${lexerResult.offset}!` 287 | ); 288 | } 289 | return p.parse(expression_, lexerResult.tokens, undefined); 290 | } 291 | 292 | 293 | // Usage and output 294 | 295 | console.log(calc('2m + 2k')); // 2000.002 296 | console.log(calc('1ki/2*0.5/2/2')); // 64 297 | console.log(calc('1ki * 1e-3')); // 1.024 298 | console.log(calc('--3!!')); // 720 299 | console.log(calc('log(2 Mi, 2)')); // 21 300 | console.log(calc('ln(e^-2^2)')); // -3.9999999999999996 301 | console.log(calc('2*-asin(cos(pi))')); // 3.141592653589793 302 | console.log(calc('cos^2(42) + sin(42)^2')); // 1 303 | console.log(calc('cos^2(pi/3)^0.5')); // 0.5000000000000001 304 | console.log(calc('(pi/2 + tau/2) / (e+2)')); // 0.9987510606003204 305 | -------------------------------------------------------------------------------- /examples/csv.ts: -------------------------------------------------------------------------------- 1 | 2 | // https://datatracker.ietf.org/doc/html/rfc4180 3 | // https://www.w3.org/TR/tabular-data-model/ 4 | 5 | // This example is actually overcomplicated so it can parse 6 | // tabular data separated by different characters. 7 | 8 | import { inspect } from 'util'; 9 | import * as p from '../src/core'; 10 | import * as pc from '../src/char'; 11 | 12 | 13 | // Types 14 | 15 | type Options = { 16 | fieldSeparator: string, 17 | whitespaces: string, 18 | headerRow: boolean, 19 | trimUnquotedFields: boolean 20 | } 21 | 22 | type Csv = { 23 | header?: string[], 24 | records: string[][] 25 | } 26 | 27 | 28 | // Build up the CSV parser from smaller, simpler parsers 29 | 30 | const whitespace_: p.Parser = p.satisfy( 31 | (c, data) => data.options.whitespaces.includes(c) 32 | ); 33 | 34 | const optionalWhitespaces_ = p.many(whitespace_); 35 | 36 | function possiblyCaptureWhitespace ( 37 | p1: p.Parser 38 | ): p.Parser { 39 | return p.condition( 40 | (data) => data.options.trimUnquotedFields, 41 | pc.concat( 42 | optionalWhitespaces_, 43 | p1, 44 | optionalWhitespaces_ 45 | ), 46 | p1 47 | ); 48 | } 49 | 50 | const linebreak_ = possiblyCaptureWhitespace( 51 | pc.concat( 52 | p.option(pc.char('\r'), ''), 53 | pc.char('\n') 54 | ) 55 | ); 56 | 57 | const sep_ = possiblyCaptureWhitespace( 58 | p.satisfy( 59 | (c, data) => c === data.options.fieldSeparator 60 | ) 61 | ); 62 | 63 | const qchar_ = p.or( 64 | p.map( 65 | pc.str('""'), 66 | () => '"' 67 | ), 68 | pc.noneOf('"') 69 | ); 70 | 71 | const qstr_ = p.middle( 72 | p.right(optionalWhitespaces_, pc.char('"')), 73 | pc.concat(p.many(qchar_)), 74 | p.left(pc.char('"'), optionalWhitespaces_) 75 | ); 76 | 77 | const schar_: p.Parser = p.satisfy( 78 | (c, data) => !'"\r\n'.includes(c) && c != data.options.fieldSeparator 79 | ); 80 | 81 | const sstr_ = pc.concat(p.takeUntilP( 82 | schar_, 83 | p.or(sep_, linebreak_) 84 | )); 85 | 86 | const field_ = p.or(qstr_, sstr_); 87 | 88 | const record_ = p.sepBy(field_, sep_); 89 | 90 | const csv_: p.Parser = p.condition( 91 | (data) => data.options.headerRow, 92 | p.map( 93 | p.sepBy1(record_, linebreak_), 94 | (rs) => ({ header: rs[0], records: rs.slice(1) }) 95 | ), 96 | p.map( 97 | p.sepBy(record_, linebreak_), 98 | (rs) => ({ records: rs }) 99 | ) 100 | ); 101 | 102 | 103 | // Complete parser 104 | 105 | function parseCsv (str: string, options: Options): Csv { 106 | return pc.parse(csv_, str, options); 107 | } 108 | 109 | 110 | // Usage 111 | 112 | const sampleCsv = ` 113 | foo, bar , baz 114 | 111, 222222, 33333 115 | ",", , \t 116 | \\, "" ," "" " 117 | `.trim(); 118 | 119 | console.log('Commas, with header, trim spaces:'); 120 | console.log(inspect(parseCsv(sampleCsv, { 121 | fieldSeparator: ',', 122 | whitespaces: ' \t', 123 | headerRow: true, 124 | trimUnquotedFields: true 125 | }))); 126 | 127 | console.log('Commas, no header, keep spaces:'); 128 | console.log(inspect(parseCsv(sampleCsv, { 129 | fieldSeparator: ',', 130 | whitespaces: ' \t', 131 | headerRow: false, 132 | trimUnquotedFields: false 133 | }))); 134 | 135 | const sampleTsv = ` 136 | foo\t bar \t baz 137 | 111\t 222222\t 33333 138 | ","\t \t \\t 139 | \\\t "" \t" "" " 140 | `.trim(); 141 | 142 | console.log('Tabs, header, keep spaces:'); 143 | console.log(inspect(parseCsv(sampleTsv, { 144 | fieldSeparator: '\t', 145 | whitespaces: ' ', 146 | headerRow: true, 147 | trimUnquotedFields: false 148 | }))); 149 | 150 | 151 | // Output 152 | 153 | // Commas, with header, trim spaces: 154 | // { 155 | // header: [ 'foo', 'bar', 'baz' ], 156 | // records: [ 157 | // [ '111', '222222', '33333' ], 158 | // [ ',', '', '' ], 159 | // [ '\\', '', ' " ' ] 160 | // ] 161 | // } 162 | // Commas, no header, keep spaces: 163 | // { 164 | // records: [ 165 | // [ 'foo', ' bar ', ' baz ' ], 166 | // [ '111', ' 222222', ' 33333' ], 167 | // [ ',', ' ', ' \t ' ], 168 | // [ ' \\', '', ' " ' ] 169 | // ] 170 | // } 171 | // Tabs, header, keep spaces: 172 | // { 173 | // header: [ 'foo', ' bar ', ' baz ' ], 174 | // records: [ 175 | // [ '111', ' 222222', ' 33333' ], 176 | // [ ',', ' ', ' \\t ' ], 177 | // [ ' \\', '', ' " ' ] 178 | // ] 179 | // } 180 | -------------------------------------------------------------------------------- /examples/hexColor.ts: -------------------------------------------------------------------------------- 1 | 2 | // Parser for hexadecimal color codes - like CSS colors, 3 | // except there are few more options. 4 | 5 | import * as p from '../src/core'; 6 | import * as pc from '../src/char'; 7 | 8 | 9 | // Options 10 | 11 | type FourNumbersFormat = 'rgba' | 'argb' | 'off'; 12 | type HashFormat = 'on' | 'off' | 'either'; 13 | 14 | type Options = { 15 | allowShortNotation: boolean, 16 | fourNumbersFormat: FourNumbersFormat, 17 | hashFormat: HashFormat 18 | }; 19 | 20 | 21 | // Result value types 22 | 23 | type Color = { 24 | r: number, 25 | g: number, 26 | b: number 27 | }; 28 | 29 | type ColorAlpha = Color & { a: number }; 30 | 31 | 32 | // Build up the color parser from smaller, simpler parsers 33 | 34 | const hexDigit_ = pc.charTest(/[0-9a-f]/i); 35 | 36 | const shortHexNumber_ = p.map( 37 | hexDigit_, 38 | (c) => parseInt(c + c, 16) 39 | ); 40 | 41 | const longHexNumber_ = p.map( 42 | pc.concat( 43 | hexDigit_, 44 | hexDigit_ 45 | ), 46 | (c) => parseInt(c, 16) 47 | ); 48 | 49 | function parseColorNumbers( 50 | parseHexNumber: p.Parser 51 | ): p.Parser { 52 | return p.abc( 53 | parseHexNumber, 54 | parseHexNumber, 55 | parseHexNumber, 56 | (v1, v2, v3) => ({ 57 | r: v1, 58 | g: v2, 59 | b: v3 60 | }) 61 | ); 62 | } 63 | 64 | function parseColorAlphaNumbers( 65 | parseHexNumber: p.Parser 66 | ): p.Parser { 67 | return p.condition( 68 | (data) => data.options.fourNumbersFormat === 'off', 69 | p.fail, 70 | p.condition( 71 | (data) => data.options.fourNumbersFormat === 'rgba', 72 | p.ab( 73 | parseColorNumbers(parseHexNumber), 74 | parseHexNumber, 75 | (color, alpha) => ({ ... color, a: alpha }) 76 | ), 77 | p.ab( 78 | parseHexNumber, 79 | parseColorNumbers(parseHexNumber), 80 | (alpha, color) => ({ ... color, a: alpha }) 81 | ), 82 | ) 83 | ); 84 | } 85 | 86 | const hashSign_: p.Parser = p.chain( 87 | p.option(pc.char('#'), ''), 88 | (v1, data) => 89 | (v1 === '' && data.options.hashFormat === 'on') ? p.fail 90 | : (v1 === '#' && data.options.hashFormat === 'off') ? p.fail 91 | : p.emit(true) 92 | ); 93 | 94 | const numberOfHexDigits_ = p.map( 95 | p.many(hexDigit_), 96 | (cs) => cs.length 97 | ); 98 | 99 | const parseColor_ = p.middle( 100 | hashSign_, 101 | p.chain( 102 | p.ahead(numberOfHexDigits_), 103 | (n, data: p.Data) => { 104 | switch (n) { 105 | case 3: 106 | if (data.options.allowShortNotation) { 107 | return parseColorNumbers(shortHexNumber_); 108 | } 109 | break; 110 | case 4: 111 | if (data.options.allowShortNotation) { 112 | return parseColorAlphaNumbers(shortHexNumber_); 113 | } 114 | break; 115 | case 6: 116 | return parseColorNumbers(longHexNumber_); 117 | case 8: 118 | return parseColorAlphaNumbers(longHexNumber_); 119 | } 120 | return p.fail; 121 | } 122 | ), 123 | p.end // You may not need this when combining into bigger grammar. 124 | ); 125 | 126 | 127 | // Usage 128 | 129 | const samples = [ 130 | '#00f', 131 | '#00F0', 132 | '#ff0000', 133 | '#ff000000', 134 | '039', 135 | '3069', 136 | '339900', 137 | '33669900', 138 | '#aabbc', 139 | 'aabbccdde', 140 | '#aabbcc#' 141 | ]; 142 | 143 | for (const sample of samples) { 144 | const maybeColor = pc.tryParse(parseColor_, sample, { 145 | allowShortNotation: true, 146 | fourNumbersFormat: 'rgba', 147 | hashFormat: 'either' 148 | }); 149 | console.log( 150 | sample.padStart(12).padEnd(15) + 151 | (maybeColor ? JSON.stringify(maybeColor) : 'didn\'t match') 152 | ); 153 | } 154 | 155 | 156 | // Output 157 | 158 | // #00f {"r":0,"g":0,"b":255} 159 | // #00F0 {"r":0,"g":0,"b":255,"a":0} 160 | // #ff0000 {"r":255,"g":0,"b":0} 161 | // #ff000000 {"r":255,"g":0,"b":0,"a":0} 162 | // 039 {"r":0,"g":51,"b":153} 163 | // 3069 {"r":51,"g":0,"b":102,"a":153} 164 | // 339900 {"r":51,"g":153,"b":0} 165 | // 33669900 {"r":51,"g":102,"b":153,"a":0} 166 | // #aabbc didn't match 167 | // aabbccdde didn't match 168 | // #aabbcc# didn't match 169 | -------------------------------------------------------------------------------- /examples/json.ts: -------------------------------------------------------------------------------- 1 | 2 | // https://www.json.org/json-en.html 3 | 4 | import { createLexer, Token } from 'leac'; 5 | import { inspect } from 'util'; 6 | import * as p from '../src/core'; 7 | 8 | 9 | // Types 10 | 11 | type JsonValue = null | true | false | number | string | JsonValue[] | JsonObject; 12 | type JsonObject = { [name: string]: JsonValue }; 13 | type ObjectMember = { name: string; value: JsonValue; }; 14 | 15 | 16 | // Lexer / tokenizer 17 | 18 | const lex = createLexer([ 19 | { name: '{' }, 20 | { name: '}' }, 21 | { name: '[' }, 22 | { name: ']' }, 23 | { name: ',' }, 24 | { name: ':' }, 25 | { name: 'null' }, 26 | { name: 'true' }, 27 | { name: 'false' }, 28 | { 29 | name: 'space', 30 | regex: /\s+/, 31 | discard: true 32 | }, 33 | { name: 'number', regex: /-?(?:[0-9]|[1-9][0-9]+)(?:\.[0-9]+)?(?:[eE][-+]?[0-9]+)?\b/ }, 34 | { name: 'string', regex: /"(?:\\["bfnrt/\\]|\\u[a-fA-F0-9]{4}|[^"\\])*"/ } 35 | ]); 36 | 37 | 38 | // Build up the JSON parser from smaller, simpler parsers 39 | 40 | function literal (name: string): p.Parser { 41 | return p.token((t) => t.name === name ? true : undefined); 42 | } 43 | 44 | const null_: p.Parser 45 | = p.token((t) => t.name === 'null' ? null : undefined); 46 | 47 | const true_: p.Parser 48 | = p.token((t) => t.name === 'true' ? true : undefined); 49 | 50 | const false_: p.Parser 51 | = p.token((t) => t.name === 'false' ? false : undefined); 52 | 53 | const number_: p.Parser 54 | = p.token((t) => t.name === 'number' ? parseFloat(t.text) : undefined); 55 | 56 | const string_: p.Parser 57 | = p.token((t) => t.name === 'string' ? JSON.parse(t.text) as string : undefined); 58 | 59 | const array_ = p.middle( 60 | literal('['), 61 | p.sepBy( 62 | p.recursive(() => value_), 63 | literal(',') 64 | ), 65 | literal(']') 66 | ); 67 | 68 | const member_: p.Parser = p.ab( 69 | string_, 70 | p.right( 71 | literal(':'), 72 | p.recursive(() => value_) 73 | ), 74 | (name,value) => ({ name: name, value: value }) 75 | ); 76 | 77 | const object_ = p.middle( 78 | literal('{'), 79 | p.map( 80 | p.sepBy( 81 | member_, 82 | literal(',') 83 | ), 84 | (pairs) => pairs.reduce( 85 | (acc, pair) => { acc[pair.name] = pair.value; return acc; }, 86 | {} as JsonObject 87 | ) 88 | ), 89 | literal('}') 90 | ); 91 | 92 | const value_: p.Parser = p.choice( 93 | null_ as p.Parser, 94 | true_, 95 | false_, 96 | number_, 97 | string_, 98 | array_, 99 | object_ 100 | ); 101 | 102 | 103 | // Complete parser 104 | 105 | function parseJson (json: string): JsonValue { 106 | const lexerResult = lex(json); 107 | if (!lexerResult.complete) { 108 | console.warn( 109 | `Input string was only partially tokenized, stopped at offset ${lexerResult.offset}!` 110 | ); 111 | } 112 | return p.parse(value_, lexerResult.tokens, {}); 113 | } 114 | 115 | 116 | // Usage 117 | 118 | console.log(inspect(parseJson(`"Hello World!"`))); 119 | console.log(inspect(parseJson(` 120 | { 121 | "foo": true, "bar": "baz", 122 | "qux": [1, 2, [3], [ ], { "quux": "quuz" }] 123 | } 124 | `), { breakLength: 50 })); 125 | 126 | 127 | // Output 128 | 129 | // 'Hello World!' 130 | // { 131 | // foo: true, 132 | // bar: 'baz', 133 | // qux: [ 1, 2, [ 3 ], [], { quux: 'quuz' } ] 134 | // } 135 | -------------------------------------------------------------------------------- /examples/nonDec.ts: -------------------------------------------------------------------------------- 1 | 2 | // Break a numbers array into non-decreasing fragments 3 | // (i.e. arrays where `x[i+1] >= x[i]`). 4 | 5 | import * as p from '../src/core'; 6 | 7 | 8 | // Build up the parser from smaller, simpler parts 9 | 10 | const number_: p.Parser = p.any; 11 | 12 | // Cutting corners here by using the fact `p.map` exposes indices before and after a match. 13 | // If everything were hidden it would've required to rely on accumulator instead. 14 | const nonDecSequence_ = p.map( 15 | p.chain( 16 | number_, 17 | (v1) => p.chainReduce( 18 | v1, 19 | (maxValue) => p.satisfy((t) => t >= maxValue) 20 | ) 21 | ), 22 | (v, data, i, j) => data.tokens.slice(i, j) 23 | ); 24 | 25 | const nonDecSequences_ = p.many(nonDecSequence_); 26 | 27 | 28 | // Complete parser 29 | 30 | function breakApart (xs: number[]): number[][] { 31 | return p.parse(nonDecSequences_, xs, undefined); 32 | } 33 | 34 | 35 | // Sample data 36 | 37 | // Pseudo-random number generator to get the same random array. 38 | const nextInt = (function () { 39 | // LCG using GCC's constants 40 | const m = 0x80000000; // 2**31; 41 | const a = 1103515245; 42 | const c = 12345; 43 | let state = 1; 44 | return function () { 45 | state = (a * state + c) % m; 46 | return state; 47 | }; 48 | })(); 49 | 50 | const randomArray = Array.from({length: 1_000_000}, () => nextInt()); 51 | 52 | const increasingArray = Array.from({length: 1_000_000}, (e,i) => i); 53 | 54 | const arrayWithRepetitions = [1, 1, 2, 2, 3, 3, 4, 5, -6, 5, 4, 3, 3, 2, 7, 8, 8, 9]; 55 | 56 | 57 | // Usage 58 | 59 | const orderedResult = breakApart(increasingArray); 60 | console.log(`\`increasingArray\` is broken into ${orderedResult.length} part(s).`); 61 | console.log(`First part is of length ${orderedResult[0].length}.`); 62 | console.log(''); 63 | 64 | const repetitiveResult = breakApart(arrayWithRepetitions); 65 | console.log(`\`arrayWithRepetitions\` is broken into ${repetitiveResult.length} part(s).`); 66 | console.log('Parts:'); 67 | repetitiveResult.forEach((xs) => { 68 | console.log( 69 | '[ ' + xs.join(', ') + ' ]' 70 | ); 71 | }); 72 | console.log(''); 73 | 74 | const pseudorandomResult = breakApart(randomArray); 75 | console.log(`\`randomArray\` is broken into ${pseudorandomResult.length} part(s).`); 76 | console.log('First 16 parts:'); 77 | pseudorandomResult.slice(0, 16).forEach((xs) => { 78 | console.log( 79 | '[ ' + xs.map((x) => String(x).replace(/\B(?=(\d{3})+(?!\d))/g, '_')).join(', ') + ' ]' 80 | ); 81 | }); 82 | 83 | 84 | // Output 85 | 86 | // `increasingArray` is broken into 1 part(s). 87 | // First part is of length 1000000. 88 | // 89 | // `arrayWithRepetitions` is broken into 5 part(s). 90 | // Parts: 91 | // [ 1, 1, 2, 2, 3, 3, 4, 5 ] 92 | // [ -6, 5 ] 93 | // [ 4 ] 94 | // [ 3, 3 ] 95 | // [ 2, 7, 8, 8, 9 ] 96 | // 97 | // `randomArray` is broken into 498177 part(s). 98 | // First 16 parts: 99 | // [ 1_103_527_590 ] 100 | // [ 377_401_600 ] 101 | // [ 333_417_792 ] 102 | // [ 314_102_912, 611_429_056, 1_995_203_584 ] 103 | // [ 18_793_472, 1_909_564_472 ] 104 | // [ 295_447_552, 484_895_808, 600_721_280, 1_704_829_312 ] 105 | // [ 877_851_648, 1_168_774_144, 1_937_945_600 ] 106 | // [ 964_613_120 ] 107 | // [ 395_867_136, 927_044_672, 1_805_111_040 ] 108 | // [ 716_526_336 ] 109 | // [ 545_163_008, 1_291_954_944 ] 110 | // [ 231_735_040, 2_067_907_392 ] 111 | // [ 1_207_816_704, 1_762_564_608 ] 112 | // [ 1_243_857_408 ] 113 | // [ 39_176_704, 1_578_316_344, 1_992_925_696 ] 114 | // [ 1_328_949_760 ] 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peberminta", 3 | "version": "0.9.0", 4 | "description": "Simple, transparent parser combinators toolkit that supports any tokens", 5 | "keywords": [ 6 | "parser", 7 | "parser-combinators", 8 | "parsec" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/mxxii/peberminta.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/mxxii/peberminta/issues" 16 | }, 17 | "homepage": "https://github.com/mxxii/peberminta", 18 | "author": "KillyMXI", 19 | "funding": "https://ko-fi.com/killymxi", 20 | "license": "MIT", 21 | "exports": { 22 | ".": { 23 | "import": "./lib/core.mjs", 24 | "require": "./lib/core.cjs" 25 | }, 26 | "./char": { 27 | "import": "./lib/char.mjs", 28 | "require": "./lib/char.cjs" 29 | } 30 | }, 31 | "type": "module", 32 | "main": "./lib/core.cjs", 33 | "module": "./lib/core.mjs", 34 | "types": "./lib/core.d.ts", 35 | "typesVersions": { 36 | "*": { 37 | "char": [ 38 | "./lib/char.d.ts" 39 | ], 40 | "*": [ 41 | "./lib/core.d.ts" 42 | ] 43 | } 44 | }, 45 | "files": [ 46 | "lib" 47 | ], 48 | "sideEffects": false, 49 | "scripts": { 50 | "build:docs": "typedoc", 51 | "build:deno": "denoify", 52 | "build:rollup": "rollup -c", 53 | "build:types": "tsc --declaration --emitDeclarationOnly && rimraf lib/util.d.ts", 54 | "build": "npm run clean && concurrently npm:build:*", 55 | "checkAll": "npm run lint && npm test", 56 | "clean": "rimraf lib && rimraf docs && rimraf deno", 57 | "cover": "c8 --reporter=lcov --reporter=text-summary ava \"test/!(examples).ts\" --timeout=60s", 58 | "example:bf1": "npm run ts -- ./examples/bf1.ts", 59 | "example:bf2": "npm run ts -- ./examples/bf2.ts", 60 | "example:calc": "npm run ts -- ./examples/calc.ts", 61 | "example:csv": "npm run ts -- ./examples/csv.ts", 62 | "example:hexColor": "npm run ts -- ./examples/hexColor.ts", 63 | "example:json": "npm run ts -- ./examples/json.ts", 64 | "example:nonDec": "npm run ts -- ./examples/nonDec.ts", 65 | "lint:eslint": "eslint .", 66 | "lint:md": "markdownlint-cli2", 67 | "lint": "concurrently npm:lint:*", 68 | "prepublishOnly": "npm run build && npm run checkAll", 69 | "test:ava": "ava --timeout=20s", 70 | "test:tsc": "tsc --noEmit --project tsconfig.tsc.json", 71 | "test": "concurrently npm:test:*", 72 | "ts": "node --experimental-specifier-resolution=node --loader ts-node/esm" 73 | }, 74 | "dependencies": {}, 75 | "devDependencies": { 76 | "@rollup/plugin-typescript": "^11.1.1", 77 | "@tsconfig/node14": "^1.0.3", 78 | "@types/node": "14.18.47", 79 | "@typescript-eslint/eslint-plugin": "^5.59.7", 80 | "@typescript-eslint/parser": "^5.59.7", 81 | "ava": "^5.3.0", 82 | "c8": "^7.13.0", 83 | "concurrently": "^8.0.1", 84 | "denoify": "^1.5.6", 85 | "eslint": "^8.41.0", 86 | "eslint-plugin-jsonc": "^2.8.0", 87 | "eslint-plugin-tsdoc": "^0.2.17", 88 | "expect-type": "^0.15.0", 89 | "leac": "^0.6.0", 90 | "markdownlint-cli2": "^0.7.1", 91 | "rimraf": "^5.0.1", 92 | "rollup": "^2.79.1", 93 | "rollup-plugin-cleanup": "^3.2.1", 94 | "ts-node": "^10.9.1", 95 | "tslib": "^2.5.2", 96 | "typedoc": "~0.23.28", 97 | "typescript": "~4.9.5" 98 | }, 99 | "ava": { 100 | "extensions": { 101 | "ts": "module" 102 | }, 103 | "files": [ 104 | "test/**/*" 105 | ], 106 | "nodeArguments": [ 107 | "--loader=ts-node/esm", 108 | "--experimental-specifier-resolution=node" 109 | ], 110 | "verbose": true 111 | }, 112 | "denoify": { 113 | "out": "./deno" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import cleanup from 'rollup-plugin-cleanup'; 3 | 4 | export default [ 5 | { 6 | external: [], 7 | input: 'src/char.ts', 8 | treeshake: false, 9 | plugins: [ 10 | typescript(), 11 | cleanup({ extensions: ['ts'] }) 12 | ], 13 | output: [ 14 | { 15 | dir: 'lib', 16 | format: 'es', 17 | preserveModules: true, 18 | entryFileNames: '[name].mjs', 19 | }, 20 | { 21 | dir: 'lib', 22 | format: 'cjs', 23 | preserveModules: true, 24 | entryFileNames: '[name].cjs', 25 | }, 26 | ], 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /src/char.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an additional module specifically for string parsers. 3 | * 4 | * It contains parsers with token type bound to be `string` 5 | * and expected to work with individual characters. 6 | * 7 | * It should work even if you have a custom way to split 8 | * a string into symbols such as graphemes. 9 | * 10 | * Node: 11 | * ```ts 12 | * import * as pc from 'peberminta/char'; 13 | * ``` 14 | * 15 | * Deno: 16 | * ```ts 17 | * import * as p from 'https://deno.land/x/peberminta@.../char.ts'; 18 | * ``` 19 | * 20 | * @packageDocumentation 21 | */ 22 | 23 | import { 24 | Parser, Matcher, Data, 25 | token, map, flatten, remainingTokensNumber, 26 | tryParse as tryParseCore, match as matchCore, 27 | parserPosition as parserPositionCore 28 | } from './core'; 29 | import { clamp, escapeWhitespace } from './util'; 30 | 31 | /** 32 | * Make a parser that looks for the exact match for a given character 33 | * and returns a match with that character. 34 | * 35 | * Tokens expected to be individual characters/graphemes. 36 | * 37 | * @param char - A character to look for. 38 | */ 39 | export function char ( 40 | char: string 41 | ): Parser { 42 | return token((c) => (c === char) ? c : undefined); 43 | } 44 | 45 | /** 46 | * Make a parser that matches and returns a character 47 | * if it is present in a given character samples string/array. 48 | * 49 | * Tokens expected to be individual characters/graphemes. 50 | * 51 | * @param chars - An array (or a string) of all acceptable characters. 52 | */ 53 | export function oneOf ( 54 | chars: string | string[] 55 | ): Parser { 56 | return token((c) => (chars.includes(c)) ? c : undefined); 57 | } 58 | 59 | export { oneOf as anyOf }; 60 | 61 | /** 62 | * Make a parser that matches and returns a character 63 | * if it is absent in a given character samples string/array. 64 | * 65 | * Tokens expected to be individual characters/graphemes. 66 | * 67 | * @param chars - An array (or a string) of all characters that are not acceptable. 68 | */ 69 | export function noneOf ( 70 | chars: string | string[] 71 | ): Parser { 72 | return token((c) => (chars.includes(c)) ? undefined : c); 73 | } 74 | 75 | /** 76 | * Make a parser that matches input character against given regular expression. 77 | * 78 | * Use this to match characters belonging to a certain range 79 | * or having a certain [unicode property](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes). 80 | * 81 | * Use `satisfy` from core module instead if you need a predicate. 82 | * 83 | * Tokens expected to be individual characters/graphemes. 84 | * 85 | * @param regex - Tester regular expression. 86 | */ 87 | export function charTest ( 88 | regex: RegExp 89 | ): Parser { 90 | return token((c) => regex.test(c) ? c : undefined); 91 | } 92 | 93 | /** 94 | * Make a parser that looks for the exact match for a given string, 95 | * returns a match with that string and consumes an according number of tokens. 96 | * 97 | * Empty string matches without consuming input. 98 | * 99 | * Tokens expected to be individual characters/graphemes. 100 | * 101 | * @param str - A string to look for. 102 | */ 103 | export function str ( 104 | str: string 105 | ): Parser { 106 | const len = str.length; 107 | return (data, i) => { 108 | // Keep working even if tokens are 109 | // not single chars but multi-char graphemes etc. 110 | // Can't just take a slice of len from tokens. 111 | const tokensNumber = remainingTokensNumber(data, i); 112 | let substr = ''; 113 | let j = 0; 114 | while (j < tokensNumber && substr.length < len) { 115 | substr += data.tokens[i+j]; 116 | j++; 117 | } 118 | return (substr === str) 119 | ? { 120 | matched: true, 121 | position: i + j, 122 | value: str 123 | } 124 | : { matched: false }; 125 | }; 126 | } 127 | 128 | /** 129 | * Make a parser that concatenates characters/strings 130 | * from all provided parsers into a single string. 131 | * 132 | * Nonmatch is returned if any of parsers didn't match. 133 | * 134 | * @param ps - Parsers sequence. 135 | * Each parser can return a string or an array of strings. 136 | */ 137 | export function concat ( 138 | ...ps: Parser[] 139 | ): Parser { 140 | return map( 141 | flatten(...ps), 142 | (vs) => vs.join('') 143 | ); 144 | } 145 | 146 | 147 | //------------------------------------------------------------ 148 | 149 | 150 | /** 151 | * Utility function to render a given parser position 152 | * for error reporting and debug purposes. 153 | * 154 | * This is a version specific for char parsers. 155 | * 156 | * Note: it will fall back to core version (one token per line) 157 | * in case any multicharacter tokens are present. 158 | * 159 | * @param data - Data object (tokens and options). 160 | * @param i - Parser position in the tokens array. 161 | * @param contextTokens - How many tokens (characters) around the current one to render. 162 | * @returns A multiline string. 163 | * 164 | * @category Utility functions 165 | */ 166 | export function parserPosition ( 167 | data: Data, 168 | i: number, 169 | contextTokens = 11 170 | ): string { 171 | const len = data.tokens.length; 172 | const lowIndex = clamp(0, i - contextTokens, len - contextTokens); 173 | const highIndex = clamp(contextTokens, i + 1 + contextTokens, len); 174 | const tokensSlice = data.tokens.slice(lowIndex, highIndex); 175 | if (tokensSlice.some((t) => t.length !== 1)) { 176 | return parserPositionCore(data, i, (t) => t); 177 | } 178 | let line = ''; 179 | let offset = 0; 180 | let markerLen = 1; 181 | if (i < 0) { line += ' '; } 182 | if (0 < lowIndex) { line += '...'; } 183 | for (let j = 0; j < tokensSlice.length; j++) { 184 | const token = escapeWhitespace(tokensSlice[j]); 185 | if (lowIndex + j === i) { 186 | offset = line.length; 187 | markerLen = token.length; 188 | } 189 | line += token; 190 | } 191 | if (highIndex < len) { line += '...'; } 192 | if (len <= i) { offset = line.length; } 193 | return `${''.padEnd(offset)}${i}\n${line}\n${''.padEnd(offset)}${'^'.repeat(markerLen)}`; 194 | } 195 | 196 | /** 197 | * Utility function that provides a bit cleaner interface for running a parser. 198 | * 199 | * This one throws an error in case parser didn't match 200 | * OR the match is incomplete (some part of input string left unparsed). 201 | * 202 | * Input string is broken down to characters as `[...str]` 203 | * unless you provide a pre-split array. 204 | * 205 | * @param parser - A parser to run. 206 | * @param str - Input string or an array of graphemes. 207 | * @param options - Parser options. 208 | * @returns A matched value. 209 | * 210 | * @category Utility functions 211 | */ 212 | export function parse ( 213 | parser: Parser, 214 | str: string | string[], 215 | options: TOptions 216 | ): TValue { 217 | const data: Data = { tokens: [...str], options: options }; 218 | const result = parser(data, 0); 219 | if (!result.matched) { 220 | throw new Error('No match'); 221 | } 222 | if (result.position < data.tokens.length) { 223 | throw new Error( 224 | `Partial match. Parsing stopped at:\n${parserPosition(data, result.position)}` 225 | ); 226 | } 227 | return result.value; 228 | } 229 | 230 | /** 231 | * Utility function that provides a bit cleaner interface 232 | * for running a parser over a string. 233 | * Returns `undefined` in case parser did not match. 234 | * 235 | * Input string is broken down to characters as `[...str]` 236 | * unless you provide a pre-split array. 237 | * 238 | * Note: this doesn't capture errors thrown during parsing. 239 | * Nonmatch is considered a part or normal flow. 240 | * Errors mean unrecoverable state and it's up to client code to decide 241 | * where to throw errors and how to get back to safe state. 242 | * 243 | * @param parser - A parser to run. 244 | * @param str - Input string or an array of graphemes. 245 | * @param options - Parser options. 246 | * @returns A matched value or `undefined` in case of nonmatch. 247 | * 248 | * @category Utility functions 249 | */ 250 | export function tryParse ( 251 | parser: Parser, 252 | str: string | string[], 253 | options: TOptions 254 | ): TValue | undefined { 255 | return tryParseCore(parser, [...str], options); 256 | } 257 | 258 | /** 259 | * Utility function that provides a bit cleaner interface 260 | * for running a {@link Matcher} over a string. 261 | * 262 | * Input string is broken down to characters as `[...str]` 263 | * unless you provide a pre-split array. 264 | * 265 | * @param matcher - A matcher to run. 266 | * @param str - Input string or an array of graphemes. 267 | * @param options - Parser options. 268 | * @returns A matched value. 269 | * 270 | * @category Utility functions 271 | */ 272 | export function match ( 273 | matcher: Matcher, 274 | str: string | string[], 275 | options: TOptions 276 | ): TValue { 277 | return matchCore(matcher, [...str], options); 278 | } 279 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Clamp `x` between `left` and `right`. 3 | * 4 | * `left` is expected to be less then `right`. 5 | */ 6 | export function clamp (left: number, x: number, right: number): number { 7 | return Math.max(left, Math.min(x, right)); 8 | } 9 | 10 | /** 11 | * Escape \\r, \\n, \\t characters in a string. 12 | */ 13 | export function escapeWhitespace (str: string): string { 14 | return str.replace(/(\t)|(\r)|(\n)/g, (m,t,r) => t ? '\\t' : r ? '\\r' : '\\n'); 15 | } 16 | -------------------------------------------------------------------------------- /test-types/core.ts: -------------------------------------------------------------------------------- 1 | import { expectTypeOf } from 'expect-type'; 2 | 3 | import { 4 | Parser, Matcher, 5 | make, token, map, peek, choice, otherwise, 6 | many, many1, ab, left, right, abc, middle, all, skip, 7 | flatten, flatten1, condition, ahead, recursive, 8 | } from '../src/core'; 9 | 10 | 11 | const parser = token((t) => t === 42 ? 42 as const : undefined); 12 | const matcher = make(() => 39 as const); 13 | 14 | // map 15 | 16 | expectTypeOf( map(parser, x => x) ).toEqualTypeOf>(); 17 | 18 | expectTypeOf( map(matcher, x => x) ).toEqualTypeOf>(); 19 | 20 | // peek 21 | 22 | expectTypeOf( peek(parser, () => { /* placeholder */ }) ).toEqualTypeOf>(); 23 | 24 | expectTypeOf( peek(matcher, () => { /* placeholder */ }) ).toEqualTypeOf>(); 25 | 26 | // choice 27 | 28 | expectTypeOf( choice(parser) ).toEqualTypeOf>(); 29 | 30 | expectTypeOf( choice() ).toEqualTypeOf>(); 31 | 32 | expectTypeOf( choice(matcher) ).toEqualTypeOf>(); 33 | 34 | expectTypeOf( choice(parser, matcher) ).toEqualTypeOf>(); 35 | 36 | // otherwise 37 | 38 | expectTypeOf( otherwise(parser, parser) ).toEqualTypeOf>(); 39 | 40 | expectTypeOf( otherwise(parser, matcher) ).toEqualTypeOf>(); 41 | 42 | // ab 43 | 44 | expectTypeOf( ab(parser, parser, (a, b) => [a, b] as const) ).toEqualTypeOf>(); 45 | 46 | expectTypeOf( ab(parser, matcher, (a, b) => [a, b] as const) ).toEqualTypeOf>(); 47 | 48 | expectTypeOf( ab(matcher, parser, (a, b) => [a, b] as const) ).toEqualTypeOf>(); 49 | 50 | expectTypeOf( ab(matcher, matcher, (a, b) => [a, b] as const) ).toEqualTypeOf>(); 51 | 52 | // left 53 | 54 | expectTypeOf( left(parser, parser) ).toEqualTypeOf>(); 55 | 56 | expectTypeOf( left(parser, matcher) ).toEqualTypeOf>(); 57 | 58 | expectTypeOf( left(matcher, parser) ).toEqualTypeOf>(); 59 | 60 | expectTypeOf( left(matcher, matcher) ).toEqualTypeOf>(); 61 | 62 | // right 63 | 64 | expectTypeOf( right(parser, parser) ).toEqualTypeOf>(); 65 | 66 | expectTypeOf( right(parser, matcher) ).toEqualTypeOf>(); 67 | 68 | expectTypeOf( right(matcher, parser) ).toEqualTypeOf>(); 69 | 70 | expectTypeOf( right(matcher, matcher) ).toEqualTypeOf>(); 71 | 72 | // abc 73 | 74 | expectTypeOf( abc(parser, parser, matcher, (a, b, c) => [a, b, c] as const) ).toEqualTypeOf>(); 75 | 76 | expectTypeOf( abc(parser, matcher, matcher, (a, b, c) => [a, b, c] as const) ).toEqualTypeOf>(); 77 | 78 | expectTypeOf( abc(matcher, parser, matcher, (a, b, c) => [a, b, c] as const) ).toEqualTypeOf>(); 79 | 80 | expectTypeOf( abc(matcher, matcher, matcher, (a, b, c) => [a, b, c] as const) ).toEqualTypeOf>(); 81 | 82 | // middle 83 | 84 | expectTypeOf( middle(parser, parser, parser) ).toEqualTypeOf>(); 85 | 86 | expectTypeOf( middle(parser, matcher, matcher) ).toEqualTypeOf>(); 87 | 88 | expectTypeOf( middle(matcher, parser, matcher) ).toEqualTypeOf>(); 89 | 90 | expectTypeOf( middle(matcher, matcher, matcher) ).toEqualTypeOf>(); 91 | 92 | // all 93 | 94 | expectTypeOf( all(parser) ).toEqualTypeOf>(); 95 | 96 | expectTypeOf( all(parser, matcher) ).toEqualTypeOf>(); 97 | 98 | expectTypeOf( all() ).toEqualTypeOf>(); 99 | 100 | expectTypeOf( all(matcher, matcher) ).toEqualTypeOf>(); 101 | 102 | // skip 103 | 104 | expectTypeOf( skip(parser) ).toEqualTypeOf>(); 105 | 106 | expectTypeOf( skip(parser, matcher) ).toEqualTypeOf>(); 107 | 108 | expectTypeOf( skip() ).toEqualTypeOf>(); 109 | 110 | expectTypeOf( skip(matcher, matcher) ).toEqualTypeOf>(); 111 | 112 | // flatten 113 | 114 | expectTypeOf( flatten(parser) ).toEqualTypeOf>(); 115 | 116 | expectTypeOf( flatten(parser, matcher) ).toEqualTypeOf>(); 117 | 118 | expectTypeOf( flatten() ).toEqualTypeOf>(); 119 | 120 | expectTypeOf( flatten(matcher, matcher) ).toEqualTypeOf>(); 121 | 122 | // flatten1 123 | 124 | expectTypeOf( flatten1(many1(parser)) ).toEqualTypeOf>(); 125 | 126 | expectTypeOf( flatten1(many(matcher)) ).toEqualTypeOf>(); 127 | 128 | // condition 129 | 130 | expectTypeOf( condition(() => true, parser, parser) ).toEqualTypeOf>(); 131 | 132 | expectTypeOf( condition(() => true, parser, matcher) ).toEqualTypeOf>(); 133 | 134 | expectTypeOf( condition(() => true, matcher, parser) ).toEqualTypeOf>(); 135 | 136 | expectTypeOf( condition(() => true, matcher, matcher) ).toEqualTypeOf>(); 137 | 138 | // ahead 139 | 140 | expectTypeOf( ahead(parser) ).toEqualTypeOf>(); 141 | 142 | expectTypeOf( ahead(matcher) ).toEqualTypeOf>(); 143 | 144 | // recursive 145 | 146 | expectTypeOf( recursive(() => parser) ).toEqualTypeOf>(); 147 | 148 | expectTypeOf( recursive(() => matcher) ).toEqualTypeOf>(); 149 | -------------------------------------------------------------------------------- /test/char.ts: -------------------------------------------------------------------------------- 1 | import test, {ExecutionContext} from 'ava'; 2 | 3 | import { Parser, Result, all, many } from '../src/core'; 4 | import { 5 | char, oneOf, anyOf, noneOf, charTest, str, concat, 6 | parserPosition, parse, tryParse, match 7 | } from '../src/char'; 8 | 9 | 10 | const dataHello = { tokens: [...'hello'], options: {} }; 11 | 12 | const dataLorem = { tokens: ['Ĺ','o͂','ř','ȩ','m̅','🏳️‍🌈'], options: {} }; 13 | 14 | function helloMacro ( 15 | t: ExecutionContext, 16 | p: Parser, 17 | i: number, 18 | expected: Result 19 | ) { 20 | t.deepEqual(p(dataHello, i), expected); 21 | } 22 | 23 | function loremMacro ( 24 | t: ExecutionContext, 25 | p: Parser, 26 | i: number, 27 | expected: Result 28 | ) { 29 | t.deepEqual(p(dataLorem, i), expected); 30 | } 31 | 32 | 33 | test('char - match', helloMacro, char('h'), 0, { 34 | matched: true, 35 | position: 1, 36 | value: 'h' 37 | }); 38 | 39 | test('char - match - combining', loremMacro, char('Ĺ'), 0, { 40 | matched: true, 41 | position: 1, 42 | value: 'Ĺ' 43 | }); 44 | 45 | test('char - nonmatch', helloMacro, char('a'), 0, { matched: false }); 46 | 47 | test('char - on end', helloMacro, char('a'), 5, { matched: false }); 48 | 49 | test('oneOf - match - string', helloMacro, oneOf('fghi'), 0, { 50 | matched: true, 51 | position: 1, 52 | value: 'h' 53 | }); 54 | 55 | test('oneOf - match - array', helloMacro, oneOf(['f', 'g', 'h', 'i']), 0, { 56 | matched: true, 57 | position: 1, 58 | value: 'h' 59 | }); 60 | 61 | test('oneOf - match - combining', loremMacro, oneOf(['Ĺ', 'ȩ', 'm̅']), 0, { 62 | matched: true, 63 | position: 1, 64 | value: 'Ĺ' 65 | }); 66 | 67 | test('oneOf - nonmatch - anyOf alias', helloMacro, anyOf('elopqrst'), 0, { matched: false }); 68 | 69 | test('oneOf - on end', helloMacro, oneOf(['a']), 5, { matched: false }); 70 | 71 | test('noneOf - match - string', helloMacro, noneOf('elopqrst'), 0, { 72 | matched: true, 73 | position: 1, 74 | value: 'h' 75 | }); 76 | 77 | test('noneOf - match - array', helloMacro, noneOf(['e', 'l', 'o']), 0, { 78 | matched: true, 79 | position: 1, 80 | value: 'h' 81 | }); 82 | 83 | test('noneOf - match - combining', loremMacro, noneOf(['Ĺ', 'ȩ', 'm̅']), 1, { 84 | matched: true, 85 | position: 2, 86 | value: 'o͂' 87 | }); 88 | 89 | test('noneOf - nonmatch', helloMacro, noneOf('fghi'), 0, { matched: false }); 90 | 91 | test('noneOf - on end', helloMacro, noneOf(['a']), 5, { matched: false }); 92 | 93 | test('charTest - match - character class', helloMacro, charTest(/[a-z]/), 0, { 94 | matched: true, 95 | position: 1, 96 | value: 'h' 97 | }); 98 | 99 | test('charTest - match - unicode property', helloMacro, charTest(/\p{Letter}/u), 1, { 100 | matched: true, 101 | position: 2, 102 | value: 'e' 103 | }); 104 | 105 | test('charTest - nonmatch', helloMacro, charTest(/\P{Letter}/u), 1, { matched: false }); 106 | 107 | test('charTest - on end', helloMacro, charTest(/\p{Letter}/u), 5, { matched: false }); 108 | 109 | test('str - match', helloMacro, str('hell'), 0, { 110 | matched: true, 111 | position: 4, 112 | value: 'hell' 113 | }); 114 | 115 | test('str - match - combining', loremMacro, str('Ĺo͂řȩ'), 0, { 116 | matched: true, 117 | position: 4, 118 | value: 'Ĺo͂řȩ' 119 | }); 120 | 121 | test('str - nonmatch', helloMacro, str('help'), 0, { matched: false }); 122 | 123 | test('str - on end', helloMacro, str('world'), 5, { matched: false }); 124 | 125 | test('str - empty string matches without consuming input', helloMacro, str(''), 0, { 126 | matched: true, 127 | position: 0, 128 | value: '' 129 | }); 130 | 131 | test('str - empty string matches on end', helloMacro, str(''), 5, { 132 | matched: true, 133 | position: 5, 134 | value: '' 135 | }); 136 | 137 | test('concat - match', helloMacro, concat( 138 | char('h'), 139 | str(''), 140 | concat( 141 | char('e'), 142 | all( 143 | char('l'), 144 | char('l') 145 | ) 146 | ) 147 | ), 0, { 148 | matched: true, 149 | position: 4, 150 | value: 'hell' 151 | }); 152 | 153 | test('concat - nonmatch', helloMacro, concat( 154 | char('h'), 155 | char('e'), 156 | char('l'), 157 | char('p') 158 | ), 0, { matched: false }); 159 | 160 | test('concat - empty match', helloMacro, concat(), 0, { 161 | matched: true, 162 | position: 0, 163 | value: '' 164 | }); 165 | 166 | test('parserPosition - on token', t => { 167 | t.is( 168 | parserPosition(dataHello, 1, 1), 169 | ' 1\nhel...\n ^' 170 | ); 171 | }); 172 | 173 | test('parserPosition - on token - combining fallback', t => { 174 | t.is( 175 | parserPosition(dataLorem, 5, 1), 176 | ' ...\n 2 ř\n 3 ȩ\n 4 m̅\n 5 > 🏳️‍🌈' 177 | ); 178 | }); 179 | 180 | test('parserPosition - on end', t => { 181 | t.is( 182 | parserPosition(dataHello, 5, 1), 183 | ' 5\n...o\n ^' 184 | ); 185 | }); 186 | 187 | test('parserPosition - before start', t => { 188 | t.is( 189 | parserPosition(dataHello, -2, 1), 190 | '-2\n h...\n^' 191 | ); 192 | }); 193 | 194 | test('parse - match', t => { 195 | t.deepEqual( 196 | parse(str('hello'), 'hello', {}), 197 | 'hello' 198 | ); 199 | }); 200 | 201 | test('parse - nonmatch', t => { 202 | t.throws( 203 | () => parse(str('bye'), 'hello', {}), 204 | { message: 'No match' } 205 | ); 206 | }); 207 | 208 | test('parse - partial', t => { 209 | t.throws( 210 | () => parse(str('hell'), ['h', 'e', 'l', 'l', 'o'], undefined), 211 | { message: 'Partial match. Parsing stopped at:\n 4\nhello\n ^' } 212 | ); 213 | }); 214 | 215 | test('parse - partial - combining fallback', t => { 216 | t.throws( 217 | () => parse(str('Ĺo͂řȩm̅'), dataLorem.tokens, undefined), 218 | { message: 'Partial match. Parsing stopped at:\n ...\n 2 ř\n 3 ȩ\n 4 m̅\n 5 > 🏳️‍🌈' } 219 | ); 220 | }); 221 | 222 | test('tryParse - match', t => { 223 | t.deepEqual( 224 | tryParse(concat(many(charTest(/[a-m]/))), 'hello', {}), 225 | 'hell' 226 | ); 227 | }); 228 | 229 | test('tryParse - nonmatch', t => { 230 | t.is( 231 | tryParse(str('bye'), 'hello', {}), 232 | undefined 233 | ); 234 | }); 235 | 236 | test('match - match', t => { 237 | t.deepEqual( 238 | match(many(charTest(/\p{Number}/u)), 'hello', {}), 239 | [] 240 | ); 241 | }); 242 | -------------------------------------------------------------------------------- /test/examples.ts: -------------------------------------------------------------------------------- 1 | import test, {ExecutionContext} from 'ava'; 2 | 3 | import { execFileSync } from 'child_process'; 4 | 5 | function snapshotMacro(t: ExecutionContext, examplePath: string) { 6 | const stdout = execFileSync('node', [ 7 | '--experimental-specifier-resolution=node', 8 | '--loader', 9 | 'ts-node/esm', 10 | examplePath 11 | ]); 12 | t.snapshot(stdout.toString(), examplePath); 13 | } 14 | 15 | test('brainfuck-1', snapshotMacro, './examples/bf1.ts'); 16 | 17 | test('brainfuck-2', snapshotMacro, './examples/bf2.ts'); 18 | 19 | test('calc', snapshotMacro, './examples/calc.ts'); 20 | 21 | test('csv', snapshotMacro, './examples/csv.ts'); 22 | 23 | test('hexColor', snapshotMacro, './examples/hexColor.ts'); 24 | 25 | test('json', snapshotMacro, './examples/json.ts'); 26 | 27 | test('nonDec', snapshotMacro, './examples/nonDec.ts'); 28 | -------------------------------------------------------------------------------- /test/snapshots/examples.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/examples.ts` 2 | 3 | The actual snapshot is saved in `examples.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## brainfuck-1 8 | 9 | > ./examples/bf1.ts 10 | 11 | `Meow!␊ 12 | Hello World!␊ 13 | ␊ 14 | ` 15 | 16 | ## brainfuck-2 17 | 18 | > ./examples/bf2.ts 19 | 20 | `Meow!␊ 21 | *␊ 22 | ***␊ 23 | *****␊ 24 | *******␊ 25 | *********␊ 26 | ***********␊ 27 | *************␊ 28 | ***************␊ 29 | *****************␊ 30 | *******************␊ 31 | *********************␊ 32 | *␊ 33 | ␊ 34 | ` 35 | 36 | ## calc 37 | 38 | > ./examples/calc.ts 39 | 40 | `2000.002␊ 41 | 64␊ 42 | 1.024␊ 43 | 720␊ 44 | 21␊ 45 | -3.9999999999999996␊ 46 | 3.141592653589793␊ 47 | 1␊ 48 | 0.5000000000000001␊ 49 | 0.9987510606003204␊ 50 | ` 51 | 52 | ## csv 53 | 54 | > ./examples/csv.ts 55 | 56 | `Commas, with header, trim spaces:␊ 57 | {␊ 58 | header: [ 'foo', 'bar', 'baz' ],␊ 59 | records: [␊ 60 | [ '111', '222222', '33333' ],␊ 61 | [ ',', '', '' ],␊ 62 | [ '\\\\', '', ' " ' ]␊ 63 | ]␊ 64 | }␊ 65 | Commas, no header, keep spaces:␊ 66 | {␊ 67 | records: [␊ 68 | [ 'foo', ' bar ', ' baz ' ],␊ 69 | [ '111', ' 222222', ' 33333' ],␊ 70 | [ ',', ' ', ' \\t ' ],␊ 71 | [ ' \\\\', '', ' " ' ]␊ 72 | ]␊ 73 | }␊ 74 | Tabs, header, keep spaces:␊ 75 | {␊ 76 | header: [ 'foo', ' bar ', ' baz ' ],␊ 77 | records: [␊ 78 | [ '111', ' 222222', ' 33333' ],␊ 79 | [ ',', ' ', ' \\\\t ' ],␊ 80 | [ ' \\\\', '', ' " ' ]␊ 81 | ]␊ 82 | }␊ 83 | ` 84 | 85 | ## hexColor 86 | 87 | > ./examples/hexColor.ts 88 | 89 | ` #00f {"r":0,"g":0,"b":255}␊ 90 | #00F0 {"r":0,"g":0,"b":255,"a":0}␊ 91 | #ff0000 {"r":255,"g":0,"b":0}␊ 92 | #ff000000 {"r":255,"g":0,"b":0,"a":0}␊ 93 | 039 {"r":0,"g":51,"b":153}␊ 94 | 3069 {"r":51,"g":0,"b":102,"a":153}␊ 95 | 339900 {"r":51,"g":153,"b":0}␊ 96 | 33669900 {"r":51,"g":102,"b":153,"a":0}␊ 97 | #aabbc didn't match␊ 98 | aabbccdde didn't match␊ 99 | #aabbcc# didn't match␊ 100 | ` 101 | 102 | ## json 103 | 104 | > ./examples/json.ts 105 | 106 | `'Hello World!'␊ 107 | {␊ 108 | foo: true,␊ 109 | bar: 'baz',␊ 110 | qux: [ 1, 2, [ 3 ], [], { quux: 'quuz' } ]␊ 111 | }␊ 112 | ` 113 | 114 | ## nonDec 115 | 116 | > ./examples/nonDec.ts 117 | 118 | `\`increasingArray\` is broken into 1 part(s).␊ 119 | First part is of length 1000000.␊ 120 | ␊ 121 | \`arrayWithRepetitions\` is broken into 5 part(s).␊ 122 | Parts:␊ 123 | [ 1, 1, 2, 2, 3, 3, 4, 5 ]␊ 124 | [ -6, 5 ]␊ 125 | [ 4 ]␊ 126 | [ 3, 3 ]␊ 127 | [ 2, 7, 8, 8, 9 ]␊ 128 | ␊ 129 | \`randomArray\` is broken into 498177 part(s).␊ 130 | First 16 parts:␊ 131 | [ 1_103_527_590 ]␊ 132 | [ 377_401_600 ]␊ 133 | [ 333_417_792 ]␊ 134 | [ 314_102_912, 611_429_056, 1_995_203_584 ]␊ 135 | [ 18_793_472, 1_909_564_472 ]␊ 136 | [ 295_447_552, 484_895_808, 600_721_280, 1_704_829_312 ]␊ 137 | [ 877_851_648, 1_168_774_144, 1_937_945_600 ]␊ 138 | [ 964_613_120 ]␊ 139 | [ 395_867_136, 927_044_672, 1_805_111_040 ]␊ 140 | [ 716_526_336 ]␊ 141 | [ 545_163_008, 1_291_954_944 ]␊ 142 | [ 231_735_040, 2_067_907_392 ]␊ 143 | [ 1_207_816_704, 1_762_564_608 ]␊ 144 | [ 1_243_857_408 ]␊ 145 | [ 39_176_704, 1_578_316_344, 1_992_925_696 ]␊ 146 | [ 1_328_949_760 ]␊ 147 | ` 148 | -------------------------------------------------------------------------------- /test/snapshots/examples.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxxii/peberminta/7abfa2eff57bac1435188addfe17eccd8b74aa6c/test/snapshots/examples.ts.snap -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "test/**/*.ts", 6 | "test-types/**/*.ts", 7 | "examples/**/*.ts", 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node14/tsconfig.json", 3 | "include": [ 4 | "src/**/*.ts" 5 | ], 6 | "compilerOptions": { 7 | "outDir": "./lib" 8 | }, 9 | "ts-node": { 10 | "compilerOptions": { 11 | "module": "ESNext", 12 | "esModuleInterop": true, 13 | "moduleResolution": "Node" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.tsc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "test-types/**/*.ts", 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", 3 | "tagDefinitions": [ 4 | { 5 | "tagName": "@category", 6 | "syntaxKind": "block" 7 | }, 8 | { 9 | "tagName": "@module", 10 | "syntaxKind": "modifier" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "categorizeByGroup": false, 4 | "categoryOrder": [ 5 | "Type aliases", 6 | "Parsers", 7 | "Utility functions" 8 | ], 9 | "customCss": "./assets/docs.css", 10 | "defaultCategory": "Parsers", 11 | "disableSources": true, 12 | "entryPoints": [ 13 | "src/core.ts", 14 | "src/char.ts" 15 | ], 16 | "includeVersion": true, 17 | "out": "docs", 18 | "searchInComments": true, 19 | "sort": [ "alphabetical" ], 20 | "navigationLinks": { 21 | "docs": "https://mxxii.github.io/peberminta/", 22 | "github": "https://github.com/mxxii/peberminta", 23 | "npm": "https://www.npmjs.com/package/peberminta", 24 | "deno": "https://deno.land/x/peberminta" 25 | } 26 | } 27 | --------------------------------------------------------------------------------