├── .eslintrc ├── .gitignore ├── README.md ├── api.md ├── internal-api.md ├── lib ├── assert-no-self-loops.js ├── decision-graph.js ├── grammar-graph.js ├── guided-decision-graph.js ├── parse-grammar.js ├── recognizer.js ├── reduce-grammar.js └── utils │ └── clone.js ├── package.json └── test ├── assert-no-self-loops.test.js ├── decision-graph.test.js ├── grammar-graph.test.js ├── guided-decision-graph.test.js ├── parse-grammar.test.js ├── recognizer.test.js ├── reduce-grammar.test.js └── utils └── clone.test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "root": true, 7 | "extends": "eslint:recommended", 8 | "rules": { 9 | "indent": [ 10 | 2, 11 | 2, 12 | { "ArrayExpression": "first" } 13 | ], 14 | "key-spacing": [ 15 | 1, 16 | { "align": { 17 | "beforeColon": true, 18 | "afterColon": true, 19 | "on": "colon", 20 | "mode": "strict" 21 | } 22 | } 23 | ], 24 | "linebreak-style": [ 25 | 2, 26 | "unix" 27 | ], 28 | "quotes": [ 29 | 2, 30 | "single" 31 | ], 32 | "semi": [ 33 | 2, 34 | "never" 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Interactively construct a sentence from a [context-free grammar](https://en.wikipedia.org/wiki/Context-free_grammar), or check if some text is a valid sentence in a grammar. 2 | 3 | 4 | ## Create a GrammarGraph 5 | 6 | Install the npm module. 7 | ``` 8 | npm install grammar-graph 9 | ``` 10 | 11 | Require GrammarGraph, input a grammar, and construct a new graph. See [grammar format](https://github.com/jrleszcz/grammar-graph#grammar) for details on how a grammar works. 12 | ```js 13 | var GrammarGraph = require('grammar-graph') 14 | 15 | var grammar = { 16 | Sentence: ['NounPhrase VerbPhrase'], 17 | NounPhrase: ['the Noun', 18 | 'the Noun RelativeClause'], 19 | VerbPhrase: ['Verb', 20 | 'Verb NounPhrase'], 21 | RelativeClause: ['that VerbPhrase'], 22 | Noun: ['dog', 23 | 'cat', 24 | 'bird', 25 | 'squirrel'], 26 | Verb: ['befriended', 27 | 'loved', 28 | 'ate', 29 | 'attacked'] 30 | } 31 | 32 | var graph = new GrammarGraph(grammar) 33 | ``` 34 | 35 | 36 | Check out the vertices in your graph. The constructor creates a vertex for every terminal and non-terminal symbol in the grammar. 37 | ```js 38 | graph.vertices() => 39 | [ 'Sentence', 'NounPhrase', 'VerbPhrase', 'RelativeClause', 'Noun', 40 | 'Verb', '_NounPhrase_1', '_NounPhrase_2', '_VerbPhrase_1', 'that', 41 | 'dog', 'cat', 'bird', 'squirrel', 'befriended', 'loved', 'ate', 42 | 'attacked', 'the' ] 43 | ``` 44 | Where did `'_NounPhrase_1'`, `'_NounPhrase_2'`, and `'_VerbPhrase_1'` come from? Look at the definition of `NounPhrase` in the original grammar declaration. Both options contained multiple symbols, and the constructor has automatically created a name for each combination. In the case of `VerbPhrase`, only the second option contained multiple symbols, so only one extra name is needed. The automatic expansion of the original `NounPhrase` and `VerbPhrase` definitions result in the following equivalent definitions: 45 | ```js 46 | { 47 | NounPhrase: ['_NounPhrase_1', 48 | '_NounPhrase_2'], 49 | VerbPhrase: ['Verb', 50 | '_VerbPhrase_1'], 51 | _NounPhrase_1: ['the Noun'], 52 | _NounPhrase_2: ['the Noun RelativeClause'], 53 | _VerbPhrase_1: ['Verb NounPhrase'] 54 | } 55 | ``` 56 | 57 | 58 | ## GrammarGraph.createGuide 59 | 60 | Let's create a new guide for constructing sentences from the langauge. Just indicate a starting point in the grammar, in this case `Sentence`. The guide will help you construct a complete Sentence. 61 | ```js 62 | var guide = graph.createGuide('Sentence') 63 | ``` 64 | 65 | The guide gives choices for the next terminal in your construction. Behind the scenes, it is doing a breadth-first search for terminals from the current position. In our grammar, the only possible first terminal is `'the'`: 66 | ```js 67 | guide.choices() => ['the'] 68 | ``` 69 | 70 | You can also check all the possible constructs at any point in time. In this case, we will see that even though `'the'` is the only possible first terminal, there are actually two possible paths for the construction. 71 | ```js 72 | guide.constructs() => 73 | [ 'the Noun RelativeClause VerbPhrase', 'the Noun VerbPhrase' ] 74 | ``` 75 | To get to this point, the Guide has expanded `'Sentence'` => `'NounPhrase VerbPhrase'` => `'the Noun RelativeClause VerbPhrase'` **or** `'the Noun VerbPhrase'`. It stops at this point because it has reached terminal symbol `'the'` in both possible paths. 76 | 77 | 78 | `'the'` is the only choice, so let's choose it. We can then check our construction and possible constructs. 79 | ```js 80 | guide.choose('the') 81 | guide.construction() => ['the'] 82 | guide.constructs() => 83 | [ 'the bird RelativeClause VerbPhrase', 84 | 'the bird VerbPhrase', 85 | 'the cat RelativeClause VerbPhrase', 86 | 'the cat VerbPhrase', 87 | 'the dog RelativeClause VerbPhrase', 88 | 'the dog VerbPhrase', 89 | 'the squirrel RelativeClause VerbPhrase', 90 | 'the squirrel VerbPhrase' ] 91 | guide.choices() => ['squirrel', 'bird', 'cat', 'dog' ] 92 | ``` 93 | 94 | 95 | Let's continue the construction. 96 | ```js 97 | guide.choices() => ['dog', 'cat', 'squirrel', 'bird'] 98 | guide.choose('dog') 99 | 100 | guide.choices() => ['that', 'befriended', 'loved', 'ate', 'attacked'] 101 | guide.choose('ate') 102 | 103 | guide.choices() => ['the'] 104 | ``` 105 | 106 | We could choose 'the' from this last set of choices, but it just so happens that the current construction could be considered a complete `Sentence` (our starting point): 107 | ```js 108 | guide.construction() => ['the', 'dog', 'ate'] 109 | guide.isComplete() => true 110 | guide.constructs() => 111 | [ 'the dog ate', 112 | 'the dog ate the Noun', 113 | 'the dog ate the Noun RelativeClause' ] 114 | ``` 115 | 116 | If we go ahead and choose 'the', we no longer have a complete sentence. 117 | ```js 118 | guide.choose('the') 119 | guide.construction() => ['the', 'dog', 'ate', 'the'] 120 | guide.isComplete() => false 121 | ``` 122 | 123 | At any point, you can move back a step by popping off the last choice. 124 | ```js 125 | guide.pop() => 'the' 126 | guide.construction() => ['the', 'dog', 'ate'] 127 | guide.isComplete() => true 128 | ``` 129 | 130 | You can optionally provide `guide.choices()` with a number indicating the depth of choices you want. If you request a depth greater than 1, instead of an array of strings, it will return an array of TreeNodes which are each at most nDeep (or less if a path ends in a terminal). 131 | 132 | ```js 133 | guide.choose('the') 134 | guide.construction() => ['the', 'dog', 'ate', 'the'] 135 | guide.choices() => ['squirrel', 'bird', 'cat', 'dog'] 136 | guide.choices(3) => 137 | [ { val: 'squirrel', // squirrel 138 | next: [ { val: 'that', // squirrel that 139 | next: 140 | [ { val: 'attacked', next: [] }, // squirrel that attacked 141 | { val: 'ate', next: [] }, // squirrel that ate 142 | { val: 'loved', next: [] }, // squirrel that loved 143 | { val: 'befriended', next: [] } // squirrel that befriended 144 | ] 145 | }, 146 | 147 | { val: 'bird', // bird 148 | next: [ { val: 'that', // bird that 149 | next: 150 | [ { val: 'attacked', next: [] }, // bird that attacked 151 | { val: 'ate', next: [] }, // bird that ate 152 | { val: 'loved', next: [] }, // bird that loved 153 | { val: 'befriended', next: [] } // bird that befriended 154 | ] 155 | }, 156 | 157 | { val: 'cat', 158 | next: [ etc... ] }, 159 | 160 | { val: 'dog', 161 | next: [ etc... ] } 162 | ] 163 | ``` 164 | 165 | In addition to a single terminal string, `guide.choose()` can also accept an array of terminal strings. 166 | ```js 167 | guide.choose(['squirrel', 'that', 'attacked']) 168 | guide.construction() => ['the', 'dog', 'ate', 'the', 'squirrel', 'that', 'attacked'] 169 | guide.complete() => true 170 | ``` 171 | 172 | ## GrammarGraph.createRecognizer 173 | A `Recognizer` can be used to check if some text is a valid sentence in a grammar. Just like when creating a guide, you need to indicate a starting terminal: 174 | ```js 175 | // (using graph declared before) var graph = new GrammarGraph(grammar) 176 | var sentence = graph.createRecognizer('Sentence') 177 | ``` 178 | A recognizer can check whether or not a text is a valid and complete construction: 179 | ```js 180 | sentence.isComplete('the dog ate the cat') => true 181 | sentence.isComplete('the dog ate the cat that') => false 182 | sentence.isComplete('the dog ate the cat that orange juice') => false 183 | sentence.isComplete('the dog ate the cat that attacked') => true 184 | ``` 185 | 186 | or whether the text is valid so far (though it may not be complete): 187 | ```js 188 | sentence.isValid('the dog ate the cat') => true 189 | sentence.isValid('the dog ate the cat that') => true 190 | sentence.isValid('the dog ate the cat that orange juice') => false 191 | sentence.isValid('the dog ate the cat that attacked') => true 192 | ``` 193 | 194 | 195 | ## Grammar 196 | A context-free grammar is a list of rules. Here is a grammar with eight rules that builds creatures like this: 197 | 198 | `~~(^__^)~~` or `~~(-______-)~~` or `~~(*_*)~~`. 199 | 200 | ```js 201 | { 202 | Creature: ['Arm Head Arm'], 203 | Head: ['( Face )'], 204 | Face: ['HappyFace', 205 | 'ZenFace', 206 | 'SleepyFace'], 207 | HappyFace: ['^ Mouth ^'], 208 | ZenFace: ['- Mouth -'], 209 | SleepyFace: ['* Mouth *'], 210 | Mouth: ['_', 211 | '_ Mouth'], 212 | Arm: ['~~'] 213 | } 214 | ``` 215 | 216 | #### Rules 217 | A rule simply means to replace a word like `Creature` with its definition. If we are [constructing](https://github.com/jrleszcz/grammar-graph#building-a-creature) a sentence with the Creature grammar and come across the word `Head`, we will replace it with its definition: `( Face )`. Some rules have multiple options, such as a `Face` which can be rewritten as `HappyFace`, `ZenFace`, or `SleepyFace`. 218 | 219 | More formally, a grammar is an object consisting of key-value pairs, with each [non-terminal symbol](https://github.com/jrleszcz/grammar-graph#non-terminal-symbols) pointing to an array of one or more [symbol chains](https://github.com/jrleszcz/grammar-graph#symbol-chains) choices for this non-terminal. 220 | 221 | #### Symbol Chains 222 | `Arm Head Arm` and `( Face )` are symbol chains. By default, each symbol is seperated by white-space, so both of these symbol chains are made up of three symbols: `Arm, Head, Arm` and `(, Face, )`. 223 | 224 | #### Terminal Symbols 225 | If a symbol has no definition in the grammar, it is a terminal. The six terminal symbols in the creature grammar are: `(, ), ^, *, _, ~~`. These are the actual building blocks of the language, and are the only symbols that will make it into a final construction. 226 | 227 | #### Non-terminal Symbols 228 | If a symbol has a definition in the grammar, it is non-terminal and can be broken down further. A non-terminal's definition is an array of one or more symbol chains indicating possible choices for this rule. 229 | ``` 230 | { 231 | RuleName: ['I am this', 'or this', 'or could be this'], 232 | RuleName2: ['I mean only one thing'] 233 | } 234 | ``` 235 | #### Recursive definitions 236 | Recursive definitions are what make a grammar interesting and powerful. The creature grammar has only one recursive definition: `Mouth: ['_', '_ Mouth']`. This allows creatures to have mouths of one character if we immediately choose the first option, or up to infinite characters if we always choose the second option. 237 | 238 | Do not define a non-terminal to equal only itself. This will not work: `Mouth: ['Mouth']`. 239 | 240 | #### Building a Creature 241 | When constructing from a grammar, you need to indicate a starting point. In this case it only makes sense to start from `Creature`. Let's break down `Creature` until we are left with only terminal symbols. 242 | ``` 243 | // construction // replacement on this step 244 | 245 | Creature // Creature => Arm Head Arm 246 | Arm Head Arm // Arm => ~~ 247 | ~~Head Arm // Head => ( Face ) 248 | ~~(Face) Arm // Face => ZenFace 249 | ~~(ZenFace) Arm // Mouth => _ Mouth 250 | ~~(-Mouth-) Arm // Mouth => _ Mouth 251 | ~~(-_Mouth-) Arm // Mouth => _ Mouth 252 | ~~(-__Mouth-) Arm // Mouth => _ 253 | ~~(-___-) Arm // Arm => ~~ 254 | ~~(-___-)~~ 255 | ``` 256 | 257 | 258 | ## Docs 259 | [View the api documentation here.](api.md) 260 | 261 | ## Development 262 | [View the internal api documentation here.](internal-api.md) 263 | 264 | Clone the git repository and install development dependencies: 265 | ``` 266 | git clone https://github.com/jrleszcz/grammar-graph.git 267 | cd grammar-graph 268 | npm install 269 | ``` 270 | 271 | To run eslint and tape tests: 272 | ``` 273 | npm test 274 | ``` 275 | 276 | To generate api documentation for [api.md](api.md): 277 | ``` 278 | npm run docs 279 | ``` 280 | 281 | ## Credit 282 | This module is based on Alex Shkotin's [Graph representation of context-free grammars](http://arxiv.org/pdf/cs/0703015.pdf). -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | ## Classes 2 | 3 |
stringa string of one or more symbol names seperated by whitespace or 17 | another user defined seperator (see: seperator param for GrammarGraph)
18 |Objecta user defined context-free grammar formatted as an object consisting of key-value pairs, 21 | with each non-terminal symbol 22 | pointing to an array of one or more symbol chains 23 | choices for this non-terminal.
24 |objectArray.<string>
36 | * [.adj(v)](#GrammarGraph+adj) ⇒ Array.<string>
37 | * [.isTypeAND(v)](#GrammarGraph+isTypeAND) ⇒ boolean
38 | * [.createGuide(start)](#GrammarGraph+createGuide) ⇒ [GuidedDecisionGraph](#GuidedDecisionGraph)
39 | * [.createRecognizer(start)](#GrammarGraph+createRecognizer) ⇒ [Recognizer](#Recognizer)
40 |
41 |
42 | ### new GrammarGraph(grammar, [seperator], [epsilonSymbol])
43 | creates a new GrammarGraph which can generate guides.
44 |
45 |
46 | | Param | Type | Default | Description |
47 | | --- | --- | --- | --- |
48 | | grammar | [Grammar](#Grammar) | | an object representing a grammar. |
49 | | [seperator] | string | RegExp | "/\\s+/" | how tokens will be divided in rules |
50 | | [epsilonSymbol] | string | "''" | Special terminal symbol that indicates this is an end of a construction. Defaults to the empty string. |
51 |
52 |
53 | ### grammarGraph.vertices() ⇒ Array.<string>
54 | get an array of vertex names in the graph
55 |
56 | **Kind**: instance method of [GrammarGraph](#GrammarGraph)
57 | **Returns**: Array.<string> - the vertex names in this graph
58 | **See**: [DecisionGraph#vertices](DecisionGraph#vertices)
59 |
60 | ### grammarGraph.adj(v) ⇒ Array.<string>
61 | get an array of all the vertices this vertex points to
62 |
63 | **Kind**: instance method of [GrammarGraph](#GrammarGraph)
64 | **Returns**: Array.<string> - an ordered list of all the vertices that v points to
65 | **See**: [DecisionGraph#adj](DecisionGraph#adj)
66 |
67 | | Param | Type | Description |
68 | | --- | --- | --- |
69 | | v | string | the name of a vertex |
70 |
71 |
72 | ### grammarGraph.isTypeAND(v) ⇒ boolean
73 | is this a type AND vertex (and not a type OR)?
74 |
75 | **Kind**: instance method of [GrammarGraph](#GrammarGraph)
76 | **Returns**: boolean - is this a type AND vertex (and not a type OR)?
77 | **See**: [DecisionGraph#isTypeAND](DecisionGraph#isTypeAND)
78 |
79 | | Param | Type | Description |
80 | | --- | --- | --- |
81 | | v | string | the name of a vertex |
82 |
83 |
84 | ### grammarGraph.createGuide(start) ⇒ [GuidedDecisionGraph](#GuidedDecisionGraph)
85 | get a new GuidedDecisionGraph using this decision graph
86 |
87 | **Kind**: instance method of [GrammarGraph](#GrammarGraph)
88 | **Returns**: [GuidedDecisionGraph](#GuidedDecisionGraph) - a new guide from the provided start point
89 | **See**: [GuidedDecisionGraph](#GuidedDecisionGraph) for the methods available on the Guide
90 |
91 | | Param | Type | Description |
92 | | --- | --- | --- |
93 | | start | string | the name of a vertex in the decision graph from which to start the guided expansion |
94 |
95 |
96 | ### grammarGraph.createRecognizer(start) ⇒ [Recognizer](#Recognizer)
97 | Returns a new Recognizer from the given start vertex
98 |
99 | **Kind**: instance method of [GrammarGraph](#GrammarGraph)
100 | **Returns**: [Recognizer](#Recognizer) - a new Recognizer
101 |
102 | | Param | Type | Description |
103 | | --- | --- | --- |
104 | | start | string | the name of a vertex in the decision graph from which to start the recognizer test |
105 |
106 |
107 | ## GuidedDecisionGraph
108 | **Kind**: global class
109 |
110 | * [GuidedDecisionGraph](#GuidedDecisionGraph)
111 | * [new GuidedDecisionGraph(dg, start)](#new_GuidedDecisionGraph_new)
112 | * [.construction()](#GuidedDecisionGraph+construction) ⇒ Array.<string>
113 | * [.isComplete()](#GuidedDecisionGraph+isComplete) ⇒ boolean
114 | * [.choose(terminal)](#GuidedDecisionGraph+choose)
115 | * [.constructs()](#GuidedDecisionGraph+constructs) ⇒ Array.<string>
116 | * [.pop()](#GuidedDecisionGraph+pop) ⇒ string
117 | * [.choices([nDeep])](#GuidedDecisionGraph+choices) ⇒ Array.<string> | [Array.<TreeNode>](#TreeNode)
118 |
119 |
120 | ### new GuidedDecisionGraph(dg, start)
121 | step-by-step construction of a language from a decision graph
122 |
123 |
124 | | Param | Type | Description |
125 | | --- | --- | --- |
126 | | dg | DecisionGraph | a Decision Graph that defines a grammar |
127 | | start | string | the name of a vertex in the decision graph from which to start the guided expansion |
128 |
129 |
130 | ### guidedDecisionGraph.construction() ⇒ Array.<string>
131 | the current construction
132 |
133 | **Kind**: instance method of [GuidedDecisionGraph](#GuidedDecisionGraph)
134 | **Returns**: Array.<string> - a terminal symbol chain
135 |
136 | ### guidedDecisionGraph.isComplete() ⇒ boolean
137 | is the current construction a valid, complete construction from the starting
138 | nonterminal? ie, could the construction be haulted at this point? Depending
139 | on the grammar, this may be true even if there are more choices at this point.
140 |
141 | **Kind**: instance method of [GuidedDecisionGraph](#GuidedDecisionGraph)
142 | **Returns**: boolean - is the construction complete
143 |
144 | ### guidedDecisionGraph.choose(terminal)
145 | adds the given terminal to the construction
146 |
147 | **Kind**: instance method of [GuidedDecisionGraph](#GuidedDecisionGraph)
148 |
149 | | Param | Type | Description |
150 | | --- | --- | --- |
151 | | terminal | string | Array.<string> | the name of a terminal vertex in the Decision Graph which is in the current set of possible choices. Or a valid sequence of terminal symbols as an array. |
152 |
153 |
154 | ### guidedDecisionGraph.constructs() ⇒ Array.<string>
155 | get a sorted array of possible construction strings from the current state,
156 | possibly including nonterminals after the next terminal
157 |
158 | **Kind**: instance method of [GuidedDecisionGraph](#GuidedDecisionGraph)
159 | **Returns**: Array.<string> - a list of possible constructions
160 | **Example**
161 | ```js
162 | // guide is an in-progress GuidedDecisionGraph
163 | guide.construction() => ['the', 'dog', 'ate']
164 | guide.choices() => ['', 'the']
165 | guide.constructs()
166 | => [ 'the dog ate',
167 | 'the dog ate the Noun'
168 | 'the dog ate the Noun RelativeClause' ]
169 | ```
170 |
171 | ### guidedDecisionGraph.pop() ⇒ string
172 | pop the last choice off the construction
173 |
174 | **Kind**: instance method of [GuidedDecisionGraph](#GuidedDecisionGraph)
175 | **Returns**: string - the last element of the construction that was
176 | submitted through [choose](#GuidedDecisionGraph+choose)
177 | **Throws**:
178 |
179 | - throws an error if called when construction is empty
180 |
181 |
182 | ### guidedDecisionGraph.choices([nDeep]) ⇒ Array.<string> | [Array.<TreeNode>](#TreeNode)
183 | returns all possible next terminals, or an array of nDeep [TreeNodes](#TreeNode)
184 |
185 | **Kind**: instance method of [GuidedDecisionGraph](#GuidedDecisionGraph)
186 | **Returns**: Array.<string> | [Array.<TreeNode>](#TreeNode) - if nDeep=1, an array of terminal symbols (strings),
187 | else an array of [TreeNodes](#TreeNode)
188 |
189 | | Param | Type | Default | Description |
190 | | --- | --- | --- | --- |
191 | | [nDeep] | number | 1 | will search for nDeep possible choices |
192 |
193 | **Example**
194 | ```js
195 | // guide is an in-progress GuidedDecisionGraph
196 | guide.construction() => ['the', 'dog', 'ate']
197 | guide.choices() => ['', 'the']
198 | guide.choices(3) =>
199 | [ { val: '',
200 | next: [] },
201 | { val: 'the',
202 | next: [ { val: 'squirrel',
203 | next: [ { val: 'that', next: [] },
204 | { val: '', next: [] } ]
205 | },
206 | { val: 'bird',
207 | next: [ { val: 'that', next: [] },
208 | { val: '', next: [] } ]
209 | },
210 | { val: 'cat',
211 | next: [ { val: 'that', next: [] },
212 | { val: '', next: [] } ]
213 | },
214 | { val: 'dog',
215 | next: [ { val: 'that', next: [] },
216 | { val: '', next: [] } ]
217 | }
218 | ]
219 | }
220 | ]
221 | ```
222 |
223 | ## Recognizer
224 | **Kind**: global class
225 |
226 | * [Recognizer](#Recognizer)
227 | * [new Recognizer(dg, start, [seperator])](#new_Recognizer_new)
228 | * [.isValid(text)](#Recognizer+isValid) ⇒ boolean
229 | * [.isComplete(text)](#Recognizer+isComplete) ⇒ boolean
230 |
231 |
232 | ### new Recognizer(dg, start, [seperator])
233 | create a Recognizer that can test if text is a valid sentence in a grammar
234 |
235 |
236 | | Param | Type | Default | Description |
237 | | --- | --- | --- | --- |
238 | | dg | DecisionGraph | | a Decision Graph that defines a grammar |
239 | | start | string | | the name of a vertex in the decision graph from which to start the test |
240 | | [seperator] | string | RegExp | "/\\s+/" | how tokens will be divided in given text |
241 |
242 |
243 | ### recognizer.isValid(text) ⇒ boolean
244 | is the text a valid in progress sentence in the grammar? Will return true
245 | even if the text is not complete.
246 |
247 | **Kind**: instance method of [Recognizer](#Recognizer)
248 | **Returns**: boolean - is the text valid?
249 |
250 | | Param | Type | Description |
251 | | --- | --- | --- |
252 | | text | string | the text to check |
253 |
254 |
255 | ### recognizer.isComplete(text) ⇒ boolean
256 | is the text a valid and complete text in the grammar? Will return true
257 | only if the text is complete.
258 |
259 | **Kind**: instance method of [Recognizer](#Recognizer)
260 | **Returns**: boolean - is the text valid and complete?
261 |
262 | | Param | Type | Description |
263 | | --- | --- | --- |
264 | | text | string | the text to check |
265 |
266 |
267 | ## SymbolChain : string
268 | a string of one or more symbol names seperated by whitespace or
269 | another user defined seperator (see: seperator param for [GrammarGraph](#GrammarGraph))
270 |
271 | **Kind**: global typedef
272 | **See**: a SymbolChain is used as definitions in [Grammar](#Grammar)
273 | **Example**
274 | ```js
275 | 'dog' // just a single symbol, the word 'dog'
276 | 'the Noun RelativeClause' // three symbols
277 | ```
278 |
279 | ## Grammar : Object
280 | a user defined context-free grammar formatted as an object consisting of key-value pairs,
281 | with each [non-terminal symbol](https://github.com/jrleszcz/grammar-graph#non-terminal-symbols)
282 | pointing to an array of one or more [symbol chains](https://github.com/jrleszcz/grammar-graph#symbol-chains)
283 | choices for this non-terminal.
284 |
285 | **Kind**: global typedef
286 | **Properties**
287 |
288 | | Name | Type | Description |
289 | | --- | --- | --- |
290 | | symbol | [Array.<SymbolChain>](#SymbolChain) | each element of the array is a possible definition for this symbol. |
291 |
292 | **Example**
293 | ```js
294 | var grammar = {
295 | Sentence: ['NounPhrase VerbPhrase'], // only one definition of 'Sentence'
296 | NounPhrase: ['the Noun', 'the Noun RelativeClause'], // two possible definitions of 'NounPhrase'
297 | VerbPhrase: ['Verb', 'Verb NounPhrase'],
298 | RelativeClause: ['that VerbPhrase'],
299 | Noun: ['dog', 'cat', 'bird', 'squirrel'], // four possible definitions of 'Noun'
300 | Verb: ['befriended', 'loved', 'ate', 'attacked']
301 | }
302 | // non-terminals: Sentence, NounPhrase, VerbPhrase, RelativeClause, Noun, Verb
303 | // terminals: the, that, dog, cat, bird, squirrel, befriended, loved, ate, attacked
304 | ```
305 |
306 | ## TreeNode : object
307 | **Kind**: global typedef
308 | **Properties**
309 |
310 | | Name | Type | Description |
311 | | --- | --- | --- |
312 | | val | string | a terminal string |
313 | | next | [Array.<TreeNode>](#TreeNode) | a list of TreeNodes this node links to |
314 |
315 |
--------------------------------------------------------------------------------
/internal-api.md:
--------------------------------------------------------------------------------
1 | ## Classes
2 |
3 | check a grammar for direct self-loops
19 |DecisionGraphparse a grammar given as an object and compile it into a decision graph
22 |objectreduces the rules of a grammar into a one to one form by assigning a name 25 | to all non-terminals. The end result is that each option on a rule with 26 | more than one choice will either be a single AND-rule or a single terminal.
27 |object | arrayhelper function to clone a simple object/array made up of primitives. 30 | Will not work if the object or array contains non-primitives.
31 |stringa string of one or more symbol names seperated by whitespace or 39 | another user defined seperator (see: seperator param for GrammarGraph)
40 |Objecta user defined context-free grammar formatted as an object consisting of key-value pairs, 43 | with each non-terminal symbol 44 | pointing to an array of one or more symbol chains 45 | choices for this non-terminal.
46 |objectArray.<string>
61 | * [.V()](#DecisionGraph+V) ⇒ number
62 | * [.isTerminal(v)](#DecisionGraph+isTerminal) ⇒ boolean
63 | * [.isVertex(v)](#DecisionGraph+isVertex) ⇒ boolean
64 | * [.isTypeAND(v)](#DecisionGraph+isTypeAND) ⇒ boolean
65 | * [.vertices()](#DecisionGraph+vertices) ⇒ Array.<string>
66 |
67 |
68 | ### new DecisionGraph()
69 | creates a new DecisionGraph
70 |
71 |
72 | ### decisionGraph.addVertexAND(name)
73 | add AND vertex to the graph. When moving through the decision graph, an AND vertex
74 | will require a visit down each of its outgoing edges, in the order the edges were
75 | added.
76 |
77 | **Kind**: instance method of [DecisionGraph](#DecisionGraph)
78 | **See**: [addVertexOR](#DecisionGraph+addVertexOR)
79 |
80 | | Param | Type | Description |
81 | | --- | --- | --- |
82 | | name | string | the name of this vertex |
83 |
84 |
85 | ### decisionGraph.addVertexOR(name)
86 | add OR vertex to the graph. When moving through the decision graph, an OR vertex
87 | chooses just one of its outgoing vertices.
88 |
89 | **Kind**: instance method of [DecisionGraph](#DecisionGraph)
90 | **See**: [addVertexAND](#DecisionGraph+addVertexAND)
91 |
92 | | Param | Type | Description |
93 | | --- | --- | --- |
94 | | name | string | the name of this vertex |
95 |
96 |
97 | ### decisionGraph.addEdge(v, w)
98 | add edge v->w to the graph
99 |
100 | **Kind**: instance method of [DecisionGraph](#DecisionGraph)
101 |
102 | | Param | Type | Description |
103 | | --- | --- | --- |
104 | | v | string | the name of a vertex this edge points from |
105 | | w | string | Array.<string> | the name of a vertex this edge points to or an array of vertex names. If vertex v is type AND, the order of w will be the exact order required. |
106 |
107 |
108 | ### decisionGraph.adj(v) ⇒ Array.<string>
109 | get an array of all the vertices this vertex points to
110 |
111 | **Kind**: instance method of [DecisionGraph](#DecisionGraph)
112 | **Returns**: Array.<string> - an ordered list of all the vertices that v points to
113 |
114 | | Param | Type | Description |
115 | | --- | --- | --- |
116 | | v | string | the name of a vertex |
117 |
118 |
119 | ### decisionGraph.V() ⇒ number
120 | get the number of vertices in this graph
121 |
122 | **Kind**: instance method of [DecisionGraph](#DecisionGraph)
123 | **Returns**: number - the number of vertices in this graph
124 |
125 | ### decisionGraph.isTerminal(v) ⇒ boolean
126 | is this a terminal vertex (does it have no outgoing edges?)
127 |
128 | **Kind**: instance method of [DecisionGraph](#DecisionGraph)
129 | **Returns**: boolean - is this a terminal vertex
130 |
131 | | Param | Type | Description |
132 | | --- | --- | --- |
133 | | v | string | the name of a vertex |
134 |
135 |
136 | ### decisionGraph.isVertex(v) ⇒ boolean
137 | is this the name of a vertex in the graph?
138 |
139 | **Kind**: instance method of [DecisionGraph](#DecisionGraph)
140 | **Returns**: boolean - is this a vertex in the graph
141 |
142 | | Param | Type | Description |
143 | | --- | --- | --- |
144 | | v | string | the name of a vertex |
145 |
146 |
147 | ### decisionGraph.isTypeAND(v) ⇒ boolean
148 | is this a type AND vertex (and not a type OR)?
149 |
150 | **Kind**: instance method of [DecisionGraph](#DecisionGraph)
151 | **Returns**: boolean - is this a type AND vertex (and not a type OR)?
152 |
153 | | Param | Type | Description |
154 | | --- | --- | --- |
155 | | v | string | the name of a vertex |
156 |
157 |
158 | ### decisionGraph.vertices() ⇒ Array.<string>
159 | get an array of vertex names
160 |
161 | **Kind**: instance method of [DecisionGraph](#DecisionGraph)
162 | **Returns**: Array.<string> - the vertex names in this graph
163 |
164 | ## GrammarGraph
165 | **Kind**: global class
166 |
167 | * [GrammarGraph](#GrammarGraph)
168 | * [new GrammarGraph(grammar, [seperator], [epsilonSymbol])](#new_GrammarGraph_new)
169 | * [.vertices()](#GrammarGraph+vertices) ⇒ Array.<string>
170 | * [.adj(v)](#GrammarGraph+adj) ⇒ Array.<string>
171 | * [.isTypeAND(v)](#GrammarGraph+isTypeAND) ⇒ boolean
172 | * [.createGuide(start)](#GrammarGraph+createGuide) ⇒ [GuidedDecisionGraph](#GuidedDecisionGraph)
173 | * [.createRecognizer(start)](#GrammarGraph+createRecognizer) ⇒ [Recognizer](#Recognizer)
174 |
175 |
176 | ### new GrammarGraph(grammar, [seperator], [epsilonSymbol])
177 | creates a new GrammarGraph which can generate guides.
178 |
179 |
180 | | Param | Type | Default | Description |
181 | | --- | --- | --- | --- |
182 | | grammar | [Grammar](#Grammar) | | an object representing a grammar. |
183 | | [seperator] | string | RegExp | "/\\s+/" | how tokens will be divided in rules |
184 | | [epsilonSymbol] | string | "''" | Special terminal symbol that indicates this is an end of a construction. Defaults to the empty string. |
185 |
186 |
187 | ### grammarGraph.vertices() ⇒ Array.<string>
188 | get an array of vertex names in the graph
189 |
190 | **Kind**: instance method of [GrammarGraph](#GrammarGraph)
191 | **Returns**: Array.<string> - the vertex names in this graph
192 | **See**: [vertices](#DecisionGraph+vertices)
193 |
194 | ### grammarGraph.adj(v) ⇒ Array.<string>
195 | get an array of all the vertices this vertex points to
196 |
197 | **Kind**: instance method of [GrammarGraph](#GrammarGraph)
198 | **Returns**: Array.<string> - an ordered list of all the vertices that v points to
199 | **See**: [adj](#DecisionGraph+adj)
200 |
201 | | Param | Type | Description |
202 | | --- | --- | --- |
203 | | v | string | the name of a vertex |
204 |
205 |
206 | ### grammarGraph.isTypeAND(v) ⇒ boolean
207 | is this a type AND vertex (and not a type OR)?
208 |
209 | **Kind**: instance method of [GrammarGraph](#GrammarGraph)
210 | **Returns**: boolean - is this a type AND vertex (and not a type OR)?
211 | **See**: [isTypeAND](#DecisionGraph+isTypeAND)
212 |
213 | | Param | Type | Description |
214 | | --- | --- | --- |
215 | | v | string | the name of a vertex |
216 |
217 |
218 | ### grammarGraph.createGuide(start) ⇒ [GuidedDecisionGraph](#GuidedDecisionGraph)
219 | get a new GuidedDecisionGraph using this decision graph
220 |
221 | **Kind**: instance method of [GrammarGraph](#GrammarGraph)
222 | **Returns**: [GuidedDecisionGraph](#GuidedDecisionGraph) - a new guide from the provided start point
223 | **See**: [GuidedDecisionGraph](#GuidedDecisionGraph) for the methods available on the Guide
224 |
225 | | Param | Type | Description |
226 | | --- | --- | --- |
227 | | start | string | the name of a vertex in the decision graph from which to start the guided expansion |
228 |
229 |
230 | ### grammarGraph.createRecognizer(start) ⇒ [Recognizer](#Recognizer)
231 | Returns a new Recognizer from the given start vertex
232 |
233 | **Kind**: instance method of [GrammarGraph](#GrammarGraph)
234 | **Returns**: [Recognizer](#Recognizer) - a new Recognizer
235 |
236 | | Param | Type | Description |
237 | | --- | --- | --- |
238 | | start | string | the name of a vertex in the decision graph from which to start the recognizer test |
239 |
240 |
241 | ## GuidedDecisionGraph
242 | **Kind**: global class
243 |
244 | * [GuidedDecisionGraph](#GuidedDecisionGraph)
245 | * [new GuidedDecisionGraph(dg, start)](#new_GuidedDecisionGraph_new)
246 | * [.construction()](#GuidedDecisionGraph+construction) ⇒ Array.<string>
247 | * [.isComplete()](#GuidedDecisionGraph+isComplete) ⇒ boolean
248 | * [.choose(terminal)](#GuidedDecisionGraph+choose)
249 | * [.constructs()](#GuidedDecisionGraph+constructs) ⇒ Array.<string>
250 | * [.pop()](#GuidedDecisionGraph+pop) ⇒ string
251 | * [.choices([nDeep])](#GuidedDecisionGraph+choices) ⇒ Array.<string> | [Array.<TreeNode>](#TreeNode)
252 |
253 |
254 | ### new GuidedDecisionGraph(dg, start)
255 | step-by-step construction of a language from a decision graph
256 |
257 |
258 | | Param | Type | Description |
259 | | --- | --- | --- |
260 | | dg | [DecisionGraph](#DecisionGraph) | a Decision Graph that defines a grammar |
261 | | start | string | the name of a vertex in the decision graph from which to start the guided expansion |
262 |
263 |
264 | ### guidedDecisionGraph.construction() ⇒ Array.<string>
265 | the current construction
266 |
267 | **Kind**: instance method of [GuidedDecisionGraph](#GuidedDecisionGraph)
268 | **Returns**: Array.<string> - a terminal symbol chain
269 |
270 | ### guidedDecisionGraph.isComplete() ⇒ boolean
271 | is the current construction a valid, complete construction from the starting
272 | nonterminal? ie, could the construction be haulted at this point? Depending
273 | on the grammar, this may be true even if there are more choices at this point.
274 |
275 | **Kind**: instance method of [GuidedDecisionGraph](#GuidedDecisionGraph)
276 | **Returns**: boolean - is the construction complete
277 |
278 | ### guidedDecisionGraph.choose(terminal)
279 | adds the given terminal to the construction
280 |
281 | **Kind**: instance method of [GuidedDecisionGraph](#GuidedDecisionGraph)
282 |
283 | | Param | Type | Description |
284 | | --- | --- | --- |
285 | | terminal | string | Array.<string> | the name of a terminal vertex in the Decision Graph which is in the current set of possible choices. Or a valid sequence of terminal symbols as an array. |
286 |
287 |
288 | ### guidedDecisionGraph.constructs() ⇒ Array.<string>
289 | get a sorted array of possible construction strings from the current state,
290 | possibly including nonterminals after the next terminal
291 |
292 | **Kind**: instance method of [GuidedDecisionGraph](#GuidedDecisionGraph)
293 | **Returns**: Array.<string> - a list of possible constructions
294 | **Example**
295 | ```js
296 | // guide is an in-progress GuidedDecisionGraph
297 | guide.construction() => ['the', 'dog', 'ate']
298 | guide.choices() => ['', 'the']
299 | guide.constructs()
300 | => [ 'the dog ate',
301 | 'the dog ate the Noun'
302 | 'the dog ate the Noun RelativeClause' ]
303 | ```
304 |
305 | ### guidedDecisionGraph.pop() ⇒ string
306 | pop the last choice off the construction
307 |
308 | **Kind**: instance method of [GuidedDecisionGraph](#GuidedDecisionGraph)
309 | **Returns**: string - the last element of the construction that was
310 | submitted through [choose](#GuidedDecisionGraph+choose)
311 | **Throws**:
312 |
313 | - throws an error if called when construction is empty
314 |
315 |
316 | ### guidedDecisionGraph.choices([nDeep]) ⇒ Array.<string> | [Array.<TreeNode>](#TreeNode)
317 | returns all possible next terminals, or an array of nDeep [TreeNodes](#TreeNode)
318 |
319 | **Kind**: instance method of [GuidedDecisionGraph](#GuidedDecisionGraph)
320 | **Returns**: Array.<string> | [Array.<TreeNode>](#TreeNode) - if nDeep=1, an array of terminal symbols (strings),
321 | else an array of [TreeNodes](#TreeNode)
322 |
323 | | Param | Type | Default | Description |
324 | | --- | --- | --- | --- |
325 | | [nDeep] | number | 1 | will search for nDeep possible choices |
326 |
327 | **Example**
328 | ```js
329 | // guide is an in-progress GuidedDecisionGraph
330 | guide.construction() => ['the', 'dog', 'ate']
331 | guide.choices() => ['', 'the']
332 | guide.choices(3) =>
333 | [ { val: '',
334 | next: [] },
335 | { val: 'the',
336 | next: [ { val: 'squirrel',
337 | next: [ { val: 'that', next: [] },
338 | { val: '', next: [] } ]
339 | },
340 | { val: 'bird',
341 | next: [ { val: 'that', next: [] },
342 | { val: '', next: [] } ]
343 | },
344 | { val: 'cat',
345 | next: [ { val: 'that', next: [] },
346 | { val: '', next: [] } ]
347 | },
348 | { val: 'dog',
349 | next: [ { val: 'that', next: [] },
350 | { val: '', next: [] } ]
351 | }
352 | ]
353 | }
354 | ]
355 | ```
356 |
357 | ## Recognizer
358 | **Kind**: global class
359 |
360 | * [Recognizer](#Recognizer)
361 | * [new Recognizer(dg, start, [seperator])](#new_Recognizer_new)
362 | * [.isValid(text)](#Recognizer+isValid) ⇒ boolean
363 | * [.isComplete(text)](#Recognizer+isComplete) ⇒ boolean
364 |
365 |
366 | ### new Recognizer(dg, start, [seperator])
367 | create a Recognizer that can test if text is a valid sentence in a grammar
368 |
369 |
370 | | Param | Type | Default | Description |
371 | | --- | --- | --- | --- |
372 | | dg | [DecisionGraph](#DecisionGraph) | | a Decision Graph that defines a grammar |
373 | | start | string | | the name of a vertex in the decision graph from which to start the test |
374 | | [seperator] | string | RegExp | "/\\s+/" | how tokens will be divided in given text |
375 |
376 |
377 | ### recognizer.isValid(text) ⇒ boolean
378 | is the text a valid in progress sentence in the grammar? Will return true
379 | even if the text is not complete.
380 |
381 | **Kind**: instance method of [Recognizer](#Recognizer)
382 | **Returns**: boolean - is the text valid?
383 |
384 | | Param | Type | Description |
385 | | --- | --- | --- |
386 | | text | string | the text to check |
387 |
388 |
389 | ### recognizer.isComplete(text) ⇒ boolean
390 | is the text a valid and complete text in the grammar? Will return true
391 | only if the text is complete.
392 |
393 | **Kind**: instance method of [Recognizer](#Recognizer)
394 | **Returns**: boolean - is the text valid and complete?
395 |
396 | | Param | Type | Description |
397 | | --- | --- | --- |
398 | | text | string | the text to check |
399 |
400 |
401 | ## noSelfDefinitions(grammar, returns)
402 | check a grammar for direct self-loops
403 |
404 | **Kind**: global function
405 | **Throws**:
406 |
407 | - an error if one definition of a nonterminal is exactly the nonterminal itself
408 |
409 |
410 | | Param | Type | Description |
411 | | --- | --- | --- |
412 | | grammar | object | the grammar to check |
413 | | returns | true | true if no errors |
414 |
415 |
416 | ## parseGrammar(grammar, [seperator]) ⇒ [DecisionGraph](#DecisionGraph)
417 | parse a grammar given as an object and compile it into a decision graph
418 |
419 | **Kind**: global function
420 | **Returns**: [DecisionGraph](#DecisionGraph) - the grammar converted into a decision graph
421 |
422 | | Param | Type | Default | Description |
423 | | --- | --- | --- | --- |
424 | | grammar | object | | an object representing a grammar |
425 | | [seperator] | string | RegExp | "/\\s+/" | how tokens will be divided in rules |
426 |
427 |
428 | ## reduceGrammar(grammar, [seperator]) ⇒ object
429 | reduces the rules of a grammar into a one to one form by assigning a name
430 | to all non-terminals. The end result is that each option on a rule with
431 | more than one choice will either be a single AND-rule or a single terminal.
432 |
433 | **Kind**: global function
434 | **Returns**: object - the modified grammar object with newly created rules
435 | as needed. New rules will be given the name of their parent rule
436 | surrounded by underscores and followed by a number.
437 |
438 | | Param | Type | Default | Description |
439 | | --- | --- | --- | --- |
440 | | grammar | object | | an object representing a grammar |
441 | | [seperator] | string | RegExp | "/\\s+/" | how tokens will be divided in rules |
442 |
443 | **Example**
444 | ```js
445 | var grammar = {
446 | NounPhrase: ['the Noun', 'the Noun RelativeClause'],
447 | RelativeClause: ['that VerbPhrase'],
448 | Noun: ['dog', 'cat', 'bird']
449 | }
450 |
451 | reduceGrammar(grammar) =>
452 | {
453 | NounPhrase: ['_NounPhrase_1', '_NounPhrase_2'],
454 | _NounPhrase_1: ['the Noun'],
455 | _NounPhrase_2: ['the Noun RelativeClause'],
456 | RelativeClause: ['that VerbPhrase'],
457 | Noun: ['dog', 'cat', 'bird']
458 | }
459 | ```
460 |
461 | ## clone(obj) ⇒ object | array
462 | helper function to clone a simple object/array made up of primitives.
463 | Will not work if the object or array contains non-primitives.
464 |
465 | **Kind**: global function
466 | **Returns**: object | array - a new clone of the provided object or array
467 |
468 | | Param | Type | Description |
469 | | --- | --- | --- |
470 | | obj | object | array | an object array made up only of primitives |
471 |
472 |
473 | ## SymbolChain : string
474 | a string of one or more symbol names seperated by whitespace or
475 | another user defined seperator (see: seperator param for [GrammarGraph](#GrammarGraph))
476 |
477 | **Kind**: global typedef
478 | **See**: a SymbolChain is used as definitions in [Grammar](#Grammar)
479 | **Example**
480 | ```js
481 | 'dog' // just a single symbol, the word 'dog'
482 | 'the Noun RelativeClause' // three symbols
483 | ```
484 |
485 | ## Grammar : Object
486 | a user defined context-free grammar formatted as an object consisting of key-value pairs,
487 | with each [non-terminal symbol](https://github.com/jrleszcz/grammar-graph#non-terminal-symbols)
488 | pointing to an array of one or more [symbol chains](https://github.com/jrleszcz/grammar-graph#symbol-chains)
489 | choices for this non-terminal.
490 |
491 | **Kind**: global typedef
492 | **Properties**
493 |
494 | | Name | Type | Description |
495 | | --- | --- | --- |
496 | | symbol | [Array.<SymbolChain>](#SymbolChain) | each element of the array is a possible definition for this symbol. |
497 |
498 | **Example**
499 | ```js
500 | var grammar = {
501 | Sentence: ['NounPhrase VerbPhrase'], // only one definition of 'Sentence'
502 | NounPhrase: ['the Noun', 'the Noun RelativeClause'], // two possible definitions of 'NounPhrase'
503 | VerbPhrase: ['Verb', 'Verb NounPhrase'],
504 | RelativeClause: ['that VerbPhrase'],
505 | Noun: ['dog', 'cat', 'bird', 'squirrel'], // four possible definitions of 'Noun'
506 | Verb: ['befriended', 'loved', 'ate', 'attacked']
507 | }
508 | // non-terminals: Sentence, NounPhrase, VerbPhrase, RelativeClause, Noun, Verb
509 | // terminals: the, that, dog, cat, bird, squirrel, befriended, loved, ate, attacked
510 | ```
511 |
512 | ## TreeNode : object
513 | **Kind**: global typedef
514 | **Properties**
515 |
516 | | Name | Type | Description |
517 | | --- | --- | --- |
518 | | val | string | a terminal string |
519 | | next | [Array.<TreeNode>](#TreeNode) | a list of TreeNodes this node links to |
520 |
521 |
--------------------------------------------------------------------------------
/lib/assert-no-self-loops.js:
--------------------------------------------------------------------------------
1 | /**
2 | * check a grammar for direct self-loops
3 | * @param {object} grammar - the grammar to check
4 | * @param {true} returns true if no errors
5 | * @throws an error if one definition of a nonterminal is exactly the nonterminal itself
6 | */
7 | function noSelfDefinitions (grammar) {
8 | for (var token of Object.keys(grammar)) {
9 | for (var definition of grammar[token]) {
10 | if (definition === token) {
11 | throw new Error('Nonterminal ' + token + ' has a definition defining it as itself: ' + grammar[token])
12 | }
13 | }
14 | }
15 | return true
16 | }
17 |
18 | module.exports = noSelfDefinitions
19 |
--------------------------------------------------------------------------------
/lib/decision-graph.js:
--------------------------------------------------------------------------------
1 | /**
2 | * creates a new DecisionGraph
3 | * @constructor
4 | */
5 | var DecisionGraph = function () {
6 | var V = 0 // the number of vertices
7 | var names = {} // vertex string -> index number
8 | var keys = [] // vertex index number -> string
9 | var isAND = [] // boolean array: true = AND; false = OR
10 | var adj = [] // edge arrays
11 |
12 | /**
13 | * add AND vertex to the graph. When moving through the decision graph, an AND vertex
14 | * will require a visit down each of its outgoing edges, in the order the edges were
15 | * added.
16 | * @param {string} name - the name of this vertex
17 | *
18 | * @see {@link DecisionGraph#addVertexOR}
19 | */
20 | this.addVertexAND = function (name) {
21 | if (names[name] !== undefined) return
22 | names[name] = V
23 | keys[V] = name
24 | isAND[V] = true
25 | adj[V] = []
26 | V++
27 | }
28 |
29 | /**
30 | * add OR vertex to the graph. When moving through the decision graph, an OR vertex
31 | * chooses just one of its outgoing vertices.
32 | * @param {string} name - the name of this vertex
33 | *
34 | * @see {@link DecisionGraph#addVertexAND}
35 | */
36 | this.addVertexOR = function (name) {
37 | if (names[name] !== undefined) return
38 | names[name] = V
39 | keys[V] = name
40 | isAND[V] = false
41 | adj[V] = []
42 | V++
43 | }
44 |
45 | /**
46 | * add edge v->w to the graph
47 | * @param {string} v - the name of a vertex this edge points from
48 | * @param {(string|string[])} w - the name of a vertex this edge points to or an
49 | * array of vertex names. If vertex v is type AND, the order of w will be the exact order
50 | * required.
51 | */
52 | this.addEdge = function (v, w) {
53 | if (Array.isArray(w)) {
54 | w.forEach(function (child) {
55 | if (!(child in names)) throw new Error('Not a vertex name:', child)
56 | })
57 | Array.prototype.push.apply(adj[names[v]], w.map(function (vName) {
58 | return names[vName]
59 | }))
60 | } else {
61 | if (!(w in names)) throw new Error('Not a vertex name:', w)
62 | adj[names[v]].push(names[w])
63 | }
64 | }
65 |
66 | /**
67 | * get an array of all the vertices this vertex points to
68 | * @param {string} v - the name of a vertex
69 | * @returns {string[]} an ordered list of all the vertices that v points to
70 | */
71 | this.adj = function (v) {
72 | return adj[names[v]].map(function (vIndex) {
73 | return keys[vIndex]
74 | })
75 | }
76 |
77 | /**
78 | * get the number of vertices in this graph
79 | * @returns {number} the number of vertices in this graph
80 | */
81 | this.V = function () {
82 | return V
83 | }
84 |
85 | /**
86 | * is this a terminal vertex (does it have no outgoing edges?)
87 | * @param {string} v - the name of a vertex
88 | * @returns {boolean} is this a terminal vertex
89 | */
90 | this.isTerminal = function (v) {
91 | return adj[names[v]].length === 0
92 | }
93 |
94 | /**
95 | * is this the name of a vertex in the graph?
96 | * @param {string} v - the name of a vertex
97 | * @returns {boolean} is this a vertex in the graph
98 | */
99 | this.isVertex = function (v) {
100 | return v in names
101 | }
102 |
103 | /**
104 | * is this a type AND vertex (and not a type OR)?
105 | * @param {string} v - the name of a vertex
106 | * @returns {boolean} is this a type AND vertex (and not a type OR)?
107 | */
108 | this.isTypeAND = function (v) {
109 | return isAND[names[v]]
110 | }
111 |
112 | /**
113 | * get an array of vertex names
114 | * @returns {string[]} the vertex names in this graph
115 | */
116 | this.vertices = function () {
117 | return Object.keys(names)
118 | }
119 | }
120 |
121 | module.exports = DecisionGraph
122 |
--------------------------------------------------------------------------------
/lib/grammar-graph.js:
--------------------------------------------------------------------------------
1 | var parseGrammar = require('./parse-grammar')
2 | var GuidedDecisionGraph = require('./guided-decision-graph')
3 | var Recognizer = require('./recognizer.js')
4 |
5 | /**
6 | * @typedef {string} SymbolChain - a string of one or more symbol names seperated by whitespace or
7 | * another user defined seperator (see: seperator param for {@link GrammarGraph})
8 | *
9 | * @see a SymbolChain is used as definitions in {@link Grammar}
10 | *
11 | * @example
12 | * 'dog' // just a single symbol, the word 'dog'
13 | * 'the Noun RelativeClause' // three symbols
14 | */
15 |
16 | /**
17 | * a user defined context-free grammar formatted as an object consisting of key-value pairs,
18 | * with each [non-terminal symbol](https://github.com/jrleszcz/grammar-graph#non-terminal-symbols)
19 | * pointing to an array of one or more [symbol chains](https://github.com/jrleszcz/grammar-graph#symbol-chains)
20 | * choices for this non-terminal.
21 | *
22 | * @typedef {Object} Grammar
23 | * @property {SymbolChain[]} symbol - each element of the array is a possible definition
24 | * for this symbol.
25 | * @example
26 | * var grammar = {
27 | * Sentence: ['NounPhrase VerbPhrase'], // only one definition of 'Sentence'
28 | * NounPhrase: ['the Noun', 'the Noun RelativeClause'], // two possible definitions of 'NounPhrase'
29 | * VerbPhrase: ['Verb', 'Verb NounPhrase'],
30 | * RelativeClause: ['that VerbPhrase'],
31 | * Noun: ['dog', 'cat', 'bird', 'squirrel'], // four possible definitions of 'Noun'
32 | * Verb: ['befriended', 'loved', 'ate', 'attacked']
33 | * }
34 | * // non-terminals: Sentence, NounPhrase, VerbPhrase, RelativeClause, Noun, Verb
35 | * // terminals: the, that, dog, cat, bird, squirrel, befriended, loved, ate, attacked
36 | */
37 |
38 | /**
39 | * creates a new GrammarGraph which can generate guides.
40 | * @constructor
41 | *
42 | * @param {Grammar} grammar - an object representing a grammar.
43 | * @param {string|RegExp} [seperator=/\s+/] - how tokens will be divided in rules
44 | * @param {string} [epsilonSymbol=''] - Special terminal symbol
45 | * that indicates this is an end of a construction. Defaults to the
46 | * empty string.
47 | */
48 | var GrammarGraph = function (grammar, seperator, epsilon) {
49 | var decisionGraph = parseGrammar(grammar, seperator)
50 |
51 | /**
52 | * get an array of vertex names in the graph
53 | * @returns {string[]} the vertex names in this graph
54 | * @see {@link DecisionGraph#vertices}
55 | */
56 | this.vertices = function () {
57 | return decisionGraph.vertices()
58 | }
59 |
60 | /**
61 | * get an array of all the vertices this vertex points to
62 | * @param {string} v - the name of a vertex
63 | * @returns {string[]} an ordered list of all the vertices that v points to
64 | * @see {@link DecisionGraph#adj}
65 | */
66 | this.adj = function (v) {
67 | return decisionGraph.adj(v)
68 | }
69 |
70 | /**
71 | * is this a type AND vertex (and not a type OR)?
72 | * @param {string} v - the name of a vertex
73 | * @returns {boolean} is this a type AND vertex (and not a type OR)?
74 | * @see {@link DecisionGraph#isTypeAND}
75 | */
76 | this.isTypeAND = function (v) {
77 | return decisionGraph.isTypeAND(v)
78 | }
79 |
80 | /**
81 | * get a new GuidedDecisionGraph using this decision graph
82 | * @param {string} start - the name of a vertex in the decision graph from which
83 | * to start the guided expansion
84 | * @returns {GuidedDecisionGraph} a new guide from the provided start point
85 | * @see {@link GuidedDecisionGraph} for the methods available on the Guide
86 | */
87 | this.createGuide = function (start) {
88 | return new GuidedDecisionGraph(decisionGraph, start)
89 | }
90 |
91 | /**
92 | * Returns a new Recognizer from the given start vertex
93 | * @param {string} start - the name of a vertex in the decision graph
94 | * from which to start the recognizer test
95 | * @returns {Recognizer} a new Recognizer
96 | */
97 | this.createRecognizer = function (start) {
98 | return new Recognizer(decisionGraph, start)
99 | }
100 | }
101 |
102 | module.exports = GrammarGraph
103 |
--------------------------------------------------------------------------------
/lib/guided-decision-graph.js:
--------------------------------------------------------------------------------
1 | var clone = require('./utils/clone.js')
2 |
3 | /**
4 | * step-by-step construction of a language from a decision graph
5 | * @constructor
6 | *
7 | * @param {DecisionGraph} dg - a Decision Graph that defines a grammar
8 | * @param {string} start - the name of a vertex in the decision graph from which
9 | * to start the guided expansion
10 | */
11 | var GuidedDecisionGraph = function (dg, start) {
12 | if (!(dg.isVertex(start))) throw new Error('given start is not a vertex in dg:', start)
13 |
14 | var construction = [] // chosen terminals, i.e. the current construction
15 | var expanded // dictionary of possible next terminals, + their stateStacks
16 | var acceptState = false // could the current construction stand complete as-is?
17 | var history = [] // stack of previous expanded objects (see previous line)
18 | var acceptStateHistory = [] // stack of booleans remembering old acceptStates
19 | var guide = this // scope to be used in recursive functions: choose & choices
20 |
21 | // expands all stateStacks in unexpanded until they begin in a terminal symbol
22 | // and adds them to expanded
23 | function expand (unexpanded) {
24 | acceptState = false // reset for this expansion
25 | var expandedChoices = {} // dictionary of possible next terminals, + their stateStacks
26 | while (unexpanded.length !== 0) {
27 | var stateStack = unexpanded.pop()
28 | if (stateStack.length === 0) { // if true, the current construction could be complete
29 | acceptState = true
30 | } else {
31 | var node = stateStack.pop()
32 |
33 | if (dg.isTerminal(node)) { // terminal, add to expandedChoices dictionary
34 | if (!(node in expandedChoices)) expandedChoices[node] = []
35 | expandedChoices[node].push(stateStack)
36 | } else if (dg.isTypeAND(node)) { // type AND, expand and put back on unexpanded
37 | var children = dg.adj(node)
38 | // add to stateStack in reverse order
39 | for (var i = children.length - 1; i >= 0; i--) {
40 | stateStack.push(children[i])
41 | }
42 | unexpanded.push(stateStack)
43 | } else { // type OR, add all possible stacks to unexpanded
44 | var orChildren = dg.adj(node)
45 | orChildren.forEach(function (child) {
46 | var stackCopy = clone(stateStack)
47 | stackCopy.push(child)
48 | unexpanded.push(stackCopy)
49 | })
50 | }
51 | }
52 | }
53 | return expandedChoices
54 | }
55 |
56 | // initialize the guided decision graph with an array of possible state stacks,
57 | // in this case, just a single state stack consisting of the start symbol
58 | expanded = expand([[start]])
59 |
60 | /**
61 | * the current construction
62 | * @returns {string[]} a terminal symbol chain
63 | */
64 | this.construction = function () {
65 | return clone(construction)
66 | }
67 |
68 | /**
69 | * is the current construction a valid, complete construction from the starting
70 | * nonterminal? ie, could the construction be haulted at this point? Depending
71 | * on the grammar, this may be true even if there are more choices at this point.
72 | * @returns {boolean} is the construction complete
73 | */
74 | this.isComplete = function () {
75 | return acceptState
76 | }
77 |
78 | /**
79 | * adds the given terminal to the construction
80 | * @param {string|string[]} terminal - the name of a terminal vertex in the
81 | * Decision Graph which is in the current set of possible choices. Or a valid
82 | * sequence of terminal symbols as an array.
83 | */
84 | this.choose = function (terminal) {
85 | if (Array.isArray(terminal)) {
86 | terminal.forEach(function (t) {
87 | guide.choose(t)
88 | })
89 | } else {
90 | if (!(terminal in expanded)) throw new Error('Not a valid next terminal:', terminal)
91 |
92 | construction.push(terminal) // add terminal to construction
93 | history.push(clone(expanded)) // add expanded dictionary to history
94 | acceptStateHistory.push(acceptState) // add acceptState to history
95 | expanded = expand(expanded[terminal]) // expand the stateStacks of the choice
96 | }
97 | }
98 |
99 | /**
100 | * get a sorted array of possible construction strings from the current state,
101 | * possibly including nonterminals after the next terminal
102 | * @returns {string[]} a list of possible constructions
103 | * @example
104 | * // guide is an in-progress GuidedDecisionGraph
105 | * guide.construction() => ['the', 'dog', 'ate']
106 | * guide.choices() => ['', 'the']
107 | * guide.constructs()
108 | * => [ 'the dog ate',
109 | * 'the dog ate the Noun'
110 | * 'the dog ate the Noun RelativeClause' ]
111 | */
112 | this.constructs = function () {
113 | var states = []
114 | for (var terminal in expanded) {
115 | expanded[terminal].forEach(function (stack) {
116 | var choice = clone(stack)
117 | choice.push(terminal)
118 | choice.reverse()
119 | var cur = construction.length > 0 ? construction.join(' ')
120 | : ''
121 | var next = choice.join(' ')
122 | var sentence = (cur && next) ? cur + ' ' + next
123 | : cur + next
124 | states.push(sentence)
125 | })
126 | }
127 | if (guide.isComplete()) { // also add the current construction as a seperate choice
128 | states.push(guide.construction().join(' '))
129 | }
130 | return states.sort()
131 | }
132 |
133 | /**
134 | * pop the last choice off the construction
135 | * @throws throws an error if called when construction is empty
136 | * @returns {string} the last element of the construction that was
137 | * submitted through {@link GuidedDecisionGraph#choose}
138 | */
139 | this.pop = function () {
140 | if (construction.length === 0) throw new Error('Cannot pop empty construction.')
141 | expanded = history.pop()
142 | acceptState = acceptStateHistory.pop()
143 | return construction.pop()
144 | }
145 |
146 | /**
147 | * @typedef {object} TreeNode
148 | * @property {string} val - a terminal string
149 | * @property {TreeNode[]} next - a list of TreeNodes this node links to
150 | */
151 |
152 | /**
153 | * returns all possible next terminals, or an array of nDeep [TreeNodes]{@link TreeNode}
154 | *
155 | * @param {number} [nDeep=1] - will search for nDeep possible choices
156 | * @returns {string[]|TreeNode[]} if nDeep=1, an array of terminal symbols (strings),
157 | * else an array of [TreeNodes]{@link TreeNode}
158 | * @example
159 | * // guide is an in-progress GuidedDecisionGraph
160 | * guide.construction() => ['the', 'dog', 'ate']
161 | * guide.choices() => ['', 'the']
162 | * guide.choices(3) =>
163 | * [ { val: '',
164 | * next: [] },
165 | * { val: 'the',
166 | * next: [ { val: 'squirrel',
167 | * next: [ { val: 'that', next: [] },
168 | * { val: '', next: [] } ]
169 | * },
170 | * { val: 'bird',
171 | * next: [ { val: 'that', next: [] },
172 | * { val: '', next: [] } ]
173 | * },
174 | * { val: 'cat',
175 | * next: [ { val: 'that', next: [] },
176 | * { val: '', next: [] } ]
177 | * },
178 | * { val: 'dog',
179 | * next: [ { val: 'that', next: [] },
180 | * { val: '', next: [] } ]
181 | * }
182 | * ]
183 | * }
184 | * ]
185 | */
186 | this.choices = function (nDeep) {
187 | if (nDeep === undefined || nDeep === 1) {
188 | return Object.keys(expanded)
189 | }
190 | // if nDeep > 1, map to TreeNodes and recursively expand to nDeep choices
191 | return Object.keys(expanded).map(function (choice) {
192 | var node = new TreeNode(choice)
193 | guide.choose(choice)
194 | node.next = (nDeep - 1 > 1) ? guide.choices(nDeep - 1)
195 | : guide.choices(nDeep - 1).map(TreeNode)
196 | guide.pop()
197 | return node
198 | })
199 | }
200 | }
201 |
202 | /*
203 | * Tree nodes to return decision trees
204 | *
205 | * @param {string} val - a terminal string
206 | * @property {string} val - a terminal string
207 | * @property {TreeNode[]} next - a list of TreeNodes this node links to
208 | * @see TreeNodes are returned from [GuidedDecisionGraph.choices]{@link GuidedDecisionGraph#choices}
209 | */
210 | var TreeNode = function (val) {
211 | if (!(this instanceof TreeNode)) return new TreeNode(val)
212 | this.val = val
213 | this.next = []
214 | }
215 |
216 | module.exports = GuidedDecisionGraph
217 |
--------------------------------------------------------------------------------
/lib/parse-grammar.js:
--------------------------------------------------------------------------------
1 | var reduceGrammar = require('./reduce-grammar.js')
2 | var DecisionGraph = require('./decision-graph.js')
3 | var assertNoSelfLoops = require('./assert-no-self-loops.js')
4 |
5 | /**
6 | * parse a grammar given as an object and compile it into a decision graph
7 | *
8 | * @param {object} grammar - an object representing a grammar
9 | * @param {string|RegExp} [seperator=/\s+/] - how tokens will be divided in rules
10 | * @returns {DecisionGraph} the grammar converted into a decision graph
11 | *
12 | */
13 | var parseGrammar = function (grammar, seperator) {
14 | seperator = (seperator === undefined) ? /\s+/
15 | : seperator
16 | // make sure the rules are in a one-to-one relationship
17 | grammar = reduceGrammar(grammar, seperator)
18 | assertNoSelfLoops(grammar)
19 |
20 | var dg = new DecisionGraph()
21 |
22 | // add all nontermintal vertices
23 | var rules = Object.keys(grammar)
24 | rules.forEach(function (rule) {
25 | if (grammar[rule].length > 1) {
26 | dg.addVertexOR(rule)
27 | } else {
28 | dg.addVertexAND(rule)
29 | }
30 | })
31 | // add all edges as well as terminal vertices as they are encountered
32 | rules.forEach(function (rule) {
33 | grammar[rule].forEach(function (definition) {
34 | var tokens = definition.split(seperator)
35 | tokens.forEach(function (token) {
36 | if (!(token in grammar)) {
37 | dg.addVertexAND(token)
38 | }
39 | })
40 | dg.addEdge(rule, tokens)
41 | })
42 | })
43 | return dg
44 | }
45 |
46 | module.exports = parseGrammar
47 |
--------------------------------------------------------------------------------
/lib/recognizer.js:
--------------------------------------------------------------------------------
1 | var GuidedDecisionGraph = require('./guided-decision-graph.js')
2 |
3 | /**
4 | * create a Recognizer that can test if text is a valid sentence in a grammar
5 | * @constructor
6 | *
7 | * @param {DecisionGraph} dg - a Decision Graph that defines a grammar
8 | * @param {string} start - the name of a vertex in the decision graph from which
9 | * to start the test
10 | * @param {string|RegExp} [seperator=/\s+/] - how tokens will be divided in
11 | * given text
12 | */
13 | var Recognizer = function (dg, start, seperator) {
14 | seperator = seperator || /\s+/
15 |
16 | /*
17 | * tries to add all tokens and return true if successful, else false
18 | *
19 | * @param {GuidedDecisionGraph} gdg - the guide to add tokens to
20 | * @param {string[]} tokens - an array of strings to be tested as
21 | * a terminal chain in the grammar
22 | *
23 | * @returns {boolean} were all tokens succesfully added to the gdg?
24 | */
25 | var addedTokens = function (gdg, tokens) {
26 | for (var i = 0; i < tokens.length; i++) {
27 | if (gdg.choices().indexOf(tokens[i]) === -1) {
28 | return false // next token cannot be added to guide
29 | }
30 | gdg.choose(tokens[i])
31 | }
32 | return true // all tokens were added to guide
33 | }
34 |
35 | /**
36 | * is the text a valid in progress sentence in the grammar? Will return true
37 | * even if the text is not complete.
38 | * @param {string} text - the text to check
39 | * @returns {boolean} is the text valid?
40 | */
41 | this.isValid = function (text) {
42 | // by definition, an empty string is considered a valid start
43 | if (text.length === 0) {
44 | return true
45 | }
46 | var gdg = new GuidedDecisionGraph(dg, start)
47 | return addedTokens(gdg, text.split(seperator))
48 | }
49 |
50 | /**
51 | * is the text a valid and complete text in the grammar? Will return true
52 | * only if the text is complete.
53 | * @param {string} text - the text to check
54 | * @returns {boolean} is the text valid and complete?
55 | */
56 | this.isComplete = function (text) {
57 | var gdg = new GuidedDecisionGraph(dg, start)
58 | if (!addedTokens(gdg, text.split(seperator))) {
59 | return false // guide is not valid, so no need to check completeness
60 | }
61 | return gdg.isComplete() // is this a complete construction?
62 | }
63 | }
64 |
65 | module.exports = Recognizer
66 |
--------------------------------------------------------------------------------
/lib/reduce-grammar.js:
--------------------------------------------------------------------------------
1 | var clone = require('./utils/clone.js')
2 |
3 | /**
4 | * reduces the rules of a grammar into a one to one form by assigning a name
5 | * to all non-terminals. The end result is that each option on a rule with
6 | * more than one choice will either be a single AND-rule or a single terminal.
7 | *
8 | * @param {object} grammar - an object representing a grammar
9 | * @param {string|RegExp} [seperator=/\s+/] - how tokens will be divided in rules
10 | * @returns {object} the modified grammar object with newly created rules
11 | * as needed. New rules will be given the name of their parent rule
12 | * surrounded by underscores and followed by a number.
13 | *
14 | * @example
15 | * var grammar = {
16 | * NounPhrase: ['the Noun', 'the Noun RelativeClause'],
17 | * RelativeClause: ['that VerbPhrase'],
18 | * Noun: ['dog', 'cat', 'bird']
19 | * }
20 | *
21 | * reduceGrammar(grammar) =>
22 | * {
23 | * NounPhrase: ['_NounPhrase_1', '_NounPhrase_2'],
24 | * _NounPhrase_1: ['the Noun'],
25 | * _NounPhrase_2: ['the Noun RelativeClause'],
26 | * RelativeClause: ['that VerbPhrase'],
27 | * Noun: ['dog', 'cat', 'bird']
28 | * }
29 | *
30 | */
31 | var reduceGrammar = function (grammar, seperator) {
32 | grammar = clone(grammar) // clone to avoid mutating the argument
33 | seperator = (seperator === undefined) ? /\s+/
34 | : seperator
35 | var rules = Object.keys(grammar)
36 | rules.forEach(function (rule) {
37 | if (grammar[rule].length > 1) {
38 | var id = 1
39 | grammar[rule].forEach(function (definition, i) {
40 | var tokens = definition.split(seperator)
41 | // create a new rule if length greater than 1
42 | if (tokens.length > 1) {
43 | var ruleName = '_' + rule + '_' + id
44 | grammar[ruleName] = [definition]
45 | grammar[rule][i] = ruleName
46 | id++
47 | }
48 | })
49 | }
50 | })
51 | return grammar
52 | }
53 |
54 | module.exports = reduceGrammar
55 |
--------------------------------------------------------------------------------
/lib/utils/clone.js:
--------------------------------------------------------------------------------
1 | /**
2 | * helper function to clone a simple object/array made up of primitives.
3 | * Will not work if the object or array contains non-primitives.
4 | * @param {object|array} obj - an object array made up only of primitives
5 | * @returns {object|array} a new clone of the provided object or array
6 | */
7 | function clone (obj) {
8 | return JSON.parse(JSON.stringify(obj))
9 | }
10 |
11 | module.exports = clone
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grammar-graph",
3 | "version": "3.1.1",
4 | "description": "Takes a context-free grammar and converts it into a decision-making graph. Can produce interactive Guides which generate valid sentences from the grammar.",
5 | "main": "./lib/grammar-graph.js",
6 | "directories": {
7 | "lib": "./lib",
8 | "test": "./test"
9 | },
10 | "scripts": {
11 | "test": "eslint lib test && tape \"test/**/*.test.js\" | faucet",
12 | "docs": "jsdoc2md lib/grammar-graph.js lib/guided-decision-graph.js lib/recognizer.js > api.md && jsdoc2md \"lib/**/*.js\" > internal-api.md",
13 | "preversion": "npm test",
14 | "postversion": "git push"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/jrleszcz/grammar-graph.git"
19 | },
20 | "keywords": [
21 | "graph",
22 | "state machine",
23 | "automaton",
24 | "non-deterministic finite state machine",
25 | "grammar graph",
26 | "generator",
27 | "language generator",
28 | "digraph",
29 | "directed graph",
30 | "decision-making graph",
31 | "decision graph",
32 | "decision tree",
33 | "grammar",
34 | "context-free grammar",
35 | "nondeterministic",
36 | "nfa",
37 | "formal language"
38 | ],
39 | "author": {
40 | "name": "John Leszczynski",
41 | "email": "jrleszczynski@gmail.com",
42 | "url": "http://johnleszczynski.com"
43 | },
44 | "license": "MIT",
45 | "bugs": {
46 | "url": "https://github.com/jrleszcz/grammar-graph/issues"
47 | },
48 | "homepage": "https://github.com/jrleszcz/grammar-graph#readme",
49 | "devDependencies": {
50 | "faucet": "0.0.1",
51 | "jsdoc-to-markdown": "^1.1.1",
52 | "tape": "^4.2.0",
53 | "eslint": "^3.14.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/test/assert-no-self-loops.test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 | var assertNoSelfLoops = require('../lib/assert-no-self-loops.js')
3 | var reduceGrammar = require('../lib/reduce-grammar.js')
4 |
5 | test('assertNoSelfLoops', function (t) {
6 | var g = {
7 | Sentence : ['NounPhrase VerbPhrase'],
8 | NounPhrase : ['the Noun', 'the Noun RelativeClause'],
9 | VerbPhrase : ['Verb', 'Verb NounPhrase'],
10 | RelativeClause : ['that VerbPhrase'],
11 | Noun : ['dog', 'cat', 'bird', 'squirrel'],
12 | Verb : ['befriended', 'loved', 'ate', 'attacked']
13 | }
14 | var grammar = reduceGrammar(g)
15 |
16 | t.true(assertNoSelfLoops(grammar))
17 |
18 | var g2 = {
19 | grammar: ['something else', 'grammar', 'another thing']
20 | }
21 |
22 | t.throws(function () {
23 | assertNoSelfLoops(g2)
24 | })
25 |
26 | var g3 = {
27 | grammar : ['something else', '2342', 'another thing'],
28 | lottery : ['winner', 'loser', 'lottery winner']
29 | }
30 |
31 | t.true(assertNoSelfLoops(g3))
32 |
33 | g3.paper = ['water', 'scissors', 'paper', 'rock']
34 |
35 | t.throws(function () {
36 | assertNoSelfLoops(g3)
37 | })
38 |
39 | t.end()
40 | })
41 |
--------------------------------------------------------------------------------
/test/decision-graph.test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 | var DecisionGraph = require('../lib/decision-graph.js')
3 |
4 | test('DecisionGraph methods', function (t) {
5 | var dg = new DecisionGraph()
6 |
7 | t.true(dg instanceof DecisionGraph)
8 | t.equal(dg.V(), 0)
9 |
10 | dg.addVertexAND('Sentence')
11 | t.equal(dg.V(), 1)
12 | t.deepEqual(dg.adj('Sentence'), [])
13 | t.true(dg.isTerminal('Sentence'))
14 | t.true(dg.isTypeAND('Sentence'))
15 |
16 | dg.addVertexOR('NounPhrase')
17 | t.equal(dg.V(), 2)
18 | t.deepEqual(dg.adj('NounPhrase'), [])
19 | t.true(dg.isTerminal('Sentence'))
20 | t.false(dg.isTypeAND('NounPhrase'))
21 |
22 | var ANDs = ['_NounPhrase1', '_NounPhrase2', '_VerbPhrase1',
23 | 'RelativeClause', 'the', 'that', 'dog', 'cat', 'bird', 'squirrel',
24 | 'befriended', 'loved', 'ate', 'attacked']
25 | ANDs.forEach(function (andVertex) {
26 | dg.addVertexAND(andVertex)
27 | })
28 | t.equal(dg.V(), 16)
29 |
30 | var ORs = ['VerbPhrase', 'Noun', 'Verb']
31 | ORs.forEach(function (orVertex) {
32 | dg.addVertexAND(orVertex)
33 | })
34 | t.equal(dg.V(), 19)
35 |
36 | dg.addEdge('Sentence', ['NounPhrase', 'VerbPhrase'])
37 | t.deepEqual(dg.adj('Sentence'), ['NounPhrase', 'VerbPhrase'])
38 | t.false(dg.isTerminal('Sentence'))
39 |
40 | dg.addEdge('NounPhrase', '_NounPhrase1')
41 | t.deepEqual(dg.adj('NounPhrase'), ['_NounPhrase1'])
42 | t.false(dg.isTerminal('NounPhrase'))
43 |
44 | dg.addEdge('NounPhrase', '_NounPhrase2')
45 | t.deepEqual(dg.adj('NounPhrase'), ['_NounPhrase1', '_NounPhrase2'],
46 | 'add edges one by one as string')
47 | t.false(dg.isTerminal('NounPhrase'))
48 |
49 | dg.addEdge('VerbPhrase', ['Verb', '_VerbPhrase1'])
50 | t.deepEqual(dg.adj('VerbPhrase'), ['Verb', '_VerbPhrase1'])
51 |
52 | dg.addEdge('_NounPhrase1', ['the', 'Noun'])
53 | dg.addEdge('_NounPhrase2', ['the', 'Noun', 'RelativeClause'])
54 | dg.addEdge('_VerbPhrase1', ['Verb', 'NounPhrase'])
55 | dg.addEdge('RelativeClause', ['that', 'VerbPhrase'])
56 | dg.addEdge('Noun', ['dog', 'cat', 'squirrel', 'bird'])
57 | dg.addEdge('Verb', ['befriended', 'loved', 'ate', 'attacked'])
58 |
59 | t.equal(dg.V(), 19)
60 | t.true(dg.isTerminal('that'))
61 | t.false(dg.isTerminal('Verb'))
62 |
63 | // try to add nodes again and make sure V still == 19
64 | dg.addVertexAND('Sentence')
65 | t.equal(dg.V(), 19)
66 | dg.addVertexOR('VerbPhrase')
67 | t.equal(dg.V(), 19)
68 |
69 | // check that error is thrown if adding edge to vertex which is not present
70 | t.throws(function () {
71 | dg.addEdge('Sentence', 'NotAVertex')
72 | }, Error)
73 | t.throws(function () {
74 | dg.addEdge('Sentence', ['cat', 'bat'])
75 | }, Error)
76 |
77 | t.end()
78 | })
79 |
--------------------------------------------------------------------------------
/test/grammar-graph.test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 | var GrammarGraph = require('../lib/grammar-graph.js')
3 | var GuidedDecisionGraph = require('../lib/guided-decision-graph.js')
4 |
5 | test('GrammarGraph', function (t) {
6 | var g = {
7 | Sentence : ['NounPhrase VerbPhrase'],
8 | NounPhrase : ['the Noun', 'the Noun RelativeClause'],
9 | VerbPhrase : ['Verb', 'Verb NounPhrase'],
10 | RelativeClause : ['that VerbPhrase'],
11 | Noun : ['dog', 'cat', 'bird', 'squirrel'],
12 | Verb : ['befriended', 'loved', 'ate', 'attacked']
13 | }
14 |
15 | var graph = new GrammarGraph(g)
16 | t.deepEqual(graph.vertices(),
17 | [ 'Sentence', 'NounPhrase', 'VerbPhrase', 'RelativeClause', 'Noun',
18 | 'Verb', '_NounPhrase_1', '_NounPhrase_2', '_VerbPhrase_1', 'that',
19 | 'dog', 'cat', 'bird', 'squirrel', 'befriended', 'loved', 'ate',
20 | 'attacked', 'the' ])
21 |
22 | t.true(graph instanceof GrammarGraph)
23 |
24 | var guide = graph.createGuide('NounPhrase')
25 | t.true(guide instanceof GuidedDecisionGraph)
26 |
27 | t.deepEqual(graph.adj('bird'), [])
28 | t.deepEqual(graph.adj('RelativeClause'), ['that', 'VerbPhrase'])
29 |
30 | t.true(graph.isTypeAND('loved'))
31 | t.false(graph.isTypeAND('NounPhrase'))
32 |
33 | var isNoun = graph.createRecognizer('Noun').isComplete
34 | t.true(isNoun('cat'))
35 | t.true(isNoun('bird'))
36 | t.false(isNoun('attacked'))
37 |
38 | var isSentence = graph.createRecognizer('Sentence').isComplete
39 | t.true(isSentence('the dog ate'))
40 | t.false(isSentence('the dog ate the'))
41 | t.true(isSentence('the dog ate the cat'))
42 |
43 | t.end()
44 | })
45 |
--------------------------------------------------------------------------------
/test/guided-decision-graph.test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 | var DecisionGraph = require('../lib/decision-graph.js')
3 | var GuidedDecisionGraph = require('../lib/guided-decision-graph.js')
4 | var GrammarGraph = require('../lib/grammar-graph.js')
5 |
6 | test('GuidedDecisionGraph methods', function (t) {
7 | var dg = new DecisionGraph()
8 |
9 | var ANDs = ['Sentence', '_NounPhrase_1', '_NounPhrase_2', '_VerbPhrase_1',
10 | 'RelativeClause', 'the', 'that', 'dog', 'cat', 'bird', 'squirrel',
11 | 'befriended', 'loved', 'ate', 'attacked']
12 | var ORs = ['NounPhrase', 'VerbPhrase', 'Noun', 'Verb']
13 |
14 | ANDs.forEach(function (andVertex) {
15 | dg.addVertexAND(andVertex)
16 | })
17 |
18 | ORs.forEach(function (orVertex) {
19 | dg.addVertexOR(orVertex)
20 | })
21 |
22 | dg.addEdge('Sentence', ['NounPhrase', 'VerbPhrase'])
23 | dg.addEdge('NounPhrase', ['_NounPhrase_1', '_NounPhrase_2'])
24 | dg.addEdge('VerbPhrase', ['Verb', '_VerbPhrase_1'])
25 | dg.addEdge('_NounPhrase_1', ['the', 'Noun'])
26 | dg.addEdge('_NounPhrase_2', ['the', 'Noun', 'RelativeClause'])
27 | dg.addEdge('_VerbPhrase_1', ['Verb', 'NounPhrase'])
28 | dg.addEdge('RelativeClause', ['that', 'VerbPhrase'])
29 | dg.addEdge('Noun', ['dog', 'cat', 'squirrel', 'bird'])
30 | dg.addEdge('Verb', ['befriended', 'loved', 'ate', 'attacked'])
31 |
32 | t.equal(dg.V(), 19)
33 |
34 | t.throws(function () {
35 | var a = new GuidedDecisionGraph(dg)
36 | a // Standard is complaining that a is defined and not used
37 | }, Error)
38 |
39 | var guide = new GuidedDecisionGraph(dg, 'Sentence')
40 | t.deepEqual(guide.construction(), [])
41 | t.deepEqual(guide.choices(), ['the'])
42 | t.false(guide.isComplete())
43 | t.throws(function () {
44 | guide.pop()
45 | }, Error, 'Should not be able to pop empty construction')
46 | t.deepEqual(guide.constructs(),
47 | ['the Noun RelativeClause VerbPhrase',
48 | 'the Noun VerbPhrase'])
49 |
50 | guide.choose('the')
51 | t.deepEqual(guide.construction(), ['the'])
52 | t.deepEqual(guide.choices().sort(), ['dog', 'cat', 'squirrel', 'bird'].sort())
53 | t.false(guide.isComplete())
54 | t.deepEqual(guide.constructs(),
55 | [ 'the bird RelativeClause VerbPhrase',
56 | 'the bird VerbPhrase',
57 | 'the cat RelativeClause VerbPhrase',
58 | 'the cat VerbPhrase',
59 | 'the dog RelativeClause VerbPhrase',
60 | 'the dog VerbPhrase',
61 | 'the squirrel RelativeClause VerbPhrase',
62 | 'the squirrel VerbPhrase' ])
63 |
64 | guide.choose('dog')
65 | t.deepEqual(guide.construction(), ['the', 'dog'])
66 | t.deepEqual(guide.choices().sort(),
67 | ['befriended', 'loved', 'attacked', 'ate', 'that'].sort())
68 | t.deepEqual(guide.constructs(),
69 | [ 'the dog ate',
70 | 'the dog ate NounPhrase',
71 | 'the dog attacked',
72 | 'the dog attacked NounPhrase',
73 | 'the dog befriended',
74 | 'the dog befriended NounPhrase',
75 | 'the dog loved',
76 | 'the dog loved NounPhrase',
77 | 'the dog that VerbPhrase VerbPhrase' ])
78 |
79 | guide.choose('ate')
80 | t.deepEqual(guide.construction(), ['the', 'dog', 'ate'])
81 | t.true(guide.isComplete())
82 | t.deepEqual(guide.choices().sort(),
83 | ['the'].sort())
84 | t.deepEqual(guide.constructs(),
85 | [ 'the dog ate',
86 | 'the dog ate the Noun',
87 | 'the dog ate the Noun RelativeClause' ])
88 |
89 | t.equal(guide.pop(), 'ate')
90 | t.false(guide.isComplete())
91 | t.deepEqual(guide.construction(), ['the', 'dog'])
92 | t.deepEqual(guide.choices().sort(),
93 | ['befriended', 'loved', 'attacked', 'ate', 'that'].sort())
94 |
95 | guide.choose('ate')
96 | t.deepEqual(guide.construction(), ['the', 'dog', 'ate'])
97 | t.true(guide.isComplete())
98 | t.deepEqual(guide.choices().sort(),
99 | ['the'].sort())
100 |
101 | guide.choose('the')
102 | t.false(guide.isComplete())
103 | t.deepEqual(guide.construction(), ['the', 'dog', 'ate', 'the'])
104 | t.deepEqual(guide.choices().sort(),
105 | ['dog', 'cat', 'squirrel', 'bird'].sort())
106 |
107 | ;'cat that ate the bird that attacked the squirrel'.split(' ').forEach(function (t) {
108 | guide.choose(t)
109 | })
110 |
111 | t.equal(guide.construction().join(' '),
112 | 'the dog ate the cat that ate the bird that attacked the squirrel')
113 |
114 | // throws errors when given a terminal which is not a valid next choice
115 | t.throws(function () {
116 | guide.choose('the')
117 | }, Error)
118 | t.throws(function () {
119 | guide.choose(' ')
120 | }, Error)
121 |
122 | t.equal(guide.pop(), 'squirrel')
123 | t.equal(guide.pop(), 'the')
124 | t.deepEqual(guide.choices().sort(),
125 | ['the'].sort())
126 | t.true(guide.isComplete())
127 |
128 | t.deepEqual(guide.constructs().sort(),
129 | ['the dog ate the cat that ate the bird that attacked',
130 | 'the dog ate the cat that ate the bird that attacked the Noun',
131 | 'the dog ate the cat that ate the bird that attacked the Noun RelativeClause'].sort())
132 |
133 | t.end()
134 | })
135 |
136 | test('GuidedDecisionGraph nDeep choices and multiple .choose()', function (t) {
137 | var g = {
138 | Sentence : ['NounPhrase VerbPhrase'],
139 | NounPhrase : ['the Noun', 'the Noun RelativeClause'],
140 | VerbPhrase : ['Verb', 'Verb NounPhrase'],
141 | RelativeClause : ['that VerbPhrase'],
142 | Noun : ['dog', 'cat', 'bird', 'squirrel'],
143 | Verb : ['befriended', 'loved', 'ate', 'attacked']
144 | }
145 | var graph = new GrammarGraph(g)
146 | var guide = graph.createGuide('Sentence')
147 |
148 | t.deepEqual(guide.choices(), ['the'])
149 | t.deepEqual(guide.choices(1), ['the'])
150 | t.deepEqual(guide.choices(2),
151 | [
152 | { val : 'the',
153 | next : [ { val: 'squirrel', next: [] },
154 | { val: 'bird', next: [] },
155 | { val: 'cat', next: [] },
156 | { val: 'dog', next: [] }
157 | ]
158 | }
159 | ])
160 |
161 | guide.choose(['the', 'dog', 'ate'])
162 | t.deepEqual(guide.construction(), ['the', 'dog', 'ate'])
163 |
164 | t.deepEqual(guide.choices(3),
165 | [
166 | { val : 'the',
167 | next : [
168 | { val : 'squirrel',
169 | next : [ { val: 'that', next: [] } ]
170 | },
171 | { val : 'bird',
172 | next : [ { val: 'that', next: [] } ]
173 | },
174 | { val : 'cat',
175 | next : [ { val: 'that', next: [] } ]
176 | },
177 | { val : 'dog',
178 | next : [ { val: 'that', next: [] } ]
179 | }
180 | ]
181 | }
182 | ])
183 |
184 | guide.choose([ 'the', 'squirrel', 'that', 'loved', 'the' ])
185 | t.deepEqual(guide.construction(),
186 | [ 'the', 'dog', 'ate', 'the', 'squirrel', 'that', 'loved', 'the' ])
187 |
188 | t.end()
189 | })
190 |
--------------------------------------------------------------------------------
/test/parse-grammar.test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 | var parseGrammar = require('../lib/parse-grammar.js')
3 | var DecisionGraph = require('../lib/decision-graph.js')
4 |
5 | test('parseGrammar', function (t) {
6 | var g = {
7 | Sentence : ['NounPhrase VerbPhrase'],
8 | NounPhrase : ['the Noun', 'the Noun RelativeClause'],
9 | VerbPhrase : ['Verb', 'Verb NounPhrase'],
10 | RelativeClause : ['that VerbPhrase'],
11 | Noun : ['dog', 'cat', 'bird', 'squirrel'],
12 | Verb : ['befriended', 'loved', 'ate', 'attacked']
13 | }
14 |
15 | var dg = parseGrammar(g)
16 |
17 | t.true(dg instanceof DecisionGraph)
18 | t.true(dg.isVertex('dog'))
19 | t.true(dg.isVertex('VerbPhrase'))
20 | t.true(dg.isTerminal('bird'))
21 | t.true(dg.isVertex('dog'))
22 | t.true(dg.isTypeAND('Sentence'))
23 | t.false(dg.isTypeAND('NounPhrase'))
24 |
25 | t.end()
26 | })
27 |
--------------------------------------------------------------------------------
/test/recognizer.test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 | var Recognizer = require('../lib/recognizer.js')
3 | var DecisionGraph = require('../lib/decision-graph.js')
4 |
5 | test('Recognizer', function (t) {
6 | // set up decision graph
7 | var dg = new DecisionGraph()
8 |
9 | var ANDs = ['Sentence', '_NounPhrase_1', '_NounPhrase_2', '_VerbPhrase_1',
10 | 'RelativeClause', 'the', 'that', 'dog', 'cat', 'bird', 'squirrel',
11 | 'befriended', 'loved', 'ate', 'attacked']
12 | var ORs = ['NounPhrase', 'VerbPhrase', 'Noun', 'Verb']
13 |
14 | ANDs.forEach(function (andVertex) {
15 | dg.addVertexAND(andVertex)
16 | })
17 |
18 | ORs.forEach(function (orVertex) {
19 | dg.addVertexOR(orVertex)
20 | })
21 |
22 | dg.addEdge('Sentence', ['NounPhrase', 'VerbPhrase'])
23 | dg.addEdge('NounPhrase', ['_NounPhrase_1', '_NounPhrase_2'])
24 | dg.addEdge('VerbPhrase', ['Verb', '_VerbPhrase_1'])
25 | dg.addEdge('_NounPhrase_1', ['the', 'Noun'])
26 | dg.addEdge('_NounPhrase_2', ['the', 'Noun', 'RelativeClause'])
27 | dg.addEdge('_VerbPhrase_1', ['Verb', 'NounPhrase'])
28 | dg.addEdge('RelativeClause', ['that', 'VerbPhrase'])
29 | dg.addEdge('Noun', ['dog', 'cat', 'squirrel', 'bird'])
30 | dg.addEdge('Verb', ['befriended', 'loved', 'ate', 'attacked'])
31 |
32 | var recognizer = new Recognizer(dg, 'Sentence')
33 | t.true(recognizer instanceof Recognizer)
34 |
35 | t.true(recognizer.isValid(''))
36 | t.false(recognizer.isComplete(''))
37 |
38 | t.true(recognizer.isValid('the'))
39 | t.false(recognizer.isComplete('the'))
40 |
41 | t.true(recognizer.isValid('the dog'))
42 | t.false(recognizer.isComplete('the dog'))
43 |
44 | t.true(recognizer.isValid('the dog ate'))
45 | t.true(recognizer.isComplete('the dog ate'))
46 |
47 | t.false(recognizer.isValid('the dog ate the cat that is my friend'))
48 | t.false(recognizer.isComplete('the dog ate the cat that is my friend'))
49 |
50 | t.true(recognizer.isValid('the dog ate the cat that attacked the bird'))
51 | t.true(recognizer.isComplete('the dog ate the cat that attacked the bird'))
52 | t.end()
53 | })
54 |
--------------------------------------------------------------------------------
/test/reduce-grammar.test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 | var reduceGrammar = require('../lib/reduce-grammar.js')
3 |
4 | test('reduceGrammar', function (t) {
5 | var g = {
6 | NounPhrase : ['the Noun', 'the Noun RelativeClause'],
7 | RelativeClause : ['that VerbPhrase'],
8 | Noun : ['dog', 'cat', 'bird']
9 | }
10 |
11 | t.deepEqual(g,
12 | {
13 | NounPhrase : ['the Noun', 'the Noun RelativeClause'],
14 | RelativeClause : ['that VerbPhrase'],
15 | Noun : ['dog', 'cat', 'bird']
16 | }, 'does not mutate argument')
17 |
18 | t.notEqual(reduceGrammar(g), g)
19 |
20 | t.deepEqual(reduceGrammar(g), {
21 | NounPhrase : ['_NounPhrase_1', '_NounPhrase_2'],
22 | _NounPhrase_1 : ['the Noun'],
23 | _NounPhrase_2 : ['the Noun RelativeClause'],
24 | RelativeClause : ['that VerbPhrase'],
25 | Noun : ['dog', 'cat', 'bird']
26 | })
27 |
28 | g = {
29 | Sentence : ['NounPhrase VerbPhrase'],
30 | NounPhrase : ['the Noun', 'the Noun RelativeClause'],
31 | VerbPhrase : ['Verb', 'Verb NounPhrase'],
32 | RelativeClause : ['that VerbPhrase'],
33 | Noun : ['dog', 'cat', 'bird', 'squirrel'],
34 | Verb : ['befriended', 'loved', 'ate', 'attacked']
35 | }
36 |
37 | t.deepEqual(reduceGrammar(g), {
38 | Sentence : [ 'NounPhrase VerbPhrase' ],
39 | NounPhrase : [ '_NounPhrase_1', '_NounPhrase_2' ],
40 | VerbPhrase : [ 'Verb', '_VerbPhrase_1' ],
41 | RelativeClause : [ 'that VerbPhrase' ],
42 | Noun : [ 'dog', 'cat', 'bird', 'squirrel' ],
43 | Verb : [ 'befriended', 'loved', 'ate', 'attacked' ],
44 | _NounPhrase_1 : [ 'the Noun' ],
45 | _NounPhrase_2 : [ 'the Noun RelativeClause' ],
46 | _VerbPhrase_1 : [ 'Verb NounPhrase' ]
47 | })
48 |
49 | g = {
50 | NounPhrase : ['the+Noun', 'the+Noun+RelativeClause'],
51 | RelativeClause : ['that+VerbPhrase'],
52 | Noun : ['dog', 'cat', 'bird']
53 | }
54 |
55 | t.deepEqual(reduceGrammar(g, '+'), {
56 | NounPhrase : ['_NounPhrase_1', '_NounPhrase_2'],
57 | _NounPhrase_1 : ['the+Noun'],
58 | _NounPhrase_2 : ['the+Noun+RelativeClause'],
59 | RelativeClause : ['that+VerbPhrase'],
60 | Noun : ['dog', 'cat', 'bird']
61 | })
62 |
63 | t.end()
64 | })
65 |
--------------------------------------------------------------------------------
/test/utils/clone.test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 | var clone = require('../../lib/utils/clone.js')
3 |
4 | test('clone()', function (t) {
5 | var sample = [1, 'a', 3, '4', 22, true, null, 0]
6 | t.deepEqual(clone(sample), sample)
7 | t.notEqual(clone(sample), sample)
8 |
9 | var sample2 = {tree: 'acorn', test: true, state: 42, nothing: null}
10 | t.deepEqual(clone(sample2), sample2)
11 | t.notEqual(clone(sample2), sample2)
12 |
13 | t.end()
14 | })
15 |
--------------------------------------------------------------------------------