├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── benchmarks ├── index.js └── wrapper.js ├── docs ├── README.md ├── cookbook │ ├── complete-example.md │ ├── creating-unordered-strategies.md │ ├── parsing-with-loops.md │ ├── rest-arguments.md │ ├── subcommands.md │ └── using-option-and-result.md └── reference │ ├── README.md │ ├── classes │ ├── args.md │ ├── lexer.md │ └── parser.md │ ├── enums │ └── looptag.md │ └── interfaces │ ├── argsstate.md │ ├── err.md │ ├── fail.md │ ├── finish.md │ ├── loopstrategy.md │ ├── loopstrategyasync.md │ ├── none.md │ ├── ok.md │ ├── parseroutput.md │ ├── some.md │ ├── step.md │ ├── token.md │ └── unorderedstrategy.md ├── esm └── index.mjs ├── package.json ├── src ├── args.ts ├── index.ts ├── lexer.ts ├── loopAction.ts ├── loops.ts ├── option.ts ├── parser.ts ├── parserOutput.ts ├── result.ts ├── tokens.ts ├── unordered.ts └── util.ts ├── test ├── args.test.ts ├── index.test.ts ├── lexer.test.ts ├── loops.test.ts ├── option.test.ts ├── parser.test.ts ├── parserOutput.test.ts ├── result.test.ts ├── tokens.test.ts ├── unordered.test.ts └── util.test.ts ├── tsconfig.build.json ├── tsconfig.json └── typedoc.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # JavaScript 2 | .eslintrc.js 3 | benchmarks 4 | 5 | # Node 6 | node_modules 7 | 8 | # Output 9 | coverage 10 | dist 11 | docs 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | 'jest', 7 | ], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/eslint-recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'plugin:jest/recommended', 13 | ], 14 | rules: { 15 | 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0, maxBOF: 0 }], 16 | 'no-trailing-spaces': 'error', 17 | 'generator-star-spacing': ['error', 'after'], 18 | '@typescript-eslint/semi': 'error', 19 | '@typescript-eslint/indent': ['error', 4, { 'SwitchCase': 1 }], 20 | '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true, allowTypedFunctionExpressions: true }], 21 | '@typescript-eslint/no-non-null-assertion': ['off'] 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | 4 | # Output 5 | coverage 6 | dist 7 | package-lock.json 8 | 9 | # Other 10 | .DS_Store 11 | *.log 12 | .vscode 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | 4 | # Source 5 | .eslintignore 6 | .eslintrc.js 7 | benchmarks 8 | src 9 | test 10 | tsconfig.json 11 | 12 | # Output 13 | coverage 14 | docs 15 | package-lock.json 16 | 17 | # Other 18 | .DS_Store 19 | *.log 20 | .vscode 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 1Computer 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 | # lexure 2 | 3 | `npm i lexure` 4 | 5 | Parser and utilities for non-technical user input. 6 | [Documentation (includes reference and cookbook) available here](./docs). 7 | 8 | ## Features 9 | 10 | - Parses quoted input with multiple quote types. 11 | - Parses flags and options with customizable parsing implementation. 12 | - Keeps trailing whitespace. 13 | - Always parses input by allowing some mis-inputs. 14 | - Includes a convenient wrapper to retrieve arguments. 15 | - Includes abstractions for creating an input loop. 16 | 17 | ## Example 18 | 19 | Check out the [cookbook](./docs/cookbook) for complete examples. 20 | First, import lexure: 21 | 22 | ```ts 23 | // TypeScript or ES Module 24 | import * as lexure from 'lexure'; 25 | 26 | // CommonJS 27 | const lexure = require('lexure'); 28 | ``` 29 | 30 | Consider some user input in the form of a command like so: 31 | 32 | ```ts 33 | const input = '!hello world "cool stuff" --foo --bar=baz a b c'; 34 | ``` 35 | 36 | We first tokenize the input string to individual tokens. 37 | As you can see, lexure supports custom open and close quotes for devices with special keyboards and other locales. 38 | 39 | The `!hello` part of the input is usually interpreted as a command, which the Lexer class can handle too. 40 | The remaining input is delayed as a function so that you can ignore the rest of the input if it is an invalid command. 41 | 42 | ```ts 43 | const lexer = new lexure.Lexer(input) 44 | .setQuotes([ 45 | ['"', '"'], 46 | ['“', '”'] 47 | ]); 48 | 49 | const res = lexer.lexCommand(s => s.startsWith('!') ? 1 : null); 50 | if (res == null) { 51 | // The input might be invalid. 52 | // You might do something else here. 53 | return; 54 | } 55 | 56 | const cmd = res[0]; 57 | >>> { value: 'hello', raw: 'hello', trailing: ' ' } 58 | 59 | const tokens = res[1](); 60 | >>> [ 61 | { value: 'world', raw: 'world', trailing: ' ' }, 62 | { value: 'cool stuff', raw: '"cool stuff"', trailing: ' ' }, 63 | { value: '--foo', raw: '--foo', trailing: ' ' }, 64 | { value: '--bar=baz', raw: '--bar=baz', trailing: ' ' }, 65 | { value: 'a', raw: 'a', trailing: ' ' }, 66 | { value: 'b', raw: 'b', trailing: ' ' }, 67 | { value: 'c', raw: 'c', trailing: '' } 68 | ] 69 | ``` 70 | 71 | Now, we can take the tokens and parse them into a structure. 72 | In lexure, you are free to describe how you want to match unordered arguments like flags. 73 | There are also several built-in strategies for common usecases. 74 | 75 | ```ts 76 | const parser = new lexure.Parser(tokens) 77 | .setUnorderedStrategy(lexure.longStrategy()); 78 | 79 | const out = parser.parse(); 80 | >>> { 81 | ordered: [ 82 | { value: 'world', raw: 'world', trailing: ' ' }, 83 | { value: 'cool stuff', raw: '"cool stuff"', trailing: ' ' }, 84 | { value: 'a', raw: 'a', trailing: ' ' }, 85 | { value: 'b', raw: 'b', trailing: ' ' }, 86 | { value: 'c', raw: 'c', trailing: '' } 87 | ], 88 | flags: Set { 'foo' }, 89 | options: Map { 'bar' => ['baz'] } 90 | } 91 | 92 | lexure.joinTokens(out.ordered) 93 | >>> 'world "cool stuff" a b c' 94 | ``` 95 | 96 | A wrapper class Args is available for us to retrieve the arguments from the output of the parser. 97 | It keeps track of what has already been retrieved and has several helpful methods. 98 | 99 | ```ts 100 | const args = new lexure.Args(out); 101 | 102 | args.single() 103 | >>> 'world' 104 | 105 | args.single() 106 | >>> 'cool stuff' 107 | 108 | args.findMap(x => x === 'c' ? lexure.some('it was a C') : lexure.none()) 109 | >>> { exists: true, value: 'it was a C' } 110 | 111 | args.many() 112 | >>> [ 113 | { value: 'a', raw: 'a', trailing: ' ' }, 114 | { value: 'b', raw: 'b', trailing: ' ' } 115 | ] 116 | 117 | args.flag('foo') 118 | >>> true 119 | 120 | args.option('bar') 121 | >>> 'baz' 122 | ``` 123 | 124 | Suppose we would like to prompt the user input, and retry until a valid input is given. 125 | lexure has various functions for this, in which the logic of an input loop is abstracted out. 126 | 127 | ```ts 128 | // Suppose we have access to this function that prompts the user. 129 | // You can imagine this as a CLI or chat bot. 130 | function prompt(): string | null { 131 | return '100'; 132 | } 133 | 134 | const result = lexure.loop1({ 135 | getInput() { 136 | const input = prompt(); 137 | if (input == null) { 138 | return lexure.fail('no input'); 139 | } else { 140 | return lexure.step(input); 141 | } 142 | } 143 | 144 | parse(s: string) { 145 | const n = Number(s); 146 | if (isNaN(n)) { 147 | return lexure.fail('cannot parse input'); 148 | } else { 149 | return lexure.finish(n); 150 | } 151 | } 152 | }); 153 | 154 | result 155 | >>> { success: true, value: 100 } 156 | ``` 157 | -------------------------------------------------------------------------------- /benchmarks/index.js: -------------------------------------------------------------------------------- 1 | const { benchmarks } = require('./wrapper'); 2 | const { 3 | Lexer, Parser, 4 | mergeOutputs, 5 | longStrategy, longShortStrategy, matchingStrategy 6 | } = require('..'); 7 | 8 | benchmarks(suite => { 9 | suite('lexing: lex with double quotes', add => { 10 | const string = Array.from({ length: 1000 }, i => { 11 | const quoted = i % 11 == 0; 12 | const trailing = ' '.repeat(i % 4 + 1); 13 | return (quoted ? '"hey"' : 'hey') + trailing; 14 | }).join(''); 15 | 16 | add('lexer.lex()', () => { 17 | const lexer = new Lexer(string).setQuotes(['"', '"']); 18 | lexer.lex(); 19 | }); 20 | }); 21 | 22 | suite('parsing: parse with longStrategy', add => { 23 | const tokens = Array.from({ length: 1000 }, i => { 24 | const flag = i % 11 == 0; 25 | const option = !flag && i % 13 == 0; 26 | const v = i % 26 == 0; 27 | const s = flag ? '--hey' : option ? '--hey=' + (v ? 'there' : '') : 'hey'; 28 | return { value: s, raw: s, trailing: ' ' }; 29 | }); 30 | 31 | add('parser.parse()', () => { 32 | const parser = new Parser(tokens).setUnorderedStrategy(longStrategy()); 33 | parser.parse(); 34 | }); 35 | }); 36 | 37 | suite('parsing: parse with longShortStrategy', add => { 38 | const tokens = Array.from({ length: 1000 }, i => { 39 | const flag = i % 11 == 0; 40 | const option = !flag && i % 13 == 0; 41 | const d = '-'.repeat(i % 2 + 1); 42 | const v = i % 26 == 0; 43 | const s = flag ? d + 'hey' : option ? d + 'hey=' + (v ? 'there' : '') : 'hey'; 44 | return { value: s, raw: s, trailing: ' ' }; 45 | }); 46 | 47 | add('parser.parse()', () => { 48 | const parser = new Parser(tokens).setUnorderedStrategy(longShortStrategy()); 49 | parser.parse(); 50 | }); 51 | }); 52 | 53 | suite('parsing: parse with matchingStrategy', add => { 54 | const tokens = Array.from({ length: 1000 }, i => { 55 | const flag = i % 11 == 0; 56 | const option = !flag && i % 13 == 0; 57 | const v = i % 26 == 0; 58 | const s = flag ? 'flag!' : option ? 'opti:' + (v ? 'x' : '') : 'hey'; 59 | return { value: s, raw: s, trailing: ' ' }; 60 | }); 61 | 62 | add('parser.parse()', () => { 63 | const parser = new Parser(tokens) 64 | .setUnorderedStrategy(matchingStrategy( 65 | { flag: ['flag!'] }, 66 | { opti: ['opti:'] }, 67 | 'en-US', 68 | { sensitivity: 'base' } 69 | )); 70 | 71 | parser.parse(); 72 | }); 73 | }); 74 | 75 | suite('parsing: collect output with mutations vs merging', add => { 76 | const tokens = Array.from( 77 | { length: 1000 }, 78 | () => ({ value: 'hey', raw: 'hey', trailing: ' ' }) 79 | ); 80 | 81 | add('parser.parse()', () => { 82 | const parser = new Parser(tokens); 83 | parser.parse(); 84 | }); 85 | 86 | add('mergeOutputs(...parser)', () => { 87 | const parser = new Parser(tokens); 88 | mergeOutputs(...parser); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /benchmarks/wrapper.js: -------------------------------------------------------------------------------- 1 | const Benchmark = require('benchmark'); 2 | 3 | function bold(s) { 4 | return `\x1B[1m${s}\x1B[0m`; 5 | } 6 | 7 | function benchmarks(fn) { 8 | const suites = []; 9 | fn((name, fn) => { 10 | suites.push([name, makeSuite(fn)]); 11 | }); 12 | 13 | for (const [name, suite] of suites) { 14 | console.log(`${bold('>>>>')} ${name}`); 15 | suite.run(); 16 | console.log(''); 17 | } 18 | 19 | console.log('Benchmarks completed.'); 20 | } 21 | 22 | function makeSuite(fn) { 23 | let n = 0; 24 | const suite = new Benchmark.Suite() 25 | .on('cycle', (event) => { 26 | console.log(` ${String(event.target)}`); 27 | }) 28 | .on('complete', function () { 29 | if (n > 1) { 30 | const name = this.filter('fastest').map('name'); 31 | console.log(` Fastest is ${bold(name)}`); 32 | } 33 | }); 34 | 35 | fn((name, fn) => { 36 | n++; 37 | suite.add(name, fn); 38 | }); 39 | 40 | return suite; 41 | } 42 | 43 | module.exports = { benchmarks }; 44 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # lexure 2 | 3 | The documentation for lexure is split into the [reference](./reference/) and the [cookbook](./cookbook/). 4 | The [reference](./reference/) is auto-generated from the source code using [TypeDoc](https://typedoc.org/). 5 | The [cookbook](./cookbook/) is a collection of hand-written guides and examples using lexure. 6 | -------------------------------------------------------------------------------- /docs/cookbook/complete-example.md: -------------------------------------------------------------------------------- 1 | # Complete Example 2 | 3 | This is a complete example of using lexure from the lexer to the arguments wrapper. 4 | 5 | ```ts 6 | // ----------- 7 | // myparser.ts 8 | // ----------- 9 | 10 | import { Lexer, Parser, Args, prefixedStrategy } from 'lexure'; 11 | 12 | /** 13 | * This function, given a command string, will return a pair which contains the name of the command and the arguments. 14 | * It will return null if the string does not start with the correct prefix of '!'. 15 | */ 16 | export function parseCommand(s: string): [string, Args] | null { 17 | const lexer = new Lexer(s) 18 | .setQuotes([ 19 | ['"', '"'], // Double quotes 20 | ['“', '”'], // Fancy quotes (on iOS) 21 | ["「", "」"] // Corner brackets (CJK) 22 | ]); // Add more as you see fit! 23 | 24 | const lout = lexer.lexCommand(s => s.startsWith('!') ? 1 : null); 25 | if (lout == null) { 26 | return null; 27 | } 28 | 29 | const [command, getTokens] = lout; 30 | const tokens = getTokens(); 31 | const parser = new Parser(tokens) 32 | .setUnorderedStrategy(prefixedStrategy( 33 | ['--', '-', '—'], // Various flag prefixes including em dash. 34 | ['=', ':'] // Various separators for options. 35 | )); 36 | 37 | const pout = parser.parse(); 38 | return [command.value, new Args(pout)]; 39 | } 40 | ``` 41 | 42 | We can now use our `parseCommand` function to write a command runner. 43 | 44 | ```ts 45 | // ------------ 46 | // mycommand.ts 47 | // ------------ 48 | 49 | import { parseCommand } from './myparser'; 50 | 51 | /** 52 | * This function runs a command that replies with a string. 53 | */ 54 | export function runCommand(s: string): string { 55 | const out = parseCommand(s); 56 | if (out == null) { 57 | return 'Not a command.'; 58 | } 59 | 60 | const [command, args] = out; 61 | if (command === 'add') { 62 | // These calls to `Args#single` can give a string or null. 63 | const x = args.single(); 64 | const y = args.single(); 65 | // Which means this could give NaN on bad inputs. 66 | const z = Number(x) + Number(y); 67 | return `The answer is ${z}.`; 68 | } else { 69 | return 'Not an implemented command.'; 70 | } 71 | } 72 | ``` 73 | 74 | See it in action! 75 | 76 | ```ts 77 | // -------- 78 | // index.ts 79 | // -------- 80 | 81 | import { runCommand } from './mycommand'; 82 | 83 | console.log(runCommand('!add 1 2')); 84 | >>> 'The answer is 3.' 85 | 86 | console.log(runCommand('!add 1 x')); 87 | >>> 'The answer is NaN.' 88 | 89 | console.log(runCommand('!foo')); 90 | >>> 'Not an implemented command.' 91 | 92 | console.log(runCommand('hello')); 93 | >>> 'Not a command.' 94 | ``` 95 | -------------------------------------------------------------------------------- /docs/cookbook/creating-unordered-strategies.md: -------------------------------------------------------------------------------- 1 | # Creating Unordered Strategies 2 | 3 | This is an example of using creating unordered strategies for parsing flags and options. 4 | There are the following built-in strategies: 5 | 6 | - `prefixedStrategy` for matching structured flags. 7 | - `longStrategy`, same as `prefixedStrategy(['--'], ['='])`. 8 | - `longShortStrategy`, same as `prefixedStrategy(['--', '-'], ['='])`. 9 | - `matchingStrategy` for matching strings in a locale-sensitive manner. 10 | 11 | Example of `prefixedStrategy` (and its derivatives): 12 | 13 | ```ts 14 | const tokens = new Lexer('--nix /dos').lex(); 15 | 16 | const nix = longShortStrategy(); 17 | const parserNix = new Parser(tokens).setUnorderedStrategy(nix); 18 | console.log(parser.parse()); 19 | >>> { 20 | ordered: ['/dos'], 21 | flags: Set { 'nix' }, 22 | options: Map {} 23 | } 24 | 25 | const dos = prefixedStrategy(['/'], [':']); 26 | const parserDos = new Parser(tokens).setUnorderedStrategy(dos); 27 | console.log(parser.parse()); 28 | >>> { 29 | ordered: ['--nix'], 30 | flags: Set { 'dos' }, 31 | options: Map {} 32 | } 33 | ``` 34 | 35 | The `matchingStrategy` takes in a record of words to use, and will give back the key. 36 | Example of `matchingStrategy`: 37 | 38 | ```ts 39 | const st = matchingStrategy({ foo: ['foo', 'fooooo'] }, { bar: ['bar:'] }); 40 | console.log(st.matchFlag('fooooo')); 41 | >>> 'foo' 42 | 43 | console.log(st.matchCompactOption('bar:baz')); 44 | >>> ['bar', 'baz'] 45 | ``` 46 | 47 | This strategy supports [locales and collator options](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator); you can use this to do case-insensitive matching, among other kinds of matching. 48 | 49 | ```ts 50 | const st = matchingStrategy({ foo: ['foo', 'fooooo'] }, { bar: ['bar:'] }, 'en-US', { sensitivity: 'base' }); 51 | console.log(st.matchFlag('foOoOo')); 52 | >>> 'foo' 53 | 54 | console.log(st.matchCompactOption('bár:baz')); 55 | >>> ['bar', 'baz'] 56 | ``` 57 | 58 | ## Augment an Existing Strategy 59 | 60 | We can augment an existing strategy using the `mapKeys` and the `renameKeys` functions. 61 | The `mapKeys` function lets us transform all the keys or remove them from being matched. 62 | We can use it to lowercase all the keys for example. 63 | 64 | ```ts 65 | const st = mapKeys(longStrategy(), k => k.toLowerCase()); 66 | console.log(st.matchFlag('--FOO')); 67 | >>> 'foo' 68 | ``` 69 | 70 | The `renameKeys` function is best used to map aliases of flags and options to one name. 71 | Like `matchingStrategy`, it support locales and collator options. 72 | Below, we tell it to map "flag" and "f" to "flag", keep keys that are not found, and be base-sensitive when comparing. 73 | 74 | ```ts 75 | const st = renameKeys(longStrategy(), { flag: ['flag', 'f'] }, true, 'en-US', { sensitivity: 'base' }); 76 | console.log(st.matchFlag('--F')); 77 | >>> 'flag' 78 | ``` 79 | 80 | ## Custom Strategies 81 | 82 | We will make a strategy where flags and options are specified by an exclamation mark and an equal sign. 83 | It wil be equivalent to `prefixedStrategy(['!'], ['='])`. 84 | 85 | ```ts 86 | // An unordered strategy is made of three functions. 87 | // They match either flags, options, or 'compact' options. 88 | // For best results, if one matcher accepts a string, none of the other matchers should. 89 | const screamingStrategy: UnorderedStrategy = { 90 | // This one returns the name of the flag. 91 | // e.g. !flag 92 | matchFlag(s: string): string | null { 93 | return s.startsWith('!') && !s.includes('=') ? s.slice(1) : null; 94 | }, 95 | 96 | // This one returns the name of the option. 97 | // An option does not have the value with it. 98 | // e.g. !option= 99 | matchOption(s: string): string | null { 100 | return s.startsWith('!') && s.endsWith('=') 101 | ? s.slice(1, -1) 102 | : null; 103 | } 104 | 105 | // This one returns the name of the option as well as the value. 106 | // For your users, you should make this almost identical to the above. 107 | // e.g. !option=value 108 | matchCompactOption(s: string): [string, string] | null { 109 | if (!s.startsWith('!')) { 110 | return null; 111 | } 112 | 113 | const i = s.indexOf('='); 114 | if (i === -1) { 115 | return null; 116 | } 117 | 118 | return [s.slice(1, i), s.slice(i)]; 119 | } 120 | }; 121 | ``` 122 | 123 | You can now use this with a `Parser`. 124 | 125 | ```ts 126 | const tokens = new Lexer('!flag !opt1 1 !opt1=2').lex(); 127 | const parser = new Parser(tokens).setUnorderedStrategy(screamingStrategy); 128 | console.log(parser.parse()); 129 | >>> { 130 | ordered: [], 131 | flags: Set { 'flag' }, 132 | options: Map { 'opt1' => '1', 'opt2' => '2' } 133 | } 134 | ``` 135 | -------------------------------------------------------------------------------- /docs/cookbook/parsing-with-loops.md: -------------------------------------------------------------------------------- 1 | # Parsing With Loops 2 | 3 | This is an example of using the loop utilities alongside Args. 4 | In particular, we will combine `loopAsync`, `loop1Async`, and `Args#singleParseAsync`. 5 | Though, note that everything here applies to the non-async variants too. 6 | 7 | To facilitate the example, assume that the following functions exists in this made-up library `talking`: 8 | 9 | ```ts 10 | // ------- 11 | // talking 12 | // ------- 13 | 14 | /** 15 | * Gets input from the user you are talking to. 16 | * Returns null if they do not answer. 17 | */ 18 | export function ask(): Promise; 19 | 20 | /** 21 | * Sends a message to the user. 22 | */ 23 | export function say(s: string): Promise; 24 | ``` 25 | 26 | Essentially, we want the user to give use an input, and if that input is invalid, we will prompt them for it. 27 | An example conversation could be like so: 28 | 29 | ``` 30 | User: !add 1 b 31 | You: Invalid input b, please give a valid number. 32 | User: ok 33 | You: Invalid input ok, please give a valid number. 34 | User: 3 35 | You: That adds to 4. 36 | ``` 37 | 38 | And, if they try too many times: 39 | 40 | ``` 41 | User: !add 1 b 42 | You: Invalid input b, please give a valid number. 43 | User: bad 44 | You: Invalid input bad, please give a valid number. 45 | User: badder 46 | You: Invalid input badder, please give a valid number. 47 | User: baddest 48 | You: Invalid input baddest, please give a valid number. 49 | You: You took too many tries. 50 | ``` 51 | 52 | Now, we will write functions on top of `loopAsync` and `loop1Async`. 53 | 54 | ```ts 55 | // ---------- 56 | // helpers.ts 57 | // ---------- 58 | 59 | import { Args, Result, LoopStrategyAsync, loopAsync, loop1Async, err, ok, step, fail, finish } from 'lexure'; 60 | import { ask, say } from 'talking'; 61 | 62 | /** 63 | * An error that can occur when parsing. 64 | */ 65 | export enum ParseError { 66 | PARSE_FAILURE, // A general parse error e.g. invalid input. 67 | NO_INPUT_GIVEN, // No input was given. 68 | TOO_MANY_TRIES // Took too many tries to give a good input. 69 | } 70 | 71 | /** 72 | * This function will reply with a message based on the error. 73 | */ 74 | export function sayError(e: ParseError): Promise { 75 | switch (e) { 76 | case ParseError.PARSE_FAILURE: 77 | return say('Invalid input.'); 78 | 79 | case ParseError.NO_INPUT_GIVEN: 80 | return say('You did not give a value in time.'); 81 | 82 | case ParseError.TOO_MANY_TRIES: 83 | return say('You took too many tries.'); 84 | } 85 | } 86 | 87 | // Type synonyms, because it gets long. 88 | type Parser = (x: string) => Result; 89 | type ParserAsync = (x: string) => Promise>; 90 | 91 | /** 92 | * This function creates a loop strategy that has inputs `string`, outputs `T`, and has errors `ParseError`. 93 | * A loop strategy is basically a bunch of functions that define the behavior of our input loop. 94 | * This includes where to get input, when to exit, and so on. 95 | */ 96 | function makeLoopStrategy(expected: string, runParser: Parser): LoopStrategyAsync { 97 | // To count the number of retries. 98 | let retries = 0; 99 | 100 | // Each function must return a `LoopAction`, which is one of `step`, `fail`, or `finish`. 101 | return { 102 | // This function will be called to prompt for input. 103 | // When using a loop strategy with `loopAsync`, this gets skipped the first time since we have an initial input. 104 | // On the other hand, `loop1Async` will call this the first time around. 105 | async getInput() { 106 | // Allow only three tries. 107 | if (retries >= 3) { 108 | // `fail` exits us out of the loop with an error. 109 | return fail(ParseError.TOO_MANY_TRIES); 110 | } 111 | 112 | const s = await ask(); 113 | retries++; 114 | if (s == null) { 115 | return fail(ParseError.NO_INPUT_GIVEN); 116 | } 117 | 118 | // `step` tells the loop to continue with the input `s`. 119 | return step(s); 120 | }, 121 | 122 | // This function is called to parse the input from `getInput`. 123 | // It is also called to parse the initial input from `loopAsync`. 124 | async parse(s: string) { 125 | const res = runParser(s); 126 | if (res.success) { 127 | // `finish` exits us out the loop with the resulting value. 128 | return finish(res.value); 129 | } 130 | 131 | // We will tell the user their problem here. 132 | // We don't actually care about the error value from `res`, though you can do that in your design. 133 | // For a more robust design, you can imagine passing custom formatters into `makeLoopStrategy` instead. 134 | await say(`Invalid input ${s}, please give a valid ${expected}:`); 135 | 136 | // This `fail` does not exit the loop immediately, it goes back to `getInput`. 137 | return fail(ParseError.PARSE_FAILURE); 138 | } 139 | 140 | // You might notice that `fail` acts differently in `getInput` and `parse`. 141 | // There are two more optional functions, `onInputError` and `onParseError`, that changes that behavior. 142 | // That is also where the values passed to `fail` are used. 143 | // They won't be used here, since they are for more complicated loops. 144 | }; 145 | } 146 | 147 | /** 148 | * This function wraps an existing parser with a loop, allowing us to prompt until the user inputs correctly. 149 | * In other words, it is a higher-order function. 150 | * We will also take in a string, for the expected type of value. 151 | */ 152 | export function loopParse(expected: string, runParser: Parser): ParserAsync { 153 | // We return a `ParserAsync`, which is a function. 154 | return (init: string) => loopAsync(makeLoopStrategy(expected, runParser)); 155 | } 156 | 157 | /** 158 | * This function is the same as `loopParse`, but wraps `loop1Async` instead. 159 | * Since `loop1Async` does not take in an initial input, we won't be using this with `Args`. 160 | */ 161 | export async function loop1Parse(expected: string, runParser: Parser): Promise> { 162 | await say(`No input given, please give a valid ${expected}.`); 163 | return loop1Async(makeLoopStrategy(expected, runParser)); 164 | } 165 | 166 | /** 167 | * A function that combines the above with `Args#singleParseAsync`. 168 | */ 169 | export async function singleParseWithLoop( 170 | args: Args, 171 | expected: string, 172 | parser: Parser 173 | ): Promise> { 174 | // `Args#singleParseAsync` takes the next ordered token and passes it to a parser. 175 | // If there are tokens to parse, we will return the result of the loop and parser, 176 | // and `args` will consider the token to be used. 177 | // If there are no tokens left, `Args#singleParseAsync` returns null, so `??` will move on to `loop1Parse`. 178 | // That is, there was no input in the first place so we should prompt for it. 179 | return await args.singleParseAsync(loopParse(expected, parser)) 180 | ?? await loop1Parse(expected, parser); 181 | } 182 | ``` 183 | 184 | Now we can use our new functions. 185 | 186 | ```ts 187 | // ------ 188 | // add.ts 189 | // ------ 190 | 191 | import { Args, Result, ok, err } from 'lexure'; 192 | import { ask, say } from 'talking'; 193 | import { ParseError, singleParseWithLoop, sayError } from './helpers'; 194 | 195 | export async function addCommand(args: Args): Promise { 196 | const n1 = await singleParseWithLoop(args, 'number', parseNumber); 197 | if (!n1.success) { 198 | return sayError(n1.error); 199 | } 200 | 201 | const n2 = await singleParseWithLoop(args, 'number', parseNumber); 202 | if (!n2.success) { 203 | return sayError(n2.error); 204 | } 205 | 206 | const z = n1.value + n2.value; 207 | return say(`That adds to ${z}.`); 208 | } 209 | 210 | function parseNumber(x: string): Result { 211 | const n = Number(x); 212 | return isNaN(n) ? err(ParseError.PARSE_FAILURE) : ok(n); 213 | } 214 | ``` 215 | -------------------------------------------------------------------------------- /docs/cookbook/rest-arguments.md: -------------------------------------------------------------------------------- 1 | # Rest Arguments 2 | 3 | This is an example of using `Arg#many` and `joinTokens` to get the rest of the arguments. 4 | 5 | ```ts 6 | // ------ 7 | // say.ts 8 | // ------ 9 | 10 | import { Args, joinTokens } from 'lexure'; 11 | 12 | export function sayCommand(args: Args): [string, string] { 13 | const tokens = args.many(); 14 | // This joins the tokens with the trailing space and raw values. 15 | const rest1 = joinTokens(tokens); 16 | // This joins the tokens with a space and unquoted values. 17 | const rest2 = joinTokens(tokens, ' ', false); 18 | return [rest1, rest2]; 19 | } 20 | ``` 21 | 22 | Now the function can be used. 23 | 24 | ```ts 25 | // -------- 26 | // index.ts 27 | // -------- 28 | 29 | // To save on time, assume `args` is from '!say text "quoted text"'. 30 | // Note the three spaces and the quotes. 31 | 32 | console.log(sayCommand(args)); 33 | >>> [ 34 | 'text "quoted text"', // Trailing space and raw values. 35 | 'text quoted text' // A space and unquoted values. 36 | ] 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/cookbook/subcommands.md: -------------------------------------------------------------------------------- 1 | # Subcommands 2 | 3 | This is an example of subcommands, i.e. commands that branch out to different commands based on an argument. 4 | In this example we will have the root command be `root`, and subcommands `sub1` and `sub2`. 5 | 6 | ```ts 7 | function rootCommand(tokens: Token[]): string { 8 | // Remove the first token and get its value if it exists. 9 | const sub = tokens.shift()?.value ?? null; 10 | switch (sub) { 11 | case 'sub1': 12 | return sub1Command(tokens); 13 | case 'sub2': 14 | return sub2Command(tokens); 15 | case null: 16 | // This occurs when there are no tokens left, i.e. `sub == null`. 17 | return 'No subcommand given.'; 18 | default: 19 | // This occurs when we are given a string that does not match the above. 20 | return 'Unknown subcommand.'; 21 | } 22 | } 23 | 24 | function sub1Command(tokens: Token[]): string { 25 | const args = new Args(new Parser(tokens).parse()); 26 | // ... 27 | } 28 | 29 | function sub2Command(tokens: Token[]): string { 30 | const args = new Args(new Parser(tokens).parse()); 31 | // ... 32 | } 33 | ``` 34 | 35 | ## Passing Tokens 36 | 37 | Note that in the example above, we were passing `Token[]` around instead of `Args`. 38 | This is for two reasons: 39 | 40 | 1. Each subcommand may have their own flag parsing strategy. 41 | This is especially relevant for `matchingStrategy`, which may not have delimiters. 42 | 43 | 2. `Args` is not sensitive to the possibility of flags in between ordered tokens. 44 | Consider the input `!root --flag sub1`, which `args.single()` will retrieve `sub1`. 45 | This may not be expected behavior, and the `sub1` command will also have `--flag` if the same `Args` instance is passed to it. 46 | -------------------------------------------------------------------------------- /docs/cookbook/using-option-and-result.md: -------------------------------------------------------------------------------- 1 | # Using Option and Result 2 | 3 | Within lexure, `Option` and `Result` are often used, usually around the argument retriever and the loop functions. 4 | These types are often known as algebraic data types or tagged union types, which you may be unfamiliar with. 5 | Thankfully though, most of the time you will only construct them and not actually manipulate them. 6 | This guide will go through why they are used and how to use them. 7 | 8 | ## The Semipredicate Problem 9 | 10 | This is a fancy term for the fact that the type `T | U` can lose information if `T` can contain `U` or vice-versa. 11 | Suppose for example, that we have this function to return a value from a map or a default value if it is not there: 12 | 13 | ```ts 14 | function defaultGet(map: Map, key: K, defaultValue: V): V { 15 | // Note that `Map#get` returns undefined when the key isn't a member. 16 | return map.get(key) ?? defaultValue; 17 | } 18 | ``` 19 | 20 | This looks innocuous at first, but when `V` is substituted with a type that contains `undefined`, we see a problem. 21 | 22 | ```ts 23 | const map: Map = new Map(); 24 | map.set('abc', 1); 25 | map.set('xyz', undefined); 26 | 27 | console.log(defaultGet(map, 'abc', 100)); 28 | >>> 1 29 | 30 | // This should be undefined! 31 | console.log(defaultGet(map, 'xyz', 100)); 32 | >>> 2 33 | ``` 34 | 35 | The solution to this are tagged union types like `Option` and `Result`, which explicitly state with type is which. 36 | 37 | ## Examples of Usage 38 | 39 | In lexure, the `Option` type replaces `T | null` with either: 40 | 41 | - `Some = { exists: true, value: T }` 42 | - `None = { exists: false }` 43 | 44 | Similarly, the `Result` type replaces `T | E` with either: 45 | 46 | - `Ok = { success: true, value: T }` 47 | - `Err = { success: false, error: E }` 48 | 49 | Cases of `T | null` where `T` is not generic e.g. `string | null` are not replaced with `Option`. 50 | This is because the operators `??` and `?.` are very convenient when working with these kinds of types. 51 | 52 | With `Args#singleMap` and related methods, the usual pattern is like the following. 53 | Note we can use `some` and `none` to construct an `Option`, and `ok` and `err` to construct a `Result`. 54 | 55 | ```ts 56 | const x = args.singleMap(s => { 57 | if (valid) { 58 | return some(s); 59 | } 60 | 61 | return none(); 62 | }); 63 | 64 | if (x.exists) { 65 | doSomething(x.value); 66 | } else { 67 | doSomethingElse(); 68 | } 69 | 70 | const y = args.singleParse(s => { 71 | if (valid) { 72 | return some(s); 73 | } 74 | 75 | return err('an error occurred'); 76 | }); 77 | 78 | if (y.success) { 79 | doSomething(y.value); 80 | } else { 81 | doSomethingElse(y.error); 82 | } 83 | ``` 84 | 85 | In the case where you have an existing function that gives `T | null`, you can convert it to an `Option` or `Result`: 86 | 87 | ```ts 88 | function number(x: string): number | null { 89 | const n = Number(x); 90 | if (isNaN(n)) { 91 | return null; 92 | } 93 | 94 | return x; 95 | } 96 | 97 | // `maybeOption` turns null and undefined into `None`. 98 | const x = args.singleMap(x => maybeOption(number(x))); 99 | 100 | // `maybeResult` turns null and undefined into an `Err` containing your error. 101 | const y = args.singleParse(x => maybeResult(number(x), 'not a number')); 102 | ``` 103 | 104 | There are also various conversion functions between `Option` and `Result`: 105 | 106 | ```ts 107 | console.log(okToSome(ok(1))); 108 | >>> { exists: true, value: 1 } 109 | 110 | console.log(okToSome(err('bad'))); 111 | >>> { exists: false } 112 | 113 | console.log(someToOk(some(1), 'was none')); 114 | >>> { success: true, value: 1 } 115 | 116 | console.log(someToOk(none(1), 'was none')); 117 | >>> { success: false, error: 'was none' } 118 | ``` 119 | 120 | ## Loop Actions 121 | 122 | The loop strategies require that you return a `LoopAction`. 123 | It too is a tagged union type like `Option` or `Result`; specifically, it is a union of three types: 124 | 125 | - `Step = { action: 'step', value: A }` 126 | - `Finish = { action: 'finish', value: B }` 127 | - `Fail = { action: 'fail', error: E }` 128 | 129 | These have special meanings when used inside a loop strategy to control the behavior of an input loop. 130 | Do read the documentation and the [Parsing With Loops](./parsing-with-loops.md) guide to see what they do. 131 | 132 | They can be constructed with `step`, `finish`, and `fail` respectively. 133 | There are conversion functions from a `Option` or a `Result` to a `LoopAction` as well. 134 | -------------------------------------------------------------------------------- /docs/reference/classes/args.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [Args](args.md) 2 | 3 | # Class: Args 4 | 5 | A wrapper around the parser output for retrieving command arguments. 6 | 7 | ## Hierarchy 8 | 9 | * **Args** 10 | 11 | ## Implements 12 | 13 | * IterableIterator\ 14 | 15 | ## Index 16 | 17 | ### Constructors 18 | 19 | * [constructor](args.md#constructor) 20 | 21 | ### Properties 22 | 23 | * [parserOutput](args.md#readonly-parseroutput) 24 | * [state](args.md#state) 25 | 26 | ### Accessors 27 | 28 | * [finished](args.md#finished) 29 | * [length](args.md#length) 30 | * [remaining](args.md#remaining) 31 | 32 | ### Methods 33 | 34 | * [next](args.md#next) 35 | * [single](args.md#single) 36 | * [singleMap](args.md#singlemap) 37 | * [singleMapAsync](args.md#singlemapasync) 38 | * [singleParse](args.md#singleparse) 39 | * [singleParseAsync](args.md#singleparseasync) 40 | * [singleFromEnd](args.md#singlefromend) 41 | * [many](args.md#many) 42 | * [manyFromEnd](args.md#manyfromend) 43 | * [flag](args.md#flag) 44 | * [option](args.md#option) 45 | * [options](args.md#options) 46 | * [findMap](args.md#findmap) 47 | * [findMapAsync](args.md#findmapasync) 48 | * [findParse](args.md#findparse) 49 | * [findParseAsync](args.md#findparseasync) 50 | * [filterMap](args.md#filtermap) 51 | * [filterMapAsync](args.md#filtermapasync) 52 | * [save](args.md#save) 53 | * [restore](args.md#restore) 54 | 55 | ## Constructors 56 | 57 | ### constructor 58 | 59 | * **new Args**(parserOutput: [ParserOutput](../interfaces/parseroutput.md)): [Args](args.md) 60 | 61 | **Parameters:** 62 | 63 | Name | Type | Description | 64 | ------ | ------ | ------ | 65 | parserOutput | [ParserOutput](../interfaces/parseroutput.md) | The parser output. | 66 | 67 | **Returns:** [Args](args.md) 68 | 69 | ## Properties 70 | 71 | ### readonly parserOutput 72 | 73 | * **parserOutput**: [ParserOutput](../interfaces/parseroutput.md) 74 | 75 | The parser output. 76 | 77 | ___ 78 | 79 | ### state 80 | 81 | * **state**: [ArgsState](../interfaces/argsstate.md) 82 | 83 | The state of this instance. 84 | 85 | ## Accessors 86 | 87 | ### finished 88 | 89 | * **get finished**(): boolean 90 | 91 | Whether all ordered tokens have been used. 92 | 93 | **Returns:** boolean 94 | 95 | ___ 96 | 97 | ### length 98 | 99 | * **get length**(): number 100 | 101 | The amount of ordered tokens. 102 | 103 | **Returns:** number 104 | 105 | ___ 106 | 107 | ### remaining 108 | 109 | * **get remaining**(): number 110 | 111 | The amount of remaining ordered tokens. 112 | 113 | **Returns:** number 114 | 115 | ## Methods 116 | 117 | ### next 118 | 119 | * **next**(): IteratorResult\ 120 | 121 | Gets the next ordered argument. 122 | 123 | **Returns:** IteratorResult\ 124 | 125 | An iterator result containing a string. 126 | 127 | ___ 128 | 129 | ### single 130 | 131 | * **single**(): string | null 132 | 133 | Retrieves the value of the next unused ordered token. 134 | That token will now be consider used. 135 | 136 | ```ts 137 | // Suppose args are from '1 2 3'. 138 | console.log(args.single()); 139 | >>> '1' 140 | 141 | console.log(args.single()); 142 | >>> '2' 143 | 144 | console.log(args.single()); 145 | >>> '3' 146 | 147 | console.log(args.single()); 148 | >>> null 149 | ``` 150 | 151 | **Returns:** string | null 152 | 153 | The value if there are tokens left. 154 | 155 | ___ 156 | 157 | ### singleMap 158 | 159 | * **singleMap**\<**T**\>(f: function, useAnyways: boolean): [Option](../README.md#option)\ | null 160 | 161 | Retrieves the value of the next unused ordered token, but only if it could be transformed. 162 | That token will now be consider used if the transformation succeeds. 163 | 164 | ```ts 165 | // Suppose args are from '1 2 3'. 166 | const parse = (x: string) => { 167 | const n = Number(x); 168 | return isNaN(n) ? none() : some(n); 169 | }; 170 | 171 | console.log(args.singleMap(parse)); 172 | >>> { exists: true, value: 1 } 173 | ``` 174 | 175 | **Type parameters:** 176 | 177 | * **T** 178 | 179 | Output type. 180 | 181 | **Parameters:** 182 | 183 | * **f**: function 184 | 185 | Gives an option of either the resulting value, or nothing if failed. 186 | 187 | * (x: string): [Option](../README.md#option)\ 188 | 189 | **Parameters:** 190 | 191 | Name | Type | 192 | ------ | ------ | 193 | x | string | 194 | 195 | *default value **useAnyways**: boolean= false 196 | 197 | Whether to consider the token used even if the transformation fails; defaults to false. 198 | 199 | **Returns:** [Option](../README.md#option)\ | null 200 | 201 | The value if the transformation succeeds. 202 | If there are no tokens left, null is returned. 203 | 204 | ___ 205 | 206 | ### singleMapAsync 207 | 208 | * **singleMapAsync**\<**T**\>(f: function, useAnyways: boolean): Promise\<[Option](../README.md#option)\ | null\> 209 | 210 | Retrieves the value of the next unused ordered token, but only if it could be transformed. 211 | This variant of the function is asynchronous using `Promise`. 212 | That token will now be consider used if the transformation succeeds. 213 | 214 | **Type parameters:** 215 | 216 | * **T** 217 | 218 | Output type. 219 | 220 | **Parameters:** 221 | 222 | * **f**: function 223 | 224 | Gives an option of either the resulting value, or nothing if failed. 225 | 226 | * (x: string): Promise\<[Option](../README.md#option)\\> 227 | 228 | **Parameters:** 229 | 230 | Name | Type | 231 | ------ | ------ | 232 | x | string | 233 | 234 | *default value **useAnyways**: boolean= false 235 | 236 | Whether to consider the token used even if the transformation fails; defaults to false. 237 | 238 | **Returns:** Promise\<[Option](../README.md#option)\ | null\> 239 | 240 | The value if the transformation succeeds. 241 | If there are no tokens left, null is returned. 242 | 243 | ___ 244 | 245 | ### singleParse 246 | 247 | * **singleParse**\<**T**, **E**\>(f: function, useAnyways: boolean): [Result](../README.md#result)\ | null 248 | 249 | Retrieves the value of the next unused ordered token, but only if it could be transformed. 250 | That token will now be consider used if the transformation succeeds. 251 | This is a variant of {@linkcode Args#singleMap} that allows for a Result to be returned. 252 | 253 | ```ts 254 | // Suppose args are from '1 a'. 255 | const parse = (x: string) => { 256 | const n = Number(x); 257 | return isNaN(n) ? err(x + ' is not a number') : ok(n); 258 | }; 259 | 260 | console.log(args.singleParse(parse)); 261 | >>> { success: true, value: 1 } 262 | 263 | console.log(args.singleParse(parse)); 264 | >>> { success: false, error: 'a is not a number' } 265 | 266 | console.log(args.singleParse(parse)); 267 | >>> null 268 | ``` 269 | 270 | **Type parameters:** 271 | 272 | * **T** 273 | 274 | Output type. 275 | 276 | * **E** 277 | 278 | Error type. 279 | 280 | **Parameters:** 281 | 282 | * **f**: function 283 | 284 | Gives a result of either the resulting value, or an error. 285 | 286 | * (x: string): [Result](../README.md#result)\ 287 | 288 | **Parameters:** 289 | 290 | Name | Type | 291 | ------ | ------ | 292 | x | string | 293 | 294 | *default value **useAnyways**: boolean= false 295 | 296 | Whether to consider the token used even if the transformation fails; defaults to false. 297 | 298 | **Returns:** [Result](../README.md#result)\ | null 299 | 300 | The result which succeeds if the transformation succeeds. 301 | If there are no tokens left, null is returned. 302 | 303 | ___ 304 | 305 | ### singleParseAsync 306 | 307 | * **singleParseAsync**\<**T**, **E**\>(f: function, useAnyways: boolean): Promise\<[Result](../README.md#result)\ | null\> 308 | 309 | Retrieves the value of the next unused ordered token, but only if it could be transformed. 310 | That token will now be consider used if the transformation succeeds. 311 | This variant of the function is asynchronous using `Promise`. 312 | This is a variant of {@linkcode Args#singleMapAsync} that allows for a Result to be returned. 313 | 314 | **Type parameters:** 315 | 316 | * **T** 317 | 318 | Output type. 319 | 320 | * **E** 321 | 322 | Error type. 323 | 324 | **Parameters:** 325 | 326 | * **f**: function 327 | 328 | Gives a result of either the resulting value, or an error. 329 | 330 | * (x: string): Promise\<[Result](../README.md#result)\\> 331 | 332 | **Parameters:** 333 | 334 | Name | Type | 335 | ------ | ------ | 336 | x | string | 337 | 338 | *default value **useAnyways**: boolean= false 339 | 340 | Whether to consider the token used even if the transformation fails; defaults to false. 341 | 342 | **Returns:** Promise\<[Result](../README.md#result)\ | null\> 343 | 344 | The result which succeeds if the transformation succeeds. 345 | If there are no tokens left, null is returned. 346 | 347 | ___ 348 | 349 | ### singleFromEnd 350 | 351 | * **singleFromEnd**(): string | null 352 | 353 | Retrieves the value of the next unused ordered token from the end. 354 | That token will now be consider used. 355 | 356 | ```ts 357 | // Suppose args are from '1 2 3'. 358 | console.log(args.singleFromEnd()); 359 | >>> '3' 360 | 361 | console.log(args.singleFromEnd()); 362 | >>> '2' 363 | 364 | console.log(args.singleFromEnd()); 365 | >>> '1' 366 | 367 | console.log(args.singleFromEnd()); 368 | >>> null 369 | ``` 370 | 371 | **Returns:** string | null 372 | 373 | The value if there are tokens left. 374 | 375 | ___ 376 | 377 | ### many 378 | 379 | * **many**(limit: number, from: number): [Token](../interfaces/token.md)[] 380 | 381 | Retrieves many unused tokens. 382 | 383 | ```ts 384 | // Suppose args are from '1 2 3'. 385 | const xs = args.many(); 386 | console.log(joinTokens(xs)); 387 | >>> '1 2 3' 388 | 389 | // Suppose args are from '1 2 3'. 390 | const xs = args.many(2); 391 | console.log(joinTokens(xs)); 392 | >>> '1 2' 393 | ``` 394 | 395 | **Parameters:** 396 | 397 | Name | Type | Default | Description | 398 | ------ | ------ | ------ | ------ | 399 | limit | number | Infinity | The limit on the amount of tokens to retrieve; defaults to infinite. | 400 | from | number | this.state.position | Where to start looking for tokens; defaults to current position. | 401 | 402 | **Returns:** [Token](../interfaces/token.md)[] 403 | 404 | The tokens. 405 | 406 | ___ 407 | 408 | ### manyFromEnd 409 | 410 | * **manyFromEnd**(limit: number, from: number): [Token](../interfaces/token.md)[] 411 | 412 | Retrieves many unused tokens from the end. 413 | Note that the order of retrieved tokens will be the same order as in the ordered tokens list. 414 | 415 | ```ts 416 | // Suppose args are from '1 2 3'. 417 | const xs = args.manyFromEnd(); 418 | console.log(joinTokens(xs)); 419 | >>> '1 2 3' 420 | 421 | // Suppose args are from '1 2 3'. 422 | const xs = args.manyFromEnd(2); 423 | console.log(joinTokens(xs)); 424 | >>> '2 3' 425 | ``` 426 | 427 | **Parameters:** 428 | 429 | Name | Type | Default | Description | 430 | ------ | ------ | ------ | ------ | 431 | limit | number | Infinity | The limit on the amount of tokens to retrieve; defaults to infinite. | 432 | from | number | this.state.positionFromEnd | Where to start looking for tokens; defaults to current position from end. | 433 | 434 | **Returns:** [Token](../interfaces/token.md)[] 435 | 436 | The tokens. 437 | 438 | ___ 439 | 440 | ### flag 441 | 442 | * **flag**(...keys: string[]): boolean 443 | 444 | Checks if a flag was given. 445 | 446 | ```ts 447 | // Suppose args are from '--f --g'. 448 | console.log(args.flag('f')); 449 | >>> true 450 | 451 | console.log(args.flag('g', 'h')); 452 | >>> true 453 | 454 | console.log(args.flag('h')); 455 | >>> false 456 | ``` 457 | 458 | **Parameters:** 459 | 460 | Name | Type | Description | 461 | ------ | ------ | ------ | 462 | ...keys | string[] | The name(s) of the flag. | 463 | 464 | **Returns:** boolean 465 | 466 | Whether the flag was given. 467 | 468 | ___ 469 | 470 | ### option 471 | 472 | * **option**(...keys: string[]): string | null 473 | 474 | Gets the last value of an option. 475 | 476 | ```ts 477 | // Suppose args are from '--a=1 --b=2 --c=3'. 478 | console.log(args.option('a')); 479 | >>> '1' 480 | 481 | console.log(args.option('b', 'c')); 482 | >>> '2' 483 | 484 | console.log(args.option('d')); 485 | >>> null 486 | ``` 487 | 488 | **Parameters:** 489 | 490 | Name | Type | Description | 491 | ------ | ------ | ------ | 492 | ...keys | string[] | The name(s) of the option. | 493 | 494 | **Returns:** string | null 495 | 496 | The last value of the option if it was given. 497 | When there are multiple names, the last value of the first found name is given. 498 | 499 | ___ 500 | 501 | ### options 502 | 503 | * **options**(...keys: string[]): string[] | null 504 | 505 | Gets all the values of an option. 506 | 507 | ```ts 508 | // Suppose args are from '--a=1 --a=1 --b=2 --c=3'. 509 | console.log(args.options('a')); 510 | >>> ['1', '1'] 511 | 512 | console.log(args.option('b', 'c')); 513 | >>> ['2', '3'] 514 | 515 | console.log(args.option('d')); 516 | >>> null 517 | ``` 518 | 519 | **Parameters:** 520 | 521 | Name | Type | Description | 522 | ------ | ------ | ------ | 523 | ...keys | string[] | The name(s) of the option. | 524 | 525 | **Returns:** string[] | null 526 | 527 | The values of the option if it was given. 528 | 529 | ___ 530 | 531 | ### findMap 532 | 533 | * **findMap**\<**T**\>(f: function, from: number): [Option](../README.md#option)\ 534 | 535 | Finds and retrieves the first unused token that could be transformed. 536 | That token will now be consider used. 537 | 538 | ```ts 539 | // Suppose args are from '1 2 3'. 540 | const parse = (x: string) => { 541 | const n = Number(x); 542 | return isNaN(n) || n === 1 ? none() : some(n); 543 | }; 544 | 545 | console.log(args.findMap(parse)); 546 | >>> { exists: true, value: 2 } 547 | ``` 548 | 549 | **Type parameters:** 550 | 551 | * **T** 552 | 553 | Output type. 554 | 555 | **Parameters:** 556 | 557 | * **f**: function 558 | 559 | Gives an option of either the resulting value, or nothing if failed. 560 | 561 | * (x: string): [Option](../README.md#option)\ 562 | 563 | **Parameters:** 564 | 565 | Name | Type | 566 | ------ | ------ | 567 | x | string | 568 | 569 | *default value **from**: number= this.state.position 570 | 571 | Where to start looking for tokens; defaults to current position. 572 | 573 | **Returns:** [Option](../README.md#option)\ 574 | 575 | The resulting value if it was found. 576 | 577 | ___ 578 | 579 | ### findMapAsync 580 | 581 | * **findMapAsync**\<**T**\>(f: function, from: number): Promise\<[Option](../README.md#option)\\> 582 | 583 | Finds and retrieves the first unused token that could be transformed. 584 | This variant of the function is asynchronous using `Promise`. 585 | That token will now be consider used. 586 | 587 | **Type parameters:** 588 | 589 | * **T** 590 | 591 | Output type. 592 | 593 | **Parameters:** 594 | 595 | * **f**: function 596 | 597 | Gives an option of either the resulting value, or nothing if failed. 598 | 599 | * (x: string): Promise\<[Option](../README.md#option)\\> 600 | 601 | **Parameters:** 602 | 603 | Name | Type | 604 | ------ | ------ | 605 | x | string | 606 | 607 | *default value **from**: number= this.state.position 608 | 609 | Where to start looking for tokens; defaults to current position. 610 | 611 | **Returns:** Promise\<[Option](../README.md#option)\\> 612 | 613 | The resulting value if it was found. 614 | 615 | ___ 616 | 617 | ### findParse 618 | 619 | * **findParse**\<**T**, **E**\>(f: function, from: number): [Result](../README.md#result)\ 620 | 621 | Finds and retrieves the first unused token that could be transformed. 622 | That token will now be consider used. 623 | This is a variant of {@linkcode Args#findMap} that allows for a Result to be returned. 624 | 625 | **Type parameters:** 626 | 627 | * **T** 628 | 629 | Output type. 630 | 631 | * **E** 632 | 633 | Error type. 634 | 635 | **Parameters:** 636 | 637 | * **f**: function 638 | 639 | Gives a result of either the resulting value, or an error. 640 | 641 | * (x: string): [Result](../README.md#result)\ 642 | 643 | **Parameters:** 644 | 645 | Name | Type | 646 | ------ | ------ | 647 | x | string | 648 | 649 | *default value **from**: number= this.state.position 650 | 651 | Where to start looking for tokens; defaults to current position. 652 | 653 | **Returns:** [Result](../README.md#result)\ 654 | 655 | The resulting value if it was found or a list of errors during parsing. 656 | 657 | ___ 658 | 659 | ### findParseAsync 660 | 661 | * **findParseAsync**\<**T**, **E**\>(f: function, from: number): Promise\<[Result](../README.md#result)\\> 662 | 663 | Finds and retrieves the first unused token that could be transformed. 664 | That token will now be consider used. 665 | This variant of the function is asynchronous using `Promise`. 666 | This is a variant of {@linkcode Args#findMapAsync} that allows for a Result to be returned. 667 | 668 | **Type parameters:** 669 | 670 | * **T** 671 | 672 | Output type. 673 | 674 | * **E** 675 | 676 | Error type. 677 | 678 | **Parameters:** 679 | 680 | * **f**: function 681 | 682 | Gives a result of either the resulting value, or an error. 683 | 684 | * (x: string): Promise\<[Result](../README.md#result)\\> 685 | 686 | **Parameters:** 687 | 688 | Name | Type | 689 | ------ | ------ | 690 | x | string | 691 | 692 | *default value **from**: number= this.state.position 693 | 694 | Where to start looking for tokens; defaults to current position. 695 | 696 | **Returns:** Promise\<[Result](../README.md#result)\\> 697 | 698 | The resulting value if it was found or a list of errors during parsing. 699 | 700 | ___ 701 | 702 | ### filterMap 703 | 704 | * **filterMap**\<**T**\>(f: function, limit: number, from: number): T[] 705 | 706 | Filters and retrieves all unused tokens that could be transformed. 707 | Those tokens will now be consider used. 708 | 709 | ```ts 710 | // Suppose args are from '1 2 3'. 711 | const parse = (x: string) => { 712 | const n = Number(x); 713 | return isNaN(n) || n === 1 ? none() : some(n); 714 | }; 715 | 716 | console.log(args.filterMap(parse)); 717 | >>> [2, 3] 718 | ``` 719 | 720 | **Type parameters:** 721 | 722 | * **T** 723 | 724 | Output type. 725 | 726 | **Parameters:** 727 | 728 | * **f**: function 729 | 730 | Gives an option of either the resulting value, or nothing if failed. 731 | 732 | * (x: string): [Option](../README.md#option)\ 733 | 734 | **Parameters:** 735 | 736 | Name | Type | 737 | ------ | ------ | 738 | x | string | 739 | 740 | *default value **limit**: number= Infinity 741 | 742 | The limit on the amount of tokens to retrieve; defaults to infinite. 743 | 744 | *default value **from**: number= this.state.position 745 | 746 | Where to start looking for tokens; defaults to current position. 747 | 748 | **Returns:** T[] 749 | 750 | The resulting values. 751 | 752 | ___ 753 | 754 | ### filterMapAsync 755 | 756 | * **filterMapAsync**\<**T**\>(f: function, limit: number, from: number): Promise\ 757 | 758 | Filters and retrieves all unused tokens that could be transformed. 759 | This variant of the function is asynchronous using `Promise`. 760 | Those tokens will now be consider used. 761 | 762 | **Type parameters:** 763 | 764 | * **T** 765 | 766 | Output type. 767 | 768 | **Parameters:** 769 | 770 | * **f**: function 771 | 772 | Gives an option of either the resulting value, or nothing if failed. 773 | 774 | * (x: string): Promise\<[Option](../README.md#option)\\> 775 | 776 | **Parameters:** 777 | 778 | Name | Type | 779 | ------ | ------ | 780 | x | string | 781 | 782 | *default value **limit**: number= Infinity 783 | 784 | The limit on the amount of tokens to retrieve; defaults to infinite. 785 | 786 | *default value **from**: number= this.state.position 787 | 788 | Where to start looking for tokens; defaults to current position. 789 | 790 | **Returns:** Promise\ 791 | 792 | The resulting values. 793 | 794 | ___ 795 | 796 | ### save 797 | 798 | * **save**(): [ArgsState](../interfaces/argsstate.md) 799 | 800 | Saves the current state that can then be restored later by using {@linkcode Args#restore}. 801 | 802 | **Returns:** [ArgsState](../interfaces/argsstate.md) 803 | 804 | The current state. 805 | 806 | ___ 807 | 808 | ### restore 809 | 810 | * **restore**(state: [ArgsState](../interfaces/argsstate.md)): void 811 | 812 | Sets the current state to the given state from {@linkcode Args#save}. 813 | Use this to backtrack after a series of retrievals. 814 | 815 | **Parameters:** 816 | 817 | Name | Type | Description | 818 | ------ | ------ | ------ | 819 | state | [ArgsState](../interfaces/argsstate.md) | State to restore to. | 820 | 821 | **Returns:** void 822 | -------------------------------------------------------------------------------- /docs/reference/classes/lexer.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [Lexer](lexer.md) 2 | 3 | # Class: Lexer 4 | 5 | The lexer turns input into a list of tokens. 6 | 7 | ## Hierarchy 8 | 9 | * **Lexer** 10 | 11 | ## Implements 12 | 13 | * IterableIterator\<[Token](../interfaces/token.md)\> 14 | 15 | ## Index 16 | 17 | ### Constructors 18 | 19 | * [constructor](lexer.md#constructor) 20 | 21 | ### Accessors 22 | 23 | * [finished](lexer.md#finished) 24 | 25 | ### Methods 26 | 27 | * [setInput](lexer.md#setinput) 28 | * [setQuotes](lexer.md#setquotes) 29 | * [reset](lexer.md#reset) 30 | * [next](lexer.md#next) 31 | * [lex](lexer.md#lex) 32 | * [lexCommand](lexer.md#lexcommand) 33 | 34 | ## Constructors 35 | 36 | ### constructor 37 | 38 | * **new Lexer**(input?: undefined | string): [Lexer](lexer.md) 39 | 40 | **Parameters:** 41 | 42 | Name | Type | Description | 43 | ------ | ------ | ------ | 44 | input? | undefined | string | Input string. | 45 | 46 | **Returns:** [Lexer](lexer.md) 47 | 48 | ## Accessors 49 | 50 | ### finished 51 | 52 | * **get finished**(): boolean 53 | 54 | Whether the lexer is finished. 55 | 56 | **Returns:** boolean 57 | 58 | ## Methods 59 | 60 | ### setInput 61 | 62 | * **setInput**(input: string): this 63 | 64 | Sets the input to use. 65 | This will reset the lexer. 66 | 67 | **Parameters:** 68 | 69 | Name | Type | Description | 70 | ------ | ------ | ------ | 71 | input | string | Input to use. | 72 | 73 | **Returns:** this 74 | 75 | The lexer. 76 | 77 | ___ 78 | 79 | ### setQuotes 80 | 81 | * **setQuotes**(quotes: [string, string][]): this 82 | 83 | Sets the quotes to use. 84 | This can be done in the middle of lexing. 85 | 86 | ```ts 87 | const lexer = new Lexer('"hello"'); 88 | lexer.setQuotes([['"', '"']]); 89 | const xs = lexer.lex(); 90 | console.log(xs); 91 | >>> [{ value: 'hello', raw: '"hello"', trailing: '' }] 92 | ``` 93 | 94 | **Parameters:** 95 | 96 | Name | Type | Description | 97 | ------ | ------ | ------ | 98 | quotes | [string, string][] | List of pairs of open and close quotes. It is required that these strings do not contain any whitespace characters. The matching of these quotes will be case-sensitive. | 99 | 100 | **Returns:** this 101 | 102 | The lexer. 103 | 104 | ___ 105 | 106 | ### reset 107 | 108 | * **reset**(): this 109 | 110 | Resets the position of the lexer. 111 | 112 | **Returns:** this 113 | 114 | The lexer. 115 | 116 | ___ 117 | 118 | ### next 119 | 120 | * **next**(): IteratorResult\<[Token](../interfaces/token.md)\> 121 | 122 | Gets the next token. 123 | 124 | **Returns:** IteratorResult\<[Token](../interfaces/token.md)\> 125 | 126 | An iterator result containing the next token. 127 | 128 | ___ 129 | 130 | ### lex 131 | 132 | * **lex**(): [Token](../interfaces/token.md)[] 133 | 134 | Runs the lexer. 135 | This consumes the lexer. 136 | 137 | ```ts 138 | const lexer = new Lexer('hello world'); 139 | const xs = lexer.lex(); 140 | console.log(xs); 141 | >>> [ 142 | { value: 'hello', raw: 'hello', trailing: ' ' }, 143 | { value: 'world', raw: 'world', trailing: '' } 144 | ] 145 | ``` 146 | 147 | **Returns:** [Token](../interfaces/token.md)[] 148 | 149 | All the tokens lexed. 150 | 151 | ___ 152 | 153 | ### lexCommand 154 | 155 | * **lexCommand**(matchPrefix: [MatchPrefix](../README.md#matchprefix)): [[Token](../interfaces/token.md), function] | null 156 | 157 | Runs the lexer, matching a prefix and command. 158 | This consumes at most two tokens of the lexer. 159 | This uses [`extractCommand`](../README.md#extractcommand) under the hood. 160 | 161 | ```ts 162 | const lexer = new Lexer('!help me'); 163 | const r = lexer.lexCommand(s => s.startsWith('!') ? 1 : null); 164 | if (r != null) { 165 | const [command, getRest] = r; 166 | console.log(command.value); 167 | >>> 'help' 168 | console.log(getRest()[0].value); 169 | >>> 'me' 170 | } 171 | ``` 172 | 173 | **Parameters:** 174 | 175 | Name | Type | Description | 176 | ------ | ------ | ------ | 177 | matchPrefix | [MatchPrefix](../README.md#matchprefix) | A function that gives the length of the prefix if there is one. | 178 | 179 | **Returns:** [[Token](../interfaces/token.md), function] | null 180 | 181 | The command and the rest of the lexed tokens, as long as the prefix was matched. 182 | The rest of the tokens are delayed as a function. 183 | -------------------------------------------------------------------------------- /docs/reference/classes/parser.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [Parser](parser.md) 2 | 3 | # Class: Parser 4 | 5 | Parses a list of tokens to separate out flags and options. 6 | 7 | ## Hierarchy 8 | 9 | * **Parser** 10 | 11 | ## Implements 12 | 13 | * IterableIterator\<[ParserOutput](../interfaces/parseroutput.md)\> 14 | * Iterator\<[ParserOutput](../interfaces/parseroutput.md), null, [ParserOutput](../interfaces/parseroutput.md) | undefined\> 15 | 16 | ## Index 17 | 18 | ### Constructors 19 | 20 | * [constructor](parser.md#constructor) 21 | 22 | ### Accessors 23 | 24 | * [finished](parser.md#finished) 25 | 26 | ### Methods 27 | 28 | * [setInput](parser.md#setinput) 29 | * [setUnorderedStrategy](parser.md#setunorderedstrategy) 30 | * [reset](parser.md#reset) 31 | * [next](parser.md#next) 32 | * [parse](parser.md#parse) 33 | 34 | ## Constructors 35 | 36 | ### constructor 37 | 38 | * **new Parser**(input?: [Token](../interfaces/token.md)[]): [Parser](parser.md) 39 | 40 | **Parameters:** 41 | 42 | Name | Type | Description | 43 | ------ | ------ | ------ | 44 | input? | [Token](../interfaces/token.md)[] | The input tokens. | 45 | 46 | **Returns:** [Parser](parser.md) 47 | 48 | ## Accessors 49 | 50 | ### finished 51 | 52 | * **get finished**(): boolean 53 | 54 | Whether the parser is finished. 55 | 56 | **Returns:** boolean 57 | 58 | ## Methods 59 | 60 | ### setInput 61 | 62 | * **setInput**(input: [Token](../interfaces/token.md)[]): this 63 | 64 | Sets the input to use. 65 | This will reset the parser. 66 | 67 | **Parameters:** 68 | 69 | Name | Type | Description | 70 | ------ | ------ | ------ | 71 | input | [Token](../interfaces/token.md)[] | Input to use. | 72 | 73 | **Returns:** this 74 | 75 | The parser. 76 | 77 | ___ 78 | 79 | ### setUnorderedStrategy 80 | 81 | * **setUnorderedStrategy**(s: [UnorderedStrategy](../interfaces/unorderedstrategy.md)): this 82 | 83 | Sets the strategy for parsing unordered arguments. 84 | This can be done in the middle of parsing. 85 | 86 | ```ts 87 | const parser = new Parser(tokens) 88 | .setUnorderedStrategy(longStrategy()) 89 | .parse(); 90 | ``` 91 | 92 | **Parameters:** 93 | 94 | Name | Type | 95 | ------ | ------ | 96 | s | [UnorderedStrategy](../interfaces/unorderedstrategy.md) | 97 | 98 | **Returns:** this 99 | 100 | The parser. 101 | 102 | ___ 103 | 104 | ### reset 105 | 106 | * **reset**(): this 107 | 108 | Resets the state of the parser. 109 | 110 | **Returns:** this 111 | 112 | The parser. 113 | 114 | ___ 115 | 116 | ### next 117 | 118 | * **next**(output?: [ParserOutput](../interfaces/parseroutput.md)): IteratorResult\<[ParserOutput](../interfaces/parseroutput.md)\> 119 | 120 | Gets the next parsed tokens. 121 | If a parser output is passed in, that output will be mutated, otherwise a new one is made. 122 | 123 | **Parameters:** 124 | 125 | Name | Type | Description | 126 | ------ | ------ | ------ | 127 | output? | [ParserOutput](../interfaces/parseroutput.md) | Parser output to mutate. | 128 | 129 | **Returns:** IteratorResult\<[ParserOutput](../interfaces/parseroutput.md)\> 130 | 131 | An iterator result containing parser output. 132 | 133 | ___ 134 | 135 | ### parse 136 | 137 | * **parse**(): [ParserOutput](../interfaces/parseroutput.md) 138 | 139 | Runs the parser. 140 | 141 | ```ts 142 | const lexer = new Lexer(input); 143 | const tokens = lexer.lex(); 144 | const parser = new Parser(tokens); 145 | const output = parser.parse(); 146 | ``` 147 | 148 | **Returns:** [ParserOutput](../interfaces/parseroutput.md) 149 | 150 | The parser output. 151 | -------------------------------------------------------------------------------- /docs/reference/enums/looptag.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [LoopTag](looptag.md) 2 | 3 | # Enumeration: LoopTag 4 | 5 | Tag for the loop action variants. 6 | 7 | ## Index 8 | 9 | ### Enumeration members 10 | 11 | * [STEP](looptag.md#step) 12 | * [FINISH](looptag.md#finish) 13 | * [FAIL](looptag.md#fail) 14 | 15 | ## Enumeration members 16 | 17 | ### STEP 18 | 19 | * **STEP**: = "step" 20 | 21 | ___ 22 | 23 | ### FINISH 24 | 25 | * **FINISH**: = "finish" 26 | 27 | ___ 28 | 29 | ### FAIL 30 | 31 | * **FAIL**: = "fail" 32 | -------------------------------------------------------------------------------- /docs/reference/interfaces/argsstate.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [ArgsState](argsstate.md) 2 | 3 | # Interface: ArgsState 4 | 5 | The state for the argument wrapper. 6 | 7 | ## Hierarchy 8 | 9 | * **ArgsState** 10 | 11 | ## Index 12 | 13 | ### Properties 14 | 15 | * [usedIndices](argsstate.md#usedindices) 16 | * [position](argsstate.md#position) 17 | * [positionFromEnd](argsstate.md#positionfromend) 18 | 19 | ## Properties 20 | 21 | ### usedIndices 22 | 23 | * **usedIndices**: Set\ 24 | 25 | The indices of the ordered tokens already retrieved. 26 | 27 | ___ 28 | 29 | ### position 30 | 31 | * **position**: number 32 | 33 | The current position in the ordered tokens. 34 | Increments from 0. 35 | 36 | ___ 37 | 38 | ### positionFromEnd 39 | 40 | * **positionFromEnd**: number 41 | 42 | The current position backwards in the ordered tokens. 43 | Decrements from the end. 44 | -------------------------------------------------------------------------------- /docs/reference/interfaces/err.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [Err](err.md) 2 | 3 | # Interface: Err \<**E**\> 4 | 5 | The computation failed. 6 | 7 | ## Type parameters 8 | 9 | * **E** 10 | 11 | Type of errors. 12 | 13 | ## Hierarchy 14 | 15 | * **Err** 16 | 17 | ## Index 18 | 19 | ### Properties 20 | 21 | * [success](err.md#readonly-success) 22 | * [error](err.md#readonly-error) 23 | 24 | ## Properties 25 | 26 | ### readonly success 27 | 28 | * **success**: false 29 | 30 | If this an Err, this is false. 31 | 32 | ___ 33 | 34 | ### readonly error 35 | 36 | * **error**: E 37 | 38 | The resulting error, which only exists on an Err. 39 | -------------------------------------------------------------------------------- /docs/reference/interfaces/fail.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [Fail](fail.md) 2 | 3 | # Interface: Fail \<**E**\> 4 | 5 | The loop should fail due to an error. 6 | 7 | ## Type parameters 8 | 9 | * **E** 10 | 11 | Type of errors. 12 | 13 | ## Hierarchy 14 | 15 | * **Fail** 16 | 17 | ## Index 18 | 19 | ### Properties 20 | 21 | * [action](fail.md#readonly-action) 22 | * [error](fail.md#readonly-error) 23 | 24 | ## Properties 25 | 26 | ### readonly action 27 | 28 | * **action**: [FAIL](../enums/looptag.md#fail) 29 | 30 | If this is a Fail, this is 'fail'. 31 | 32 | ___ 33 | 34 | ### readonly error 35 | 36 | * **error**: E 37 | 38 | The resulting error. 39 | -------------------------------------------------------------------------------- /docs/reference/interfaces/finish.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [Finish](finish.md) 2 | 3 | # Interface: Finish \<**B**\> 4 | 5 | The loop should finish successfully. 6 | 7 | ## Type parameters 8 | 9 | * **B** 10 | 11 | Type of finish results. 12 | 13 | ## Hierarchy 14 | 15 | * **Finish** 16 | 17 | ## Index 18 | 19 | ### Properties 20 | 21 | * [action](finish.md#readonly-action) 22 | * [value](finish.md#readonly-value) 23 | 24 | ## Properties 25 | 26 | ### readonly action 27 | 28 | * **action**: [FINISH](../enums/looptag.md#finish) 29 | 30 | If this is a Finish, this is 'finish'. 31 | 32 | ___ 33 | 34 | ### readonly value 35 | 36 | * **value**: B 37 | 38 | The resulting value. 39 | -------------------------------------------------------------------------------- /docs/reference/interfaces/loopstrategy.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [LoopStrategy](loopstrategy.md) 2 | 3 | # Interface: LoopStrategy \<**A, Z, E**\> 4 | 5 | A strategy for running an input loop. 6 | 7 | ## Type parameters 8 | 9 | * **A** 10 | 11 | Input type. 12 | 13 | * **Z** 14 | 15 | Output type. 16 | 17 | * **E** 18 | 19 | Error type. 20 | 21 | ## Hierarchy 22 | 23 | * **LoopStrategy** 24 | 25 | ## Index 26 | 27 | ### Methods 28 | 29 | * [getInput](loopstrategy.md#getinput) 30 | * [parse](loopstrategy.md#parse) 31 | * [onInputError](loopstrategy.md#optional-oninputerror) 32 | * [onParseError](loopstrategy.md#optional-onparseerror) 33 | 34 | ## Methods 35 | 36 | ### getInput 37 | 38 | * **getInput**(): [LoopAction](../README.md#loopaction)\ 39 | 40 | Gets new input from somewhere e.g. reading a line. 41 | 42 | **Returns:** [LoopAction](../README.md#loopaction)\ 43 | 44 | A loop action that can: step with the input; finish with some parsed value; fail due to an error. 45 | 46 | ___ 47 | 48 | ### parse 49 | 50 | * **parse**(input: A): [LoopAction](../README.md#loopaction)\ 51 | 52 | Parses given input into the desired type. 53 | 54 | **Parameters:** 55 | 56 | Name | Type | Description | 57 | ------ | ------ | ------ | 58 | input | A | The input. | 59 | 60 | **Returns:** [LoopAction](../README.md#loopaction)\ 61 | 62 | A loop action that can: step on; finish with some parsed value; fail due to an error. 63 | 64 | ___ 65 | 66 | ### optional onInputError 67 | 68 | * **onInputError**(error: E): [LoopAction](../README.md#loopaction)\ 69 | 70 | Handles error on getting new input. 71 | This function intercepts the `fail` case of `getInput`. 72 | 73 | **Parameters:** 74 | 75 | Name | Type | Description | 76 | ------ | ------ | ------ | 77 | error | E | The error encountered. | 78 | 79 | **Returns:** [LoopAction](../README.md#loopaction)\ 80 | 81 | A loop action that can: step on; finish with some parsed value; fail due to an error. 82 | 83 | ___ 84 | 85 | ### optional onParseError 86 | 87 | * **onParseError**(error: E, input: A): [LoopAction](../README.md#loopaction)\ 88 | 89 | Handles error on parsing input. 90 | This function intercepts the `fail` case of `parse`. 91 | 92 | **Parameters:** 93 | 94 | Name | Type | Description | 95 | ------ | ------ | ------ | 96 | error | E | The error encountered. | 97 | input | A | The input that could not be parsed. | 98 | 99 | **Returns:** [LoopAction](../README.md#loopaction)\ 100 | 101 | A loop action that can: step on; finish with some parsed value; fail due to an error. 102 | -------------------------------------------------------------------------------- /docs/reference/interfaces/loopstrategyasync.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [LoopStrategyAsync](loopstrategyasync.md) 2 | 3 | # Interface: LoopStrategyAsync \<**A, Z, E**\> 4 | 5 | A strategy for running an input loop asynchronously via `Promise`. 6 | 7 | ## Type parameters 8 | 9 | * **A** 10 | 11 | Input type. 12 | 13 | * **Z** 14 | 15 | Output type. 16 | 17 | * **E** 18 | 19 | Error type. 20 | 21 | ## Hierarchy 22 | 23 | * **LoopStrategyAsync** 24 | 25 | ## Index 26 | 27 | ### Methods 28 | 29 | * [getInput](loopstrategyasync.md#getinput) 30 | * [parse](loopstrategyasync.md#parse) 31 | * [onInputError](loopstrategyasync.md#optional-oninputerror) 32 | * [onParseError](loopstrategyasync.md#optional-onparseerror) 33 | 34 | ## Methods 35 | 36 | ### getInput 37 | 38 | * **getInput**(): Promise\<[LoopAction](../README.md#loopaction)\\> 39 | 40 | Gets new input from somewhere e.g. reading a line. 41 | 42 | **Returns:** Promise\<[LoopAction](../README.md#loopaction)\\> 43 | 44 | A loop action that can: step with the input; finish with some parsed value; fail due to an error. 45 | 46 | ___ 47 | 48 | ### parse 49 | 50 | * **parse**(input: A): Promise\<[LoopAction](../README.md#loopaction)\\> 51 | 52 | Parses given input into the desired type. 53 | 54 | **Parameters:** 55 | 56 | Name | Type | Description | 57 | ------ | ------ | ------ | 58 | input | A | The input. | 59 | 60 | **Returns:** Promise\<[LoopAction](../README.md#loopaction)\\> 61 | 62 | A loop action that can: step on; finish with some parsed value; fail due to an error. 63 | 64 | ___ 65 | 66 | ### optional onInputError 67 | 68 | * **onInputError**(error: E): Promise\<[LoopAction](../README.md#loopaction)\\> 69 | 70 | Handles error on getting new input. 71 | This function intercepts the `fail` case of `getInput`. 72 | 73 | **Parameters:** 74 | 75 | Name | Type | Description | 76 | ------ | ------ | ------ | 77 | error | E | The error encountered. | 78 | 79 | **Returns:** Promise\<[LoopAction](../README.md#loopaction)\\> 80 | 81 | A loop action that can: step on; finish with some parsed value; fail due to an error. 82 | 83 | ___ 84 | 85 | ### optional onParseError 86 | 87 | * **onParseError**(error: E, input: A): Promise\<[LoopAction](../README.md#loopaction)\\> 88 | 89 | Handles error on parsing input. 90 | This function intercepts the `fail` case of `parse`. 91 | 92 | **Parameters:** 93 | 94 | Name | Type | Description | 95 | ------ | ------ | ------ | 96 | error | E | The error encountered. | 97 | input | A | The input that could not be parsed. | 98 | 99 | **Returns:** Promise\<[LoopAction](../README.md#loopaction)\\> 100 | 101 | A loop action that can: step on; finish with some parsed value; fail due to an error. 102 | -------------------------------------------------------------------------------- /docs/reference/interfaces/none.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [None](none.md) 2 | 3 | # Interface: None 4 | 5 | The value does not exist. 6 | 7 | ## Hierarchy 8 | 9 | * **None** 10 | 11 | ## Index 12 | 13 | ### Properties 14 | 15 | * [exists](none.md#readonly-exists) 16 | 17 | ## Properties 18 | 19 | ### readonly exists 20 | 21 | * **exists**: false 22 | 23 | If this is a None, this is false. 24 | -------------------------------------------------------------------------------- /docs/reference/interfaces/ok.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [Ok](ok.md) 2 | 3 | # Interface: Ok \<**T**\> 4 | 5 | The computation is successful. 6 | 7 | ## Type parameters 8 | 9 | * **T** 10 | 11 | Type of results. 12 | 13 | ## Hierarchy 14 | 15 | * **Ok** 16 | 17 | ## Index 18 | 19 | ### Properties 20 | 21 | * [success](ok.md#readonly-success) 22 | * [value](ok.md#readonly-value) 23 | 24 | ## Properties 25 | 26 | ### readonly success 27 | 28 | * **success**: true 29 | 30 | If this is an Ok, this is true. 31 | 32 | ___ 33 | 34 | ### readonly value 35 | 36 | * **value**: T 37 | 38 | The resulting value, which only exists on an Ok. 39 | -------------------------------------------------------------------------------- /docs/reference/interfaces/parseroutput.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [ParserOutput](parseroutput.md) 2 | 3 | # Interface: ParserOutput 4 | 5 | Output of the parser. 6 | 7 | ## Hierarchy 8 | 9 | * **ParserOutput** 10 | 11 | ## Index 12 | 13 | ### Properties 14 | 15 | * [ordered](parseroutput.md#ordered) 16 | * [flags](parseroutput.md#flags) 17 | * [options](parseroutput.md#options) 18 | 19 | ## Properties 20 | 21 | ### ordered 22 | 23 | * **ordered**: [Token](token.md)[] 24 | 25 | All the tokens that are not flags or options, in order. 26 | 27 | ___ 28 | 29 | ### flags 30 | 31 | * **flags**: Set\ 32 | 33 | The parsed flags. 34 | 35 | ___ 36 | 37 | ### options 38 | 39 | * **options**: Map\ 40 | 41 | The parsed options mapped to their value. 42 | -------------------------------------------------------------------------------- /docs/reference/interfaces/some.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [Some](some.md) 2 | 3 | # Interface: Some \<**T**\> 4 | 5 | The value exists. 6 | 7 | ## Type parameters 8 | 9 | * **T** 10 | 11 | Type of results. 12 | 13 | ## Hierarchy 14 | 15 | * **Some** 16 | 17 | ## Index 18 | 19 | ### Properties 20 | 21 | * [exists](some.md#readonly-exists) 22 | * [value](some.md#readonly-value) 23 | 24 | ## Properties 25 | 26 | ### readonly exists 27 | 28 | * **exists**: true 29 | 30 | If this is a Some, this is true. 31 | 32 | ___ 33 | 34 | ### readonly value 35 | 36 | * **value**: T 37 | 38 | The value, which only exists on a Some. 39 | -------------------------------------------------------------------------------- /docs/reference/interfaces/step.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [Step](step.md) 2 | 3 | # Interface: Step \<**A**\> 4 | 5 | The loop should continue being stepped through. 6 | 7 | ## Type parameters 8 | 9 | * **A** 10 | 11 | Type of step results. 12 | 13 | ## Hierarchy 14 | 15 | * **Step** 16 | 17 | ## Index 18 | 19 | ### Properties 20 | 21 | * [action](step.md#readonly-action) 22 | * [item](step.md#readonly-item) 23 | 24 | ## Properties 25 | 26 | ### readonly action 27 | 28 | * **action**: [STEP](../enums/looptag.md#step) 29 | 30 | If this is a Step, this is 'step'. 31 | 32 | ___ 33 | 34 | ### readonly item 35 | 36 | * **item**: A 37 | 38 | The item to step with. 39 | -------------------------------------------------------------------------------- /docs/reference/interfaces/token.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [Token](token.md) 2 | 3 | # Interface: Token 4 | 5 | Represents a token. 6 | 7 | ## Hierarchy 8 | 9 | * **Token** 10 | 11 | ## Index 12 | 13 | ### Properties 14 | 15 | * [value](token.md#readonly-value) 16 | * [raw](token.md#readonly-raw) 17 | * [trailing](token.md#readonly-trailing) 18 | 19 | ## Properties 20 | 21 | ### readonly value 22 | 23 | * **value**: string 24 | 25 | The value of the token. 26 | 27 | ___ 28 | 29 | ### readonly raw 30 | 31 | * **raw**: string 32 | 33 | The raw value of the token e.g. with quotes. 34 | 35 | ___ 36 | 37 | ### readonly trailing 38 | 39 | * **trailing**: string 40 | 41 | Trailing whitespace. 42 | -------------------------------------------------------------------------------- /docs/reference/interfaces/unorderedstrategy.md: -------------------------------------------------------------------------------- 1 | [lexure](../README.md) › [UnorderedStrategy](unorderedstrategy.md) 2 | 3 | # Interface: UnorderedStrategy 4 | 5 | A strategy to match unordered arguments in parsing. 6 | 7 | ## Hierarchy 8 | 9 | * **UnorderedStrategy** 10 | 11 | ## Index 12 | 13 | ### Methods 14 | 15 | * [matchFlag](unorderedstrategy.md#matchflag) 16 | * [matchOption](unorderedstrategy.md#matchoption) 17 | * [matchCompactOption](unorderedstrategy.md#matchcompactoption) 18 | 19 | ## Methods 20 | 21 | ### matchFlag 22 | 23 | * **matchFlag**(s: string): string | null 24 | 25 | Match a flag. 26 | 27 | **Parameters:** 28 | 29 | Name | Type | Description | 30 | ------ | ------ | ------ | 31 | s | string | The string. | 32 | 33 | **Returns:** string | null 34 | 35 | The name of the flag. 36 | 37 | ___ 38 | 39 | ### matchOption 40 | 41 | * **matchOption**(s: string): string | null 42 | 43 | Match an option. 44 | 45 | **Parameters:** 46 | 47 | Name | Type | Description | 48 | ------ | ------ | ------ | 49 | s | string | The string. | 50 | 51 | **Returns:** string | null 52 | 53 | The name of the option. 54 | 55 | ___ 56 | 57 | ### matchCompactOption 58 | 59 | * **matchCompactOption**(s: string): [string, string] | null 60 | 61 | Match a compact option. 62 | 63 | **Parameters:** 64 | 65 | Name | Type | Description | 66 | ------ | ------ | ------ | 67 | s | string | The string. | 68 | 69 | **Returns:** [string, string] | null 70 | 71 | A pair containing the name of the option and the value. 72 | -------------------------------------------------------------------------------- /esm/index.mjs: -------------------------------------------------------------------------------- 1 | import lexure from '../dist/lib/index.js'; 2 | 3 | export const { 4 | // args.js 5 | args, 6 | Args, 7 | // lexer.js 8 | lexer, 9 | Lexer, 10 | // loops.js 11 | loops, 12 | loop, 13 | loop1, 14 | loopAsync, 15 | loop1Async, 16 | // loopAction.js 17 | loopAction, 18 | LoopTag, 19 | step, 20 | step_, 21 | finish, 22 | fail, 23 | fail_, 24 | // option.js 25 | option, 26 | some, 27 | none, 28 | maybeOption, 29 | orOption, 30 | // parser.js 31 | parser, 32 | Parser, 33 | // parserOutput.js 34 | parserOutput, 35 | outputFromJSON, 36 | outputToJSON, 37 | mergeOutputs, 38 | emptyOutput, 39 | // result.js 40 | result, 41 | ok, 42 | err, 43 | err_, 44 | maybeResult, 45 | orResultAll, 46 | orResultFirst, 47 | orResultLast, 48 | // tokens.js 49 | tokens, 50 | extractCommand, 51 | joinTokens, 52 | // unordered.js 53 | unordered, 54 | noStrategy, 55 | longStrategy, 56 | longShortStrategy, 57 | prefixedStrategy, 58 | matchingStrategy, 59 | mapKeys, 60 | renameKeys, 61 | // util.js 62 | util, 63 | someToOk, 64 | okToSome, 65 | errToSome, 66 | someToStep, 67 | someToFinish, 68 | okToStep, 69 | okToFinish 70 | } = lexure; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lexure", 3 | "version": "0.17.0", 4 | "description": "Parser and utilities for non-technical user input.", 5 | "keywords": [ 6 | "lexer", 7 | "parser", 8 | "parsing", 9 | "command", 10 | "commands", 11 | "api", 12 | "args", 13 | "arguments" 14 | ], 15 | "main": "dist/lib/index.js", 16 | "typings": "dist/types/index.d.ts", 17 | "exports": { 18 | "import": "./esm/index.mjs", 19 | "require": "./dist/lib/index.js" 20 | }, 21 | "files": [ 22 | "dist", 23 | "esm" 24 | ], 25 | "author": "1Computer ", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/1Computer1/lexure.git" 29 | }, 30 | "license": "MIT", 31 | "engines": { 32 | "node": ">=12.0.0" 33 | }, 34 | "scripts": { 35 | "lint": "eslint ./src ./test --ext .ts", 36 | "bench": "npm run build && node benchmarks", 37 | "build": "rimraf dist && tsc -p tsconfig.build.json", 38 | "docs": "rimraf docs/reference && typedoc", 39 | "test": "jest --coverage", 40 | "prepare": "npm run build", 41 | "prepublishOnly": "npm run lint && npm run test" 42 | }, 43 | "jest": { 44 | "transform": { 45 | ".ts": "ts-jest" 46 | }, 47 | "testEnvironment": "node", 48 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$", 49 | "moduleFileExtensions": [ 50 | "ts", 51 | "js" 52 | ], 53 | "coveragePathIgnorePatterns": [ 54 | "/node_modules/", 55 | "/test/" 56 | ], 57 | "collectCoverageFrom": [ 58 | "src/*.{ts,js}" 59 | ], 60 | "globals": { 61 | "ts-jest": { 62 | "tsConfig": "tsconfig.json" 63 | } 64 | } 65 | }, 66 | "devDependencies": { 67 | "@types/benchmark": "^1.0.33", 68 | "@types/jest": "^26.0.0", 69 | "@types/node": "^14.0.13", 70 | "@typescript-eslint/eslint-plugin": "^3.2.0", 71 | "@typescript-eslint/parser": "^3.2.0", 72 | "benchmark": "^2.1.4", 73 | "eslint": "^7.2.0", 74 | "eslint-plugin-jest": "^23.13.2", 75 | "fast-check": "^1.25.1", 76 | "jest": "^26.0.1", 77 | "jest-config": "^26.0.1", 78 | "rimraf": "^3.0.2", 79 | "ts-jest": "^26.1.0", 80 | "typedoc": "^0.18.0", 81 | "typedoc-plugin-markdown": "github:1Computer1/typedoc-plugin-markdown#custom", 82 | "typescript": "^3.9.5" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * as args from './args'; 2 | export * as lexer from './lexer'; 3 | export * as loops from './loops'; 4 | export * as loopAction from './loopAction'; 5 | export * as option from './option'; 6 | export * as parser from './parser'; 7 | export * as parserOutput from './parserOutput'; 8 | export * as result from './result'; 9 | export * as tokens from './tokens'; 10 | export * as unordered from './unordered'; 11 | export * as util from './util'; 12 | 13 | export * from './args'; 14 | export * from './lexer'; 15 | export * from './loops'; 16 | export * from './loopAction'; 17 | export * from './option'; 18 | export * from './parser'; 19 | export * from './parserOutput'; 20 | export * from './result'; 21 | export * from './tokens'; 22 | export * from './unordered'; 23 | export * from './util'; 24 | -------------------------------------------------------------------------------- /src/lexer.ts: -------------------------------------------------------------------------------- 1 | import { Token, extractCommand, MatchPrefix } from './tokens'; 2 | 3 | /** 4 | * The lexer turns input into a list of tokens. 5 | */ 6 | export class Lexer implements IterableIterator { 7 | private input: string; 8 | private quotes: [string, string][] = []; 9 | private position = 0; 10 | 11 | /** 12 | * @param input - Input string. 13 | */ 14 | public constructor(input?: string) { 15 | this.input = input ?? ''; 16 | if (input != null) { 17 | this.pWs(); 18 | } 19 | } 20 | 21 | /** 22 | * Sets the input to use. 23 | * This will reset the lexer. 24 | * @param input - Input to use. 25 | * @returns The lexer. 26 | */ 27 | public setInput(input: string): this { 28 | this.input = input; 29 | this.reset(); 30 | this.pWs(); 31 | return this; 32 | } 33 | 34 | /** 35 | * Sets the quotes to use. 36 | * This can be done in the middle of lexing. 37 | * 38 | * ```ts 39 | * const lexer = new Lexer('"hello"'); 40 | * lexer.setQuotes([['"', '"']]); 41 | * const xs = lexer.lex(); 42 | * console.log(xs); 43 | * >>> [{ value: 'hello', raw: '"hello"', trailing: '' }] 44 | * ``` 45 | * 46 | * @param quotes - List of pairs of open and close quotes. 47 | * It is required that these strings do not contain any whitespace characters. 48 | * The matching of these quotes will be case-sensitive. 49 | * @returns The lexer. 50 | */ 51 | public setQuotes(quotes: [string, string][]): this { 52 | this.quotes = quotes; 53 | return this; 54 | } 55 | 56 | /** 57 | * Resets the position of the lexer. 58 | * @return The lexer. 59 | */ 60 | public reset(): this { 61 | this.position = 0; 62 | return this; 63 | } 64 | 65 | /** 66 | * Whether the lexer is finished. 67 | */ 68 | public get finished(): boolean { 69 | return this.position >= this.input.length; 70 | } 71 | 72 | private match(s: string): string | null { 73 | const sub = this.input.slice(this.position, this.position + s.length); 74 | if (s === sub) { 75 | return sub; 76 | } 77 | 78 | return null; 79 | } 80 | 81 | private matchR(r: RegExp): RegExpMatchArray | null { 82 | return this.input.slice(this.position).match(r); 83 | } 84 | 85 | private shift(n: number): void { 86 | this.position += n; 87 | } 88 | 89 | /** 90 | * Gets the next token. 91 | * @returns An iterator result containing the next token. 92 | */ 93 | public next(): IteratorResult { 94 | if (this.finished) { 95 | return { done: true, value: null }; 96 | } 97 | 98 | const t = this.nextToken(); 99 | if (t == null) { 100 | throw new Error('Unexpected end of input (this should never happen).'); 101 | } 102 | 103 | return { done: false, value: t }; 104 | } 105 | 106 | private nextToken(): Token | null { 107 | return this.pQuoted() || this.pWord(); 108 | } 109 | 110 | private pWs(): string { 111 | const w = this.matchR(/^\s*/)?.[0] ?? ''; 112 | this.shift(w.length); 113 | return w; 114 | } 115 | 116 | private pQuoted(): Token | null { 117 | for (const [openQ, closeQ] of this.quotes) { 118 | const open = this.match(openQ); 119 | if (open == null) { 120 | continue; 121 | } 122 | 123 | this.shift(open.length); 124 | 125 | let inner = this.input.slice(this.position); 126 | inner = sliceTo(inner, [closeQ]); 127 | this.shift(inner.length); 128 | 129 | const close = this.match(closeQ) ?? ''; 130 | this.shift(close.length); 131 | 132 | const s = this.pWs(); 133 | return { value: inner, raw: open + inner + close, trailing: s }; 134 | } 135 | 136 | return null; 137 | } 138 | 139 | private pWord(): Token | null { 140 | let w = this.matchR(/^\S+/)?.[0]; 141 | if (w == null) { 142 | return null; 143 | } 144 | 145 | w = sliceTo(w, this.quotes.flat()); 146 | this.shift(w.length); 147 | 148 | const s = this.pWs(); 149 | return { value: w, raw: w, trailing: s }; 150 | } 151 | 152 | public [Symbol.iterator](): this { 153 | return this; 154 | } 155 | 156 | /** 157 | * Runs the lexer. 158 | * This consumes the lexer. 159 | * 160 | * ```ts 161 | * const lexer = new Lexer('hello world'); 162 | * const xs = lexer.lex(); 163 | * console.log(xs); 164 | * >>> [ 165 | * { value: 'hello', raw: 'hello', trailing: ' ' }, 166 | * { value: 'world', raw: 'world', trailing: '' } 167 | * ] 168 | * ``` 169 | * 170 | * @returns All the tokens lexed. 171 | */ 172 | public lex(): Token[] { 173 | return [...this]; 174 | } 175 | 176 | /** 177 | * Runs the lexer, matching a prefix and command. 178 | * This consumes at most two tokens of the lexer. 179 | * This uses {@linkcode extractCommand} under the hood. 180 | * 181 | * ```ts 182 | * const lexer = new Lexer('!help me'); 183 | * const r = lexer.lexCommand(s => s.startsWith('!') ? 1 : null); 184 | * if (r != null) { 185 | * const [command, getRest] = r; 186 | * console.log(command.value); 187 | * >>> 'help' 188 | * console.log(getRest()[0].value); 189 | * >>> 'me' 190 | * } 191 | * ``` 192 | * 193 | * @param matchPrefix - A function that gives the length of the prefix if there is one. 194 | * @returns The command and the rest of the lexed tokens, as long as the prefix was matched. 195 | * The rest of the tokens are delayed as a function. 196 | */ 197 | public lexCommand(matchPrefix: MatchPrefix): [Token, () => Token[]] | null { 198 | const t1 = this.next(); 199 | if (t1.done) { 200 | return null; 201 | } 202 | 203 | const cmd1 = extractCommand(matchPrefix, [t1.value]); 204 | if (cmd1 != null) { 205 | return [cmd1, () => [...this]]; 206 | } 207 | 208 | const t2 = this.next(); 209 | if (t2.done) { 210 | return null; 211 | } 212 | 213 | const cmd2 = extractCommand(matchPrefix, [t1.value, t2.value]); 214 | if (cmd2 == null) { 215 | return null; 216 | } 217 | 218 | return [cmd2, () => [...this]]; 219 | } 220 | } 221 | 222 | function sliceTo(word: string, xs: string[]): string { 223 | const is = xs.map(x => word.indexOf(x)).filter(x => x !== -1); 224 | if (is.length === 0) { 225 | return word; 226 | } 227 | 228 | const i = Math.min(...is); 229 | return word.slice(0, i); 230 | } 231 | -------------------------------------------------------------------------------- /src/loopAction.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A type used to express actions in the loop. 3 | * Each action can have a value with it. 4 | * @typeparam A - Type of step results. 5 | * @typeparam B - Type of finish results. 6 | * @typeparam E - Type of errors. 7 | */ 8 | export type LoopAction = Step | Finish | Fail; 9 | 10 | /** 11 | * Tag for the loop action variants. 12 | */ 13 | export enum LoopTag { 14 | STEP = 'step', 15 | FINISH = 'finish', 16 | FAIL = 'fail' 17 | } 18 | 19 | /** 20 | * The loop should continue being stepped through. 21 | * @typeparam A - Type of step results. 22 | */ 23 | export interface Step { 24 | /** 25 | * If this is a Step, this is 'step'. 26 | */ 27 | readonly action: LoopTag.STEP; 28 | 29 | /** 30 | * The item to step with. 31 | */ 32 | readonly item: A; 33 | 34 | readonly value?: undefined; 35 | 36 | readonly error?: undefined; 37 | } 38 | 39 | /** 40 | * The loop should finish successfully. 41 | * @typeparam B - Type of finish results. 42 | */ 43 | export interface Finish { 44 | /** 45 | * If this is a Finish, this is 'finish'. 46 | */ 47 | readonly action: LoopTag.FINISH; 48 | 49 | readonly item?: undefined; 50 | 51 | /** 52 | * The resulting value. 53 | */ 54 | readonly value: B; 55 | 56 | readonly error?: undefined; 57 | } 58 | 59 | /** 60 | * The loop should fail due to an error. 61 | * @typeparam E - Type of errors. 62 | */ 63 | export interface Fail { 64 | /** 65 | * If this is a Fail, this is 'fail'. 66 | */ 67 | readonly action: LoopTag.FAIL; 68 | 69 | readonly item?: undefined; 70 | 71 | readonly value?: undefined; 72 | 73 | /** 74 | * The resulting error. 75 | */ 76 | readonly error: E; 77 | } 78 | 79 | /** 80 | * Creates a Step. 81 | * @typeparam A - Type of step results. 82 | * @param x - Value to use. 83 | * @returns A LoopAction. 84 | */ 85 | export function step(x: A): Step { 86 | return { action: LoopTag.STEP, item: x }; 87 | } 88 | 89 | /** 90 | * Creates a Step with null value. 91 | * @returns A LoopAction. 92 | */ 93 | export function step_(): Step { 94 | return { action: LoopTag.STEP, item: null }; 95 | } 96 | 97 | /** 98 | * Creates a Finish. 99 | * @typeparam B - Type of finish results. 100 | * @param x - Value to use. 101 | * @returns A LoopAction. 102 | */ 103 | export function finish(x: B): Finish { 104 | return { action: LoopTag.FINISH, value: x }; 105 | } 106 | 107 | /** 108 | * Creates a Fail. 109 | * @typeparam E - Type of errors. 110 | * @param x - Value to use. 111 | * @returns A LoopAction. 112 | */ 113 | export function fail(x: E): Fail { 114 | return { action: LoopTag.FAIL, error: x }; 115 | } 116 | 117 | /** 118 | * Creates a Fail with null value. 119 | * @returns A LoopAction. 120 | */ 121 | export function fail_(): Fail { 122 | return { action: LoopTag.FAIL, error: null }; 123 | } 124 | -------------------------------------------------------------------------------- /src/loops.ts: -------------------------------------------------------------------------------- 1 | import { LoopAction, fail, step_, LoopTag } from './loopAction'; 2 | import { Result, ok, err } from './result'; 3 | 4 | const { STEP, FINISH, FAIL } = LoopTag; 5 | 6 | /** 7 | * A strategy for running an input loop. 8 | * @typeparam A - Input type. 9 | * @typeparam Z - Output type. 10 | * @typeparam E - Error type. 11 | */ 12 | export interface LoopStrategy { 13 | /** 14 | * Gets new input from somewhere e.g. reading a line. 15 | * @returns A loop action that can: step with the input; finish with some parsed value; fail due to an error. 16 | */ 17 | getInput(): LoopAction; 18 | 19 | /** 20 | * Parses given input into the desired type. 21 | * @param input - The input. 22 | * @returns A loop action that can: step on; finish with some parsed value; fail due to an error. 23 | */ 24 | parse(input: A): LoopAction; 25 | 26 | /** 27 | * Handles error on getting new input. 28 | * This function intercepts the `fail` case of `getInput`. 29 | * @param error - The error encountered. 30 | * @returns A loop action that can: step on; finish with some parsed value; fail due to an error. 31 | */ 32 | onInputError?(error: E): LoopAction; 33 | 34 | /** 35 | * Handles error on parsing input. 36 | * This function intercepts the `fail` case of `parse`. 37 | * @param error - The error encountered. 38 | * @param input - The input that could not be parsed. 39 | * @returns A loop action that can: step on; finish with some parsed value; fail due to an error. 40 | */ 41 | onParseError?(error: E, input: A): LoopAction; 42 | } 43 | 44 | /** 45 | * A strategy for running an input loop asynchronously via `Promise`. 46 | * @typeparam A - Input type. 47 | * @typeparam Z - Output type. 48 | * @typeparam E - Error type. 49 | */ 50 | export interface LoopStrategyAsync { 51 | /** 52 | * Gets new input from somewhere e.g. reading a line. 53 | * @returns A loop action that can: step with the input; finish with some parsed value; fail due to an error. 54 | */ 55 | getInput(): Promise>; 56 | 57 | /** 58 | * Parses given input into the desired type. 59 | * @param input - The input. 60 | * @returns A loop action that can: step on; finish with some parsed value; fail due to an error. 61 | */ 62 | parse(input: A): Promise>; 63 | 64 | /** 65 | * Handles error on getting new input. 66 | * This function intercepts the `fail` case of `getInput`. 67 | * @param error - The error encountered. 68 | * @returns A loop action that can: step on; finish with some parsed value; fail due to an error. 69 | */ 70 | onInputError?(error: E): Promise>; 71 | 72 | /** 73 | * Handles error on parsing input. 74 | * This function intercepts the `fail` case of `parse`. 75 | * @param error - The error encountered. 76 | * @param input - The input that could not be parsed. 77 | * @returns A loop action that can: step on; finish with some parsed value; fail due to an error. 78 | */ 79 | onParseError?(error: E, input: A): Promise>; 80 | } 81 | 82 | /** 83 | * Runs a loop which continuously gets input and attempts to parse it. 84 | * The loop strategy used will determine how the loop continues and ends. 85 | * 86 | * ```ts 87 | * const getInputFromSomewhere = () => '2'; 88 | * 89 | * const x = loop('1', { 90 | * getInput() { 91 | * const i = getInputFromSomewhere(); 92 | * return i == null ? fail('no input') : step(i); 93 | * }, 94 | * 95 | * parse(x: string) { 96 | * const n = Number(x); 97 | * return isNaN(n) ? fail('bad input') : finish(n); 98 | * } 99 | * }); 100 | * 101 | * console.log(x); 102 | * >>> 1 103 | * ``` 104 | * 105 | * @typeparam A - Input type. 106 | * @typeparam Z - Output type. 107 | * @typeparam E - Error type. 108 | * @param intialInput - The first input to parse. 109 | * @param strat - The loop strategy to use. 110 | * @returns Either the parsed value or an error. 111 | */ 112 | export function loop(intialInput: A, strat: LoopStrategy): Result { 113 | let inp = intialInput; 114 | let parsed = strat.parse(inp); 115 | for (;;) { 116 | switch (parsed.action) { 117 | case FINISH: 118 | return ok(parsed.value); 119 | 120 | case FAIL: { 121 | const r = strat.onParseError?.(parsed.error, inp) ?? step_(); 122 | switch (r.action) { 123 | case FINISH: 124 | return ok(r.value); 125 | 126 | case FAIL: 127 | return err(r.error); 128 | } 129 | } 130 | } 131 | 132 | const got = strat.getInput(); 133 | switch (got.action) { 134 | case STEP: { 135 | inp = got.item; 136 | parsed = strat.parse(inp); 137 | break; 138 | } 139 | 140 | case FINISH: 141 | return ok(got.value); 142 | 143 | case FAIL: { 144 | const r = strat.onInputError?.(got.error) ?? fail(got.error); 145 | switch (r.action) { 146 | case FINISH: 147 | return ok(r.value); 148 | 149 | case FAIL: 150 | return err(r.error); 151 | } 152 | } 153 | } 154 | } 155 | } 156 | 157 | /** 158 | * Runs a loop which continuously gets input and attempts to parse it. 159 | * The loop strategy used will determine how the loop continues and ends. 160 | * This variant has no initial input. 161 | * 162 | * ```ts 163 | * const getInputFromSomewhere = () => '2'; 164 | * 165 | * const x = loop1({ 166 | * getInput() { 167 | * const i = getInputFromSomewhere(); 168 | * return i == null ? fail('no input') : step(i); 169 | * }, 170 | * 171 | * parse(x: string) { 172 | * const n = Number(x); 173 | * return isNaN(n) ? fail('bad input') : finish(n); 174 | * } 175 | * }); 176 | * 177 | * console.log(x); 178 | * >>> 2 179 | * ``` 180 | * 181 | * @typeparam A - Input type. 182 | * @typeparam Z - Output type. 183 | * @typeparam E - Error type. 184 | * @param strat - The loop strategy to use. 185 | * @returns Either the parsed value or an error. 186 | */ 187 | export function loop1(strat: LoopStrategy): Result { 188 | for (;;) { 189 | const got = strat.getInput(); 190 | switch (got.action) { 191 | case STEP: { 192 | const inp = got.item; 193 | const parsed = strat.parse(inp); 194 | switch (parsed.action) { 195 | case FINISH: 196 | return ok(parsed.value); 197 | 198 | case FAIL: { 199 | const r = strat.onParseError?.(parsed.error, inp) ?? step_(); 200 | switch (r.action) { 201 | case FINISH: 202 | return ok(r.value); 203 | 204 | case FAIL: 205 | return err(r.error); 206 | } 207 | } 208 | } 209 | 210 | break; 211 | } 212 | 213 | case FINISH: 214 | return ok(got.value); 215 | 216 | case FAIL: { 217 | const r = strat.onInputError?.(got.error) ?? fail(got.error); 218 | switch (r.action) { 219 | case FINISH: 220 | return ok(r.value); 221 | 222 | case FAIL: 223 | return err(r.error); 224 | } 225 | } 226 | } 227 | } 228 | } 229 | 230 | /** 231 | * Runs a loop which continuously gets input and attempts to parse it. 232 | * The loop strategy used will determine how the loop continues and ends. 233 | * This variant of the function is asynchronous using `Promise`. 234 | * @typeparam A - Input type. 235 | * @typeparam Z - Output type. 236 | * @typeparam E - Error type. 237 | * @param intialInput - The first input to parse. 238 | * @param strat - The loop strategy to use. 239 | * @returns Either the parsed value or an error. 240 | */ 241 | export async function loopAsync(intialInput: A, strat: LoopStrategyAsync): Promise> { 242 | let inp = intialInput; 243 | let parsed = await strat.parse(inp); 244 | for (;;) { 245 | switch (parsed.action) { 246 | case FINISH: 247 | return ok(parsed.value); 248 | 249 | case FAIL: { 250 | const r = await strat.onParseError?.(parsed.error, inp) ?? step_(); 251 | switch (r.action) { 252 | case FINISH: 253 | return ok(r.value); 254 | 255 | case FAIL: 256 | return err(r.error); 257 | } 258 | } 259 | } 260 | 261 | const got = await strat.getInput(); 262 | switch (got.action) { 263 | case STEP: { 264 | inp = got.item; 265 | parsed = await strat.parse(inp); 266 | break; 267 | } 268 | 269 | case FINISH: 270 | return ok(got.value); 271 | 272 | case FAIL: { 273 | const r = await strat.onInputError?.(got.error) ?? fail(got.error); 274 | switch (r.action) { 275 | case FINISH: 276 | return ok(r.value); 277 | 278 | case FAIL: 279 | return err(r.error); 280 | } 281 | } 282 | } 283 | } 284 | } 285 | 286 | /** 287 | * Runs a loop which continuously gets input and attempts to parse it. 288 | * The loop strategy used will determine how the loop continues and ends. 289 | * This variant has no initial input. 290 | * This variant of the function is asynchronous using `Promise`. 291 | * @typeparam A - Input type. 292 | * @typeparam Z - Output type. 293 | * @typeparam E - Error type. 294 | * @param strat - The loop strategy to use. 295 | * @returns Either the parsed value or an error. 296 | */ 297 | export async function loop1Async(strat: LoopStrategyAsync): Promise> { 298 | for (;;) { 299 | const got = await strat.getInput(); 300 | switch (got.action) { 301 | case STEP: { 302 | const inp = got.item; 303 | const parsed = await strat.parse(inp); 304 | switch (parsed.action) { 305 | case FINISH: 306 | return ok(parsed.value); 307 | 308 | case FAIL: { 309 | const r = await strat.onParseError?.(parsed.error, inp) ?? step_(); 310 | switch (r.action) { 311 | case FINISH: 312 | return ok(r.value); 313 | 314 | case FAIL: 315 | return err(r.error); 316 | } 317 | } 318 | } 319 | 320 | break; 321 | } 322 | 323 | case FINISH: 324 | return ok(got.value); 325 | 326 | case FAIL: { 327 | const r = await strat.onInputError?.(got.error) ?? fail(got.error); 328 | switch (r.action) { 329 | case FINISH: 330 | return ok(r.value); 331 | 332 | case FAIL: 333 | return err(r.error); 334 | } 335 | } 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/option.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A type that can express the lack of a value. 3 | * Used in this library for when a generic type could be nullable. 4 | * @typeparam T - Type of results. 5 | */ 6 | export type Option = Some | None; 7 | 8 | /** 9 | * The value exists. 10 | * @typeparam T - Type of results. 11 | */ 12 | export interface Some { 13 | /** 14 | * If this is a Some, this is true. 15 | */ 16 | readonly exists: true; 17 | 18 | /** 19 | * The value, which only exists on a Some. 20 | */ 21 | readonly value: T; 22 | } 23 | 24 | /** 25 | * The value does not exist. 26 | */ 27 | export interface None { 28 | /** 29 | * If this is a None, this is false. 30 | */ 31 | readonly exists: false; 32 | 33 | readonly value?: undefined; 34 | } 35 | 36 | /** 37 | * Creates a Some. 38 | * @typeparam T - Type of results. 39 | * @param x - Value to use. 40 | * @returns An Option. 41 | */ 42 | export function some(x: T): Some { 43 | return { exists: true, value: x }; 44 | } 45 | 46 | /** 47 | * Creates a None. 48 | * @returns An Option. 49 | */ 50 | export function none(): None { 51 | return { exists: false }; 52 | } 53 | 54 | /** 55 | * Creates an Option from a value that could be null or undefined. 56 | * 57 | * ```ts 58 | * console.log(maybeOption(1)); 59 | * >>> { exists: true, value: 1 } 60 | * 61 | * console.log(maybeOption(null)); 62 | * >>> { exists: false } 63 | * 64 | * console.log(maybeOption(undefined)); 65 | * >>> { exists: false } 66 | * ``` 67 | * @param x - A nullable value. 68 | * @returns An Option. 69 | */ 70 | export function maybeOption(x: T | null | undefined): Option { 71 | if (x == null) { 72 | return none(); 73 | } 74 | 75 | return some(x); 76 | } 77 | 78 | /** 79 | * Gets the first Some from many Options. 80 | * @param xs - The Options. 81 | * @return The first Some, or None if there were no Some. 82 | */ 83 | export function orOption(...xs: Option[]): Option { 84 | for (const x of xs) { 85 | if (x.exists) { 86 | return x; 87 | } 88 | } 89 | 90 | return none(); 91 | } 92 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import { UnorderedStrategy, noStrategy } from './unordered'; 2 | import { Token } from './tokens'; 3 | import { ParserOutput, emptyOutput } from './parserOutput'; 4 | 5 | /** 6 | * Parses a list of tokens to separate out flags and options. 7 | */ 8 | export class Parser implements IterableIterator, Iterator { 9 | private input: Token[]; 10 | private unorderedStrategy: UnorderedStrategy = noStrategy(); 11 | private position = 0; 12 | 13 | /** 14 | * @param input - The input tokens. 15 | */ 16 | public constructor(input?: Token[]) { 17 | this.input = input ?? []; 18 | } 19 | 20 | /** 21 | * Sets the input to use. 22 | * This will reset the parser. 23 | * @param input - Input to use. 24 | * @returns The parser. 25 | */ 26 | public setInput(input: Token[]): this { 27 | this.input = input; 28 | this.reset(); 29 | return this; 30 | } 31 | 32 | /** 33 | * Sets the strategy for parsing unordered arguments. 34 | * This can be done in the middle of parsing. 35 | * 36 | * ```ts 37 | * const parser = new Parser(tokens) 38 | * .setUnorderedStrategy(longStrategy()) 39 | * .parse(); 40 | * ``` 41 | * 42 | * @returns The parser. 43 | */ 44 | public setUnorderedStrategy(s: UnorderedStrategy): this { 45 | this.unorderedStrategy = s; 46 | return this; 47 | } 48 | 49 | /** 50 | * Resets the state of the parser. 51 | * @return The parser. 52 | */ 53 | public reset(): this { 54 | this.position = 0; 55 | return this; 56 | } 57 | 58 | /** 59 | * Whether the parser is finished. 60 | */ 61 | public get finished(): boolean { 62 | return this.position >= this.input.length; 63 | } 64 | 65 | private shift(n: number): void { 66 | this.position += n; 67 | } 68 | 69 | /** 70 | * Gets the next parsed tokens. 71 | * If a parser output is passed in, that output will be mutated, otherwise a new one is made. 72 | * @param output - Parser output to mutate. 73 | * @returns An iterator result containing parser output. 74 | */ 75 | public next(output?: ParserOutput): IteratorResult { 76 | if (this.finished) { 77 | return { done: true, value: null }; 78 | } 79 | 80 | const ts = this.processToken(output); 81 | if (ts == null) { 82 | throw new Error('Unexpected end of input (this should never happen).'); 83 | } 84 | 85 | return { done: false, value: ts }; 86 | } 87 | 88 | private processToken(output?: ParserOutput): ParserOutput { 89 | return this.pFlag(output) 90 | || this.pOption(output) 91 | || this.pCompactOption(output) 92 | || this.pOrdered(output); 93 | } 94 | 95 | private pFlag(output = emptyOutput()): ParserOutput | null { 96 | const t = this.input[this.position]; 97 | const f = this.unorderedStrategy.matchFlag(t.value); 98 | if (f == null) { 99 | return null; 100 | } 101 | 102 | this.shift(1); 103 | 104 | output.flags.add(f); 105 | return output; 106 | } 107 | 108 | private pOption(output = emptyOutput()): ParserOutput | null { 109 | const t = this.input[this.position]; 110 | const o = this.unorderedStrategy.matchOption(t.value); 111 | if (o == null) { 112 | return null; 113 | } 114 | 115 | this.shift(1); 116 | 117 | if (!output.options.has(o)) { 118 | output.options.set(o, []); 119 | } 120 | 121 | const n = this.input[this.position]; 122 | if (n == null) { 123 | return output; 124 | } 125 | 126 | const bad = (this.unorderedStrategy.matchFlag(n.value) 127 | || this.unorderedStrategy.matchOption(n.value) 128 | || this.unorderedStrategy.matchCompactOption(n.value)) != null; 129 | 130 | if (bad) { 131 | return output; 132 | } 133 | 134 | this.shift(1); 135 | 136 | const xs = output.options.get(o); 137 | xs!.push(n.value); 138 | return output; 139 | } 140 | 141 | private pCompactOption(output = emptyOutput()): ParserOutput | null { 142 | const t = this.input[this.position]; 143 | const o = this.unorderedStrategy.matchCompactOption(t.value); 144 | if (o == null) { 145 | return null; 146 | } 147 | 148 | this.shift(1); 149 | 150 | if (!output.options.has(o[0])) { 151 | output.options.set(o[0], [o[1]]); 152 | } else { 153 | const a = output.options.get(o[0])!; 154 | a.push(o[1]); 155 | } 156 | 157 | return output; 158 | } 159 | 160 | private pOrdered(output = emptyOutput()): ParserOutput { 161 | const t = this.input[this.position]; 162 | this.shift(1); 163 | 164 | output.ordered.push(t); 165 | return output; 166 | } 167 | 168 | public [Symbol.iterator](): this { 169 | return this; 170 | } 171 | 172 | /** 173 | * Runs the parser. 174 | * 175 | * ```ts 176 | * const lexer = new Lexer(input); 177 | * const tokens = lexer.lex(); 178 | * const parser = new Parser(tokens); 179 | * const output = parser.parse(); 180 | * ``` 181 | * 182 | * @returns The parser output. 183 | */ 184 | public parse(): ParserOutput { 185 | const output = emptyOutput(); 186 | let r = this.next(output); 187 | while (!r.done) { 188 | r = this.next(output); 189 | } 190 | 191 | return output; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/parserOutput.ts: -------------------------------------------------------------------------------- 1 | import { Token } from './tokens'; 2 | 3 | /** 4 | * Output of the parser. 5 | */ 6 | export interface ParserOutput { 7 | /** 8 | * All the tokens that are not flags or options, in order. 9 | */ 10 | ordered: Token[]; 11 | 12 | /** 13 | * The parsed flags. 14 | */ 15 | flags: Set; 16 | 17 | /** 18 | * The parsed options mapped to their value. 19 | */ 20 | options: Map; 21 | } 22 | 23 | /** 24 | * Creates an empty parser output. 25 | * @returns An empty output. 26 | */ 27 | export function emptyOutput(): ParserOutput { 28 | return { 29 | ordered: [], 30 | flags: new Set(), 31 | options: new Map() 32 | }; 33 | } 34 | 35 | /** 36 | * Merges multiple outputs into one. 37 | * Flags and options that appear later will be preferred if there are duplicates. 38 | * @param ps - The outputs to merge. 39 | * @returns The merged output. 40 | */ 41 | export function mergeOutputs(...ps: ParserOutput[]): ParserOutput { 42 | const output = emptyOutput(); 43 | 44 | for (const p of ps) { 45 | output.ordered.push(...p.ordered); 46 | 47 | for (const f of p.flags) { 48 | output.flags.add(f); 49 | } 50 | 51 | for (const [o, xs] of p.options.entries()) { 52 | if (!output.options.has(o)) { 53 | output.options.set(o, []); 54 | } 55 | 56 | const ys = output.options.get(o); 57 | ys!.push(...xs); 58 | } 59 | } 60 | 61 | return output; 62 | } 63 | 64 | /** 65 | * Converts an output to JSON, where the flags and options are turned into arrays of entries. 66 | * You can recover the output with 'outputFromJSON'. 67 | * @param p - The output. 68 | * @returns The JSON. 69 | */ 70 | export function outputToJSON(p: ParserOutput): Record { 71 | return { 72 | ordered: p.ordered, 73 | flags: [...p.flags], 74 | options: [...p.options] 75 | }; 76 | } 77 | 78 | /** 79 | * Converts JSON to a parser output. 80 | * @param obj - A valid JSON input, following the schema from 'outputToJSON'. 81 | * @returns The output. 82 | */ 83 | export function outputFromJSON(obj: Record): ParserOutput { 84 | return { 85 | ordered: obj.ordered as Token[], 86 | flags: new Set(obj.flags as string[]), 87 | options: new Map(obj.options as [string, string[]][]) 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /src/result.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A type used to express computations that can fail. 3 | * @typeparam T - Type of results. 4 | * @typeparam E - Type of errors. 5 | */ 6 | export type Result = Ok | Err; 7 | 8 | /** 9 | * The computation is successful. 10 | * @typeparam T - Type of results. 11 | */ 12 | export interface Ok { 13 | /** 14 | * If this is an Ok, this is true. 15 | */ 16 | readonly success: true; 17 | 18 | /** 19 | * The resulting value, which only exists on an Ok. 20 | */ 21 | readonly value: T; 22 | 23 | readonly error?: undefined 24 | } 25 | 26 | /** 27 | * The computation failed. 28 | * @typeparam E - Type of errors. 29 | */ 30 | export interface Err { 31 | /** 32 | * If this an Err, this is false. 33 | */ 34 | readonly success: false; 35 | 36 | readonly value?: undefined 37 | 38 | /** 39 | * The resulting error, which only exists on an Err. 40 | */ 41 | readonly error: E; 42 | } 43 | 44 | /** 45 | * Creates an Ok. 46 | * @typeparam T - Type of results. 47 | * @param x - Value to use. 48 | * @returns A Result. 49 | */ 50 | export function ok(x: T): Ok { 51 | return { success: true, value: x }; 52 | } 53 | 54 | /** 55 | * Creates an Err. 56 | * @typeparam E - Type of errors. 57 | * @param x - Value to use. 58 | * @returns A Result. 59 | */ 60 | export function err(x: E): Err { 61 | return { success: false, error: x }; 62 | } 63 | 64 | /** 65 | * Creates an Err with null value. 66 | * @returns A Result. 67 | */ 68 | export function err_(): Err { 69 | return { success: false, error: null }; 70 | } 71 | 72 | /** 73 | * Creates a Result from a value that could be null or undefined. 74 | * 75 | * ```ts 76 | * console.log(maybeResult(1, 'bad')); 77 | * >>> { success: true, value: 1 } 78 | * 79 | * console.log(maybeResult(null, 'bad')); 80 | * >>> { success: false, error: 'bad' } 81 | * 82 | * console.log(maybeResult(undefined, 'bad')); 83 | * >>> { success: false, error: 'bad' } 84 | * ``` 85 | * @param x - A nullable value. 86 | * @param e - The error to use. 87 | * @returns A Result. 88 | */ 89 | export function maybeResult(x: T | null | undefined, e: E): Result { 90 | if (x == null) { 91 | return err(e); 92 | } 93 | 94 | return ok(x); 95 | } 96 | 97 | /** 98 | * Gets the first Ok from many Results. 99 | * @param x - The first Result. 100 | * @param xs - The remaining Results; this encoding is to ensure there is at least one Result. 101 | * @return The first Ok, or all the Errs if there were no Ok. 102 | */ 103 | export function orResultAll(x: Result, ...xs: Result[]): Result { 104 | if (x.success) { 105 | return x; 106 | } 107 | 108 | const es = [x.error]; 109 | for (const x of xs) { 110 | if (x.success) { 111 | return x; 112 | } 113 | 114 | es.push(x.error); 115 | } 116 | 117 | return err(es); 118 | } 119 | 120 | /** 121 | * Gets the first Ok from many Results. 122 | * @param x - The first Result. 123 | * @param xs - The remaining Results; this encoding is to ensure there is at least one Result. 124 | * @return The first Ok, or the first Err if there were no Ok. 125 | */ 126 | export function orResultFirst(x: Result, ...xs: Result[]): Result { 127 | if (x.success) { 128 | return x; 129 | } 130 | 131 | const e = x.error; 132 | for (const x of xs) { 133 | if (x.success) { 134 | return x; 135 | } 136 | } 137 | 138 | return err(e); 139 | } 140 | 141 | /** 142 | * Gets the first Ok from many Results. 143 | * @param x - The first Result. 144 | * @param xs - The remaining Results; this encoding is to ensure there is at least one Result. 145 | * @return The first Ok, or the last Err if there were no Ok. 146 | */ 147 | export function orResultLast(x: Result, ...xs: Result[]): Result { 148 | if (x.success) { 149 | return x; 150 | } 151 | 152 | let e = x.error; 153 | for (const x of xs) { 154 | if (x.success) { 155 | return x; 156 | } 157 | 158 | e = x.error; 159 | } 160 | 161 | return err(e); 162 | } 163 | -------------------------------------------------------------------------------- /src/tokens.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a token. 3 | */ 4 | export interface Token { 5 | /** 6 | * The value of the token. 7 | */ 8 | readonly value: string; 9 | 10 | /** 11 | * The raw value of the token e.g. with quotes. 12 | */ 13 | readonly raw: string; 14 | 15 | /** 16 | * Trailing whitespace. 17 | */ 18 | readonly trailing: string; 19 | } 20 | 21 | /** 22 | * Joins tokens together. 23 | * By default, this keeps as much of the original input as possible. 24 | * 25 | * ```ts 26 | * // Note three trailing spaces. 27 | * const tokens = new Lexer('hello "world"') 28 | * .setQuotes([['"', '"']]) 29 | * .lex(); 30 | * 31 | * console.log(joinTokens(tokens)); 32 | * >>> 'hello "world"' 33 | * 34 | * console.log(joinTokens(tokens, ' ', false)); 35 | * >>> 'hello world' 36 | * ``` 37 | * 38 | * @param tokens - Tokens to join. 39 | * @param separator - The separator, if null, will use original trailing whitespace; defaults to null. 40 | * @param raw - Whether to use raw values e.g. with quotes; defaults to true. 41 | * @returns The joined string. 42 | */ 43 | export function joinTokens(tokens: Token[], separator: string | null = null, raw = true): string { 44 | if (tokens.length === 0) { 45 | return ''; 46 | } 47 | 48 | if (separator != null && !raw) { 49 | return tokens.map(t => t.value).join(separator); 50 | } 51 | 52 | const xs = []; 53 | for (let i = 0; i < tokens.length - 1; i++) { 54 | const t = tokens[i]; 55 | xs.push(raw ? t.raw : t.value); 56 | xs.push(separator ?? t.trailing); 57 | } 58 | 59 | const last = tokens[tokens.length - 1]; 60 | xs.push(raw ? last.raw : last.value); 61 | return xs.join(''); 62 | } 63 | 64 | /** 65 | * A function to match a prefix. 66 | * @param s - A string that may start with the prefix. 67 | * @returns The length of the prefix if there is one. 68 | */ 69 | export type MatchPrefix = (s: string) => number | null; 70 | 71 | /** 72 | * Extracts a command from the first one or two tokens from a list of tokens. 73 | * The command format is ' ', and the space is optional. 74 | * @param matchPrefix - A function that gives the length of the prefix if there is one. 75 | * @param tokens - Tokens to check. 76 | * @param mutate - Whether to mutate the list of tokens. 77 | * @returns The token containing the name of the command. 78 | * This may be a token from the list or a new token. 79 | */ 80 | export function extractCommand(matchPrefix: MatchPrefix, tokens: Token[], mutate = true): Token | null { 81 | if (tokens.length < 1) { 82 | return null; 83 | } 84 | 85 | const plen = matchPrefix(tokens[0].raw); 86 | if (plen == null) { 87 | return null; 88 | } 89 | 90 | if (tokens[0].raw.length === plen) { 91 | if (tokens.length < 2) { 92 | return null; 93 | } 94 | 95 | if (mutate) { 96 | tokens.shift(); 97 | return tokens.shift()!; 98 | } 99 | 100 | return tokens[1]; 101 | } 102 | 103 | if (mutate) { 104 | const t = tokens.shift()!; 105 | const v = t.raw.slice(plen); 106 | return { value: v, raw: v, trailing: t.trailing }; 107 | } 108 | 109 | const v = tokens[0].raw.slice(plen); 110 | return { value: v, raw: v, trailing: tokens[0].trailing }; 111 | } 112 | -------------------------------------------------------------------------------- /src/unordered.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A strategy to match unordered arguments in parsing. 3 | */ 4 | export interface UnorderedStrategy { 5 | /** 6 | * Match a flag. 7 | * @param s - The string. 8 | * @returns The name of the flag. 9 | */ 10 | matchFlag(s: string): string | null; 11 | 12 | /** 13 | * Match an option. 14 | * @param s - The string. 15 | * @returns The name of the option. 16 | */ 17 | matchOption(s: string): string | null; 18 | 19 | /** 20 | * Match a compact option. 21 | * @param s - The string. 22 | * @returns A pair containing the name of the option and the value. 23 | */ 24 | matchCompactOption(s: string): [string, string] | null; 25 | } 26 | 27 | /** 28 | * Do not match any unordered argument at all. 29 | * @returns The strategy. 30 | */ 31 | export function noStrategy(): UnorderedStrategy { 32 | return { 33 | matchFlag: () => null, 34 | matchOption: () => null, 35 | matchCompactOption: () => null 36 | }; 37 | } 38 | 39 | /** 40 | * Match unordered arguments according to conventional syntax. 41 | * '--flag' is a flag. 42 | * '--opt=' is an option. 43 | * '--opt=123' is a compact option. 44 | * @returns The strategy. 45 | */ 46 | export function longStrategy(): UnorderedStrategy { 47 | return prefixedStrategy(['--'], ['=']); 48 | } 49 | 50 | /** 51 | * Match unordered arguments according to conventional syntax. 52 | * '--flag' or '-flag' is a flag. 53 | * '--opt=' or '-opt=' is an option. 54 | * '--opt=123' or '-opt=123' is a compact option. 55 | * @returns The strategy. 56 | */ 57 | export function longShortStrategy(): UnorderedStrategy { 58 | return prefixedStrategy(['--', '-'], ['=']); 59 | } 60 | 61 | /** 62 | * Match unordered arguments with custom prefix and separator. 63 | * The prefix is the part the preceeds the key name, e.g. '--' in '--foo'. 64 | * The separator is the part that delimits the key and value e.g. '=' in '--key=value'. 65 | * It is expected that there are no spaces in the prefixes and separators. 66 | * The matching is done in a case-sensitive manner. 67 | * Also note that if the input contains multiple of the separators, the matching may be ambiguous. 68 | * 69 | * ```ts 70 | * const st = prefixedStrategy(['--'], ['=']); 71 | * console.log(st.matchFlag('--f')); 72 | * >>> 'f' 73 | * 74 | * console.log(st.matchOption('--opt=')); 75 | * >>> 'opt' 76 | * 77 | * console.log(st.matchCompactOption('--opt=15')); 78 | * >>> ['opt', '15'] 79 | * ``` 80 | * 81 | * @param prefixes - The prefixes to use for unordered arguments. 82 | * They should be ordered by length in non-increasing order. 83 | * @param separators - The symbols to use to separate the key and value in options. 84 | * They should be ordered by length in non-increasing order. 85 | * @returns The strategy. 86 | */ 87 | export function prefixedStrategy(prefixes: string[], separators: string[]): UnorderedStrategy { 88 | return { 89 | matchFlag(s: string) { 90 | const pre = prefixes.find(x => s.startsWith(x)); 91 | if (pre == null) { 92 | return null; 93 | } 94 | 95 | s = s.slice(pre.length); 96 | const sep = separators.find(x => s.includes(x)); 97 | if (sep != null) { 98 | return null; 99 | } 100 | 101 | return s; 102 | }, 103 | 104 | matchOption(s: string) { 105 | const pre = prefixes.find(x => s.startsWith(x)); 106 | if (pre == null) { 107 | return null; 108 | } 109 | 110 | s = s.slice(pre.length); 111 | const sep = separators.find(x => s.endsWith(x)); 112 | if (sep == null) { 113 | return null; 114 | } 115 | 116 | return s.slice(0, -sep.length); 117 | }, 118 | 119 | matchCompactOption(s: string) { 120 | const pre = prefixes.find(x => s.startsWith(x)); 121 | if (pre == null) { 122 | return null; 123 | } 124 | 125 | s = s.slice(pre.length); 126 | const sep = separators.find(x => s.includes(x)); 127 | if (sep == null) { 128 | return null; 129 | } 130 | 131 | const i = s.indexOf(sep); 132 | if (i + sep.length === s.length) { 133 | return null; 134 | } 135 | 136 | const k = s.slice(0, i); 137 | const v = s.slice(i + sep.length); 138 | return [k, v]; 139 | } 140 | }; 141 | } 142 | 143 | /** 144 | * Pairing of flag/option names to the words usable for them. 145 | */ 146 | type Pairing = Record; 147 | 148 | function findPairing(ps: Pairing, p: (w: string) => boolean): [string, string] | null { 149 | for (const [k, ws] of Object.entries(ps)) { 150 | for (const w of ws) { 151 | if (p(w)) { 152 | return [k, w]; 153 | } 154 | } 155 | } 156 | 157 | return null; 158 | } 159 | 160 | /** 161 | * Match unordered arguments according to a record of the names to the list of words. 162 | * Prefixes like '--' and separators like '=' should be a part of the word. 163 | * This function uses 164 | * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator} 165 | * which can compare in different locales and different sensitivities. 166 | * Note that this only works for en-US if you are below Node 13.0.0. 167 | * 168 | * ```ts 169 | * const st = matchingStrategy({ flag: ['--flag', '-f'] }, {}); 170 | * console.log(st.matchFlag('--flag')); 171 | * >>> 'flag' 172 | * 173 | * console.log(st.matchOption('-f')); 174 | * >>> 'flag' 175 | * 176 | * const stbase = matchingStrategy({ flag: ['--flag'] }, {}, 'en-US', { sensitivity: 'base' }); 177 | * console.log(stbase.matchFlag('--FLAG')); 178 | * >>> 'flag' 179 | * 180 | * console.log(stbase.matchFlag('--flág')); 181 | * >>> 'flag' 182 | * ``` 183 | * 184 | * @param flags - Words usable as flags. 185 | * @param options - Words usable as options. 186 | * They should be ordered by length in non-increasing order. 187 | * @param locales - Locale(s) to use. 188 | * @param collatorOptions - Options for comparing strings. 189 | * See {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator} 190 | * for more information. 191 | * @returns The strategy. 192 | */ 193 | export function matchingStrategy( 194 | flags: Pairing, 195 | options: Pairing, 196 | locales?: string | string[] | undefined, 197 | collatorOptions?: Intl.CollatorOptions 198 | ): UnorderedStrategy { 199 | const compare = new Intl.Collator(locales, collatorOptions).compare; 200 | const eq = (w: string, s: string): boolean => compare(w, s) === 0; 201 | 202 | return { 203 | matchFlag(s: string) { 204 | const res = findPairing(flags, w => eq(w, s)); 205 | return res?.[0] ?? null; 206 | }, 207 | 208 | matchOption(s: string) { 209 | const res = findPairing(options, w => eq(w, s)); 210 | return res?.[0] ?? null; 211 | }, 212 | 213 | matchCompactOption(s: string) { 214 | const res = findPairing(options, w => eq(w, s.slice(0, w.length))); 215 | if (res == null) { 216 | return null; 217 | } 218 | 219 | const [k, w] = res; 220 | const v = s.slice(w.length); 221 | return [k, v]; 222 | } 223 | }; 224 | } 225 | 226 | /** 227 | * Creates a new strategy that maps the names of flags and options in an unordered strategy. 228 | * ```ts 229 | * const st1 = longStrategy(); 230 | * 231 | * console.log(st1.matchFlag('--foo'), st1.matchFlag('--FOO')); 232 | * >>> 'foo' 'FOO' 233 | * 234 | * const st2 = mapKeys(longStrategy(), k => k.toLowerCase()); 235 | * 236 | * console.log(st2.matchFlag('--foo'), st1.matchFlag('--FOO')); 237 | * >>> 'foo' 'foo' 238 | * ``` 239 | * @param strat - A strategy. 240 | * @param f - Creates a new name from the old name, or return null to not include it. 241 | * @returns A new strategy. 242 | */ 243 | export function mapKeys(strat: UnorderedStrategy, f: (s: string) => string | null): UnorderedStrategy { 244 | return { 245 | matchFlag(s: string) { 246 | const m = strat.matchFlag(s); 247 | return m == null ? null : f(m); 248 | }, 249 | 250 | matchOption(s: string) { 251 | const m = strat.matchOption(s); 252 | return m == null ? null : f(m); 253 | }, 254 | 255 | matchCompactOption(s: string) { 256 | const m = strat.matchCompactOption(s); 257 | if (m == null) { 258 | return null; 259 | } 260 | 261 | const k = f(m[0]); 262 | return k == null ? null : [k, m[1]]; 263 | } 264 | }; 265 | } 266 | 267 | /** 268 | * Creates a new strategy that renames the names of flags and options of another strategy. 269 | * This is done according to a record of the names to a list of words. 270 | * This function uses 271 | * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator} 272 | * which can compare in different locales and different sensitivities. 273 | * Note that this only works for en-US if you are below Node 13.0.0. 274 | * 275 | * ```ts 276 | * const st = renameKeys(longStrategy(), { foo: ['bar'] }); 277 | * 278 | * console.log(st.matchFlag('--bar')); 279 | * >>> 'foo' 280 | * ``` 281 | * 282 | * @param strat - A strategy. 283 | * @param keys - The pairing of keys. 284 | * @param keepNotFound - Whether to keep keys that are not found in `keys`; defaults to true. 285 | * @param locales - Locale(s) to use. 286 | * @param collatorOptions - Options for comparing strings. 287 | * See {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator} 288 | * for more information. 289 | * @returns A new strategy. 290 | */ 291 | export function renameKeys( 292 | strat: UnorderedStrategy, 293 | keys: Pairing, 294 | keepNotFound = true, 295 | locales?: string | string[] | undefined, 296 | collatorOptions?: Intl.CollatorOptions 297 | ): UnorderedStrategy { 298 | const compare = new Intl.Collator(locales, collatorOptions).compare; 299 | const eq = (w: string, s: string): boolean => compare(w, s) === 0; 300 | 301 | return mapKeys(strat, k => { 302 | const res = findPairing(keys, w => eq(w, k))?.[0] ?? null; 303 | return keepNotFound && res == null ? k : res; 304 | }); 305 | } 306 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { Option, some, none } from './option'; 2 | import { Result, ok, err } from './result'; 3 | import { LoopAction, step, finish, fail } from './loopAction'; 4 | 5 | /** 6 | * Converts an Option to a Result. 7 | * - Some -> Ok 8 | * - None -> Err 9 | * @param x - The Option. 10 | * @param error - The error if None. 11 | * @returns A Result. 12 | */ 13 | export function someToOk(x: Option, error: E): Result { 14 | if (x.exists) { 15 | return ok(x.value); 16 | } 17 | 18 | return err(error); 19 | } 20 | 21 | /** 22 | * Converts a Result to an Option. 23 | * - Ok -> Some 24 | * - Err -> None 25 | * @param x - The Result. 26 | * @returns An Option. 27 | */ 28 | export function okToSome(x: Result): Option { 29 | if (x.success) { 30 | return some(x.value); 31 | } 32 | 33 | return none(); 34 | } 35 | 36 | /** 37 | * Converts a Result to an Option. 38 | * - Ok -> None 39 | * - Err -> Some 40 | * @param x - The Result. 41 | * @returns An Option. 42 | */ 43 | export function errToSome(x: Result): Option { 44 | if (!x.success) { 45 | return some(x.error); 46 | } 47 | 48 | return none(); 49 | } 50 | 51 | /** 52 | * Converts an Option to a LoopAction. 53 | * - Some -> Step 54 | * - None -> Fail 55 | * @param x - The Option. 56 | * @param error - The error if None. 57 | * @returns A LoopAction. 58 | */ 59 | export function someToStep(x: Option, error: E): LoopAction { 60 | if (x.exists) { 61 | return step(x.value); 62 | } 63 | 64 | return fail(error); 65 | } 66 | 67 | /** 68 | * Converts an Option to a LoopAction. 69 | * - Some -> Finish 70 | * - None -> Fail 71 | * @param x - The Option. 72 | * @param error - The error if None. 73 | * @returns A LoopAction. 74 | */ 75 | export function someToFinish(x: Option, error: E): LoopAction { 76 | if (x.exists) { 77 | return finish(x.value); 78 | } 79 | 80 | return fail(error); 81 | } 82 | 83 | /** 84 | * Converts a Result to a LoopAction. 85 | * - Ok -> Step 86 | * - Err -> Fail 87 | * @param x - The Result. 88 | * @returns A LoopAction. 89 | */ 90 | export function okToStep(x: Result): LoopAction { 91 | if (x.success) { 92 | return step(x.value); 93 | } 94 | 95 | return fail(x.error); 96 | } 97 | 98 | /** 99 | * Converts a Result to a LoopAction. 100 | * - Ok -> Finish 101 | * - Err -> Fail 102 | * @param x - The Result. 103 | * @returns A LoopAction. 104 | */ 105 | export function okToFinish(x: Result): LoopAction { 106 | if (x.success) { 107 | return finish(x.value); 108 | } 109 | 110 | return fail(x.error); 111 | } 112 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as lexure from '../src'; 2 | import { 3 | Lexer, Parser, Args, Result, 4 | prefixedStrategy, 5 | LoopStrategyAsync, loopAsync, loop1Async, 6 | some, err, ok, step, finish, fail 7 | } from '../src'; 8 | 9 | describe('readme', () => { 10 | it('should work', () => { 11 | const input = '!hello world "cool stuff" --foo --bar=baz a b c'; 12 | 13 | const lexer = new lexure.Lexer(input) 14 | .setQuotes([['"', '"'], ['“', '”']]); 15 | 16 | const res = lexer.lexCommand(s => s.startsWith('!') ? 1 : null); 17 | expect(res).not.toBeNull(); 18 | 19 | const cmd = res![0]; 20 | expect(cmd).toEqual({ value: 'hello', raw: 'hello', trailing: ' ' }); 21 | 22 | const tokens = res![1](); 23 | expect(tokens).toEqual([ 24 | { value: 'world', raw: 'world', trailing: ' ' }, 25 | { value: 'cool stuff', raw: '"cool stuff"', trailing: ' ' }, 26 | { value: '--foo', raw: '--foo', trailing: ' ' }, 27 | { value: '--bar=baz', raw: '--bar=baz', trailing: ' ' }, 28 | { value: 'a', raw: 'a', trailing: ' ' }, 29 | { value: 'b', raw: 'b', trailing: ' ' }, 30 | { value: 'c', raw: 'c', trailing: '' } 31 | ]); 32 | 33 | const parser = new lexure.Parser(tokens) 34 | .setUnorderedStrategy(lexure.longStrategy()); 35 | 36 | const out = parser.parse(); 37 | expect(out).toEqual({ 38 | ordered: [ 39 | { value: 'world', raw: 'world', trailing: ' ' }, 40 | { value: 'cool stuff', raw: '"cool stuff"', trailing: ' ' }, 41 | { value: 'a', raw: 'a', trailing: ' ' }, 42 | { value: 'b', raw: 'b', trailing: ' ' }, 43 | { value: 'c', raw: 'c', trailing: '' } 44 | ], 45 | flags: new Set(['foo']), 46 | options: new Map([['bar', ['baz']]]) 47 | }); 48 | 49 | const j = lexure.joinTokens(out.ordered); 50 | expect(j).toEqual('world "cool stuff" a b c'); 51 | 52 | const args = new lexure.Args(out); 53 | 54 | const a1 = args.single(); 55 | expect(a1).toEqual('world'); 56 | 57 | const a2 = args.single(); 58 | expect(a2).toEqual('cool stuff'); 59 | 60 | const a3 = args.findMap(x => x === 'c' ? lexure.some('it was a C') : lexure.none()); 61 | expect(a3).toEqual(some('it was a C')); 62 | 63 | const a4 = args.many(); 64 | expect(a4).toEqual([ 65 | { value: 'a', raw: 'a', trailing: ' ' }, 66 | { value: 'b', raw: 'b', trailing: ' ' } 67 | ]); 68 | 69 | const a5 = args.flag('foo'); 70 | expect(a5).toEqual(true); 71 | 72 | const a6 = args.option('bar'); 73 | expect(a6).toEqual('baz'); 74 | 75 | function prompt(): string | null { 76 | return '100'; 77 | } 78 | 79 | const result = lexure.loop1({ 80 | getInput() { 81 | const input = prompt(); 82 | if (input == null) { 83 | return lexure.fail('bad input'); 84 | } else { 85 | return lexure.step(input); 86 | } 87 | }, 88 | 89 | parse(s: string) { 90 | const n = Number(s); 91 | if (isNaN(n)) { 92 | return lexure.fail('bad input'); 93 | } else { 94 | return lexure.finish(n); 95 | } 96 | } 97 | }); 98 | 99 | expect(result).toEqual(ok(100)); 100 | }); 101 | }); 102 | 103 | describe('complete-example', () => { 104 | function parseCommand(s: string): [string, Args] | null { 105 | const lexer = new Lexer(s) 106 | .setQuotes([ 107 | ['"', '"'], // Double quotes 108 | ['“', '”'], // Fancy quotes (on iOS) 109 | ["「", "」"] // Corner brackets (CJK) 110 | ]); // Add more as you see fit! 111 | 112 | const lout = lexer.lexCommand(s => s.startsWith('!') ? 1 : null); 113 | if (lout == null) { 114 | return null; 115 | } 116 | 117 | const [command, getTokens] = lout; 118 | const tokens = getTokens(); 119 | const parser = new Parser(tokens) 120 | .setUnorderedStrategy(prefixedStrategy( 121 | ['--', '-', '—'], // Various flag prefixes including em dash. 122 | ['=', ':'] // Various separators for options. 123 | )); 124 | 125 | const pout = parser.parse(); 126 | return [command.value, new Args(pout)]; 127 | } 128 | 129 | function runCommand(s: string): string { 130 | const out = parseCommand(s); 131 | if (out == null) { 132 | return 'Not a command.'; 133 | } 134 | 135 | const [command, args] = out; 136 | if (command === 'add') { 137 | // These calls to `Args#single` can give a string or null. 138 | const x = args.single(); 139 | const y = args.single(); 140 | // Which means this could give NaN on bad inputs. 141 | const z = Number(x) + Number(y); 142 | return `The answer is ${z}.`; 143 | } else { 144 | return 'Not an implemented command.'; 145 | } 146 | } 147 | 148 | it('should work', () => { 149 | expect(runCommand('!add 1 2')).toEqual('The answer is 3.'); 150 | expect(runCommand('!add 1 x')).toEqual('The answer is NaN.'); 151 | expect(runCommand('!foo')).toEqual('Not an implemented command.'); 152 | expect(runCommand('hello')).toEqual('Not a command.'); 153 | }); 154 | }); 155 | 156 | describe('parsing-with-loops', () => { 157 | let aski = 0; 158 | let asks: string[] = []; 159 | function ask(): Promise { 160 | if (aski >= asks.length) { 161 | return Promise.resolve(null); 162 | } 163 | 164 | return Promise.resolve(asks[aski++]); 165 | } 166 | 167 | let says: string[] = []; 168 | function say(s: string): Promise { 169 | says.push(s); 170 | return Promise.resolve(); 171 | } 172 | 173 | function sayError(e: ParseError): Promise { 174 | switch (e) { 175 | case ParseError.PARSE_FAILURE: 176 | return say('Invalid input.'); 177 | 178 | case ParseError.NO_INPUT_GIVEN: 179 | return say('You did not give a value in time.'); 180 | 181 | case ParseError.TOO_MANY_TRIES: 182 | return say('You took too many tries.'); 183 | } 184 | } 185 | 186 | enum ParseError { 187 | PARSE_FAILURE, 188 | NO_INPUT_GIVEN, 189 | TOO_MANY_TRIES 190 | } 191 | 192 | type Parser = (x: string) => Result; 193 | type ParserAsync = (x: string) => Promise>; 194 | 195 | function makeLoopStrategy(expected: string, runParser: Parser): LoopStrategyAsync { 196 | let retries = 0; 197 | return { 198 | async getInput() { 199 | if (retries >= 3) { 200 | return fail(ParseError.TOO_MANY_TRIES); 201 | } 202 | 203 | const s = await ask(); 204 | retries++; 205 | if (s == null) { 206 | return fail(ParseError.NO_INPUT_GIVEN); 207 | } 208 | 209 | return step(s); 210 | }, 211 | 212 | async parse(s: string) { 213 | const res = runParser(s); 214 | if (res.success) { 215 | return finish(res.value); 216 | } 217 | 218 | await say(`Invalid input ${s}, please give a valid ${expected}.`); 219 | return fail(ParseError.PARSE_FAILURE); 220 | } 221 | }; 222 | } 223 | 224 | function loopParse(expected: string, runParser: Parser): ParserAsync { 225 | return (init: string) => loopAsync(init, makeLoopStrategy(expected, runParser)); 226 | } 227 | 228 | async function loop1Parse(expected: string, runParser: Parser): Promise> { 229 | await say(`No input given, please give a valid ${expected}.`); 230 | return loop1Async(makeLoopStrategy(expected, runParser)); 231 | } 232 | 233 | async function singleParseWithLoop( 234 | args: Args, 235 | expected: string, 236 | parser: Parser 237 | ): Promise> { 238 | return await args.singleParseAsync(loopParse(expected, parser)) 239 | ?? await loop1Parse(expected, parser); 240 | } 241 | 242 | function parseNumber(x: string): Result { 243 | const n = Number(x); 244 | return isNaN(n) ? err(ParseError.PARSE_FAILURE) : ok(n); 245 | } 246 | 247 | async function addCommand(args: Args): Promise { 248 | const n1 = await singleParseWithLoop(args, 'number', parseNumber); 249 | if (!n1.success) { 250 | return sayError(n1.error); 251 | } 252 | 253 | const n2 = await singleParseWithLoop(args, 'number', parseNumber); 254 | if (!n2.success) { 255 | return sayError(n2.error); 256 | } 257 | 258 | const z = n1.value + n2.value; 259 | return say(`That adds to ${z}.`); 260 | } 261 | 262 | it('should work with correct inputs', async () => { 263 | asks = []; 264 | 265 | const input = '1 3'; 266 | const args = new Args(new Parser(new Lexer(input).lex()).parse()); 267 | await addCommand(args); 268 | expect(aski).toEqual(0); 269 | expect(says).toEqual(['That adds to 4.']); 270 | 271 | aski = 0; 272 | asks = []; 273 | says = []; 274 | }); 275 | 276 | it('should work with one incorrect input, one retry', async () => { 277 | asks = ['ok', '3']; 278 | 279 | const input = '1 b'; 280 | const args = new Args(new Parser(new Lexer(input).lex()).parse()); 281 | await addCommand(args); 282 | expect(aski).toEqual(2); 283 | expect(says).toEqual([ 284 | 'Invalid input b, please give a valid number.', 285 | 'Invalid input ok, please give a valid number.', 286 | 'That adds to 4.' 287 | ]); 288 | 289 | aski = 0; 290 | asks = []; 291 | says = []; 292 | }); 293 | 294 | it('should work with two incorrect inputs, no retries', async () => { 295 | asks = ['1', '3']; 296 | 297 | const input = 'a b'; 298 | const args = new Args(new Parser(new Lexer(input).lex()).parse()); 299 | await addCommand(args); 300 | expect(aski).toEqual(2); 301 | expect(says).toEqual([ 302 | 'Invalid input a, please give a valid number.', 303 | 'Invalid input b, please give a valid number.', 304 | 'That adds to 4.' 305 | ]); 306 | 307 | aski = 0; 308 | asks = []; 309 | says = []; 310 | }); 311 | 312 | it('should work with no initial inputs', async () => { 313 | asks = ['1', '3']; 314 | 315 | const input = ''; 316 | const args = new Args(new Parser(new Lexer(input).lex()).parse()); 317 | await addCommand(args); 318 | expect(aski).toEqual(2); 319 | expect(says).toEqual([ 320 | 'No input given, please give a valid number.', 321 | 'No input given, please give a valid number.', 322 | 'That adds to 4.' 323 | ]); 324 | 325 | aski = 0; 326 | asks = []; 327 | says = []; 328 | }); 329 | 330 | it('should work one incorrect input, retry limit reached', async () => { 331 | asks = ['bad', 'badder', 'baddest', 'bad?', 'bad!']; 332 | 333 | const input = '1 b'; 334 | const args = new Args(new Parser(new Lexer(input).lex()).parse()); 335 | await addCommand(args); 336 | expect(aski).toEqual(3); 337 | expect(says).toEqual([ 338 | 'Invalid input b, please give a valid number.', 339 | 'Invalid input bad, please give a valid number.', 340 | 'Invalid input badder, please give a valid number.', 341 | 'Invalid input baddest, please give a valid number.', 342 | 'You took too many tries.' 343 | ]); 344 | 345 | aski = 0; 346 | asks = []; 347 | says = []; 348 | }); 349 | 350 | it('should work with two incorrect inputs, no input given', async () => { 351 | asks = []; 352 | 353 | const input = 'a b'; 354 | const args = new Args(new Parser(new Lexer(input).lex()).parse()); 355 | await addCommand(args); 356 | expect(aski).toEqual(0); 357 | expect(says).toEqual([ 358 | 'Invalid input a, please give a valid number.', 359 | 'You did not give a value in time.' 360 | ]); 361 | 362 | aski = 0; 363 | asks = []; 364 | says = []; 365 | }); 366 | }); 367 | -------------------------------------------------------------------------------- /test/lexer.test.ts: -------------------------------------------------------------------------------- 1 | import * as fc from 'fast-check'; 2 | import { Lexer } from '../src'; 3 | 4 | describe('Lexer#lex', () => { 5 | it('with no quotes, parses text without quotes', () => { 6 | const s = 'simple text here'; 7 | const ts = new Lexer(s).lex(); 8 | expect(ts).toEqual([ 9 | { value: 'simple', raw: 'simple', trailing: ' ' }, 10 | { value: 'text', raw: 'text', trailing: ' ' }, 11 | { value: 'here', raw: 'here', trailing: '' } 12 | ]); 13 | }); 14 | 15 | it('with no quotes, parses text with quotes', () => { 16 | const s = 'simple "text" here'; 17 | const ts = new Lexer(s).lex(); 18 | expect(ts).toEqual([ 19 | { value: 'simple', raw: 'simple', trailing: ' ' }, 20 | { value: '"text"', raw: '"text"', trailing: ' ' }, 21 | { value: 'here', raw: 'here', trailing: '' } 22 | ]); 23 | }); 24 | 25 | it('with quotes, parses text without quotes', () => { 26 | const s = 'simple text here'; 27 | const ts = new Lexer(s).setQuotes([['"', '"']]).lex(); 28 | expect(ts).toEqual([ 29 | { value: 'simple', raw: 'simple', trailing: ' ' }, 30 | { value: 'text', raw: 'text', trailing: ' ' }, 31 | { value: 'here', raw: 'here', trailing: '' } 32 | ]); 33 | }); 34 | 35 | it('with quotes, parses text with quotes', () => { 36 | const s = 'simple "text" here'; 37 | const ts = new Lexer(s).setQuotes([['"', '"']]).lex(); 38 | expect(ts).toEqual([ 39 | { value: 'simple', raw: 'simple', trailing: ' ' }, 40 | { value: 'text', raw: '"text"', trailing: ' ' }, 41 | { value: 'here', raw: 'here', trailing: '' } 42 | ]); 43 | }); 44 | 45 | it('with quotes, parses text with spaces in quotes', () => { 46 | const s = 'simple " text here "'; 47 | const ts = new Lexer(s).setQuotes([['"', '"']]).lex(); 48 | expect(ts).toEqual([ 49 | { value: 'simple', raw: 'simple', trailing: ' ' }, 50 | { value: ' text here ', raw: '" text here "', trailing: '' } 51 | ]); 52 | }); 53 | 54 | it('with quotes, parses quotes without spaces around it', () => { 55 | const s = 'simple"text"here'; 56 | const ts = new Lexer(s).setQuotes([['"', '"']]).lex(); 57 | expect(ts).toEqual([ 58 | { value: 'simple', raw: 'simple', trailing: '' }, 59 | { value: 'text', raw: '"text"', trailing: '' }, 60 | { value: 'here', raw: 'here', trailing: '' }, 61 | ]); 62 | }); 63 | 64 | it('with multiple quotes, parses text with multiple quotes', () => { 65 | const s = 'simple "text" “here”'; 66 | const ts = new Lexer(s).setQuotes([['"', '"'], ['“', '”']]).lex(); 67 | expect(ts).toEqual([ 68 | { value: 'simple', raw: 'simple', trailing: ' ' }, 69 | { value: 'text', raw: '"text"', trailing: ' ' }, 70 | { value: 'here', raw: '“here”', trailing: '' } 71 | ]); 72 | }); 73 | 74 | it('with multiple quotes, parses text with unclosed quotes', () => { 75 | const s = 'simple "text" “here'; 76 | const ts = new Lexer(s).setQuotes([['"', '"'], ['“', '”']]).lex(); 77 | expect(ts).toEqual([ 78 | { value: 'simple', raw: 'simple', trailing: ' ' }, 79 | { value: 'text', raw: '"text"', trailing: ' ' }, 80 | { value: 'here', raw: '“here', trailing: '' } 81 | ]); 82 | }); 83 | 84 | it('with multiple quotes, parses text with multiple unclosed quotes', () => { 85 | const s = 'simple "text “here'; 86 | const ts = new Lexer(s).setQuotes([['"', '"'], ['“', '”']]).lex(); 87 | expect(ts).toEqual([ 88 | { value: 'simple', raw: 'simple', trailing: ' ' }, 89 | { value: 'text “here', raw: '"text “here', trailing: '' } 90 | ]); 91 | }); 92 | 93 | it('with multiple quotes, parses quotes without spaces around it', () => { 94 | const s = '!foo --reset="lorem ipsum"'; 95 | const ts = new Lexer(s).setQuotes([['"', '"'], ['“', '”']]).lex(); 96 | expect(ts).toEqual([ 97 | { value: '!foo', raw: '!foo', trailing: ' ' }, 98 | { value: '--reset=', raw: '--reset=', trailing: '' }, 99 | { value: 'lorem ipsum', raw: '"lorem ipsum"', trailing: '' }, 100 | ]); 101 | }); 102 | 103 | it('can handle leading spaces', () => { 104 | const s = ' simple text here'; 105 | const ts = new Lexer(s).lex(); 106 | expect(ts).toEqual([ 107 | { value: 'simple', raw: 'simple', trailing: ' ' }, 108 | { value: 'text', raw: 'text', trailing: ' ' }, 109 | { value: 'here', raw: 'here', trailing: '' } 110 | ]); 111 | }); 112 | 113 | it('can handle just spaces', () => { 114 | const s = ' '; 115 | const ts = new Lexer(s).lex(); 116 | expect(ts).toEqual([]); 117 | }); 118 | 119 | it('can handle empty strings', () => { 120 | const s = ''; 121 | const ts = new Lexer(s).lex(); 122 | expect(ts).toEqual([]); 123 | }); 124 | 125 | it('should never error', () => { 126 | fc.assert(fc.property(fc.string(), s => { 127 | const l = new Lexer(s); 128 | expect(() => l.lex()).not.toThrow(); 129 | })); 130 | }); 131 | }); 132 | 133 | describe('Lexer#lexCommand', () => { 134 | it('can match a prefix and command together', () => { 135 | const s = '!command here'; 136 | const ts = new Lexer(s).lexCommand(s => s.startsWith('!') ? 1 : null); 137 | expect(ts).not.toBeNull(); 138 | expect(ts![0]).toEqual({ value: 'command', raw: 'command', trailing: ' ' }); 139 | expect(ts![1]()).toEqual([{ value: 'here', raw: 'here', trailing: '' }]); 140 | }); 141 | 142 | it('can match a prefix and command separated', () => { 143 | const s = '! command here'; 144 | const ts = new Lexer(s).lexCommand(s => s.startsWith('!') ? 1 : null); 145 | expect(ts).not.toBeNull(); 146 | expect(ts![0]).toEqual({ value: 'command', raw: 'command', trailing: ' ' }); 147 | expect(ts![1]()).toEqual([{ value: 'here', raw: 'here', trailing: '' }]); 148 | }); 149 | 150 | it('can match a prefix and command together lazily', () => { 151 | const s = '!first second third'; 152 | const l = new Lexer(s); 153 | const ts = l.lexCommand(s => s.startsWith('!') ? 1 : null); 154 | 155 | expect(ts).not.toBeNull(); 156 | expect(ts![0]).toEqual({ value: 'first', raw: 'first', trailing: ' ' }); 157 | 158 | const ys = l.lex(); 159 | expect(ys).toEqual([ 160 | { value: 'second', raw: 'second', trailing: ' ' }, 161 | { value: 'third', raw: 'third', trailing: '' } 162 | ]); 163 | 164 | expect(ts![1]()).toEqual([]); 165 | }); 166 | 167 | it('can match a prefix and command separately lazily', () => { 168 | const s = '! first second third'; 169 | const l = new Lexer(s); 170 | const ts = l.lexCommand(s => s.startsWith('!') ? 1 : null); 171 | 172 | expect(ts).not.toBeNull(); 173 | expect(ts![0]).toEqual({ value: 'first', raw: 'first', trailing: ' ' }); 174 | 175 | const ys = l.lex(); 176 | expect(ys).toEqual([ 177 | { value: 'second', raw: 'second', trailing: ' ' }, 178 | { value: 'third', raw: 'third', trailing: '' } 179 | ]); 180 | 181 | expect(ts![1]()).toEqual([]); 182 | }); 183 | 184 | it('can fail to match a prefix and command (no prefix)', () => { 185 | const s = 'command here'; 186 | const ts = new Lexer(s).lexCommand(s => s.startsWith('!') ? 1 : null); 187 | expect(ts).toBeNull(); 188 | }); 189 | 190 | it('can fail to match a prefix and command (no input)', () => { 191 | const s = ''; 192 | const ts = new Lexer(s).lexCommand(s => s.startsWith('!') ? 1 : null); 193 | expect(ts).toBeNull(); 194 | }); 195 | 196 | it('can lex multiple inputs', () => { 197 | const lexer = new Lexer().setQuotes([['"', '"']]); 198 | 199 | expect(lexer.setInput('"hey"').lex()).toEqual([{ value: 'hey', raw: '"hey"', trailing: '' }]); 200 | expect(lexer.setInput('"foo"').lex()).toEqual([{ value: 'foo', raw: '"foo"', trailing: '' }]); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /test/loops.test.ts: -------------------------------------------------------------------------------- 1 | import { loop, loop1, loopAsync, loop1Async, ok, err, step, step_, finish, fail } from '../src'; 2 | 3 | describe('loop', () => { 4 | it('loops until parsing completes (simple)', () => { 5 | const inputs = ['hello', 'world', '100', 'extra']; 6 | let i = 0; 7 | 8 | const result = loop(inputs[i++], { 9 | getInput() { 10 | return i < inputs.length 11 | ? step(inputs[i++]) 12 | : fail('no input'); 13 | }, 14 | 15 | parse(s) { 16 | const n = Number(s); 17 | if (isNaN(n)) { 18 | return fail('bad input'); 19 | } else { 20 | return finish(n); 21 | } 22 | }, 23 | 24 | onInputError(e) { 25 | return fail(e); 26 | }, 27 | 28 | onParseError() { 29 | return step_(); 30 | } 31 | }); 32 | 33 | expect(result).toEqual(ok(100)); 34 | expect(i).toEqual(3); 35 | }); 36 | 37 | it('loops until no more input (simple)', () => { 38 | const inputs = ['hello', 'world', 'extra']; 39 | let i = 0; 40 | 41 | const result = loop(inputs[i++], { 42 | getInput() { 43 | return i < inputs.length 44 | ? step(inputs[i++]) 45 | : fail('no input'); 46 | }, 47 | 48 | parse(s) { 49 | const n = Number(s); 50 | if (isNaN(n)) { 51 | return fail('bad input'); 52 | } else { 53 | return finish(n); 54 | } 55 | }, 56 | 57 | onInputError(e) { 58 | return fail(e); 59 | }, 60 | 61 | onParseError() { 62 | return step_(); 63 | } 64 | }); 65 | 66 | expect(result).toEqual(err('no input')); 67 | expect(i).toEqual(3); 68 | }); 69 | 70 | it('loops until first bad parse (simple)', () => { 71 | const inputs = ['hello', 'world', 'extra']; 72 | let i = 0; 73 | 74 | const result = loop(inputs[i++], { 75 | getInput() { 76 | return i < inputs.length 77 | ? step(inputs[i++]) 78 | : fail('no input'); 79 | }, 80 | 81 | parse(s) { 82 | const n = Number(s); 83 | if (isNaN(n)) { 84 | return fail('bad input'); 85 | } else { 86 | return finish(n); 87 | } 88 | }, 89 | 90 | onInputError(e) { 91 | return fail(e); 92 | }, 93 | 94 | onParseError(e, x) { 95 | return fail(e + ' ' + x); 96 | } 97 | }); 98 | 99 | expect(result).toEqual(err('bad input hello')); 100 | expect(i).toEqual(1); 101 | }); 102 | }); 103 | 104 | describe('loop1', () => { 105 | it('loops until parsing completes (simple)', () => { 106 | const inputs = ['hello', 'world', '100', 'extra']; 107 | let i = 0; 108 | 109 | const result = loop1({ 110 | getInput() { 111 | return i < inputs.length 112 | ? step(inputs[i++]) 113 | : fail('no input'); 114 | }, 115 | 116 | parse(s) { 117 | const n = Number(s); 118 | if (isNaN(n)) { 119 | return fail('bad input'); 120 | } else { 121 | return finish(n); 122 | } 123 | }, 124 | 125 | onInputError(e) { 126 | return fail(e); 127 | }, 128 | 129 | onParseError() { 130 | return step_(); 131 | } 132 | }); 133 | 134 | expect(result).toEqual(ok(100)); 135 | expect(i).toEqual(3); 136 | }); 137 | 138 | it('loops until no more input (simple)', () => { 139 | const inputs = ['hello', 'world', 'extra']; 140 | let i = 0; 141 | 142 | const result = loop1({ 143 | getInput() { 144 | return i < inputs.length 145 | ? step(inputs[i++]) 146 | : fail('no input'); 147 | }, 148 | 149 | parse(s) { 150 | const n = Number(s); 151 | if (isNaN(n)) { 152 | return fail('bad input'); 153 | } else { 154 | return finish(n); 155 | } 156 | }, 157 | 158 | onInputError(e) { 159 | return fail(e); 160 | }, 161 | 162 | onParseError() { 163 | return step_(); 164 | } 165 | }); 166 | 167 | expect(result).toEqual(err('no input')); 168 | expect(i).toEqual(3); 169 | }); 170 | 171 | it('loops until first bad parse (simple)', () => { 172 | const inputs = ['hello', 'world', 'extra']; 173 | let i = 0; 174 | 175 | const result = loop1({ 176 | getInput() { 177 | return i < inputs.length 178 | ? step(inputs[i++]) 179 | : fail('no input'); 180 | }, 181 | 182 | parse(s) { 183 | const n = Number(s); 184 | if (isNaN(n)) { 185 | return fail('bad input'); 186 | } else { 187 | return finish(n); 188 | } 189 | }, 190 | 191 | onInputError(e) { 192 | return fail(e); 193 | }, 194 | 195 | onParseError(e, x) { 196 | return fail(e + ' ' + x); 197 | } 198 | }); 199 | 200 | expect(result).toEqual(err('bad input hello')); 201 | expect(i).toEqual(1); 202 | }); 203 | }); 204 | 205 | describe('loopAsync', () => { 206 | it('loops until parsing completes (simple)', async () => { 207 | const inputs = ['hello', 'world', '100', 'extra']; 208 | let i = 0; 209 | 210 | const result = await loopAsync(inputs[i++], { 211 | async getInput() { 212 | return i < inputs.length 213 | ? step(inputs[i++]) 214 | : fail('no input'); 215 | }, 216 | 217 | async parse(s) { 218 | const n = Number(s); 219 | if (isNaN(n)) { 220 | return fail('bad input'); 221 | } else { 222 | return finish(n); 223 | } 224 | }, 225 | 226 | async onInputError(e) { 227 | return fail(e); 228 | }, 229 | 230 | async onParseError() { 231 | return step_(); 232 | } 233 | }); 234 | 235 | expect(result).toEqual(ok(100)); 236 | expect(i).toEqual(3); 237 | }); 238 | 239 | it('loops until no more input (simple)', async () => { 240 | const inputs = ['hello', 'world', 'extra']; 241 | let i = 0; 242 | 243 | const result = await loopAsync(inputs[i++], { 244 | async getInput() { 245 | return i < inputs.length 246 | ? step(inputs[i++]) 247 | : fail('no input'); 248 | }, 249 | 250 | async parse(s) { 251 | const n = Number(s); 252 | if (isNaN(n)) { 253 | return fail('bad input'); 254 | } else { 255 | return finish(n); 256 | } 257 | }, 258 | 259 | async onInputError(e) { 260 | return fail(e); 261 | }, 262 | 263 | async onParseError() { 264 | return step_(); 265 | } 266 | }); 267 | 268 | expect(result).toEqual(err('no input')); 269 | expect(i).toEqual(3); 270 | }); 271 | 272 | it('loops until bad parse (simple)', async () => { 273 | const inputs = ['hello', 'world', 'extra']; 274 | let i = 0; 275 | 276 | const result = await loopAsync(inputs[i++], { 277 | async getInput() { 278 | return i < inputs.length 279 | ? step(inputs[i++]) 280 | : fail('no input'); 281 | }, 282 | 283 | async parse(s) { 284 | const n = Number(s); 285 | if (isNaN(n)) { 286 | return fail('bad input'); 287 | } else { 288 | return finish(n); 289 | } 290 | }, 291 | 292 | async onInputError(e) { 293 | return fail(e); 294 | }, 295 | 296 | async onParseError(e, x) { 297 | return fail(e + ' ' + x); 298 | } 299 | }); 300 | 301 | expect(result).toEqual(err('bad input hello')); 302 | expect(i).toEqual(1); 303 | }); 304 | 305 | it('can do complicated things: too many retries (complex)', async () => { 306 | const inputs = ['hello', 'world', 'extra']; 307 | let i = 0; 308 | const st = { retries: 0 }; 309 | 310 | const result = await loopAsync(inputs[i++], { 311 | async getInput() { 312 | if (st.retries > 1) { 313 | return fail('too many retries'); 314 | } 315 | 316 | if (i < inputs.length) { 317 | st.retries++; 318 | return step(inputs[i++]); 319 | } 320 | 321 | return fail('no input'); 322 | }, 323 | 324 | async parse(s) { 325 | const n = Number(s); 326 | if (isNaN(n)) { 327 | return fail('bad input'); 328 | } else { 329 | return finish(n); 330 | } 331 | }, 332 | 333 | async onInputError(e) { 334 | return fail(e); 335 | }, 336 | 337 | async onParseError() { 338 | return step_(); 339 | } 340 | }); 341 | 342 | expect(result).toEqual(err('too many retries')); 343 | expect(i).toEqual(3); 344 | expect(st).toEqual({ retries: 2 }); 345 | }); 346 | 347 | it('can do complicated things: cancellation input (complex)', async () => { 348 | const inputs = ['hello', 'cancel', 'extra']; 349 | let i = 0; 350 | 351 | const result = await loopAsync(inputs[i++], { 352 | async getInput() { 353 | if (i < inputs.length) { 354 | const inp = inputs[i++]; 355 | if (inp === 'cancel') { 356 | return fail('cancelled'); 357 | } 358 | 359 | return step(inp); 360 | } 361 | 362 | return fail('no input'); 363 | }, 364 | 365 | async parse(s) { 366 | const n = Number(s); 367 | if (isNaN(n)) { 368 | return fail('bad input'); 369 | } else { 370 | return finish(n); 371 | } 372 | }, 373 | 374 | async onInputError(e) { 375 | return fail(e); 376 | }, 377 | 378 | async onParseError() { 379 | return step_(); 380 | } 381 | }); 382 | 383 | expect(result).toEqual(err('cancelled')); 384 | expect(i).toEqual(2); 385 | }); 386 | 387 | it('can do complicated things: collect multiple inputs (complex)', async () => { 388 | const inputs = ['10', 'bad', '20', '30', 'stop', '40']; 389 | let i = 0; 390 | const st: number[] = []; 391 | 392 | const result = await loopAsync(inputs[i++], { 393 | async getInput() { 394 | if (i < inputs.length) { 395 | const inp = inputs[i++]; 396 | if (inp === 'stop') { 397 | return finish(st); 398 | } 399 | 400 | return step(inp); 401 | } 402 | 403 | return finish(st); 404 | }, 405 | 406 | async parse(s) { 407 | const n = Number(s); 408 | if (isNaN(n)) { 409 | return fail('bad input'); 410 | } else { 411 | st.push(n); 412 | return step_(); 413 | } 414 | }, 415 | 416 | async onInputError(e) { 417 | return fail(e); 418 | }, 419 | 420 | async onParseError() { 421 | return step_(); 422 | } 423 | }); 424 | 425 | expect(result).toEqual(ok([10, 20, 30])); 426 | expect(i).toEqual(5); 427 | expect(st).toEqual([10, 20, 30]); 428 | }); 429 | }); 430 | 431 | describe('loop1Async', () => { 432 | it('loops until parsing completes (simple)', async () => { 433 | const inputs = ['hello', 'world', '100', 'extra']; 434 | let i = 0; 435 | 436 | const result = await loop1Async({ 437 | async getInput() { 438 | return i < inputs.length 439 | ? step(inputs[i++]) 440 | : fail('no input'); 441 | }, 442 | 443 | async parse(s) { 444 | const n = Number(s); 445 | if (isNaN(n)) { 446 | return fail('bad input'); 447 | } else { 448 | return finish(n); 449 | } 450 | }, 451 | 452 | async onInputError(e) { 453 | return fail(e); 454 | }, 455 | 456 | async onParseError() { 457 | return step_(); 458 | } 459 | }); 460 | 461 | expect(result).toEqual(ok(100)); 462 | expect(i).toEqual(3); 463 | }); 464 | 465 | it('loops until no more input (simple)', async () => { 466 | const inputs = ['hello', 'world', 'extra']; 467 | let i = 0; 468 | 469 | const result = await loop1Async({ 470 | async getInput() { 471 | return i < inputs.length 472 | ? step(inputs[i++]) 473 | : fail('no input'); 474 | }, 475 | 476 | async parse(s) { 477 | const n = Number(s); 478 | if (isNaN(n)) { 479 | return fail('bad input'); 480 | } else { 481 | return finish(n); 482 | } 483 | }, 484 | 485 | async onInputError(e) { 486 | return fail(e); 487 | }, 488 | 489 | async onParseError() { 490 | return step_(); 491 | } 492 | }); 493 | 494 | expect(result).toEqual(err('no input')); 495 | expect(i).toEqual(3); 496 | }); 497 | 498 | it('loops until bad parse (simple)', async () => { 499 | const inputs = ['hello', 'world', 'extra']; 500 | let i = 0; 501 | 502 | const result = await loop1Async({ 503 | async getInput() { 504 | return i < inputs.length 505 | ? step(inputs[i++]) 506 | : fail('no input'); 507 | }, 508 | 509 | async parse(s) { 510 | const n = Number(s); 511 | if (isNaN(n)) { 512 | return fail('bad input'); 513 | } else { 514 | return finish(n); 515 | } 516 | }, 517 | 518 | async onInputError(e) { 519 | return fail(e); 520 | }, 521 | 522 | async onParseError(e, x) { 523 | return fail(e + ' ' + x); 524 | } 525 | }); 526 | 527 | expect(result).toEqual(err('bad input hello')); 528 | expect(i).toEqual(1); 529 | }); 530 | 531 | it('can do complicated things: too many retries (complex)', async () => { 532 | const inputs = ['hello', 'world', 'extra']; 533 | let i = 0; 534 | const st = { retries: 0 }; 535 | 536 | const result = await loop1Async({ 537 | async getInput() { 538 | if (st.retries > 1) { 539 | return fail('too many retries'); 540 | } 541 | 542 | if (i < inputs.length) { 543 | st.retries++; 544 | return step(inputs[i++]); 545 | } 546 | 547 | return fail('no input'); 548 | }, 549 | 550 | async parse(s) { 551 | const n = Number(s); 552 | if (isNaN(n)) { 553 | return fail('bad input'); 554 | } else { 555 | return finish(n); 556 | } 557 | }, 558 | 559 | async onInputError(e) { 560 | return fail(e); 561 | }, 562 | 563 | async onParseError() { 564 | return step_(); 565 | } 566 | }); 567 | 568 | expect(result).toEqual(err('too many retries')); 569 | expect(i).toEqual(2); 570 | expect(st).toEqual({ retries: 2 }); 571 | }); 572 | 573 | it('can do complicated things: cancellation input (complex)', async () => { 574 | const inputs = ['hello', 'cancel', 'extra']; 575 | let i = 0; 576 | 577 | const result = await loop1Async({ 578 | async getInput() { 579 | if (i < inputs.length) { 580 | const inp = inputs[i++]; 581 | if (inp === 'cancel') { 582 | return fail('cancelled'); 583 | } 584 | 585 | return step(inp); 586 | } 587 | 588 | return fail('no input'); 589 | }, 590 | 591 | async parse(s) { 592 | const n = Number(s); 593 | if (isNaN(n)) { 594 | return fail('bad input'); 595 | } else { 596 | return finish(n); 597 | } 598 | }, 599 | 600 | async onInputError(e) { 601 | return fail(e); 602 | }, 603 | 604 | async onParseError() { 605 | return step_(); 606 | } 607 | }); 608 | 609 | expect(result).toEqual(err('cancelled')); 610 | expect(i).toEqual(2); 611 | }); 612 | 613 | it('can do complicated things: collect multiple inputs (complex)', async () => { 614 | const inputs = ['10', 'bad', '20', '30', 'stop', '40']; 615 | let i = 0; 616 | const st: number[] = []; 617 | 618 | const result = await loop1Async({ 619 | async getInput() { 620 | if (i < inputs.length) { 621 | const inp = inputs[i++]; 622 | if (inp === 'stop') { 623 | return finish(st); 624 | } 625 | 626 | return step(inp); 627 | } 628 | 629 | return finish(st); 630 | }, 631 | 632 | async parse(s) { 633 | const n = Number(s); 634 | if (isNaN(n)) { 635 | return fail('bad input'); 636 | } else { 637 | st.push(n); 638 | return step_(); 639 | } 640 | }, 641 | 642 | async onInputError(e) { 643 | return fail(e); 644 | }, 645 | 646 | async onParseError() { 647 | return step_(); 648 | } 649 | }); 650 | 651 | expect(result).toEqual(ok([10, 20, 30])); 652 | expect(i).toEqual(5); 653 | expect(st).toEqual([10, 20, 30]); 654 | }); 655 | }); 656 | -------------------------------------------------------------------------------- /test/option.test.ts: -------------------------------------------------------------------------------- 1 | import { maybeOption, some, none, orOption } from '../src'; 2 | 3 | describe('maybeOption', () => { 4 | it('should work', () => { 5 | expect(maybeOption(1)).toEqual(some(1)); 6 | expect(maybeOption(null)).toEqual(none()); 7 | expect(maybeOption(undefined)).toEqual(none()); 8 | }); 9 | }); 10 | 11 | describe('orOption', () => { 12 | it('gives none on zero options', () => { 13 | expect(orOption()).toEqual(none()); 14 | }); 15 | 16 | it('gives the first some', () => { 17 | expect(orOption(none(), some(1), some(2))).toEqual(some(1)); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/parser.test.ts: -------------------------------------------------------------------------------- 1 | import * as fc from 'fast-check'; 2 | import { Lexer, Parser, longStrategy, mergeOutputs } from '../src'; 3 | 4 | describe('Parser#parse', () => { 5 | it('should parse normal words', () => { 6 | const ts = new Lexer('foo bar baz').lex(); 7 | const out = new Parser(ts).parse(); 8 | expect(out).toEqual({ 9 | ordered: ts, 10 | flags: new Set(), 11 | options: new Map() 12 | }); 13 | }); 14 | 15 | it('should parse flags', () => { 16 | const ts = new Lexer('foo bar --baz').lex(); 17 | const out = new Parser(ts).setUnorderedStrategy(longStrategy()).parse(); 18 | expect(out).toEqual({ 19 | ordered: ts.slice(0, 2), 20 | flags: new Set(['baz']), 21 | options: new Map() 22 | }); 23 | }); 24 | 25 | it('should parse options', () => { 26 | const ts = new Lexer('foo bar --baz= quux').lex(); 27 | const out = new Parser(ts).setUnorderedStrategy(longStrategy()).parse(); 28 | expect(out).toEqual({ 29 | ordered: ts.slice(0, 2), 30 | flags: new Set(), 31 | options: new Map([['baz', ['quux']]]) 32 | }); 33 | }); 34 | 35 | it('should parse options without value', () => { 36 | const ts = new Lexer('foo bar --baz=').lex(); 37 | const out = new Parser(ts).setUnorderedStrategy(longStrategy()).parse(); 38 | expect(out).toEqual({ 39 | ordered: ts.slice(0, 2), 40 | flags: new Set(), 41 | options: new Map([['baz', []]]) 42 | }); 43 | }); 44 | 45 | it('should parse options followed by flag', () => { 46 | const ts = new Lexer('foo bar --baz= --foo').lex(); 47 | const out = new Parser(ts).setUnorderedStrategy(longStrategy()).parse(); 48 | expect(out).toEqual({ 49 | ordered: ts.slice(0, 2), 50 | flags: new Set(['foo']), 51 | options: new Map([['baz', []]]) 52 | }); 53 | }); 54 | 55 | it('should parse compact options', () => { 56 | const ts = new Lexer('foo bar --baz=quux').lex(); 57 | const out = new Parser(ts).setUnorderedStrategy(longStrategy()).parse(); 58 | expect(out).toEqual({ 59 | ordered: ts.slice(0, 2), 60 | flags: new Set(), 61 | options: new Map([['baz', ['quux']]]) 62 | }); 63 | }); 64 | 65 | it('should parse multiple same options', () => { 66 | const ts = new Lexer('--foo=w --foo=x --foo= y --foo=z').lex(); 67 | const out = new Parser(ts).setUnorderedStrategy(longStrategy()).parse(); 68 | expect(out).toEqual({ 69 | ordered: [], 70 | flags: new Set(), 71 | options: new Map([['foo', ['w', 'x', 'y', 'z']]]) 72 | }); 73 | }); 74 | 75 | it('should parse everything', () => { 76 | const ts = new Lexer('hello --foo --bar= 123 --baz=quux world').lex(); 77 | const out = new Parser(ts).setUnorderedStrategy(longStrategy()).parse(); 78 | expect(out).toEqual({ 79 | ordered: [ts[0], ts[5]], 80 | flags: new Set(['foo']), 81 | options: new Map([['bar', ['123']], ['baz', ['quux']]]) 82 | }); 83 | }); 84 | 85 | it('should not include the quotes in flags or options', () => { 86 | const ts = new Lexer('hello "--foo" "--bar=" "123" "--baz=quux" world') 87 | .setQuotes([['"', '"']]) 88 | .lex(); 89 | 90 | const out = new Parser(ts).setUnorderedStrategy(longStrategy()).parse(); 91 | expect(out).toEqual({ 92 | ordered: [ts[0], ts[5]], 93 | flags: new Set(['foo']), 94 | options: new Map([['bar', ['123']], ['baz', ['quux']]]) 95 | }); 96 | }); 97 | 98 | it('should give things in order', () => { 99 | const ts = new Lexer('hello --foo --bar= 123 --baz=quux world').lex(); 100 | const parser = new Parser(ts).setUnorderedStrategy(longStrategy()); 101 | const ps = [...parser]; 102 | expect(ps).toEqual([ 103 | { ordered: [{ value: 'hello', raw: 'hello', trailing: ' ' }], flags: new Set(), options: new Map() }, 104 | { ordered: [], flags: new Set(['foo']), options: new Map() }, 105 | { ordered: [], flags: new Set(), options: new Map([['bar', ['123']]]) }, 106 | { ordered: [], flags: new Set(), options: new Map([['baz', ['quux']]]) }, 107 | { ordered: [{ value: 'world', raw: 'world', trailing: '' }], flags: new Set(), options: new Map() } 108 | ]); 109 | }); 110 | 111 | it('should never error', () => { 112 | fc.assert(fc.property(fc.string(), s => { 113 | const ts = new Lexer(s).lex(); 114 | const p = new Parser(ts); 115 | expect(() => p.parse()).not.toThrow(); 116 | })); 117 | }); 118 | 119 | it('should be that mutation and merge are equivalent', () => { 120 | fc.assert(fc.property(fc.string(), s => { 121 | const ts = new Lexer(s).lex(); 122 | const p1 = new Parser(ts).parse(); 123 | const p2 = mergeOutputs(...new Parser(ts)); 124 | expect(p1).toEqual(p2); 125 | })); 126 | }); 127 | 128 | it('can parse multiple inputs', () => { 129 | const ts1 = new Lexer('foo bar baz').lex(); 130 | const ts2 = new Lexer('foo foo foo').lex(); 131 | const parser = new Parser(); 132 | 133 | expect(parser.setInput(ts1).parse()).toEqual({ 134 | ordered: ts1, 135 | flags: new Set(), 136 | options: new Map() 137 | }); 138 | 139 | expect(parser.setInput(ts2).parse()).toEqual({ 140 | ordered: ts2, 141 | flags: new Set(), 142 | options: new Map() 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /test/parserOutput.test.ts: -------------------------------------------------------------------------------- 1 | import * as fc from 'fast-check'; 2 | import { ParserOutput, emptyOutput, mergeOutputs, outputFromJSON, outputToJSON } from '../src'; 3 | 4 | describe('mergeOutputs', () => { 5 | it('works', () => { 6 | const e = emptyOutput(); 7 | 8 | const a: ParserOutput = { 9 | ordered: [ 10 | { value: 'world', raw: 'world', trailing: ' ' }, 11 | { value: 'cool stuff', raw: '"cool stuff"', trailing: ' ' }, 12 | ], 13 | flags: new Set(['foo']), 14 | options: new Map() 15 | }; 16 | 17 | const b: ParserOutput = { 18 | ordered: [ 19 | { value: 'a', raw: 'a', trailing: ' ' }, 20 | { value: 'b', raw: 'b', trailing: ' ' }, 21 | { value: 'c', raw: 'c', trailing: '' } 22 | ], 23 | flags: new Set(), 24 | options: new Map([['bar', ['baz']]]) 25 | }; 26 | 27 | expect(mergeOutputs(e, a, b)).toEqual({ 28 | ordered: [ 29 | { value: 'world', raw: 'world', trailing: ' ' }, 30 | { value: 'cool stuff', raw: '"cool stuff"', trailing: ' ' }, 31 | { value: 'a', raw: 'a', trailing: ' ' }, 32 | { value: 'b', raw: 'b', trailing: ' ' }, 33 | { value: 'c', raw: 'c', trailing: '' } 34 | ], 35 | flags: new Set(['foo']), 36 | options: new Map([['bar', ['baz']]]) 37 | }); 38 | }); 39 | 40 | it('has emptyOutput as the identity', () => { 41 | fc.assert(fc.property(genOutput, a => { 42 | expect(mergeOutputs(a, emptyOutput())).toEqual(a); 43 | expect(mergeOutputs(emptyOutput(), a)).toEqual(a); 44 | })); 45 | }); 46 | 47 | it('is associative', () => { 48 | fc.assert(fc.property(fc.tuple(genOutput, genOutput, genOutput), ([a, b, c]) => { 49 | const r1 = mergeOutputs(a, mergeOutputs(b, c)); 50 | const r2 = mergeOutputs(mergeOutputs(a, b), c); 51 | const r3 = mergeOutputs(a, b, c); 52 | 53 | expect(r1).toEqual(r2); 54 | expect(r2).toEqual(r3); 55 | expect(r3).toEqual(r1); 56 | })); 57 | }); 58 | }); 59 | 60 | describe('output{To,From}JSON', () => { 61 | it('works', () => { 62 | const a: ParserOutput = { 63 | ordered: [ 64 | { value: 'world', raw: 'world', trailing: ' ' }, 65 | { value: 'cool stuff', raw: '"cool stuff"', trailing: ' ' }, 66 | { value: 'a', raw: 'a', trailing: ' ' }, 67 | { value: 'b', raw: 'b', trailing: ' ' }, 68 | { value: 'c', raw: 'c', trailing: '' } 69 | ], 70 | flags: new Set(['foo']), 71 | options: new Map([['bar', ['baz']]]) 72 | }; 73 | 74 | const b = { 75 | ordered: [ 76 | { value: 'world', raw: 'world', trailing: ' ' }, 77 | { value: 'cool stuff', raw: '"cool stuff"', trailing: ' ' }, 78 | { value: 'a', raw: 'a', trailing: ' ' }, 79 | { value: 'b', raw: 'b', trailing: ' ' }, 80 | { value: 'c', raw: 'c', trailing: '' } 81 | ], 82 | flags: ['foo'], 83 | options: [['bar', ['baz']]] 84 | }; 85 | 86 | expect(outputToJSON(a)).toEqual(b); 87 | expect(outputFromJSON(b)).toEqual(a); 88 | }); 89 | 90 | it('from . to = id', () => { 91 | fc.assert(fc.property(genOutput, a => { 92 | const r = outputFromJSON(outputToJSON(a)); 93 | expect(r).toEqual(a); 94 | })); 95 | }); 96 | 97 | it('to . from = id, except for duplicates', () => { 98 | fc.assert(fc.property(genOutput, a => { 99 | const b = outputToJSON(a); 100 | const r = outputToJSON(outputFromJSON(b)); 101 | expect(r).toEqual(b); 102 | })); 103 | }); 104 | }); 105 | 106 | const genOutput: fc.Arbitrary = 107 | fc.tuple(fc.array(fc.string()), fc.array(fc.string()), fc.array(fc.tuple(fc.string(), fc.array(fc.string())))) 108 | .map(([ws, fs, os]) => ({ 109 | ordered: ws.map(w => ({ value: w, raw: w, trailing: ' ' })), 110 | flags: new Set(fs), 111 | options: new Map(os) 112 | })); 113 | -------------------------------------------------------------------------------- /test/result.test.ts: -------------------------------------------------------------------------------- 1 | import { maybeResult, ok, err, orResultAll, orResultFirst, orResultLast } from '../src'; 2 | 3 | describe('maybeResult', () => { 4 | it('should work', () => { 5 | expect(maybeResult(1, 'bad')).toEqual(ok(1)); 6 | expect(maybeResult(null, 'bad')).toEqual(err('bad')); 7 | expect(maybeResult(undefined, 'bad')).toEqual(err('bad')); 8 | }); 9 | }); 10 | 11 | describe('orResultAll', () => { 12 | it('gives the first ok', () => { 13 | expect(orResultAll(err(0), ok(1), ok(2))).toEqual(ok(1)); 14 | }); 15 | 16 | it('gives all the errs when no ok', () => { 17 | expect(orResultAll(err(0), err(1), err(2))).toEqual(err([0, 1, 2])); 18 | }); 19 | }); 20 | 21 | describe('orResultFirst', () => { 22 | it('gives the first ok', () => { 23 | expect(orResultFirst(err(0), ok(1), ok(2))).toEqual(ok(1)); 24 | }); 25 | 26 | it('gives the first err when no ok', () => { 27 | expect(orResultFirst(err(0), err(1), err(2))).toEqual(err(0)); 28 | }); 29 | }); 30 | 31 | describe('orResultLast', () => { 32 | it('gives the first ok', () => { 33 | expect(orResultLast(err(0), ok(1), ok(2))).toEqual(ok(1)); 34 | }); 35 | 36 | it('gives the last err when no ok', () => { 37 | expect(orResultLast(err(0), err(1), err(2))).toEqual(err(2)); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/tokens.test.ts: -------------------------------------------------------------------------------- 1 | import { Token, joinTokens, extractCommand } from '../src/'; 2 | 3 | describe('joinTokens', () => { 4 | it('should join tokens with original spacing', () => { 5 | const s = joinTokens([{ value: 'foo', raw: 'foo', trailing: ' ' }, { value: 'bar', raw: 'bar', trailing: '\n\n' }]); 6 | expect(s).toEqual('foo bar'); 7 | }); 8 | 9 | it('should join tokens with given spacing', () => { 10 | const s = joinTokens([{ value: 'foo', raw: 'foo', trailing: ' ' }, { value: 'bar', raw: 'bar', trailing: '\n\n' }], ' '); 11 | expect(s).toEqual('foo bar'); 12 | }); 13 | 14 | it('should join tokens with original spacing and keep quotes', () => { 15 | const s = joinTokens([ 16 | { value: 'foo', raw: '"foo"', trailing: ' ' }, 17 | { value: 'bar', raw: 'bar', trailing: '\n\n' } 18 | ], null, true); 19 | 20 | expect(s).toEqual('"foo" bar'); 21 | }); 22 | 23 | it('should join tokens with original spacing and not keep quotes', () => { 24 | const s = joinTokens([ 25 | { value: 'foo', raw: '"foo"', trailing: ' ' }, 26 | { value: 'bar', raw: 'bar', trailing: '\n\n' } 27 | ], null, false); 28 | 29 | expect(s).toEqual('foo bar'); 30 | }); 31 | 32 | it('should join tokens with given spacing and keep quotes', () => { 33 | const s = joinTokens([ 34 | { value: 'foo', raw: '"foo"', trailing: ' ' }, 35 | { value: 'bar', raw: 'bar', trailing: '\n\n' } 36 | ], ' ', true); 37 | 38 | expect(s).toEqual('"foo" bar'); 39 | }); 40 | 41 | it('should join tokens with given spacing and not keep quotes', () => { 42 | const s = joinTokens([ 43 | { value: 'foo', raw: '"foo"', trailing: ' ' }, 44 | { value: 'bar', raw: 'bar', trailing: '\n\n' } 45 | ], ' ', false); 46 | 47 | expect(s).toEqual('foo bar'); 48 | }); 49 | 50 | it('should given empty string for empty list', () => { 51 | const s = joinTokens([]); 52 | expect(s).toEqual(''); 53 | }); 54 | }); 55 | 56 | describe('extractCommand', () => { 57 | it('can extract and mutate from one token', () => { 58 | const ts = [{ value: '!help', raw: '!help', trailing: ' ' }, { value: 'me', raw: 'me', trailing: '' }]; 59 | const cmd = extractCommand(s => s.startsWith('!') ? 1 : null, ts); 60 | expect(cmd).toEqual({ value: 'help', raw: 'help', trailing: ' ' }); 61 | expect(ts).toEqual([{ value: 'me', raw: 'me', trailing: '' }]); 62 | }); 63 | 64 | it('can extract and mutate from two tokens', () => { 65 | const ts = [{ value: '!', raw: '!', trailing: ' ' }, { value: 'help', raw: 'help', trailing: ' ' }, { value: 'me', raw: 'me', trailing: '' }]; 66 | const cmd = extractCommand(s => s.startsWith('!') ? 1 : null, ts); 67 | expect(cmd).toEqual({ value: 'help', raw: 'help', trailing: ' ' }); 68 | expect(ts).toEqual([{ value: 'me', raw: 'me', trailing: '' }]); 69 | }); 70 | 71 | it('can extract from one token', () => { 72 | const ts = [{ value: '!help', raw: '!help', trailing: ' ' }, { value: 'me', raw: 'me', trailing: '' }]; 73 | const cmd = extractCommand(s => s.startsWith('!') ? 1 : null, ts, false); 74 | expect(cmd).toEqual({ value: 'help', raw: 'help', trailing: ' ' }); 75 | expect(ts).toEqual(ts); 76 | }); 77 | 78 | it('can extract from two tokens', () => { 79 | const ts = [{ value: '!', raw: '!', trailing: ' ' }, { value: 'help', raw: 'help', trailing: ' ' }, { value: 'me', raw: 'me', trailing: '' }]; 80 | const cmd = extractCommand(s => s.startsWith('!') ? 1 : null, ts, false); 81 | expect(cmd).toEqual({ value: 'help', raw: 'help', trailing: ' ' }); 82 | expect(ts).toEqual(ts); 83 | }); 84 | 85 | it('can fail when not enough tokens', () => { 86 | const ts: Token[] = []; 87 | const cmd = extractCommand(s => s.startsWith('!') ? 1 : null, ts); 88 | expect(cmd).toEqual(null); 89 | expect(ts).toEqual(ts); 90 | }); 91 | 92 | it('can fail when not enough tokens after prefix', () => { 93 | const ts = [{ value: '!', raw: '!', trailing: ' ' }]; 94 | const cmd = extractCommand(s => s.startsWith('!') ? 1 : null, ts); 95 | expect(cmd).toEqual(null); 96 | expect(ts).toEqual(ts); 97 | }); 98 | 99 | it('can fail when no matching prefix', () => { 100 | const ts = [{ value: '!', raw: '!', trailing: ' ' }, { value: 'help', raw: 'help', trailing: ' ' }, { value: 'me', raw: 'me', trailing: '' }]; 101 | const cmd = extractCommand(s => s.startsWith('?') ? 1 : null, ts); 102 | expect(cmd).toEqual(null); 103 | expect(ts).toEqual(ts); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/unordered.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | noStrategy, longStrategy, longShortStrategy, prefixedStrategy, matchingStrategy, 3 | mapKeys, renameKeys 4 | } from '../src/'; 5 | 6 | describe('noStrategy', () => { 7 | it('should be false and null', () => { 8 | const s = noStrategy(); 9 | expect(s.matchFlag('foo')).toEqual(null); 10 | expect(s.matchOption('bar')).toEqual(null); 11 | expect(s.matchCompactOption('baz')).toEqual(null); 12 | }); 13 | }); 14 | 15 | describe('longStrategy', () => { 16 | it('should parse a flag exclusively', () => { 17 | const s = longStrategy(); 18 | const x = '--foo'; 19 | expect(s.matchFlag(x)).toEqual('foo'); 20 | expect(s.matchOption(x)).toEqual(null); 21 | expect(s.matchCompactOption(x)).toEqual(null); 22 | }); 23 | 24 | it('should parse an option exclusively', () => { 25 | const s = longStrategy(); 26 | const x = '--foo='; 27 | expect(s.matchFlag(x)).toEqual(null); 28 | expect(s.matchOption(x)).toEqual('foo'); 29 | expect(s.matchCompactOption(x)).toEqual(null); 30 | }); 31 | 32 | it('should parse a compact option exclusively', () => { 33 | const s = longStrategy(); 34 | const x = '--foo=x'; 35 | expect(s.matchFlag(x)).toEqual(null); 36 | expect(s.matchOption(x)).toEqual(null); 37 | expect(s.matchCompactOption(x)).toEqual(['foo', 'x']); 38 | }); 39 | }); 40 | 41 | describe('longShortStrategy', () => { 42 | it('should parse a flag exclusively', () => { 43 | const s = longShortStrategy(); 44 | const x = '--foo'; 45 | expect(s.matchFlag(x)).toEqual('foo'); 46 | expect(s.matchOption(x)).toEqual(null); 47 | expect(s.matchCompactOption(x)).toEqual(null); 48 | }); 49 | 50 | it('should parse a short flag exclusively', () => { 51 | const s = longShortStrategy(); 52 | const x = '-f'; 53 | expect(s.matchFlag(x)).toEqual('f'); 54 | expect(s.matchOption(x)).toEqual(null); 55 | expect(s.matchCompactOption(x)).toEqual(null); 56 | }); 57 | 58 | it('should parse an option exclusively', () => { 59 | const s = longShortStrategy(); 60 | const x = '--foo='; 61 | expect(s.matchFlag(x)).toEqual(null); 62 | expect(s.matchOption(x)).toEqual('foo'); 63 | expect(s.matchCompactOption(x)).toEqual(null); 64 | }); 65 | 66 | it('should parse a short option exclusively', () => { 67 | const s = longShortStrategy(); 68 | const x = '-f='; 69 | expect(s.matchFlag(x)).toEqual(null); 70 | expect(s.matchOption(x)).toEqual('f'); 71 | expect(s.matchCompactOption(x)).toEqual(null); 72 | }); 73 | 74 | it('should parse a compact option exclusively', () => { 75 | const s = longShortStrategy(); 76 | const x = '--foo=x'; 77 | expect(s.matchFlag(x)).toEqual(null); 78 | expect(s.matchOption(x)).toEqual(null); 79 | expect(s.matchCompactOption(x)).toEqual(['foo', 'x']); 80 | }); 81 | 82 | it('should parse a short compact option exclusively', () => { 83 | const s = longShortStrategy(); 84 | const x = '-f=3'; 85 | expect(s.matchFlag(x)).toEqual(null); 86 | expect(s.matchOption(x)).toEqual(null); 87 | expect(s.matchCompactOption(x)).toEqual(['f', '3']); 88 | }); 89 | }); 90 | 91 | describe('prefixedStrategy', () => { 92 | it('should parse a flag exclusively', () => { 93 | const s = prefixedStrategy(['--'], ['=']); 94 | const x = '--foo'; 95 | expect(s.matchFlag(x)).toEqual('foo'); 96 | expect(s.matchOption(x)).toEqual(null); 97 | expect(s.matchCompactOption(x)).toEqual(null); 98 | }); 99 | 100 | it('should parse an option exclusively', () => { 101 | const s = prefixedStrategy(['--'], ['=']); 102 | const x = '--foo='; 103 | expect(s.matchFlag(x)).toEqual(null); 104 | expect(s.matchOption(x)).toEqual('foo'); 105 | expect(s.matchCompactOption(x)).toEqual(null); 106 | }); 107 | 108 | it('should parse a compact option exclusively', () => { 109 | const s = prefixedStrategy(['--'], ['=']); 110 | const x = '--foo=x'; 111 | expect(s.matchFlag(x)).toEqual(null); 112 | expect(s.matchOption(x)).toEqual(null); 113 | expect(s.matchCompactOption(x)).toEqual(['foo', 'x']); 114 | }); 115 | 116 | it('with multiple symbols, should parse a flag', () => { 117 | const s = prefixedStrategy(['--', '-', '/'], ['=', ':']); 118 | const x = '-foo'; 119 | expect(s.matchFlag(x)).toEqual('foo'); 120 | expect(s.matchOption(x)).toEqual(null); 121 | expect(s.matchCompactOption(x)).toEqual(null); 122 | }); 123 | 124 | it('with multiple symbols, should parse an option', () => { 125 | const s = prefixedStrategy(['--', '-', '/'], ['=', ':']); 126 | const x = '/foo:'; 127 | expect(s.matchFlag(x)).toEqual(null); 128 | expect(s.matchOption(x)).toEqual('foo'); 129 | expect(s.matchCompactOption(x)).toEqual(null); 130 | }); 131 | 132 | it('with multiple symbols, should parse a compact option', () => { 133 | const s = prefixedStrategy(['--', '-', '/'], ['=', ':']); 134 | const x = '-foo=x'; 135 | expect(s.matchFlag(x)).toEqual(null); 136 | expect(s.matchOption(x)).toEqual(null); 137 | expect(s.matchCompactOption(x)).toEqual(['foo', 'x']); 138 | }); 139 | }); 140 | 141 | describe('matchingStrategy', () => { 142 | it('should parse a flag', () => { 143 | const s = matchingStrategy({ flag: ['--flag'] }, { option: ['--option='] }); 144 | const x = '--flag'; 145 | expect(s.matchFlag(x)).toEqual('flag'); 146 | }); 147 | 148 | it('should parse an option', () => { 149 | const s = matchingStrategy({ flag: ['--flag'] }, { option: ['--option='] }); 150 | const x = '--option='; 151 | expect(s.matchOption(x)).toEqual('option'); 152 | }); 153 | 154 | it('should parse a compact option', () => { 155 | const s = matchingStrategy({ flag: ['--flag'] }, { option: ['--option='] }); 156 | const x = '--option=Hello'; 157 | expect(s.matchCompactOption(x)).toEqual(['option', 'Hello']); 158 | }); 159 | 160 | it('should not work when not exact', () => { 161 | const s = matchingStrategy({ flag: ['--flag'] }, { option: ['--option='] }); 162 | 163 | const x = '--flaG'; 164 | expect(s.matchFlag(x)).toEqual(null); 165 | 166 | const y = '--opTION='; 167 | expect(s.matchOption(y)).toEqual(null); 168 | expect(s.matchCompactOption(y)).toEqual(null); 169 | }); 170 | 171 | it('should parse a flag (sensitivity = base)', () => { 172 | const s = matchingStrategy({ flag: ['--flag'] }, {}, 'en-US', { sensitivity: 'base' }); 173 | 174 | const x = '--FLAG'; 175 | expect(s.matchFlag(x)).toEqual('flag'); 176 | 177 | const y = '--flág'; 178 | expect(s.matchFlag(y)).toEqual('flag'); 179 | }); 180 | }); 181 | 182 | describe('mapKeys', () => { 183 | it('maps the keys according to a function', () => { 184 | const st = mapKeys(longStrategy(), s => s + '!'); 185 | const x = '--foo'; 186 | expect(st.matchFlag(x)).toEqual('foo!'); 187 | }); 188 | 189 | it('maps the keys according to a function (compact option)', () => { 190 | const st = mapKeys(longStrategy(), s => s + '!'); 191 | const x = '--foo=bar'; 192 | expect(st.matchCompactOption(x)).toEqual(['foo!', 'bar']); 193 | }); 194 | 195 | it('can remove a match from a strategy', () => { 196 | const st = mapKeys(longStrategy(), s => s === 'foo' ? null : s + '!'); 197 | 198 | const x = '--foo'; 199 | expect(st.matchFlag(x)).toEqual(null); 200 | 201 | const y = '--bar'; 202 | expect(st.matchFlag(y)).toEqual('bar!'); 203 | }); 204 | }); 205 | 206 | describe('renameKeys', () => { 207 | it('renames keys', () => { 208 | const st = renameKeys(longStrategy(), { foo: ['bar'] }); 209 | const x = '--bar'; 210 | expect(st.matchFlag(x)).toEqual('foo'); 211 | }); 212 | 213 | it('can keep keys that are not found', () => { 214 | const st = renameKeys(longStrategy(), { foo: ['bar'] }, true); 215 | const x = '--quux'; 216 | expect(st.matchFlag(x)).toEqual('quux'); 217 | }); 218 | 219 | it('can discard keys that are not found', () => { 220 | const st = renameKeys(longStrategy(), { foo: ['bar'] }, false); 221 | const x = '--quux'; 222 | expect(st.matchFlag(x)).toEqual(null); 223 | }); 224 | 225 | it('should not rename when not exact', () => { 226 | const st = renameKeys(longStrategy(), { foo: ['bar'] }); 227 | const x = '--Bar'; 228 | expect(st.matchFlag(x)).toEqual('Bar'); 229 | }); 230 | 231 | it('renames keys (sensitivity = base)', () => { 232 | const st = renameKeys(longStrategy(), { foo: ['bar'] }, true, 'en-US', { sensitivity: 'base' }); 233 | const x = '--BAR'; 234 | expect(st.matchFlag(x)).toEqual('foo'); 235 | }); 236 | }); 237 | -------------------------------------------------------------------------------- /test/util.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | some, none, ok, err, step, finish, fail, 3 | someToOk, okToSome, errToSome, 4 | someToStep, someToFinish, okToStep, okToFinish 5 | } from '../src'; 6 | 7 | describe('someToOk', () => { 8 | it('some -> ok', () => { 9 | expect(someToOk(some(1), 0)).toEqual(ok(1)); 10 | }); 11 | 12 | it('none -> err', () => { 13 | expect(someToOk(none(), 0)).toEqual(err(0)); 14 | }); 15 | }); 16 | 17 | describe('okToSome', () => { 18 | it('ok -> some', () => { 19 | expect(okToSome(ok(1))).toEqual(some(1)); 20 | }); 21 | 22 | it('err -> none', () => { 23 | expect(okToSome(err(0))).toEqual(none()); 24 | }); 25 | }); 26 | 27 | describe('errToSome', () => { 28 | it('ok -> none', () => { 29 | expect(errToSome(ok(1))).toEqual(none()); 30 | }); 31 | 32 | it('err -> sone', () => { 33 | expect(errToSome(err(0))).toEqual(some(0)); 34 | }); 35 | }); 36 | 37 | describe('someToStep', () => { 38 | it('some -> step', () => { 39 | expect(someToStep(some(1), 0)).toEqual(step(1)); 40 | }); 41 | 42 | it('none -> fail', () => { 43 | expect(someToStep(none(), 0)).toEqual(fail(0)); 44 | }); 45 | }); 46 | 47 | describe('someToFinish', () => { 48 | it('some -> step', () => { 49 | expect(someToFinish(some(1), 0)).toEqual(finish(1)); 50 | }); 51 | 52 | it('none -> fail', () => { 53 | expect(someToFinish(none(), 0)).toEqual(fail(0)); 54 | }); 55 | }); 56 | 57 | describe('okToStep', () => { 58 | it('ok -> step', () => { 59 | expect(okToStep(ok(1))).toEqual(step(1)); 60 | }); 61 | 62 | it('err -> fail', () => { 63 | expect(okToStep(err(0))).toEqual(fail(0)); 64 | }); 65 | }); 66 | 67 | describe('okToFinish', () => { 68 | it('ok -> finish', () => { 69 | expect(okToFinish(ok(1))).toEqual(finish(1)); 70 | }); 71 | 72 | it('err -> fail', () => { 73 | expect(okToFinish(err(0))).toEqual(fail(0)); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "moduleResolution": "node", 5 | "target": "ES2017", 6 | "lib": [ 7 | "ES2019", 8 | "ESNext" 9 | ], 10 | "strict": true, 11 | "sourceMap": true, 12 | "declaration": true, 13 | "allowSyntheticDefaultImports": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "declarationDir": "dist/types", 17 | "outDir": "dist/lib" 18 | }, 19 | "include": [ 20 | "src" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "moduleResolution": "node", 5 | "target": "ES2017", 6 | "lib": [ 7 | "ES2019", 8 | "ESNext" 9 | ], 10 | "strict": true, 11 | "sourceMap": true, 12 | "declaration": true, 13 | "allowSyntheticDefaultImports": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "declarationDir": "dist/types", 17 | "outDir": "dist/lib" 18 | }, 19 | "include": [ 20 | "src", 21 | "test" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "file", 3 | "tsconfig": "tsconfig.json", 4 | "inputFiles": "src", 5 | "readme": "none", 6 | "exclude": [ 7 | "src/index.ts", 8 | "test/**" 9 | ], 10 | "excludeNotExported": true, 11 | "excludePrivate": true, 12 | "excludeNotDocumented": true, 13 | "disableSources": true, 14 | "out": "docs/reference" 15 | } --------------------------------------------------------------------------------