├── .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 |
--------------------------------------------------------------------------------