├── .gitbook.yaml ├── development └── index.html ├── .prettierrc.json ├── jsconfig.json ├── documentation ├── global.md ├── literals.md ├── check.md ├── self.md ├── print.md ├── constants.md ├── skip.md ├── match.md ├── emit.md ├── throw.md ├── functions.md ├── SUMMARY.md ├── scope.md ├── then.md ├── built-in.md ├── properties.md ├── README.md ├── operators.md ├── examples.md └── library.md ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── source └── term.js ├── mothertode-embed.js ├── mothertode-import.js └── test └── term.test.js /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./documentation 2 | -------------------------------------------------------------------------------- /development/index.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "printWidth": 120, 6 | "useTabs": true, 7 | "quoteProps": "consistent" 8 | } 9 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "lib": ["ES2017", "DOM"], 5 | "target": "ES2017", 6 | "moduleResolution": "classic" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /documentation/global.md: -------------------------------------------------------------------------------- 1 | # Global 2 | 3 | The `global` keyword refers to the global term (the **language**). 4 | 5 | This matches a number. 6 | 7 | ``` 8 | match /[0-9]/ [global] 9 | ``` 10 | -------------------------------------------------------------------------------- /documentation/literals.md: -------------------------------------------------------------------------------- 1 | # Literals 2 | 3 | Terms can be **literals**. 4 | 5 | ## Strings 6 | 7 | ``` 8 | "Hello world!" 9 | ``` 10 | 11 | ## Regular Expressions 12 | 13 | ``` 14 | /[a-z]/ 15 | ``` 16 | -------------------------------------------------------------------------------- /documentation/check.md: -------------------------------------------------------------------------------- 1 | # Check 2 | 3 | The `check` keyword adds an extra **check** to your term. 4 | 5 | This language matches an even number. 6 | 7 | ``` 8 | match /[0-9]/+ 9 | check (n) => n % 2 === 0 10 | ``` 11 | -------------------------------------------------------------------------------- /documentation/self.md: -------------------------------------------------------------------------------- 1 | # Self 2 | 3 | The `self` keyword refers to the current **term**. 4 | 5 | Both of these mean the same thing. 6 | 7 | ``` 8 | let Number = /[0-9]/ [self] 9 | ``` 10 | 11 | ``` 12 | let Number = /[0-9]/ [Number] 13 | ``` 14 | -------------------------------------------------------------------------------- /documentation/print.md: -------------------------------------------------------------------------------- 1 | # Print 2 | 3 | The `print` keyword lets you translate your language and print the output to the console. 4 | 5 | ``` 6 | match /[a-zA-Z]/ 7 | emit (name) => `Hello ${name}!` 8 | 9 | print "world" //Hello world! 10 | ``` 11 | -------------------------------------------------------------------------------- /documentation/constants.md: -------------------------------------------------------------------------------- 1 | # Constants 2 | 3 | You can define a **constant** with the `let` keyword. 4 | 5 | ``` 6 | let Greeting = "Hello world!" 7 | ``` 8 | 9 | You can use **constants** just like any other **term**. 10 | 11 | ``` 12 | let Ribbit = "ribbit" 13 | match Ribbit 14 | ``` 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "editor": { 4 | "formatOnSave": true, 5 | "codeActionsOnSave": { 6 | "source.addMissingImports": true, 7 | "source.organizeImports": true 8 | } 9 | }, 10 | "extensions": { 11 | "recommendations": ["prettier.prettier-vscode"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /documentation/skip.md: -------------------------------------------------------------------------------- 1 | # Skip 2 | 3 | The `skip` keyword lets you **skip** stuff between each term. 4 | 5 | This language allows for whitespace in between each term. 6 | 7 | ``` 8 | match "greet" Name 9 | skip Whitespace 10 | 11 | let Whitespace = { " " | " " | "\n" } 12 | let Name = /[a-zA-Z]/+ 13 | ``` 14 | -------------------------------------------------------------------------------- /documentation/match.md: -------------------------------------------------------------------------------- 1 | # Match 2 | 3 | The `match` keyword makes your language **match** a list of **terms**.\ 4 | In other words, what your user needs to write. 5 | 6 | This makes a language where the user needs to write "ribbit" (otherwise it fails). 7 | 8 | ``` 9 | match "ribbit" 10 | ``` 11 | 12 | This **matches** "ribbit!" 13 | 14 | ``` 15 | match "ribbit" "!" 16 | ``` 17 | 18 | This **matches** a digit. 19 | 20 | ``` 21 | match /[0-9]/ 22 | ``` 23 | -------------------------------------------------------------------------------- /documentation/emit.md: -------------------------------------------------------------------------------- 1 | # Emit 2 | 3 | The `emit` keyword tells your language what to **emit**.\ 4 | In other words, its output. 5 | 6 | You can emit a **javascript value**.\ 7 | This language emits "Hello world!" 8 | 9 | ``` 10 | emit "Hello world!" 11 | ``` 12 | 13 | You can write a **javascript function** to emit different things based on what the user writes.\ 14 | This language says hello to whatever the user writes. 15 | 16 | ``` 17 | emit (name) => `Hello ${name}!` 18 | ``` 19 | -------------------------------------------------------------------------------- /documentation/throw.md: -------------------------------------------------------------------------------- 1 | # Throw 2 | 3 | The `throw` keyword lets you throw an **error** if the term fails to match. 4 | 5 | ``` 6 | match "ribbit" 7 | throw "Expected 'ribbit' but got something else" 8 | ``` 9 | 10 | You can use an error function to make it dynamic. 11 | 12 | ``` 13 | match "ribbit" 14 | throw (input) => `Expected 'ribbit' but got '${input}'` 15 | ``` 16 | 17 | You can use the `error` keyword to throw the default error message. 18 | 19 | ``` 20 | match "ribbit" 21 | throw error 22 | ``` 23 | -------------------------------------------------------------------------------- /documentation/functions.md: -------------------------------------------------------------------------------- 1 | # Functions 2 | 3 | You can make a **function** by writing some parameters before a custom term. 4 | 5 | ``` 6 | => indent "ribbit" 7 | ``` 8 | 9 | You can call the function by passing arguments to it.\ 10 | Arguments can be used as a **term**. 11 | 12 | ``` 13 | match Line<" "> 14 | 15 | let Line = => indent "ribbit" 16 | ``` 17 | 18 | Arguments can be used as a **javascript value**. 19 | 20 | ``` 21 | match MultipleOf<3> 22 | emit "Fizz" 23 | 24 | let MultipleOf = => { 25 | match /[0-9]/+ 26 | check (multiple) => multiple % number === 0 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /documentation/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Intro](README.md) 4 | 5 | ## Basics 6 | 7 | - [Emit](emit.md) 8 | - [Match](match.md) 9 | 10 | ## Using Terms 11 | 12 | - [Literals](literals.md) 13 | - [Constants](constants.md) 14 | - [Operators](operators.md) 15 | - [Built-In Terms](built-in.md) 16 | 17 | ## Making Terms 18 | 19 | - [Properties](properties.md) 20 | - [Functions](functions.md) 21 | - [Scope](scope.md) 22 | 23 | ## Advanced Properties 24 | 25 | - [Skip](skip.md) 26 | - [Then](then.md) 27 | - [Check](check.md) 28 | - [Throw](throw.md) 29 | - [Print](print.md) 30 | 31 | ## Library 32 | 33 | - [Library](library.md) 34 | 35 | ## Examples 36 | 37 | - [Examples](examples.md) 38 | -------------------------------------------------------------------------------- /documentation/scope.md: -------------------------------------------------------------------------------- 1 | # Scope 2 | 3 | Terms that are defined in the same **scope** can be referenced by each other.\ 4 | `Value` can reference `Number` because they are defined in the same scope. 5 | 6 | ``` 7 | let Value = Number 8 | let Number = /[0-9]/+ 9 | ``` 10 | 11 | Terms can be defined within the scope of another term, keeping them **private**.\ 12 | `Value` can reference `Number` because it is defined inside its scope. 13 | 14 | ``` 15 | let Value = { 16 | match Number 17 | let Number = /[0-9]/+ 18 | } 19 | ``` 20 | 21 | You can also reference terms in your **javascript**.\ 22 | Terms are functions that take one argument - an input string. 23 | 24 | ``` 25 | emit ToUpperCase 26 | 27 | let ToUpperCase = { 28 | emit (value) => value.toUpperCase() 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /documentation/then.md: -------------------------------------------------------------------------------- 1 | # Then 2 | 3 | The `then` keyword adds an extra **step** to a term. 4 | 5 | This languages matches any Name, and then checks that it matches any Friend. 6 | 7 | ``` 8 | match Name 9 | then Friend 10 | 11 | let Name = /[a-zA-Z]/+ 12 | let Friend = "Bob" | "Kevin" 13 | ``` 14 | 15 | You can chain together different **emit** steps too.\ 16 | This language converts an input to upper case and then shouts hello to it. 17 | 18 | ``` 19 | match Input 20 | then ToUpperCase 21 | 22 | let Input = { 23 | match /[a-zA-Z]/+ 24 | emit (name) => name.toUpperCase() 25 | } 26 | 27 | let ToUpperCase = { 28 | match /[A-Z/+ 29 | emit (name) => `HELLO ${name}!` 30 | } 31 | ``` 32 | 33 | You can also curry an emit function to use the output of each step. 34 | 35 | ``` 36 | match /[a-zA-Z]/+ 37 | then ToUpperCase 38 | 39 | emit (name) => (greeting) => `I say '${greeting}' to ${name}!` 40 | 41 | let Input = { 42 | match /[a-zA-Z]/+ 43 | emit (name) => name.toUpperCase() 44 | } 45 | 46 | let ToUpperCase = { 47 | match /[A-Z]/+ 48 | emit (name) => `HELLO ${name}!` 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Luke Wilson 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 | -------------------------------------------------------------------------------- /documentation/built-in.md: -------------------------------------------------------------------------------- 1 | # Special Terms 2 | 3 | There are some **special terms** that you can use. 4 | 5 | ## Global 6 | 7 | The `global` keyword refers to the global term (the **language**). 8 | 9 | This matches a number with one-or-more digits. 10 | 11 | ``` 12 | match /[0-9]/ [global] 13 | ``` 14 | 15 | ## Self 16 | 17 | The `self` keyword refers to the current **term**. 18 | 19 | Both of these mean the same thing. 20 | 21 | ``` 22 | let Number = /[0-9]/ [self] 23 | ``` 24 | 25 | ``` 26 | let Number = /[0-9]/ [Number] 27 | ``` 28 | 29 | ## End 30 | 31 | The `end` keyword matches the end of the file/source. 32 | 33 | This matches everything until the end of the file/source. 34 | 35 | ``` 36 | match { !end } 37 | ``` 38 | 39 | ## Rest 40 | 41 | The `rest` term matches everything until the end of the file/source. 42 | 43 | ``` 44 | match rest 45 | ``` 46 | 47 | ## Anything 48 | 49 | The `anything` term matches any character. 50 | 51 | ``` 52 | match anything 53 | ``` 54 | 55 | ## Nothing 56 | 57 | The `nothing` term matches nothing. It's useful for emitting something without needing to match anything. 58 | 59 | ``` 60 | match nothing 61 | ``` 62 | -------------------------------------------------------------------------------- /documentation/properties.md: -------------------------------------------------------------------------------- 1 | # Properties 2 | 3 | You can make your own **term** by writing some **properties** in brace brackets. 4 | 5 | ``` 6 | { 7 | match "greeting" 8 | emit "Hello world!" 9 | } 10 | ``` 11 | 12 | All properties are optional! 13 | 14 | | Property | Language | Description | Default | 15 | | ------------------- | ---------- | ---------------------------------------------------------- | ------------------ | 16 | | [`match`](match.md) | MotherTode | Term to match | `rest` | 17 | | [`skip`](skip.md) | MotherTode | Term to ignore in-between terms | `nothing` | 18 | | [`then`](then.md) | MotherTode | Term to match after this one | `nothing` | 19 | | [`emit`](emit.md) | JavaScript | What to output (after matching) | `(input) => input` | 20 | | [`check`](check.md) | JavaScript | What to check (after matching) | `() => true` | 21 | | [`throw`](throw.md) | JavaScript | What error to throw if this term fails (if any) | `undefined` | 22 | | [`print`](print.md) | JavaScript | What to transpile and print to the console (for debugging) | `undefined` | 23 | -------------------------------------------------------------------------------- /documentation/README.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | 3 | [MotherTode](https://github.com/TodePond/MotherTode) is a language that helps me to make languages. It's a language language. 4 | 5 | You can embed it, like this: 6 | 7 | ```html 8 | 9 | 13 | ``` 14 | 15 | Or import it, like this: 16 | 17 | ```javascript 18 | import { MotherTode } from "./mothertode-import.js" 19 | const language = MotherTode("emit (name) => `Hello ${name}!`") 20 | console.log(language("world")) //Hello world! 21 | ``` 22 | 23 | Or use it from the command line, like this: 24 | 25 | ```bash 26 | mothertode ./hello.mt "world" 27 | ``` 28 | 29 | ## Basics 30 | 31 | - [Emit](emit.md) 32 | - [Match](match.md) 33 | 34 | ## Using Terms 35 | 36 | - [Literals](literals.md) 37 | - [Constants](constants.md) 38 | - [Operators](operators.md) 39 | - [Built-In Terms](built-in.md) 40 | 41 | ## Making Terms 42 | 43 | - [Properties](properties.md) 44 | - [Functions](functions.md) 45 | - [Scope](scope.md) 46 | 47 | ## Properties 48 | 49 | - [Match](match.md) 50 | - [Skip](skip.md) 51 | - [Then](then.md) 52 | - [Emit](emit.md) 53 | - [Check](check.md) 54 | - [Throw](throw.md) 55 | - [Print](print.md) 56 | 57 | ## Library 58 | 59 | - [Library](library.md) 60 | 61 | ## Examples 62 | 63 | - [Examples](examples.md) 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # As of 17th August 2022... I'm currently redesigning and rewriting MotherTode. Everything is broken and work-in-progress. For the old version, go to the [legacy](https://github.com/TodePond/MotherTodeLegacy) repo. 2 | 3 | # Follow the rewrite progress [here](https://github.com/TodePond/MotherTode/issues/26)! 4 | 5 | 6 | 7 | # MotherTode 8 | 9 | MotherTode is a language that helps me to make languages. It's a language language.
10 | For more info, check out the [documentation](https://l2wilson94.gitbook.io/mothertode/). 11 | 12 | ## How does it work? 13 | 14 | Define your language by defining terms, like these: 15 | 16 | ``` 17 | let Expression = Number | String 18 | ``` 19 | 20 | ``` 21 | let Greeting = { 22 | match "greeting" 23 | emit "Hello world!" 24 | } 25 | ``` 26 | 27 | ## What does it look like? 28 | 29 | This is a mini language that lets you add numbers (don't worry if you don't understand it yet): 30 | 31 | ``` 32 | match Number 33 | 34 | let Number = Add | Literal 35 | let Literal = /[0-9]/+ 36 | let Add = { 37 | match @(Number - Add) "+" @Number 38 | emit (left, right) => left + right 39 | ) 40 | ``` 41 | 42 | ## How do I use it? 43 | 44 | You can embed it, like this: 45 | 46 | ```html 47 | 48 | 52 | ``` 53 | 54 | Or import it, like this: 55 | 56 | ```javascript 57 | import { MotherTode } from "./mothertode-import.js" 58 | const language = MotherTode("emit (name) => `Hello ${name}!`") 59 | console.log(language("world")) //Hello world! 60 | ``` 61 | 62 | Or use it from the command line, like this: 63 | 64 | ```bash 65 | mothertode ./hello.mt "world" 66 | ``` 67 | -------------------------------------------------------------------------------- /documentation/operators.md: -------------------------------------------------------------------------------- 1 | # Operators 2 | 3 | You can use **operators** to change how **terms** get **matched**. 4 | 5 | ## Maybe 6 | 7 | Makes a term **optional**.\ 8 | These match "ribbit" or "ribbit!" 9 | 10 | ``` 11 | match "ribbit" ["!"] 12 | ``` 13 | 14 | ``` 15 | match "ribbit" "!"? 16 | ``` 17 | 18 | ## Many 19 | 20 | Allows a term to be matched **one-or-more times**.\ 21 | This matches "ribbit!" or "ribbit!!" or "ribbit!!!" or etc... 22 | 23 | ``` 24 | match "ribbit" "!"+ 25 | ``` 26 | 27 | ## Any 28 | 29 | Allows a term to be matched **any number of times**.\ 30 | These match "ribbit" or "ribbit!" or "ribbit!!" etc... 31 | 32 | ``` 33 | match "ribbit" {"!"} 34 | ``` 35 | 36 | ``` 37 | match "ribbit" "!"* 38 | ``` 39 | 40 | ## Or 41 | 42 | Matches **either** term.\ 43 | This matches "ribbit" or "hello". 44 | 45 | ``` 46 | match "ribbit" | "hello" 47 | ``` 48 | 49 | ## Not 50 | 51 | Matches **anything other** than the term.\ 52 | This matches anything other than "ribbit". 53 | 54 | ``` 55 | match !"ribbit" 56 | ``` 57 | 58 | ## And 59 | 60 | Must match **both** terms.\ 61 | This matches any word except for "ribbit". 62 | 63 | ``` 64 | match /[a-zA-Z]/+ & !"ribbit" 65 | ``` 66 | 67 | ## Except 68 | 69 | Remove a term from an 'or' operation.\ 70 | This matches "hello". 71 | 72 | ``` 73 | match ("hello" | "ribbit") - "ribbit" 74 | ``` 75 | 76 | ## Select 77 | 78 | You can **select** which terms to pass into the emit function with the `@` symbol. 79 | 80 | ``` 81 | match "greet " @/a-zA-Z/+ 82 | emit (name) => `Hello ${name}!` 83 | 84 | let Name = /[a-zA-Z]/+ 85 | ``` 86 | 87 | ## Until 88 | 89 | When using an **any** or **many** operator, you can choose to carry on **until** a certain term is matches.
90 | By default, they carry on until the end of the file/source. 91 | 92 | This matches any character until you get to a full-stop. 93 | 94 | ``` 95 | match any* until "." 96 | ``` 97 | 98 | ## Before 99 | 100 | You can say what the term should be **before**. 101 | 102 | This only matches "ribbit" if it's before an exclamation mark. 103 | 104 | ``` 105 | match "ribbit" before "!" 106 | ``` 107 | -------------------------------------------------------------------------------- /documentation/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Hello World 4 | 5 | ``` 6 | emit "Hello world!" 7 | ``` 8 | 9 | ## Greet 10 | 11 | ``` 12 | emit (name) => `Hello ${name}!` 13 | ``` 14 | 15 | ## Double 16 | 17 | ``` 18 | match Digit+ ["." Digit+] 19 | emit (number) => number * 2 20 | 21 | let Digit = /[0-9]/ 22 | ``` 23 | 24 | ## Sum 25 | 26 | ``` 27 | let Number = /[0-9]/+ 28 | 29 | match @Number {"," @Number} 30 | skip {" "} 31 | emit (...numbers) => numbers.reduce((a, b) => a + b, 0) 32 | ``` 33 | 34 | ``` 35 | let Number = /[0-9]/+ 36 | 37 | match @Number ["," @self] 38 | skip {" "} 39 | emit (head, tail = 0) => head + tail 40 | ``` 41 | 42 | ## Fizzbuzz 43 | 44 | ``` 45 | match FizzBuzz | Fizz | Buzz | Number 46 | 47 | let Number = /[0-9]/+ 48 | let FizzBuzz = Fizz & Buzz 49 | let MultipleOf = => { 50 | match Number 51 | check (v) => v % n === 0 52 | } 53 | 54 | let Fizz = { 55 | match MultipleOf<3> 56 | emit "Fizz" 57 | } 58 | 59 | let Buzz = { 60 | match MultipleOf<5> 61 | emit "Buzz" 62 | } 63 | ``` 64 | 65 | ``` 66 | match FizzBuzz | Fizz | Buzz | Number 67 | 68 | let FizzBuzz = Fizz & Buzz 69 | let Fizz = MultipleOf<3> & emit "Fizz" 70 | let Buzz = MultipleOf<5> & emit "Buzz" 71 | let Number = /[0-9]/+ 72 | let MultipleOf = => Number & check (v) => v % n === 0 73 | ``` 74 | 75 | ## Calculator 76 | 77 | ``` 78 | match Number 79 | 80 | let Number = Add | Subtract | Literal 81 | let Literal = /[0-9]/+ 82 | 83 | let Add = { 84 | match @(Number - Add) "+" @Number 85 | emit (left, right) => left + right 86 | } 87 | 88 | let Subtract = { 89 | match @(Number - Subtract) "-" @Number 90 | emit (left, right) => left - right 91 | } 92 | ``` 93 | 94 | ``` 95 | match Number 96 | 97 | let Number = Add | Subtract | Literal 98 | let Literal = /[0-9]/+ 99 | let Add = Operation<"+", (a, b) => a + b> 100 | let Subtract = Operation<"-", (a, b) => a - b> 101 | 102 | let Operation = => { 103 | match @(Number - self) operator @Number 104 | emit (a, b) => emitter(a, b) 105 | } 106 | ``` 107 | 108 | ## Fibonacci 109 | 110 | ``` 111 | match Zero | One | Number 112 | 113 | let Zero = "0" & emit "1" 114 | let One = "1" 115 | 116 | let Number = { 117 | match /[0-9]/+ 118 | emit (n) => global(n-1) + global(n-2) 119 | } 120 | ``` 121 | -------------------------------------------------------------------------------- /documentation/library.md: -------------------------------------------------------------------------------- 1 | MotherTode is built on top of a little library.
2 | If you want, you can use the library without using the language itself. 3 | 4 | # Primitives 5 | 6 | ## String 7 | 8 | ```javascript 9 | const name = Term.string("Lu") 10 | name.test("Lu") // true 11 | ``` 12 | 13 | ## Regular Expression 14 | 15 | ```javascript 16 | const name = Term.regExp(/Lu/) 17 | name.test("Lu") // true 18 | ``` 19 | 20 | ```javascript 21 | const name = Term.regExp(/Lu|Luke/) 22 | name.test("Lu") // true 23 | name.test("Luke") // true 24 | ``` 25 | 26 | ```javascript 27 | const number = Term.regExp(/[0-9]+/) 28 | number.test("123") // true 29 | ``` 30 | 31 | # Built-In Terms 32 | 33 | ## Rest 34 | 35 | ```javascript 36 | Term.rest.travel("Luke") // "Lu" 37 | ``` 38 | 39 | ## Anything 40 | 41 | ```javascript 42 | Term.anything.travel("Luke") // "L" 43 | ``` 44 | 45 | ## Nothing 46 | 47 | ```javascript 48 | Term.nothing.travel("Luke") // "" 49 | ``` 50 | 51 | ## End 52 | 53 | ```javascript 54 | Term.end.test("") // true 55 | ``` 56 | 57 | # Operators 58 | 59 | ## List 60 | 61 | ```javascript 62 | const terms = [Term.string("Lu"), Term.string("ke")] 63 | const list = Term.list(terms) 64 | list.test("Luke") // true 65 | ``` 66 | 67 | ## Maybe 68 | 69 | ```javascript 70 | const maybe = Term.maybe(Term.string("Luke")) 71 | maybe.travel("Luke") // "Luke" 72 | maybe.travel("Duke") // "" 73 | maybe.test("Luke") // true 74 | maybe.test("Duke") // true 75 | ``` 76 | 77 | ## Many 78 | 79 | ```javascript 80 | const many = Term.many(Term.string("Lu")) 81 | many.travel("Lu") // "Lu" 82 | many.travel("LuLuLu") // "LuLuLu" 83 | ``` 84 | 85 | ## Any 86 | 87 | ```javascript 88 | const any = Term.any(Term.string("Lu")) 89 | any.test("Lu") // true 90 | any.test("") // true 91 | ``` 92 | 93 | ## Or 94 | 95 | ```javascript 96 | const terms = [Term.string("Luke"), Term.string("Lu")] 97 | const or = Term.or(terms) 98 | or.test("Luke") // true 99 | or.test("Lu") // true 100 | ``` 101 | 102 | ## Not 103 | 104 | ```javascript 105 | const not = Term.not(Term.string("Luke")) 106 | not.test("Luke") // false 107 | not.test("Lu") // true 108 | ``` 109 | 110 | ## And 111 | 112 | ```javascript 113 | const terms = [Term.regExp(/[0-9]/), Term.not(Term.string("3"))] 114 | const and = Term.and(terms) 115 | and.test("1") // true 116 | and.test("3") // false 117 | ``` 118 | 119 | ## Except 120 | 121 | ```javascript 122 | const luke = Term.string("Luke") 123 | const lu = Term.string("Lu") 124 | const terms = [luke, lu] 125 | const or = Term.or(terms) 126 | const except = Term.except(or, [luke]) 127 | except.test("Luke") // false 128 | except.test("Lu") // true 129 | ``` 130 | 131 | # Scope Management 132 | 133 | ## Reference 134 | 135 | ```javascript 136 | const name = Term.string("Lu") 137 | const reference = Term.reference(name) 138 | reference.test("Lu") // true 139 | ``` 140 | 141 | ## Hoist 142 | 143 | ```javascript 144 | const { name } = Term.hoist(() => ({ 145 | name: Term.string("Lu"), 146 | })) 147 | 148 | name.test("Lu") // true 149 | ``` 150 | 151 | ```javascript 152 | const { laugh } = Term.hoist((terms) => ({ 153 | laugh: Term.many(terms.ha), 154 | ha: Term.string("ha"), 155 | })) 156 | 157 | laugh.test("hahaha") // true 158 | ``` 159 | -------------------------------------------------------------------------------- /source/term.js: -------------------------------------------------------------------------------- 1 | export const Term = {} 2 | 3 | //=========// 4 | // DEFAULT // 5 | //=========// 6 | Term.ERROR_SNIPPET_LENGTH = 20 7 | 8 | Term.default = { 9 | type: "default", 10 | name: undefined, 11 | 12 | // If the source matches the term, translate it 13 | // Otherwise, throw an error (if the term has one) 14 | translate(source, options = {}) { 15 | try { 16 | const matches = this.match(source, options) 17 | 18 | if (matches.length > 0) { 19 | const selected = this.select(matches, options) 20 | const result = this.emit(selected, options) 21 | return this.then(result, options) 22 | } 23 | 24 | const error = this.throw(source, options) 25 | if (error !== undefined) { 26 | throw Error(error) 27 | } 28 | } catch (error) { 29 | if (error instanceof RangeError) { 30 | const message = Term.default.throw.apply(this, [source, options]) 31 | throw Error(message) 32 | } else { 33 | throw error 34 | } 35 | } 36 | }, 37 | 38 | // Does the source satisfy the term? 39 | test(source, options = {}) { 40 | const matches = this.match(source, options) 41 | return matches.length > 0 42 | }, 43 | 44 | // Find matches for the term in the source 45 | match(source, options = {}) { 46 | return [source] 47 | }, 48 | 49 | // Find the longest possible snippet that is compatible with the term 50 | travel(source, options = {}) { 51 | return this.match(source, options).flat(Infinity).join("") 52 | }, 53 | 54 | // What to pass to the emit function 55 | select(matches, options = {}) { 56 | return matches.flat(Infinity) 57 | }, 58 | 59 | // What to emit if the term matches 60 | emit(selected, options = {}) { 61 | return selected.join("") 62 | }, 63 | 64 | // What to do after emitting 65 | then(result, options = {}) { 66 | return result 67 | }, 68 | 69 | // Error message to throw if the term does not match 70 | throw(source, options = {}) { 71 | if (source.length === 0) { 72 | return `Expected ${this.toString(options)} but found end of input` 73 | } 74 | return `Expected ${this.toString(options)} but found "${source.slice(0, Term.ERROR_SNIPPET_LENGTH)}"` 75 | }, 76 | 77 | toString(options = {}) { 78 | if (this.name !== undefined) { 79 | return this.name 80 | } 81 | return this.type 82 | }, 83 | } 84 | 85 | //=======// 86 | // PROXY // 87 | //=======// 88 | Term.proxy = (term, proxy) => ({ 89 | ...term, 90 | type: "proxy", 91 | 92 | translate(source, options = {}) { 93 | return proxy("translate", source, options) 94 | }, 95 | 96 | test(source, options = {}) { 97 | return proxy("test", source, options) 98 | }, 99 | 100 | match(source, options = {}) { 101 | return proxy("match", source, options) 102 | }, 103 | 104 | travel(source, options = {}) { 105 | return proxy("travel", source, options) 106 | }, 107 | 108 | select(matches, options = {}) { 109 | return proxy("select", matches, options) 110 | }, 111 | 112 | emit(selected, options = {}) { 113 | return proxy("emit", selected, options) 114 | }, 115 | 116 | then(result, options = {}) { 117 | return proxy("then", result, options) 118 | }, 119 | 120 | throw(source, options = {}) { 121 | return proxy("throw", source, options) 122 | }, 123 | 124 | toString(options = {}) { 125 | return proxy("toString", undefined, options) 126 | }, 127 | }) 128 | 129 | //=========// 130 | // OPTIONS // 131 | //=========// 132 | Term.options = (term, defaultOptions) => { 133 | return Term.proxy(term, (methodName, arg, options) => { 134 | return term[methodName](arg, { ...defaultOptions, ...options }) 135 | }) 136 | } 137 | 138 | //============// 139 | // PRIMITIVES // 140 | //============// 141 | Term.string = (string) => ({ 142 | ...Term.default, 143 | type: "string", 144 | name: `"${string}"`, 145 | 146 | match(source) { 147 | return source.startsWith(string) ? [string] : [] 148 | }, 149 | 150 | travel(source) { 151 | let snippet = "" 152 | for (let i = 0; i < string.length; i++) { 153 | if (source[i] !== string[i]) { 154 | break 155 | } 156 | snippet += source[i] 157 | } 158 | return snippet 159 | }, 160 | }) 161 | 162 | Term.regExp = (regExp) => ({ 163 | ...Term.default, 164 | type: "regExp", 165 | name: `${regExp}`, 166 | 167 | match(source) { 168 | const startRegExp = new RegExp(`^${regExp.source}`) 169 | const matches = source.match(startRegExp) 170 | return matches === null ? [] : [matches[0]] 171 | }, 172 | 173 | travel(source) { 174 | const matches = this.match(source) 175 | return matches.join("") 176 | }, 177 | }) 178 | 179 | //===========// 180 | // BUILT-INS // 181 | //===========// 182 | Term.rest = { 183 | ...Term.default, 184 | type: "rest", 185 | } 186 | 187 | Term.anything = { 188 | ...Term.default, 189 | type: "anything", 190 | 191 | match(source) { 192 | return source.length > 0 ? [source[0]] : [] 193 | }, 194 | } 195 | 196 | Term.end = { 197 | ...Term.default, 198 | type: "end of input", 199 | 200 | match(source) { 201 | return source.length === 0 ? [""] : [] 202 | }, 203 | } 204 | 205 | Term.nothing = { 206 | ...Term.default, 207 | type: "nothing", 208 | 209 | match(source) { 210 | return [""] 211 | }, 212 | } 213 | 214 | //==========// 215 | // OVERRIDE // 216 | //==========// 217 | Term.withEmit = (term, emit) => ({ 218 | ...term, 219 | emit, 220 | }) 221 | 222 | //===========// 223 | // OPERATORS // 224 | //===========// 225 | // Match terms in sequence 226 | Term.list = (terms) => ({ 227 | ...Term.default, 228 | type: "list", 229 | name: terms.length === 1 ? terms.name : `(${terms.map((term) => term.name).join(", ")})`, 230 | 231 | match(source, options = {}) { 232 | const matches = [] 233 | 234 | for (const term of terms) { 235 | const match = term.match(source, options) 236 | if (match.length === 0) { 237 | const result = [] 238 | result.term = term 239 | result.source = source 240 | return result 241 | } 242 | 243 | matches.push(match) 244 | const snippet = match.flat(Infinity).join("") 245 | source = source.slice(snippet.length) 246 | } 247 | 248 | return matches 249 | }, 250 | 251 | travel(source, options = {}) { 252 | let snippet = "" 253 | 254 | for (const term of terms) { 255 | const match = term.match(source, options) 256 | const travel = term.travel(source, options) 257 | 258 | if (match.length === 0) { 259 | snippet += travel 260 | break 261 | } 262 | 263 | const termSnippet = match.flat(Infinity).join("") 264 | snippet += termSnippet 265 | source = source.slice(termSnippet.length) 266 | } 267 | 268 | return snippet 269 | }, 270 | 271 | // Translate each match based on its term 272 | select(matches, options = {}) { 273 | const selected = [] 274 | 275 | for (let i = 0; i < terms.length; i++) { 276 | const term = terms[i] 277 | const match = matches[i] 278 | const termSelected = term.select(match, options) 279 | const termEmitted = term.emit(termSelected, options) 280 | selected.push(termEmitted) 281 | } 282 | 283 | return selected 284 | }, 285 | 286 | throw(source, options = {}) { 287 | const result = this.match(source, options) 288 | return result.term.throw(result.source, options) 289 | }, 290 | }) 291 | 292 | // Optionally match a term 293 | Term.maybe = (term) => ({ 294 | ...term, 295 | type: "maybe", 296 | name: `[${term.name}]`, 297 | 298 | match(source, options = {}) { 299 | const matches = term.match(source, options) 300 | if (matches.length === 0) { 301 | return [""] 302 | } 303 | return matches 304 | }, 305 | 306 | travel(source, options = {}) { 307 | return term.travel(source, options) 308 | }, 309 | 310 | select(matches, options = {}) { 311 | const [match] = matches 312 | if (match === "") { 313 | return [""] 314 | } 315 | return term.select(matches, options) 316 | }, 317 | }) 318 | 319 | // Match a term one or more times 320 | Term.many = (term) => ({ 321 | ...term, 322 | type: "many", 323 | name: `(${term.name})+`, 324 | match(source, options = {}) { 325 | const matches = [] 326 | 327 | while (true) { 328 | const match = term.match(source, options) 329 | if (match.length === 0) { 330 | break 331 | } 332 | 333 | matches.push(match) 334 | const snippet = match.flat(Infinity).join("") 335 | source = source.slice(snippet.length) 336 | } 337 | 338 | return matches 339 | }, 340 | 341 | travel(source, options = {}) { 342 | let snippet = "" 343 | 344 | while (true) { 345 | const match = term.match(source, options) 346 | if (match.length === 0) { 347 | const travel = term.travel(source, options) 348 | snippet += travel 349 | break 350 | } 351 | 352 | const termSnippet = match.flat(Infinity).join("") 353 | snippet += termSnippet 354 | source = source.slice(termSnippet.length) 355 | } 356 | 357 | return snippet 358 | }, 359 | 360 | // Translate each match 361 | select(matches, options = {}) { 362 | const selected = [] 363 | 364 | for (const match of matches) { 365 | const termSelected = term.select(match, options) 366 | const termEmitted = term.emit(termSelected, options) 367 | selected.push(termEmitted) 368 | } 369 | 370 | return selected 371 | }, 372 | 373 | emit(selected, options = {}) { 374 | return selected.join("") 375 | }, 376 | }) 377 | 378 | // Match a term zero or more times 379 | Term.any = (term) => ({ 380 | ...term, 381 | type: "any", 382 | name: `{${term.name}}`, 383 | 384 | match(source, options = {}) { 385 | const matches = [] 386 | 387 | while (true) { 388 | const match = term.match(source, options) 389 | if (match.length === 0) { 390 | break 391 | } 392 | 393 | matches.push(match) 394 | const snippet = match.flat(Infinity).join("") 395 | source = source.slice(snippet.length) 396 | } 397 | 398 | if (matches.length === 0) { 399 | return [""] 400 | } 401 | return matches 402 | }, 403 | 404 | travel(source, options = {}) { 405 | let snippet = "" 406 | 407 | while (true) { 408 | const match = term.match(source, options) 409 | if (match.length === 0) { 410 | const travel = term.travel(source, options) 411 | snippet += travel 412 | break 413 | } 414 | 415 | const termSnippet = match.flat(Infinity).join("") 416 | snippet += termSnippet 417 | source = source.slice(termSnippet.length) 418 | } 419 | 420 | return snippet 421 | }, 422 | 423 | // Translate each match 424 | select(matches, options = {}) { 425 | if (matches.length === 1 && matches[0] === "") { 426 | return [] 427 | } 428 | 429 | const selected = [] 430 | 431 | for (const match of matches) { 432 | const termSelected = term.select(match, options) 433 | const termEmitted = term.emit(termSelected, options) 434 | selected.push(termEmitted) 435 | } 436 | 437 | return selected 438 | }, 439 | 440 | emit(selected) { 441 | return selected.join("") 442 | }, 443 | }) 444 | 445 | Term.or = (terms) => ({ 446 | ...Term.default, 447 | type: "or", 448 | name: `(${terms.map((term) => term.name).join(" | ")})`, 449 | 450 | match(source, { exceptions = [], ...options } = {}) { 451 | for (const term of terms) { 452 | if (exceptions.includes(term)) { 453 | exceptions = exceptions.filter((exception) => exception !== term) 454 | continue 455 | } 456 | const match = term.match(source, { exceptions, ...options }) 457 | match.term = term 458 | if (match.length > 0) { 459 | const matches = [match] 460 | matches.term = term 461 | return matches 462 | } 463 | } 464 | 465 | return [] 466 | }, 467 | 468 | // Return the longest travel snippet of all terms 469 | travel(source, { exceptions = [], ...options } = {}) { 470 | let snippet = "" 471 | 472 | for (const term of terms) { 473 | if (exceptions.includes(term)) { 474 | exceptions = exceptions.filter((exception) => exception !== term) 475 | continue 476 | } 477 | const travel = term.travel(source, { exceptions, ...options }) 478 | if (travel.length > snippet.length) { 479 | snippet = travel 480 | } 481 | } 482 | 483 | return snippet 484 | }, 485 | 486 | select(matches, options = {}) { 487 | const selected = [] 488 | 489 | for (const match of matches) { 490 | const term = match.term 491 | const termSelected = term.select(match, options) 492 | const termEmitted = term.emit(termSelected, options) 493 | selected.push(termEmitted) 494 | } 495 | 496 | return selected 497 | }, 498 | 499 | throw(source, { exceptions = [], ...options } = {}) { 500 | const snippets = [] 501 | let length = 0 502 | 503 | for (const term of terms) { 504 | if (exceptions.includes(term)) { 505 | exceptions = exceptions.filter((exception) => exception !== term) 506 | snippets.push("") 507 | } 508 | const travel = term.travel(source, { exceptions, ...options }) 509 | snippets.push(travel) 510 | if (travel.length > length) { 511 | length = travel.length 512 | } 513 | } 514 | 515 | const longestTerms = snippets 516 | .map((snippet, index) => (snippet.length === length ? index : null)) 517 | .filter((index) => index !== null) 518 | .map((index) => terms[index]) 519 | 520 | // todo: fix: some of the longest terms are not terms or something, they are just arrays 521 | 522 | if (longestTerms.length === 1) { 523 | const term = longestTerms[0] 524 | return term.throw(source, { exceptions, ...options }) 525 | } 526 | 527 | const found = source.length === 0 ? "end of input" : '"' + source.slice(0, Term.ERROR_SNIPPET_LENGTH) + '"' 528 | const termNames = longestTerms.map((term) => term.toString()).join(" | ") 529 | const message = `Expected ${termNames} but found ${found}` 530 | 531 | return message 532 | }, 533 | 534 | toString({ exceptions = [], ...options } = {}) { 535 | return `(${terms 536 | .filter((term) => !exceptions.includes(term)) 537 | .map((term) => term.toString({ ...exceptions, ...options })) 538 | .join(" | ")})` 539 | }, 540 | }) 541 | 542 | Term.except = (term, exceptions) => { 543 | return Term.proxy(term, (methodName, arg, options) => { 544 | const optionExceptions = options.exceptions || [] 545 | const mergedExceptions = [...optionExceptions, ...exceptions] 546 | const mergedOptions = { ...options, exceptions: mergedExceptions } 547 | return term[methodName](arg, mergedOptions) 548 | }) 549 | } 550 | 551 | Term.and = (terms) => ({ 552 | ...Term.default, 553 | type: "and", 554 | name: `"("${terms.map((term) => term.name).join(" & ")}")"`, 555 | 556 | match(source) { 557 | let matches = [] 558 | 559 | for (const term of terms) { 560 | const match = term.match(source) 561 | if (match.length === 0) { 562 | const result = [] 563 | result.term = term 564 | return result 565 | } 566 | 567 | matches = match 568 | } 569 | 570 | return matches 571 | }, 572 | 573 | // Return the longest travel snippet that satisfies all terms 574 | travel(source, options = {}) { 575 | let snippet = source 576 | 577 | for (let i = 0; i < terms.length; i++) { 578 | const term = terms[i] 579 | const travel = term.travel(source, options) 580 | if (travel.length < snippet.length) { 581 | snippet = travel 582 | if (snippet.length === 0) { 583 | break 584 | } else { 585 | i = 0 586 | } 587 | } 588 | } 589 | 590 | return snippet 591 | }, 592 | 593 | throw(source) { 594 | const result = this.match(source) 595 | return result.term.throw(source) 596 | }, 597 | 598 | select(matches) { 599 | const term = terms.at(-1) 600 | return term.select(matches) 601 | }, 602 | 603 | emit(selected) { 604 | const term = terms.at(-1) 605 | return term.emit(selected) 606 | }, 607 | 608 | toString(options) { 609 | return `${"("}${terms.toString(options).join(" & ")}${")"}` 610 | }, 611 | }) 612 | 613 | Term.not = (term) => ({ 614 | ...Term.default, 615 | type: "not", 616 | name: `!${term.name}`, 617 | 618 | match(source) { 619 | const match = term.match(source) 620 | if (match.length === 0) { 621 | return [source] 622 | } 623 | return [] 624 | }, 625 | 626 | travel(source, options = {}) { 627 | const match = term.match(source, options) 628 | if (match.length === 0) { 629 | return source 630 | } 631 | 632 | let snippet = term.travel(source, options) 633 | while (snippet.length > 0) { 634 | snippet = snippet.slice(0, -1) 635 | const match = term.match(snippet, options) 636 | if (match.length === 0) { 637 | return snippet 638 | } 639 | } 640 | 641 | return snippet 642 | }, 643 | }) 644 | 645 | //=======// 646 | // SCOPE // 647 | //=======// 648 | Term.references = new Map() 649 | Term.reference = (object, property) => { 650 | if (Term.references.has(object)) { 651 | const references = Term.references.get(object) 652 | if (references.has(property)) { 653 | return references.get(property) 654 | } 655 | } else { 656 | Term.references.set(object, new Map()) 657 | } 658 | 659 | const reference = Term.proxy(Term.default, (methodName, arg, options) => { 660 | return object[property][methodName](arg, options) 661 | }) 662 | 663 | reference.name = property 664 | 665 | Term.references.get(object).set(property, reference) 666 | return reference 667 | } 668 | 669 | // declare: (references) => terms 670 | Term.hoist = (declare) => { 671 | const terms = {} 672 | const references = new Proxy(terms, { 673 | get(target, key) { 674 | return Term.reference(target, key) 675 | }, 676 | }) 677 | 678 | const declared = declare(references) 679 | for (const key in declared) { 680 | terms[key] = declared[key] 681 | } 682 | 683 | return terms 684 | } 685 | -------------------------------------------------------------------------------- /mothertode-embed.js: -------------------------------------------------------------------------------- 1 | //=============// 2 | // FROGASAURUS // 3 | //=============// 4 | const MotherTodeFrogasaurus = {} 5 | 6 | //========// 7 | // SOURCE // 8 | //========// 9 | { 10 | //====== ./term.js ====== 11 | { 12 | MotherTodeFrogasaurus["./term.js"] = {} 13 | const Term = {} 14 | 15 | //=========// 16 | // DEFAULT // 17 | //=========// 18 | Term.ERROR_SNIPPET_LENGTH = 20 19 | 20 | Term.default = { 21 | type: "default", 22 | name: undefined, 23 | 24 | // If the source matches the term, translate it 25 | // Otherwise, throw an error (if the term has one) 26 | translate(source, options = {}) { 27 | try { 28 | const matches = this.match(source, options) 29 | 30 | if (matches.length > 0) { 31 | const selected = this.select(matches, options) 32 | const result = this.emit(selected, options) 33 | return this.then(result, options) 34 | } 35 | 36 | const error = this.throw(source, options) 37 | if (error !== undefined) { 38 | throw Error(error) 39 | } 40 | } catch (error) { 41 | if (error instanceof RangeError) { 42 | const message = Term.default.throw.apply(this, [source, options]) 43 | throw Error(message) 44 | } else { 45 | throw error 46 | } 47 | } 48 | }, 49 | 50 | // Does the source satisfy the term? 51 | test(source, options = {}) { 52 | const matches = this.match(source, options) 53 | return matches.length > 0 54 | }, 55 | 56 | // Find matches for the term in the source 57 | match(source, options = {}) { 58 | return [source] 59 | }, 60 | 61 | // Find the longest possible snippet that is compatible with the term 62 | travel(source, options = {}) { 63 | return this.match(source, options).flat(Infinity).join("") 64 | }, 65 | 66 | // What to pass to the emit function 67 | select(matches, options = {}) { 68 | return matches.flat(Infinity) 69 | }, 70 | 71 | // What to emit if the term matches 72 | emit(selected, options = {}) { 73 | return selected.join("") 74 | }, 75 | 76 | // What to do after emitting 77 | then(result, options = {}) { 78 | return result 79 | }, 80 | 81 | // Error message to throw if the term does not match 82 | throw(source, options = {}) { 83 | if (source.length === 0) { 84 | return `Expected ${this.toString(options)} but found end of input` 85 | } 86 | return `Expected ${this.toString(options)} but found "${source.slice(0, Term.ERROR_SNIPPET_LENGTH)}"` 87 | }, 88 | 89 | toString(options = {}) { 90 | if (this.name !== undefined) { 91 | return this.name 92 | } 93 | return this.type 94 | }, 95 | } 96 | 97 | //=======// 98 | // PROXY // 99 | //=======// 100 | Term.proxy = (term, proxy) => ({ 101 | ...term, 102 | type: "proxy", 103 | 104 | translate(source, options = {}) { 105 | return proxy("translate", source, options) 106 | }, 107 | 108 | test(source, options = {}) { 109 | return proxy("test", source, options) 110 | }, 111 | 112 | match(source, options = {}) { 113 | return proxy("match", source, options) 114 | }, 115 | 116 | travel(source, options = {}) { 117 | return proxy("travel", source, options) 118 | }, 119 | 120 | select(matches, options = {}) { 121 | return proxy("select", matches, options) 122 | }, 123 | 124 | emit(selected, options = {}) { 125 | return proxy("emit", selected, options) 126 | }, 127 | 128 | then(result, options = {}) { 129 | return proxy("then", result, options) 130 | }, 131 | 132 | throw(source, options = {}) { 133 | return proxy("throw", source, options) 134 | }, 135 | 136 | toString(options = {}) { 137 | return proxy("toString", undefined, options) 138 | }, 139 | }) 140 | 141 | //=========// 142 | // OPTIONS // 143 | //=========// 144 | Term.options = (term, defaultOptions) => { 145 | return Term.proxy(term, (methodName, arg, options) => { 146 | return term[methodName](arg, { ...defaultOptions, ...options }) 147 | }) 148 | } 149 | 150 | //============// 151 | // PRIMITIVES // 152 | //============// 153 | Term.string = (string) => ({ 154 | ...Term.default, 155 | type: "string", 156 | name: `"${string}"`, 157 | 158 | match(source) { 159 | return source.startsWith(string) ? [string] : [] 160 | }, 161 | 162 | travel(source) { 163 | let snippet = "" 164 | for (let i = 0; i < string.length; i++) { 165 | if (source[i] !== string[i]) { 166 | break 167 | } 168 | snippet += source[i] 169 | } 170 | return snippet 171 | }, 172 | }) 173 | 174 | Term.regExp = (regExp) => ({ 175 | ...Term.default, 176 | type: "regExp", 177 | name: `${regExp}`, 178 | 179 | match(source) { 180 | const startRegExp = new RegExp(`^${regExp.source}`) 181 | const matches = source.match(startRegExp) 182 | return matches === null ? [] : [matches[0]] 183 | }, 184 | 185 | travel(source) { 186 | const matches = this.match(source) 187 | return matches.join("") 188 | }, 189 | }) 190 | 191 | //===========// 192 | // BUILT-INS // 193 | //===========// 194 | Term.rest = { 195 | ...Term.default, 196 | type: "rest", 197 | } 198 | 199 | Term.anything = { 200 | ...Term.default, 201 | type: "anything", 202 | 203 | match(source) { 204 | return source.length > 0 ? [source[0]] : [] 205 | }, 206 | } 207 | 208 | Term.end = { 209 | ...Term.default, 210 | type: "end of input", 211 | 212 | match(source) { 213 | return source.length === 0 ? [""] : [] 214 | }, 215 | } 216 | 217 | Term.nothing = { 218 | ...Term.default, 219 | type: "nothing", 220 | 221 | match(source) { 222 | return [""] 223 | }, 224 | } 225 | 226 | //==========// 227 | // OVERRIDE // 228 | //==========// 229 | Term.withEmit = (term, emit) => ({ 230 | ...term, 231 | emit, 232 | }) 233 | 234 | //===========// 235 | // OPERATORS // 236 | //===========// 237 | // Match terms in sequence 238 | Term.list = (terms) => ({ 239 | ...Term.default, 240 | type: "list", 241 | name: terms.length === 1 ? terms.name : `(${terms.map((term) => term.name).join(", ")})`, 242 | 243 | match(source, options = {}) { 244 | const matches = [] 245 | 246 | for (const term of terms) { 247 | const match = term.match(source, options) 248 | if (match.length === 0) { 249 | const result = [] 250 | result.term = term 251 | result.source = source 252 | return result 253 | } 254 | 255 | matches.push(match) 256 | const snippet = match.flat(Infinity).join("") 257 | source = source.slice(snippet.length) 258 | } 259 | 260 | return matches 261 | }, 262 | 263 | travel(source, options = {}) { 264 | let snippet = "" 265 | 266 | for (const term of terms) { 267 | const match = term.match(source, options) 268 | const travel = term.travel(source, options) 269 | 270 | if (match.length === 0) { 271 | snippet += travel 272 | break 273 | } 274 | 275 | const termSnippet = match.flat(Infinity).join("") 276 | snippet += termSnippet 277 | source = source.slice(termSnippet.length) 278 | } 279 | 280 | return snippet 281 | }, 282 | 283 | // Translate each match based on its term 284 | select(matches, options = {}) { 285 | const selected = [] 286 | 287 | for (let i = 0; i < terms.length; i++) { 288 | const term = terms[i] 289 | const match = matches[i] 290 | const termSelected = term.select(match, options) 291 | const termEmitted = term.emit(termSelected, options) 292 | selected.push(termEmitted) 293 | } 294 | 295 | return selected 296 | }, 297 | 298 | throw(source, options = {}) { 299 | const result = this.match(source, options) 300 | return result.term.throw(result.source, options) 301 | }, 302 | }) 303 | 304 | // Optionally match a term 305 | Term.maybe = (term) => ({ 306 | ...term, 307 | type: "maybe", 308 | name: `[${term.name}]`, 309 | 310 | match(source, options = {}) { 311 | const matches = term.match(source, options) 312 | if (matches.length === 0) { 313 | return [""] 314 | } 315 | return matches 316 | }, 317 | 318 | travel(source, options = {}) { 319 | return term.travel(source, options) 320 | }, 321 | 322 | select(matches, options = {}) { 323 | const [match] = matches 324 | if (match === "") { 325 | return [""] 326 | } 327 | return term.select(matches, options) 328 | }, 329 | }) 330 | 331 | // Match a term one or more times 332 | Term.many = (term) => ({ 333 | ...term, 334 | type: "many", 335 | name: `(${term.name})+`, 336 | match(source, options = {}) { 337 | const matches = [] 338 | 339 | while (true) { 340 | const match = term.match(source, options) 341 | if (match.length === 0) { 342 | break 343 | } 344 | 345 | matches.push(match) 346 | const snippet = match.flat(Infinity).join("") 347 | source = source.slice(snippet.length) 348 | } 349 | 350 | return matches 351 | }, 352 | 353 | travel(source, options = {}) { 354 | let snippet = "" 355 | 356 | while (true) { 357 | const match = term.match(source, options) 358 | if (match.length === 0) { 359 | const travel = term.travel(source, options) 360 | snippet += travel 361 | break 362 | } 363 | 364 | const termSnippet = match.flat(Infinity).join("") 365 | snippet += termSnippet 366 | source = source.slice(termSnippet.length) 367 | } 368 | 369 | return snippet 370 | }, 371 | 372 | // Translate each match 373 | select(matches, options = {}) { 374 | const selected = [] 375 | 376 | for (const match of matches) { 377 | const termSelected = term.select(match, options) 378 | const termEmitted = term.emit(termSelected, options) 379 | selected.push(termEmitted) 380 | } 381 | 382 | return selected 383 | }, 384 | 385 | emit(selected, options = {}) { 386 | return selected.join("") 387 | }, 388 | }) 389 | 390 | // Match a term zero or more times 391 | Term.any = (term) => ({ 392 | ...term, 393 | type: "any", 394 | name: `{${term.name}}`, 395 | 396 | match(source, options = {}) { 397 | const matches = [] 398 | 399 | while (true) { 400 | const match = term.match(source, options) 401 | if (match.length === 0) { 402 | break 403 | } 404 | 405 | matches.push(match) 406 | const snippet = match.flat(Infinity).join("") 407 | source = source.slice(snippet.length) 408 | } 409 | 410 | if (matches.length === 0) { 411 | return [""] 412 | } 413 | return matches 414 | }, 415 | 416 | travel(source, options = {}) { 417 | let snippet = "" 418 | 419 | while (true) { 420 | const match = term.match(source, options) 421 | if (match.length === 0) { 422 | const travel = term.travel(source, options) 423 | snippet += travel 424 | break 425 | } 426 | 427 | const termSnippet = match.flat(Infinity).join("") 428 | snippet += termSnippet 429 | source = source.slice(termSnippet.length) 430 | } 431 | 432 | return snippet 433 | }, 434 | 435 | // Translate each match 436 | select(matches, options = {}) { 437 | if (matches.length === 1 && matches[0] === "") { 438 | return [] 439 | } 440 | 441 | const selected = [] 442 | 443 | for (const match of matches) { 444 | const termSelected = term.select(match, options) 445 | const termEmitted = term.emit(termSelected, options) 446 | selected.push(termEmitted) 447 | } 448 | 449 | return selected 450 | }, 451 | 452 | emit(selected) { 453 | return selected.join("") 454 | }, 455 | }) 456 | 457 | Term.or = (terms) => ({ 458 | ...Term.default, 459 | type: "or", 460 | name: `(${terms.map((term) => term.name).join(" | ")})`, 461 | 462 | match(source, { exceptions = [], ...options } = {}) { 463 | for (const term of terms) { 464 | if (exceptions.includes(term)) { 465 | exceptions = exceptions.filter((exception) => exception !== term) 466 | continue 467 | } 468 | const match = term.match(source, { exceptions, ...options }) 469 | match.term = term 470 | if (match.length > 0) { 471 | const matches = [match] 472 | matches.term = term 473 | return matches 474 | } 475 | } 476 | 477 | return [] 478 | }, 479 | 480 | // Return the longest travel snippet of all terms 481 | travel(source, { exceptions = [], ...options } = {}) { 482 | let snippet = "" 483 | 484 | for (const term of terms) { 485 | if (exceptions.includes(term)) { 486 | exceptions = exceptions.filter((exception) => exception !== term) 487 | continue 488 | } 489 | const travel = term.travel(source, { exceptions, ...options }) 490 | if (travel.length > snippet.length) { 491 | snippet = travel 492 | } 493 | } 494 | 495 | return snippet 496 | }, 497 | 498 | select(matches, options = {}) { 499 | const selected = [] 500 | 501 | for (const match of matches) { 502 | const term = match.term 503 | const termSelected = term.select(match, options) 504 | const termEmitted = term.emit(termSelected, options) 505 | selected.push(termEmitted) 506 | } 507 | 508 | return selected 509 | }, 510 | 511 | throw(source, { exceptions = [], ...options } = {}) { 512 | const snippets = [] 513 | let length = 0 514 | 515 | for (const term of terms) { 516 | if (exceptions.includes(term)) { 517 | exceptions = exceptions.filter((exception) => exception !== term) 518 | snippets.push("") 519 | } 520 | const travel = term.travel(source, { exceptions, ...options }) 521 | snippets.push(travel) 522 | if (travel.length > length) { 523 | length = travel.length 524 | } 525 | } 526 | 527 | const longestTerms = snippets 528 | .map((snippet, index) => (snippet.length === length ? index : null)) 529 | .filter((index) => index !== null) 530 | .map((index) => terms[index]) 531 | 532 | // todo: fix: some of the longest terms are not terms or something, they are just arrays 533 | 534 | if (longestTerms.length === 1) { 535 | const term = longestTerms[0] 536 | return term.throw(source, { exceptions, ...options }) 537 | } 538 | 539 | const found = source.length === 0 ? "end of input" : '"' + source.slice(0, Term.ERROR_SNIPPET_LENGTH) + '"' 540 | const termNames = longestTerms.map((term) => term.toString()).join(" | ") 541 | const message = `Expected ${termNames} but found ${found}` 542 | 543 | return message 544 | }, 545 | 546 | toString({ exceptions = [], ...options } = {}) { 547 | return `(${terms 548 | .filter((term) => !exceptions.includes(term)) 549 | .map((term) => term.toString({ ...exceptions, ...options })) 550 | .join(" | ")})` 551 | }, 552 | }) 553 | 554 | Term.except = (term, exceptions) => { 555 | return Term.proxy(term, (methodName, arg, options) => { 556 | const optionExceptions = options.exceptions || [] 557 | const mergedExceptions = [...optionExceptions, ...exceptions] 558 | const mergedOptions = { ...options, exceptions: mergedExceptions } 559 | return term[methodName](arg, mergedOptions) 560 | }) 561 | } 562 | 563 | Term.and = (terms) => ({ 564 | ...Term.default, 565 | type: "and", 566 | name: `"("${terms.map((term) => term.name).join(" & ")}")"`, 567 | 568 | match(source) { 569 | let matches = [] 570 | 571 | for (const term of terms) { 572 | const match = term.match(source) 573 | if (match.length === 0) { 574 | const result = [] 575 | result.term = term 576 | return result 577 | } 578 | 579 | matches = match 580 | } 581 | 582 | return matches 583 | }, 584 | 585 | // Return the longest travel snippet that satisfies all terms 586 | travel(source, options = {}) { 587 | let snippet = source 588 | 589 | for (let i = 0; i < terms.length; i++) { 590 | const term = terms[i] 591 | const travel = term.travel(source, options) 592 | if (travel.length < snippet.length) { 593 | snippet = travel 594 | if (snippet.length === 0) { 595 | break 596 | } else { 597 | i = 0 598 | } 599 | } 600 | } 601 | 602 | return snippet 603 | }, 604 | 605 | throw(source) { 606 | const result = this.match(source) 607 | return result.term.throw(source) 608 | }, 609 | 610 | select(matches) { 611 | const term = terms.at(-1) 612 | return term.select(matches) 613 | }, 614 | 615 | emit(selected) { 616 | const term = terms.at(-1) 617 | return term.emit(selected) 618 | }, 619 | 620 | toString(options) { 621 | return `${"("}${terms.toString(options).join(" & ")}${")"}` 622 | }, 623 | }) 624 | 625 | Term.not = (term) => ({ 626 | ...Term.default, 627 | type: "not", 628 | name: `!${term.name}`, 629 | 630 | match(source) { 631 | const match = term.match(source) 632 | if (match.length === 0) { 633 | return [source] 634 | } 635 | return [] 636 | }, 637 | 638 | travel(source, options = {}) { 639 | const match = term.match(source, options) 640 | if (match.length === 0) { 641 | return source 642 | } 643 | 644 | let snippet = term.travel(source, options) 645 | while (snippet.length > 0) { 646 | snippet = snippet.slice(0, -1) 647 | const match = term.match(snippet, options) 648 | if (match.length === 0) { 649 | return snippet 650 | } 651 | } 652 | 653 | return snippet 654 | }, 655 | }) 656 | 657 | //=======// 658 | // SCOPE // 659 | //=======// 660 | Term.references = new Map() 661 | Term.reference = (object, property) => { 662 | if (Term.references.has(object)) { 663 | const references = Term.references.get(object) 664 | if (references.has(property)) { 665 | return references.get(property) 666 | } 667 | } else { 668 | Term.references.set(object, new Map()) 669 | } 670 | 671 | const reference = Term.proxy(Term.default, (methodName, arg, options) => { 672 | return object[property][methodName](arg, options) 673 | }) 674 | 675 | reference.name = property 676 | 677 | Term.references.get(object).set(property, reference) 678 | return reference 679 | } 680 | 681 | // declare: (references) => terms 682 | Term.hoist = (declare) => { 683 | const terms = {} 684 | const references = new Proxy(terms, { 685 | get(target, key) { 686 | return Term.reference(target, key) 687 | }, 688 | }) 689 | 690 | const declared = declare(references) 691 | for (const key in declared) { 692 | terms[key] = declared[key] 693 | } 694 | 695 | return terms 696 | } 697 | 698 | 699 | MotherTodeFrogasaurus["./term.js"].Term = Term 700 | } 701 | 702 | 703 | 704 | } 705 | 706 | //=========// 707 | // EXPORTS // 708 | //=========// 709 | const MotherTode = { 710 | Term: MotherTodeFrogasaurus["./term.js"].Term, 711 | } -------------------------------------------------------------------------------- /mothertode-import.js: -------------------------------------------------------------------------------- 1 | //=============// 2 | // FROGASAURUS // 3 | //=============// 4 | const MotherTodeFrogasaurus = {} 5 | 6 | //========// 7 | // SOURCE // 8 | //========// 9 | { 10 | //====== ./term.js ====== 11 | { 12 | MotherTodeFrogasaurus["./term.js"] = {} 13 | const Term = {} 14 | 15 | //=========// 16 | // DEFAULT // 17 | //=========// 18 | Term.ERROR_SNIPPET_LENGTH = 20 19 | 20 | Term.default = { 21 | type: "default", 22 | name: undefined, 23 | 24 | // If the source matches the term, translate it 25 | // Otherwise, throw an error (if the term has one) 26 | translate(source, options = {}) { 27 | try { 28 | const matches = this.match(source, options) 29 | 30 | if (matches.length > 0) { 31 | const selected = this.select(matches, options) 32 | const result = this.emit(selected, options) 33 | return this.then(result, options) 34 | } 35 | 36 | const error = this.throw(source, options) 37 | if (error !== undefined) { 38 | throw Error(error) 39 | } 40 | } catch (error) { 41 | if (error instanceof RangeError) { 42 | const message = Term.default.throw.apply(this, [source, options]) 43 | throw Error(message) 44 | } else { 45 | throw error 46 | } 47 | } 48 | }, 49 | 50 | // Does the source satisfy the term? 51 | test(source, options = {}) { 52 | const matches = this.match(source, options) 53 | return matches.length > 0 54 | }, 55 | 56 | // Find matches for the term in the source 57 | match(source, options = {}) { 58 | return [source] 59 | }, 60 | 61 | // Find the longest possible snippet that is compatible with the term 62 | travel(source, options = {}) { 63 | return this.match(source, options).flat(Infinity).join("") 64 | }, 65 | 66 | // What to pass to the emit function 67 | select(matches, options = {}) { 68 | return matches.flat(Infinity) 69 | }, 70 | 71 | // What to emit if the term matches 72 | emit(selected, options = {}) { 73 | return selected.join("") 74 | }, 75 | 76 | // What to do after emitting 77 | then(result, options = {}) { 78 | return result 79 | }, 80 | 81 | // Error message to throw if the term does not match 82 | throw(source, options = {}) { 83 | if (source.length === 0) { 84 | return `Expected ${this.toString(options)} but found end of input` 85 | } 86 | return `Expected ${this.toString(options)} but found "${source.slice(0, Term.ERROR_SNIPPET_LENGTH)}"` 87 | }, 88 | 89 | toString(options = {}) { 90 | if (this.name !== undefined) { 91 | return this.name 92 | } 93 | return this.type 94 | }, 95 | } 96 | 97 | //=======// 98 | // PROXY // 99 | //=======// 100 | Term.proxy = (term, proxy) => ({ 101 | ...term, 102 | type: "proxy", 103 | 104 | translate(source, options = {}) { 105 | return proxy("translate", source, options) 106 | }, 107 | 108 | test(source, options = {}) { 109 | return proxy("test", source, options) 110 | }, 111 | 112 | match(source, options = {}) { 113 | return proxy("match", source, options) 114 | }, 115 | 116 | travel(source, options = {}) { 117 | return proxy("travel", source, options) 118 | }, 119 | 120 | select(matches, options = {}) { 121 | return proxy("select", matches, options) 122 | }, 123 | 124 | emit(selected, options = {}) { 125 | return proxy("emit", selected, options) 126 | }, 127 | 128 | then(result, options = {}) { 129 | return proxy("then", result, options) 130 | }, 131 | 132 | throw(source, options = {}) { 133 | return proxy("throw", source, options) 134 | }, 135 | 136 | toString(options = {}) { 137 | return proxy("toString", undefined, options) 138 | }, 139 | }) 140 | 141 | //=========// 142 | // OPTIONS // 143 | //=========// 144 | Term.options = (term, defaultOptions) => { 145 | return Term.proxy(term, (methodName, arg, options) => { 146 | return term[methodName](arg, { ...defaultOptions, ...options }) 147 | }) 148 | } 149 | 150 | //============// 151 | // PRIMITIVES // 152 | //============// 153 | Term.string = (string) => ({ 154 | ...Term.default, 155 | type: "string", 156 | name: `"${string}"`, 157 | 158 | match(source) { 159 | return source.startsWith(string) ? [string] : [] 160 | }, 161 | 162 | travel(source) { 163 | let snippet = "" 164 | for (let i = 0; i < string.length; i++) { 165 | if (source[i] !== string[i]) { 166 | break 167 | } 168 | snippet += source[i] 169 | } 170 | return snippet 171 | }, 172 | }) 173 | 174 | Term.regExp = (regExp) => ({ 175 | ...Term.default, 176 | type: "regExp", 177 | name: `${regExp}`, 178 | 179 | match(source) { 180 | const startRegExp = new RegExp(`^${regExp.source}`) 181 | const matches = source.match(startRegExp) 182 | return matches === null ? [] : [matches[0]] 183 | }, 184 | 185 | travel(source) { 186 | const matches = this.match(source) 187 | return matches.join("") 188 | }, 189 | }) 190 | 191 | //===========// 192 | // BUILT-INS // 193 | //===========// 194 | Term.rest = { 195 | ...Term.default, 196 | type: "rest", 197 | } 198 | 199 | Term.anything = { 200 | ...Term.default, 201 | type: "anything", 202 | 203 | match(source) { 204 | return source.length > 0 ? [source[0]] : [] 205 | }, 206 | } 207 | 208 | Term.end = { 209 | ...Term.default, 210 | type: "end of input", 211 | 212 | match(source) { 213 | return source.length === 0 ? [""] : [] 214 | }, 215 | } 216 | 217 | Term.nothing = { 218 | ...Term.default, 219 | type: "nothing", 220 | 221 | match(source) { 222 | return [""] 223 | }, 224 | } 225 | 226 | //==========// 227 | // OVERRIDE // 228 | //==========// 229 | Term.withEmit = (term, emit) => ({ 230 | ...term, 231 | emit, 232 | }) 233 | 234 | //===========// 235 | // OPERATORS // 236 | //===========// 237 | // Match terms in sequence 238 | Term.list = (terms) => ({ 239 | ...Term.default, 240 | type: "list", 241 | name: terms.length === 1 ? terms.name : `(${terms.map((term) => term.name).join(", ")})`, 242 | 243 | match(source, options = {}) { 244 | const matches = [] 245 | 246 | for (const term of terms) { 247 | const match = term.match(source, options) 248 | if (match.length === 0) { 249 | const result = [] 250 | result.term = term 251 | result.source = source 252 | return result 253 | } 254 | 255 | matches.push(match) 256 | const snippet = match.flat(Infinity).join("") 257 | source = source.slice(snippet.length) 258 | } 259 | 260 | return matches 261 | }, 262 | 263 | travel(source, options = {}) { 264 | let snippet = "" 265 | 266 | for (const term of terms) { 267 | const match = term.match(source, options) 268 | const travel = term.travel(source, options) 269 | 270 | if (match.length === 0) { 271 | snippet += travel 272 | break 273 | } 274 | 275 | const termSnippet = match.flat(Infinity).join("") 276 | snippet += termSnippet 277 | source = source.slice(termSnippet.length) 278 | } 279 | 280 | return snippet 281 | }, 282 | 283 | // Translate each match based on its term 284 | select(matches, options = {}) { 285 | const selected = [] 286 | 287 | for (let i = 0; i < terms.length; i++) { 288 | const term = terms[i] 289 | const match = matches[i] 290 | const termSelected = term.select(match, options) 291 | const termEmitted = term.emit(termSelected, options) 292 | selected.push(termEmitted) 293 | } 294 | 295 | return selected 296 | }, 297 | 298 | throw(source, options = {}) { 299 | const result = this.match(source, options) 300 | return result.term.throw(result.source, options) 301 | }, 302 | }) 303 | 304 | // Optionally match a term 305 | Term.maybe = (term) => ({ 306 | ...term, 307 | type: "maybe", 308 | name: `[${term.name}]`, 309 | 310 | match(source, options = {}) { 311 | const matches = term.match(source, options) 312 | if (matches.length === 0) { 313 | return [""] 314 | } 315 | return matches 316 | }, 317 | 318 | travel(source, options = {}) { 319 | return term.travel(source, options) 320 | }, 321 | 322 | select(matches, options = {}) { 323 | const [match] = matches 324 | if (match === "") { 325 | return [""] 326 | } 327 | return term.select(matches, options) 328 | }, 329 | }) 330 | 331 | // Match a term one or more times 332 | Term.many = (term) => ({ 333 | ...term, 334 | type: "many", 335 | name: `(${term.name})+`, 336 | match(source, options = {}) { 337 | const matches = [] 338 | 339 | while (true) { 340 | const match = term.match(source, options) 341 | if (match.length === 0) { 342 | break 343 | } 344 | 345 | matches.push(match) 346 | const snippet = match.flat(Infinity).join("") 347 | source = source.slice(snippet.length) 348 | } 349 | 350 | return matches 351 | }, 352 | 353 | travel(source, options = {}) { 354 | let snippet = "" 355 | 356 | while (true) { 357 | const match = term.match(source, options) 358 | if (match.length === 0) { 359 | const travel = term.travel(source, options) 360 | snippet += travel 361 | break 362 | } 363 | 364 | const termSnippet = match.flat(Infinity).join("") 365 | snippet += termSnippet 366 | source = source.slice(termSnippet.length) 367 | } 368 | 369 | return snippet 370 | }, 371 | 372 | // Translate each match 373 | select(matches, options = {}) { 374 | const selected = [] 375 | 376 | for (const match of matches) { 377 | const termSelected = term.select(match, options) 378 | const termEmitted = term.emit(termSelected, options) 379 | selected.push(termEmitted) 380 | } 381 | 382 | return selected 383 | }, 384 | 385 | emit(selected, options = {}) { 386 | return selected.join("") 387 | }, 388 | }) 389 | 390 | // Match a term zero or more times 391 | Term.any = (term) => ({ 392 | ...term, 393 | type: "any", 394 | name: `{${term.name}}`, 395 | 396 | match(source, options = {}) { 397 | const matches = [] 398 | 399 | while (true) { 400 | const match = term.match(source, options) 401 | if (match.length === 0) { 402 | break 403 | } 404 | 405 | matches.push(match) 406 | const snippet = match.flat(Infinity).join("") 407 | source = source.slice(snippet.length) 408 | } 409 | 410 | if (matches.length === 0) { 411 | return [""] 412 | } 413 | return matches 414 | }, 415 | 416 | travel(source, options = {}) { 417 | let snippet = "" 418 | 419 | while (true) { 420 | const match = term.match(source, options) 421 | if (match.length === 0) { 422 | const travel = term.travel(source, options) 423 | snippet += travel 424 | break 425 | } 426 | 427 | const termSnippet = match.flat(Infinity).join("") 428 | snippet += termSnippet 429 | source = source.slice(termSnippet.length) 430 | } 431 | 432 | return snippet 433 | }, 434 | 435 | // Translate each match 436 | select(matches, options = {}) { 437 | if (matches.length === 1 && matches[0] === "") { 438 | return [] 439 | } 440 | 441 | const selected = [] 442 | 443 | for (const match of matches) { 444 | const termSelected = term.select(match, options) 445 | const termEmitted = term.emit(termSelected, options) 446 | selected.push(termEmitted) 447 | } 448 | 449 | return selected 450 | }, 451 | 452 | emit(selected) { 453 | return selected.join("") 454 | }, 455 | }) 456 | 457 | Term.or = (terms) => ({ 458 | ...Term.default, 459 | type: "or", 460 | name: `(${terms.map((term) => term.name).join(" | ")})`, 461 | 462 | match(source, { exceptions = [], ...options } = {}) { 463 | for (const term of terms) { 464 | if (exceptions.includes(term)) { 465 | exceptions = exceptions.filter((exception) => exception !== term) 466 | continue 467 | } 468 | const match = term.match(source, { exceptions, ...options }) 469 | match.term = term 470 | if (match.length > 0) { 471 | const matches = [match] 472 | matches.term = term 473 | return matches 474 | } 475 | } 476 | 477 | return [] 478 | }, 479 | 480 | // Return the longest travel snippet of all terms 481 | travel(source, { exceptions = [], ...options } = {}) { 482 | let snippet = "" 483 | 484 | for (const term of terms) { 485 | if (exceptions.includes(term)) { 486 | exceptions = exceptions.filter((exception) => exception !== term) 487 | continue 488 | } 489 | const travel = term.travel(source, { exceptions, ...options }) 490 | if (travel.length > snippet.length) { 491 | snippet = travel 492 | } 493 | } 494 | 495 | return snippet 496 | }, 497 | 498 | select(matches, options = {}) { 499 | const selected = [] 500 | 501 | for (const match of matches) { 502 | const term = match.term 503 | const termSelected = term.select(match, options) 504 | const termEmitted = term.emit(termSelected, options) 505 | selected.push(termEmitted) 506 | } 507 | 508 | return selected 509 | }, 510 | 511 | throw(source, { exceptions = [], ...options } = {}) { 512 | const snippets = [] 513 | let length = 0 514 | 515 | for (const term of terms) { 516 | if (exceptions.includes(term)) { 517 | exceptions = exceptions.filter((exception) => exception !== term) 518 | snippets.push("") 519 | } 520 | const travel = term.travel(source, { exceptions, ...options }) 521 | snippets.push(travel) 522 | if (travel.length > length) { 523 | length = travel.length 524 | } 525 | } 526 | 527 | const longestTerms = snippets 528 | .map((snippet, index) => (snippet.length === length ? index : null)) 529 | .filter((index) => index !== null) 530 | .map((index) => terms[index]) 531 | 532 | // todo: fix: some of the longest terms are not terms or something, they are just arrays 533 | 534 | if (longestTerms.length === 1) { 535 | const term = longestTerms[0] 536 | return term.throw(source, { exceptions, ...options }) 537 | } 538 | 539 | const found = source.length === 0 ? "end of input" : '"' + source.slice(0, Term.ERROR_SNIPPET_LENGTH) + '"' 540 | const termNames = longestTerms.map((term) => term.toString()).join(" | ") 541 | const message = `Expected ${termNames} but found ${found}` 542 | 543 | return message 544 | }, 545 | 546 | toString({ exceptions = [], ...options } = {}) { 547 | return `(${terms 548 | .filter((term) => !exceptions.includes(term)) 549 | .map((term) => term.toString({ ...exceptions, ...options })) 550 | .join(" | ")})` 551 | }, 552 | }) 553 | 554 | Term.except = (term, exceptions) => { 555 | return Term.proxy(term, (methodName, arg, options) => { 556 | const optionExceptions = options.exceptions || [] 557 | const mergedExceptions = [...optionExceptions, ...exceptions] 558 | const mergedOptions = { ...options, exceptions: mergedExceptions } 559 | return term[methodName](arg, mergedOptions) 560 | }) 561 | } 562 | 563 | Term.and = (terms) => ({ 564 | ...Term.default, 565 | type: "and", 566 | name: `"("${terms.map((term) => term.name).join(" & ")}")"`, 567 | 568 | match(source) { 569 | let matches = [] 570 | 571 | for (const term of terms) { 572 | const match = term.match(source) 573 | if (match.length === 0) { 574 | const result = [] 575 | result.term = term 576 | return result 577 | } 578 | 579 | matches = match 580 | } 581 | 582 | return matches 583 | }, 584 | 585 | // Return the longest travel snippet that satisfies all terms 586 | travel(source, options = {}) { 587 | let snippet = source 588 | 589 | for (let i = 0; i < terms.length; i++) { 590 | const term = terms[i] 591 | const travel = term.travel(source, options) 592 | if (travel.length < snippet.length) { 593 | snippet = travel 594 | if (snippet.length === 0) { 595 | break 596 | } else { 597 | i = 0 598 | } 599 | } 600 | } 601 | 602 | return snippet 603 | }, 604 | 605 | throw(source) { 606 | const result = this.match(source) 607 | return result.term.throw(source) 608 | }, 609 | 610 | select(matches) { 611 | const term = terms.at(-1) 612 | return term.select(matches) 613 | }, 614 | 615 | emit(selected) { 616 | const term = terms.at(-1) 617 | return term.emit(selected) 618 | }, 619 | 620 | toString(options) { 621 | return `${"("}${terms.toString(options).join(" & ")}${")"}` 622 | }, 623 | }) 624 | 625 | Term.not = (term) => ({ 626 | ...Term.default, 627 | type: "not", 628 | name: `!${term.name}`, 629 | 630 | match(source) { 631 | const match = term.match(source) 632 | if (match.length === 0) { 633 | return [source] 634 | } 635 | return [] 636 | }, 637 | 638 | travel(source, options = {}) { 639 | const match = term.match(source, options) 640 | if (match.length === 0) { 641 | return source 642 | } 643 | 644 | let snippet = term.travel(source, options) 645 | while (snippet.length > 0) { 646 | snippet = snippet.slice(0, -1) 647 | const match = term.match(snippet, options) 648 | if (match.length === 0) { 649 | return snippet 650 | } 651 | } 652 | 653 | return snippet 654 | }, 655 | }) 656 | 657 | //=======// 658 | // SCOPE // 659 | //=======// 660 | Term.references = new Map() 661 | Term.reference = (object, property) => { 662 | if (Term.references.has(object)) { 663 | const references = Term.references.get(object) 664 | if (references.has(property)) { 665 | return references.get(property) 666 | } 667 | } else { 668 | Term.references.set(object, new Map()) 669 | } 670 | 671 | const reference = Term.proxy(Term.default, (methodName, arg, options) => { 672 | return object[property][methodName](arg, options) 673 | }) 674 | 675 | reference.name = property 676 | 677 | Term.references.get(object).set(property, reference) 678 | return reference 679 | } 680 | 681 | // declare: (references) => terms 682 | Term.hoist = (declare) => { 683 | const terms = {} 684 | const references = new Proxy(terms, { 685 | get(target, key) { 686 | return Term.reference(target, key) 687 | }, 688 | }) 689 | 690 | const declared = declare(references) 691 | for (const key in declared) { 692 | terms[key] = declared[key] 693 | } 694 | 695 | return terms 696 | } 697 | 698 | 699 | MotherTodeFrogasaurus["./term.js"].Term = Term 700 | } 701 | 702 | 703 | 704 | } 705 | 706 | //=========// 707 | // EXPORTS // 708 | //=========// 709 | export const Term = MotherTodeFrogasaurus["./term.js"].Term 710 | 711 | export const MotherTode = { 712 | Term: MotherTodeFrogasaurus["./term.js"].Term, 713 | } -------------------------------------------------------------------------------- /test/term.test.js: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertThrows } from "https://deno.land/std@0.165.0/testing/asserts.ts" 2 | import { Term } from "../mothertode-import.js" 3 | 4 | //============// 5 | // PRIMITIVES // 6 | //============// 7 | Deno.test("string", () => { 8 | const hello = Term.string("hello") 9 | 10 | assertEquals(hello.translate("hello"), "hello") 11 | assertThrows(() => hello.translate("hi"), Error, 'Expected "hello" but found "hi"') 12 | 13 | assertEquals(hello.match("hello"), ["hello"]) 14 | assertEquals(hello.match("hi"), []) 15 | 16 | assertEquals(hello.test("hello"), true) 17 | assertEquals(hello.test("hi"), false) 18 | 19 | assertEquals(hello.travel("hello"), "hello") 20 | assertEquals(hello.travel("hell"), "hell") 21 | 22 | assertEquals(hello.test("hello world"), true) 23 | }) 24 | 25 | Deno.test("regular expression", () => { 26 | const hello = Term.regExp(/hello/) 27 | 28 | assertEquals(hello.translate("hello"), "hello") 29 | assertThrows(() => hello.translate("hi"), Error, 'Expected /hello/ but found "hi"') 30 | 31 | assertEquals(hello.match("hello"), ["hello"]) 32 | assertEquals(hello.match("hi"), []) 33 | 34 | assertEquals(hello.test("hello"), true) 35 | assertEquals(hello.test("hi"), false) 36 | 37 | assertEquals(hello.travel("hello"), "hello") 38 | assertEquals(hello.travel("hell"), "") 39 | 40 | const yo = Term.regExp(/(yo)+/) 41 | assertEquals(yo.translate("yo"), "yo") 42 | assertEquals(yo.translate("yoyoyo"), "yoyoyo") 43 | 44 | assertEquals(yo.match("yo"), ["yo"]) 45 | assertEquals(yo.match("yoyoyo"), ["yoyoyo"]) 46 | assertEquals(yo.match("yolo"), ["yo"]) 47 | assertEquals(yo.match("yoyolo"), ["yoyo"]) 48 | assertEquals(yo.match("hi"), []) 49 | 50 | assertEquals(yo.test("yo"), true) 51 | assertEquals(yo.test("yoyoyo"), true) 52 | assertEquals(yo.test("hi"), false) 53 | 54 | assertEquals(yo.travel("yo"), "yo") 55 | assertEquals(yo.travel("yoyoyo"), "yoyoyo") 56 | assertEquals(yo.travel("yolo"), "yo") 57 | 58 | const greeting = Term.regExp(/(hello|hi)+/) 59 | assertEquals(greeting.translate("hello"), "hello") 60 | assertEquals(greeting.translate("hihi"), "hihi") 61 | 62 | assertEquals(greeting.match("hello"), ["hello"]) 63 | assertEquals(greeting.match("hellohi"), ["hellohi"]) 64 | 65 | assertEquals(greeting.travel("hello"), "hello") 66 | assertEquals(greeting.travel("hellohi"), "hellohi") 67 | assertEquals(greeting.travel("helloribbit"), "hello") 68 | }) 69 | 70 | Deno.test("regular expression - set", () => { 71 | const digit = Term.regExp(/[0-9]+/) 72 | assertEquals(digit.translate("123"), "123") 73 | assertThrows(() => digit.translate("abc"), Error, 'Expected /[0-9]+/ but found "abc"') 74 | assertThrows(() => digit.translate("hello123"), Error, 'Expected /[0-9]+/ but found "hello123"') 75 | 76 | assertEquals(digit.match("123"), ["123"]) 77 | assertEquals(digit.match("abc"), []) 78 | assertEquals(digit.match("hello123"), []) 79 | 80 | assertEquals(digit.test("123"), true) 81 | assertEquals(digit.test("abc"), false) 82 | assertEquals(digit.test("hello123"), false) 83 | 84 | assertEquals(digit.travel("123"), "123") 85 | assertEquals(digit.travel("abc"), "") 86 | assertEquals(digit.travel("123hello"), "123") 87 | }) 88 | 89 | //================// 90 | // BUILT-IN TERMS // 91 | //================// 92 | Deno.test("rest", () => { 93 | const rest = Term.rest 94 | 95 | assertEquals(rest.translate("hello"), "hello") 96 | assertEquals(rest.translate("hi"), "hi") 97 | 98 | assertEquals(rest.match("hello"), ["hello"]) 99 | assertEquals(rest.match("hi"), ["hi"]) 100 | 101 | assertEquals(rest.test("hello"), true) 102 | assertEquals(rest.test("hi"), true) 103 | 104 | assertEquals(rest.travel("hello"), "hello") 105 | assertEquals(rest.travel("hi"), "hi") 106 | }) 107 | 108 | Deno.test("anything", () => { 109 | const any = Term.anything 110 | 111 | assertEquals(any.translate("hello"), "h") 112 | assertEquals(any.translate("hi"), "h") 113 | 114 | assertEquals(any.match("hello"), ["h"]) 115 | assertEquals(any.match("hi"), ["h"]) 116 | assertEquals(any.match(""), []) 117 | 118 | assertEquals(any.test("hello"), true) 119 | assertEquals(any.test("hi"), true) 120 | assertThrows(() => any.translate(""), Error, "Expected anything but found end of input") 121 | 122 | assertEquals(any.travel("hello"), "h") 123 | assertEquals(any.travel("hi"), "h") 124 | assertEquals(any.travel(""), "") 125 | }) 126 | 127 | Deno.test("end", () => { 128 | const end = Term.end 129 | 130 | assertEquals(end.translate(""), "") 131 | assertThrows(() => end.translate("hello"), Error, 'Expected end of input but found "hello"') 132 | 133 | assertEquals(end.match(""), [""]) 134 | assertEquals(end.match("hello"), []) 135 | 136 | assertEquals(end.test(""), true) 137 | assertEquals(end.test("hello"), false) 138 | 139 | assertEquals(end.travel(""), "") 140 | assertEquals(end.travel("hello"), "") 141 | }) 142 | 143 | Deno.test("nothing", () => { 144 | const nothing = Term.nothing 145 | 146 | assertEquals(nothing.translate("hello"), "") 147 | assertEquals(nothing.translate(""), "") 148 | 149 | assertEquals(nothing.match("hello"), [""]) 150 | assertEquals(nothing.match(""), [""]) 151 | 152 | assertEquals(nothing.test("hello"), true) 153 | assertEquals(nothing.test(""), true) 154 | 155 | assertEquals(nothing.travel("hello"), "") 156 | assertEquals(nothing.travel(""), "") 157 | }) 158 | 159 | //===========// 160 | // OPERATORS // 161 | //===========// 162 | Deno.test("list", () => { 163 | const list = Term.list([Term.string("hello"), Term.string("hi")]) 164 | 165 | assertEquals(list.translate("hellohi"), "hellohi") 166 | assertThrows(() => list.translate("helloh"), Error, 'Expected "hi" but found "h"') 167 | assertThrows(() => list.translate("hello"), Error, 'Expected "hi" but found end of input') 168 | assertThrows(() => list.translate(""), Error, 'Expected "hello" but found end of input') 169 | 170 | assertEquals(list.match("hellohi"), [["hello"], ["hi"]]) 171 | assertEquals(list.match("helloh").length, 0) 172 | 173 | assertEquals(list.test("hellohi"), true) 174 | assertEquals(list.test("helloh"), false) 175 | 176 | assertEquals(list.travel("hellohi"), "hellohi") 177 | assertEquals(list.travel("helloh"), "helloh") 178 | assertEquals(list.travel("hell"), "hell") 179 | 180 | const custom = Term.withEmit(list, ([hello, hi]) => `${hello} ${hi}`) 181 | assertEquals(custom.translate("hellohi"), "hello hi") 182 | assertThrows(() => custom.translate("helloh"), Error, 'Expected "hi" but found "h"') 183 | 184 | const shout = Term.withEmit(Term.string("hello"), (hello) => hello + "!") 185 | const listShout = Term.list([shout, shout]) 186 | assertEquals(listShout.translate("hellohello"), "hello!hello!") 187 | assertThrows(() => listShout.translate("hello"), Error, 'Expected "hello" but found end of input') 188 | }) 189 | 190 | Deno.test("list - nested", () => { 191 | const list = Term.list([Term.string("hello"), Term.list([Term.string("hi"), Term.string("yo")])]) 192 | 193 | assertEquals(list.translate("hellohiyo"), "hellohiyo") 194 | assertThrows(() => list.translate("hellohi"), Error, 'Expected "yo" but found end of input') 195 | assertThrows(() => list.translate("hello"), Error, 'Expected "hi" but found end of input') 196 | assertThrows(() => list.translate(""), Error, 'Expected "hello" but found end of input') 197 | 198 | assertEquals(list.match("hellohiyo"), [["hello"], [["hi"], ["yo"]]]) 199 | assertEquals(list.match("hellohi").length, 0) 200 | 201 | assertEquals(list.test("hellohiyo"), true) 202 | assertEquals(list.test("hellohi"), false) 203 | 204 | const list2 = Term.list([ 205 | Term.string("hello"), 206 | Term.list([Term.string("hi"), Term.string("yo")]), 207 | Term.string("hi"), 208 | ]) 209 | 210 | assertEquals(list2.translate("hellohiyohi"), "hellohiyohi") 211 | assertThrows(() => list2.translate("hellohiyo"), Error, 'Expected "hi" but found end of input') 212 | assertThrows(() => list2.translate("hellohi"), Error, 'Expected "yo" but found end of input') 213 | assertThrows(() => list2.translate("hello"), Error, 'Expected "hi" but found end of input') 214 | assertThrows(() => list2.translate(""), Error, 'Expected "hello" but found end of input') 215 | 216 | assertEquals(list2.match("hellohiyohi"), [["hello"], [["hi"], ["yo"]], ["hi"]]) 217 | assertEquals(list2.match("hellohiyo").length, 0) 218 | 219 | assertEquals(list2.test("hellohiyohi"), true) 220 | assertEquals(list2.test("hellohiyo"), false) 221 | 222 | assertEquals(list2.travel("hellohiyohi"), "hellohiyohi") 223 | assertEquals(list2.travel("hellohiyo"), "hellohiyo") 224 | assertEquals(list2.travel("hellohiy"), "hellohiy") 225 | 226 | const custom = Term.withEmit(list, ([hello, hiyo]) => `${hello} ${hiyo}`) 227 | assertEquals(custom.translate("hellohiyo"), "hello hiyo") 228 | assertEquals(custom.match("hellohiyo"), [["hello"], [["hi"], ["yo"]]]) 229 | assertThrows(() => custom.translate("hellohi"), Error, 'Expected "yo" but found end of input') 230 | 231 | assertEquals(custom.travel("hellohiyo"), "hellohiyo") 232 | assertEquals(custom.travel("hellohiy"), "hellohiy") 233 | 234 | const shout = Term.withEmit(Term.string("hello"), (hello) => hello + "!") 235 | const listShout = Term.list([shout, Term.list([shout, shout])]) 236 | assertEquals(listShout.translate("hellohellohello"), "hello!hello!hello!") 237 | assertEquals(listShout.match("hellohellohello"), [["hello"], [["hello"], ["hello"]]]) 238 | assertThrows(() => listShout.translate("hellohello"), Error, 'Expected "hello" but found end of input') 239 | 240 | assertEquals(listShout.travel("hellohellohello"), "hellohellohello") 241 | assertEquals(listShout.travel("hellohellohe"), "hellohellohe") 242 | }) 243 | 244 | Deno.test("maybe", () => { 245 | const hello = Term.string("hello") 246 | const maybe = Term.maybe(hello) 247 | 248 | assertEquals(maybe.translate("hello"), "hello") 249 | assertEquals(maybe.translate("hi"), "") 250 | 251 | assertEquals(maybe.match("hello"), ["hello"]) 252 | assertEquals(maybe.match("hi"), [""]) 253 | 254 | assertEquals(maybe.test("hello"), true) 255 | assertEquals(maybe.test("hi"), true) 256 | 257 | assertEquals(maybe.travel("hello"), "hello") 258 | assertEquals(maybe.travel("he"), "he") 259 | 260 | const customTerm = Term.withEmit(maybe, (string) => string + "!") 261 | assertEquals(customTerm.match("hello"), ["hello"]) 262 | assertEquals(customTerm.translate("hello"), "hello!") 263 | assertEquals(customTerm.translate("hi"), "!") 264 | 265 | assertEquals(customTerm.travel("hello"), "hello") 266 | assertEquals(customTerm.travel("he"), "he") 267 | 268 | const shout = Term.withEmit(Term.string("hello"), (hello) => hello + "!") 269 | const maybeShoutTerm = Term.maybe(shout) 270 | assertEquals(maybeShoutTerm.match("hello"), ["hello"]) 271 | assertEquals(maybeShoutTerm.translate("hello"), "hello!") 272 | assertEquals(maybeShoutTerm.match("hi"), [""]) 273 | 274 | assertEquals(maybeShoutTerm.travel("hello"), "hello") 275 | assertEquals(maybeShoutTerm.travel("he"), "he") 276 | }) 277 | 278 | Deno.test("many", () => { 279 | const many = Term.many(Term.string("hello")) 280 | 281 | assertEquals(many.translate("hello"), "hello") 282 | assertEquals(many.translate("hellohello"), "hellohello") 283 | assertThrows(() => many.translate(""), Error, 'Expected ("hello")+ but found end of input') 284 | 285 | assertEquals(many.match("hello"), [["hello"]]) 286 | assertEquals(many.match("hellohello"), [["hello"], ["hello"]]) 287 | assertEquals(many.match(""), []) 288 | 289 | assertEquals(many.test("hello"), true) 290 | assertEquals(many.test("hellohello"), true) 291 | assertEquals(many.test(""), false) 292 | 293 | assertEquals(many.travel("hello"), "hello") 294 | assertEquals(many.travel("hellohello"), "hellohello") 295 | assertEquals(many.travel("hellohellohello"), "hellohellohello") 296 | assertEquals(many.travel("hellohellohe"), "hellohellohe") 297 | 298 | const shout = Term.withEmit(Term.string("hello"), (hello) => hello + "!") 299 | const manyShout = Term.many(shout) 300 | assertEquals(manyShout.translate("hello"), "hello!") 301 | assertEquals(manyShout.translate("hellohello"), "hello!hello!") 302 | 303 | assertEquals(manyShout.travel("hello"), "hello") 304 | assertEquals(manyShout.travel("hellohe"), "hellohe") 305 | }) 306 | 307 | // Test matching zero or more terms 308 | Deno.test("any", () => { 309 | const any = Term.any(Term.string("hello")) 310 | 311 | assertEquals(any.translate("hello"), "hello") 312 | assertEquals(any.translate("hellohello"), "hellohello") 313 | assertEquals(any.translate(""), "") 314 | 315 | assertEquals(any.match("hello"), [["hello"]]) 316 | assertEquals(any.match("hellohello"), [["hello"], ["hello"]]) 317 | assertEquals(any.match(""), [""]) 318 | 319 | assertEquals(any.test("hello"), true) 320 | assertEquals(any.test("hellohello"), true) 321 | assertEquals(any.test(""), true) 322 | 323 | assertEquals(any.travel("hello"), "hello") 324 | assertEquals(any.travel("hellohello"), "hellohello") 325 | assertEquals(any.travel("hellohellohello"), "hellohellohello") 326 | assertEquals(any.travel("hellohellohe"), "hellohellohe") 327 | assertEquals(any.travel(""), "") 328 | 329 | const shout = Term.withEmit(Term.string("hello"), (hello) => hello + "!") 330 | const anyShout = Term.any(shout) 331 | assertEquals(anyShout.translate("hello"), "hello!") 332 | assertEquals(anyShout.translate("hellohello"), "hello!hello!") 333 | assertEquals(anyShout.translate(""), "") 334 | 335 | assertEquals(anyShout.travel("hello"), "hello") 336 | assertEquals(anyShout.travel("hellohe"), "hellohe") 337 | }) 338 | 339 | Deno.test("or", () => { 340 | const or = Term.or([Term.string("hello"), Term.string("hi")]) 341 | 342 | assertEquals(or.translate("hello"), "hello") 343 | assertEquals(or.translate("hi"), "hi") 344 | assertThrows(() => or.translate("yo"), Error, 'Expected "hello" | "hi" but found "yo"') 345 | 346 | assertEquals(or.match("hello")[0][0], "hello") 347 | assertEquals(or.match("hi")[0][0], "hi") 348 | assertEquals(or.match("yo").length, 0) 349 | 350 | assertEquals(or.test("hello"), true) 351 | assertEquals(or.test("hi"), true) 352 | assertEquals(or.test("yo"), false) 353 | 354 | assertEquals(or.travel("hello"), "hello") 355 | assertEquals(or.travel("hi"), "hi") 356 | assertEquals(or.travel("hell"), "hell") 357 | assertEquals(or.travel("h"), "h") 358 | assertEquals(or.travel("yo"), "") 359 | 360 | const shout = Term.withEmit(Term.string("hello"), ([hello]) => hello + "!") 361 | const orShout = Term.or([shout, Term.string("hi")]) 362 | assertEquals(orShout.translate("hello"), "hello!") 363 | assertEquals(orShout.translate("hi"), "hi") 364 | assertThrows(() => orShout.translate("yo"), Error, 'Expected "hello" | "hi" but found "yo"') 365 | 366 | assertEquals(orShout.travel("hello"), "hello") 367 | assertEquals(orShout.travel("hi"), "hi") 368 | assertEquals(orShout.travel("hell"), "hell") 369 | assertEquals(orShout.travel("h"), "h") 370 | assertEquals(orShout.travel("yo"), "") 371 | 372 | const or2 = Term.or([Term.string("hello"), Term.string("hi"), Term.string("yo")]) 373 | assertEquals(or2.translate("hello"), "hello") 374 | assertEquals(or2.translate("hi"), "hi") 375 | assertEquals(or2.translate("yo"), "yo") 376 | assertThrows(() => or2.translate("wassup"), Error, 'Expected "hello" | "hi" | "yo" but found "wassup"') 377 | 378 | assertEquals(or2.travel("hello"), "hello") 379 | assertEquals(or2.travel("hi"), "hi") 380 | assertEquals(or2.travel("yo"), "yo") 381 | assertEquals(or2.travel("wassup"), "") 382 | }) 383 | 384 | Deno.test("and", () => { 385 | const and = Term.and([Term.string("hello"), Term.regExp(/hello/)]) 386 | 387 | assertEquals(and.translate("hello"), "hello") 388 | assertThrows(() => and.translate("hi"), Error, 'Expected "hello" but found "hi"') 389 | 390 | assertEquals(and.match("hello"), ["hello"]) 391 | assertEquals(and.match("hi").length, 0) 392 | 393 | assertEquals(and.test("hello"), true) 394 | assertEquals(and.test("hi"), false) 395 | 396 | assertEquals(and.travel("hello"), "hello") 397 | assertEquals(and.travel("hi"), "") 398 | assertEquals(and.travel("h"), "") 399 | 400 | const and2 = Term.and([Term.regExp(/(yo)+/), Term.string("yo")]) 401 | assertEquals(and2.translate("yo"), "yo") 402 | assertEquals(and2.translate("yoyo"), "yo") 403 | 404 | assertEquals(and2.match("yo"), ["yo"]) 405 | assertEquals(and2.match("yoyo"), ["yo"]) 406 | 407 | assertEquals(and2.test("yo"), true) 408 | assertEquals(and2.test("yoyo"), true) 409 | assertEquals(and2.test("y"), false) 410 | 411 | assertEquals(and2.travel("yo"), "yo") 412 | assertEquals(and2.travel("yoyo"), "yo") 413 | assertEquals(and2.travel("y"), "") 414 | }) 415 | 416 | Deno.test("not", () => { 417 | const not = Term.not(Term.string("hello")) 418 | 419 | assertEquals(not.translate("hi"), "hi") 420 | assertThrows(() => not.translate("hello"), Error, 'Expected !"hello" but found "hello"') 421 | 422 | assertEquals(not.match("hi"), ["hi"]) 423 | assertEquals(not.match("hello").length, 0) 424 | 425 | assertEquals(not.test("hi"), true) 426 | assertEquals(not.test("hello"), false) 427 | 428 | assertEquals(not.travel("hi"), "hi") 429 | assertEquals(not.travel("hello"), "hell") 430 | 431 | const not2 = Term.and([Term.regExp(/[a-z]+/), Term.not(Term.string("hello"))]) 432 | assertEquals(not2.translate("hi"), "hi") 433 | assertThrows(() => not2.translate("hello"), Error, 'Expected !"hello" but found "hello"') 434 | 435 | assertEquals(not2.match("hi"), ["hi"]) 436 | assertEquals(not2.match("hello").length, 0) 437 | 438 | assertEquals(not2.test("hi"), true) 439 | assertEquals(not2.test("hello"), false) 440 | 441 | assertEquals(not2.travel("hi"), "hi") 442 | assertEquals(not2.travel("hello"), "hell") 443 | }) 444 | 445 | Deno.test("except", () => { 446 | const hello = Term.string("hello") 447 | const hi = Term.string("hi") 448 | const or = Term.or([hello, hi]) 449 | 450 | const exceptTerm = Term.except(or, [hello]) 451 | 452 | assertEquals(exceptTerm.translate("hi"), "hi") 453 | assertThrows(() => exceptTerm.translate("hello"), Error, 'Expected "hi" but found "hello"') 454 | 455 | assertEquals(exceptTerm.travel("hi"), "hi") 456 | assertEquals(exceptTerm.travel("hello"), "h") 457 | }) 458 | 459 | Deno.test("reference", () => { 460 | const object = { hello: Term.string("hello") } 461 | const reference = Term.reference(object, "hello") 462 | 463 | assertEquals(reference.translate("hello"), "hello") 464 | assertThrows(() => reference.translate("hi"), Error, 'Expected "hello" but found "hi"') 465 | 466 | assertEquals(reference.travel("hello"), "hello") 467 | assertEquals(reference.travel("hi"), "h") 468 | 469 | const reference2 = Term.reference(object, "hello") 470 | assertEquals(reference, reference2) 471 | }) 472 | 473 | Deno.test("hoist", () => { 474 | const { hello } = Term.hoist(() => { 475 | return { hello: Term.string("hello") } 476 | }) 477 | 478 | assertEquals(hello.translate("hello"), "hello") 479 | assertThrows(() => hello.translate("hi"), Error, 'Expected "hello" but found "hi"') 480 | 481 | assertEquals(hello.travel("hello"), "hello") 482 | assertEquals(hello.travel("hi"), "h") 483 | }) 484 | 485 | Deno.test("hoist - list", () => { 486 | const { helloHello } = Term.hoist(({ hello }) => { 487 | return { hello: Term.string("hello"), helloHello: Term.list([hello, hello]) } 488 | }) 489 | 490 | assertEquals(helloHello.translate("hellohello"), "hellohello") 491 | assertThrows(() => helloHello.translate("hi"), Error, 'Expected "hello" but found "hi"') 492 | assertThrows(() => helloHello.translate("hellohi"), Error, 'Expected "hello" but found "hi"') 493 | 494 | assertEquals(helloHello.travel("hellohello"), "hellohello") 495 | assertEquals(helloHello.travel("hi"), "h") 496 | assertEquals(helloHello.travel("hellohi"), "helloh") 497 | }) 498 | 499 | Deno.test("hoist - except", () => { 500 | const { or, except } = Term.hoist(({ hello, hi }) => { 501 | return { 502 | hello: Term.string("hello"), 503 | hi: Term.string("hi"), 504 | or: Term.or([hello, hi]), 505 | except: Term.except(Term.or([hello, hi]), [hello]), 506 | } 507 | }) 508 | 509 | assertEquals(or.translate("hello"), "hello") 510 | assertEquals(or.translate("hi"), "hi") 511 | assertEquals(except.translate("hi"), "hi") 512 | assertThrows(() => except.translate("hello"), Error, 'Expected "hi" but found "hello"') 513 | assertThrows(() => or.translate("wassup"), Error, 'Expected "hello" | "hi" but found "wassup"') 514 | //assertThrows(() => except.translate("wassup"), Error, 'Expected "hi" but found "wassup"') 515 | // todo: fix this ^ 516 | 517 | assertEquals(or.travel("hello"), "hello") 518 | assertEquals(or.travel("hi"), "hi") 519 | assertEquals(except.travel("hi"), "hi") 520 | assertEquals(except.travel("hello"), "h") 521 | }) 522 | 523 | Deno.test("list - maybe", () => { 524 | const hello = Term.string("hello") 525 | const hi = Term.string("hi") 526 | const maybe = Term.maybe(Term.list([hi, hi])) 527 | 528 | const list = Term.list([hello, maybe]) 529 | 530 | assertEquals(list.translate("hello"), "hello") 531 | assertEquals(list.translate("hellohihi"), "hellohihi") 532 | assertThrows(() => list.translate("hihi"), Error, 'Expected "hello" but found "hihi"') 533 | 534 | assertEquals(list.travel("hello"), "hello") 535 | assertEquals(list.travel("hellohihi"), "hellohihi") 536 | assertEquals(list.travel("hihi"), "h") 537 | }) 538 | 539 | Deno.test("hoist - recursive maybe", () => { 540 | const { list } = Term.hoist(({ list, hello }) => { 541 | return { 542 | list: Term.list([hello, Term.maybe(list)]), 543 | hello: Term.string("hello"), 544 | } 545 | }) 546 | 547 | assertEquals(list.translate("hello"), "hello") 548 | assertEquals(list.translate("hellohello"), "hellohello") 549 | assertEquals(list.translate("hellohellohello"), "hellohellohello") 550 | 551 | assertEquals(list.travel("hello"), "hello") 552 | assertEquals(list.travel("hellohello"), "hellohello") 553 | assertEquals(list.travel("hellohellohe"), "hellohello") 554 | }) 555 | 556 | Deno.test("hoist - or", () => { 557 | const { or, except } = Term.hoist(({ hello, hi }) => { 558 | return { 559 | hello: Term.string("hello"), 560 | hi: Term.string("hi"), 561 | or: Term.or([hello, hi]), 562 | except: Term.except(Term.or([hello, hi]), [hello]), 563 | } 564 | }) 565 | 566 | assertEquals(or.translate("hello"), "hello") 567 | assertEquals(or.translate("hi"), "hi") 568 | assertThrows(() => or.translate("yo"), Error, 'Expected "hello" | "hi" but found "yo"') 569 | assertThrows(() => or.translate("he"), Error, 'Expected "hello" but found "he"') 570 | 571 | assertEquals(or.travel("hi"), "hi") 572 | assertEquals(or.travel("hello"), "hello") 573 | assertEquals(or.travel("ho"), "h") 574 | 575 | assertEquals(except.translate("hi"), "hi") 576 | assertThrows(() => except.translate("hello"), Error, 'Expected "hi" but found "hello"') 577 | 578 | assertEquals(except.travel("hi"), "hi") 579 | assertEquals(except.travel("hello"), "h") 580 | }) 581 | 582 | Deno.test("hoist - recursive or", () => { 583 | const { list } = Term.hoist(({ list, hello, tail }) => { 584 | return { 585 | list: Term.list([hello, tail]), 586 | hello: Term.string("hello"), 587 | tail: Term.or([list, hello]), 588 | } 589 | }) 590 | 591 | assertEquals(list.translate("hellohello"), "hellohello") 592 | assertEquals(list.translate("hellohellohello"), "hellohellohello") 593 | assertThrows(() => list.translate("hello"), Error, 'Expected (hello, tail) | "hello" but found end of input') 594 | 595 | assertEquals(list.travel("hellohello"), "hellohello") 596 | assertEquals(list.travel("hello"), "hello") 597 | assertEquals(list.travel("hellohellohe"), "hellohello") 598 | }) 599 | 600 | Deno.test("hoist - left recursion", () => { 601 | const { number } = Term.hoist(({ number, add, literal }) => { 602 | return { 603 | literal: Term.regExp(/[0-9]+/), 604 | number: Term.or([add, literal]), 605 | add: Term.list([Term.except(number, [add]), Term.string("+"), number]), 606 | } 607 | }) 608 | 609 | assertEquals(number.translate("1"), "1") 610 | assertEquals(number.translate("1+2"), "1+2") 611 | assertEquals(number.translate("1+2+3"), "1+2+3") 612 | 613 | assertEquals(number.travel("1"), "1") 614 | assertEquals(number.travel("1+"), "1+") 615 | assertEquals(number.travel("1+2"), "1+2") 616 | assertEquals(number.travel("1+2+"), "1+2") 617 | assertEquals(number.travel("1+2+3"), "1+2+3") 618 | assertEquals(number.travel("1+2+3+"), "1+2+3") 619 | }) 620 | 621 | Deno.test("hoist - deep left recursion", () => { 622 | const { number } = Term.hoist(({ number, add, subtract, literal }) => { 623 | return { 624 | literal: Term.regExp(/[0-9]+/), 625 | number: Term.or([add, subtract, literal]), 626 | add: Term.list([Term.except(number, [add]), Term.string("+"), number]), 627 | subtract: Term.list([Term.except(number, [subtract]), Term.string("-"), number]), 628 | } 629 | }) 630 | 631 | assertEquals(number.translate("1"), "1") 632 | assertEquals(number.translate("1+2"), "1+2") 633 | assertEquals(number.translate("1-2"), "1-2") 634 | assertEquals(number.translate("1+2-3"), "1+2-3") 635 | assertEquals(number.translate("1-2+3-4"), "1-2+3-4") 636 | 637 | assertEquals(number.travel("1"), "1") 638 | assertEquals(number.travel("1+2"), "1+2") 639 | assertEquals(number.travel("1+"), "1+") 640 | assertEquals(number.travel("1-2"), "1-2") 641 | assertEquals(number.travel("1-"), "1-") 642 | assertEquals(number.travel("1+2-3"), "1+2-3") 643 | assertEquals(number.travel("1+2-"), "1+2-") 644 | assertEquals(number.travel("1-2+3-4"), "1-2+3-4") 645 | assertEquals(number.travel("1-2+3-"), "1-2+3") //TODO: fix, this is weird 646 | }) 647 | 648 | Deno.test("hoist - deeper left recursion", () => { 649 | const { number } = Term.hoist(({ number, add, subtract, literal, group }) => { 650 | return { 651 | literal: Term.regExp(/[0-9]+/), 652 | number: Term.or([group, add, subtract, literal]), 653 | add: Term.list([Term.except(number, [add]), Term.string("+"), number]), 654 | subtract: Term.list([Term.except(number, [subtract]), Term.string("-"), number]), 655 | group: Term.list([Term.string("("), number, Term.string(")")]), 656 | } 657 | }) 658 | 659 | assertEquals(number.translate("1"), "1") 660 | assertEquals(number.translate("1+2"), "1+2") 661 | assertEquals(number.translate("1+2-3"), "1+2-3") 662 | 663 | assertEquals(number.translate("(1+2)"), "(1+2)") 664 | //assertThrows(() => number.translate("(1+2"), Error, 'Expected ")" but found end of input') 665 | 666 | assertEquals(number.translate("(1+2-3)"), "(1+2-3)") 667 | assertEquals(number.translate("(1+2-(3+4))"), "(1+2-(3+4))") 668 | assertEquals(number.translate("(1+2-(3+4-5))"), "(1+2-(3+4-5))") 669 | 670 | assertEquals(number.travel("1"), "1") 671 | assertEquals(number.travel("1+"), "1+") 672 | assertEquals(number.travel("1+2"), "1+2") 673 | assertEquals(number.travel("1+2-"), "1+2-") 674 | assertEquals(number.travel("1+2-3"), "1+2-3") 675 | assertEquals(number.travel("(1+2)-(3+4)"), "(1+2)-(3+4)") 676 | assertEquals(number.travel("(1+2)-(3+4-"), "(1+2)-(3+4") 677 | }) 678 | 679 | Deno.test("check", () => { 680 | const number = Term.regExp(/[0-9]+/) 681 | // const check = Term.check(number, (string) => parseInt(string) > 5) 682 | 683 | // assertEquals(check.translate("6"), "6") 684 | // assertThrows(() => check.translate("5"), Error, "Expected check to pass but it failed") 685 | }) 686 | --------------------------------------------------------------------------------