| 
									
										
										
										
											2021-02-17 23:22:14 +01:00
										 |  |  | import utils from "./utils.js"; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-21 23:08:53 +02:00
										 |  |  | function lex(str) { | 
					
						
							|  |  |  |     str = str.trim(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |     const tokens = []; | 
					
						
							| 
									
										
										
										
											2020-06-03 16:24:41 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     let quotes = false; | 
					
						
							|  |  |  |     let currentWord = ''; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function isOperatorSymbol(chr) { | 
					
						
							|  |  |  |         return ['=', '*', '>', '<', '!'].includes(chr); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function previousOperatorSymbol() { | 
					
						
							|  |  |  |         if (currentWord.length === 0) { | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         else { | 
					
						
							|  |  |  |             return isOperatorSymbol(currentWord[currentWord.length - 1]); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * @param endIndex - index of the last character of the token | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     function finishWord(endIndex) { | 
					
						
							| 
									
										
										
										
											2020-06-03 16:24:41 +02:00
										 |  |  |         if (currentWord === '') { | 
					
						
							|  |  |  |             return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |         tokens.push({ | 
					
						
							|  |  |  |             text: currentWord, | 
					
						
							| 
									
										
										
										
											2020-06-06 12:56:24 +02:00
										 |  |  |             startIndex: endIndex - currentWord.length + 1, | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |             endIndex: endIndex | 
					
						
							|  |  |  |         }); | 
					
						
							| 
									
										
										
										
											2020-06-03 16:24:41 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         currentWord = ''; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for (let i = 0; i < str.length; i++) { | 
					
						
							|  |  |  |         const chr = str[i]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (chr === '\\') { | 
					
						
							|  |  |  |             if ((i + 1) < str.length) { | 
					
						
							|  |  |  |                 i++; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 currentWord += str[i]; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             else { | 
					
						
							|  |  |  |                 currentWord += chr; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             continue; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         else if (['"', "'", '`'].includes(chr)) { | 
					
						
							|  |  |  |             if (!quotes) { | 
					
						
							| 
									
										
										
										
											2020-06-03 17:11:03 +02:00
										 |  |  |                 if (previousOperatorSymbol()) { | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |                     finishWord(i - 1); | 
					
						
							| 
									
										
										
										
											2020-06-03 16:24:41 +02:00
										 |  |  |                 } | 
					
						
							| 
									
										
										
										
											2020-06-03 17:11:03 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 quotes = chr; | 
					
						
							| 
									
										
										
										
											2020-06-03 16:24:41 +02:00
										 |  |  |             } | 
					
						
							|  |  |  |             else if (quotes === chr) { | 
					
						
							|  |  |  |                 quotes = false; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |                 finishWord(i - 1); | 
					
						
							| 
									
										
										
										
											2020-06-03 16:24:41 +02:00
										 |  |  |             } | 
					
						
							|  |  |  |             else { | 
					
						
							|  |  |  |                 // it's a quote but within other kind of quotes so it's valid as a literal character
 | 
					
						
							|  |  |  |                 currentWord += chr; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             continue; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         else if (!quotes) { | 
					
						
							|  |  |  |             if (currentWord.length === 0 && (chr === '#' || chr === '~')) { | 
					
						
							|  |  |  |                 currentWord = chr; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 continue; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             else if (chr === ' ') { | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |                 finishWord(i - 1); | 
					
						
							| 
									
										
										
										
											2020-06-03 16:24:41 +02:00
										 |  |  |                 continue; | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2020-07-13 23:27:23 +02:00
										 |  |  |             else if (['(', ')'].includes(chr)) { | 
					
						
							|  |  |  |                 finishWord(i - 1); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 currentWord = chr; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 finishWord(i); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 continue; | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2020-06-03 17:11:03 +02:00
										 |  |  |             else if (previousOperatorSymbol() !== isOperatorSymbol(chr)) { | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |                 finishWord(i - 1); | 
					
						
							| 
									
										
										
										
											2020-06-03 16:24:41 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 currentWord += chr; | 
					
						
							|  |  |  |                 continue; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         currentWord += chr; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |     finishWord(str.length - 1); | 
					
						
							| 
									
										
										
										
											2020-06-03 16:24:41 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |     return tokens; | 
					
						
							| 
									
										
										
										
											2020-06-03 16:24:41 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-17 23:54:18 +02:00
										 |  |  | function checkAttributeName(attrName) { | 
					
						
							| 
									
										
										
										
											2020-08-21 23:08:53 +02:00
										 |  |  |     if (attrName.length === 0) { | 
					
						
							|  |  |  |         throw new Error("Attribute name is empty, please fill the name."); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-17 23:22:14 +01:00
										 |  |  |     if (!utils.isValidAttributeName(attrName)) { | 
					
						
							| 
									
										
										
										
											2020-08-17 23:54:18 +02:00
										 |  |  |         throw new Error(`Attribute name "${attrName}" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-21 23:08:53 +02:00
										 |  |  | function parse(tokens, str, allowEmptyRelations = false) { | 
					
						
							| 
									
										
										
										
											2020-06-04 00:04:57 +02:00
										 |  |  |     const attrs = []; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-20 09:39:44 +02:00
										 |  |  |     function context(i) { | 
					
						
							|  |  |  |         let {startIndex, endIndex} = tokens[i]; | 
					
						
							|  |  |  |         startIndex = Math.max(0, startIndex - 20); | 
					
						
							|  |  |  |         endIndex = Math.min(str.length, endIndex + 20); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return '"' + (startIndex !== 0 ? "..." : "") | 
					
						
							|  |  |  |             + str.substr(startIndex, endIndex - startIndex) | 
					
						
							|  |  |  |             + (endIndex !== str.length ? "..." : "") + '"'; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-04 00:04:57 +02:00
										 |  |  |     for (let i = 0; i < tokens.length; i++) { | 
					
						
							| 
									
										
										
										
											2020-07-13 23:27:23 +02:00
										 |  |  |         const {text, startIndex} = tokens[i]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         function isInheritable() { | 
					
						
							|  |  |  |             if (tokens.length > i + 3 | 
					
						
							|  |  |  |                 && tokens[i + 1].text === '(' | 
					
						
							|  |  |  |                 && tokens[i + 2].text === 'inheritable' | 
					
						
							|  |  |  |                 && tokens[i + 3].text === ')') { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 i += 3; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 return true; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             else { | 
					
						
							|  |  |  |                 return false; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2020-06-04 00:04:57 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |         if (text.startsWith('#')) { | 
					
						
							| 
									
										
										
										
											2020-08-17 23:54:18 +02:00
										 |  |  |             const labelName = text.substr(1); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             checkAttributeName(labelName); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-04 00:04:57 +02:00
										 |  |  |             const attr = { | 
					
						
							|  |  |  |                 type: 'label', | 
					
						
							| 
									
										
										
										
											2020-08-17 23:54:18 +02:00
										 |  |  |                 name: labelName, | 
					
						
							| 
									
										
										
										
											2020-07-13 23:27:23 +02:00
										 |  |  |                 isInheritable: isInheritable(), | 
					
						
							| 
									
										
										
										
											2020-06-25 23:56:06 +02:00
										 |  |  |                 startIndex: startIndex, | 
					
						
							| 
									
										
										
										
											2020-07-13 23:27:23 +02:00
										 |  |  |                 endIndex: tokens[i].endIndex // i could be moved by isInheritable
 | 
					
						
							| 
									
										
										
										
											2020-06-04 00:04:57 +02:00
										 |  |  |             }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |             if (i + 1 < tokens.length && tokens[i + 1].text === "=") { | 
					
						
							| 
									
										
										
										
											2020-06-04 00:04:57 +02:00
										 |  |  |                 if (i + 2 >= tokens.length) { | 
					
						
							| 
									
										
										
										
											2020-06-20 09:39:44 +02:00
										 |  |  |                     throw new Error(`Missing value for label "${text}" in ${context(i)}`); | 
					
						
							| 
									
										
										
										
											2020-06-04 00:04:57 +02:00
										 |  |  |                 } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 i += 2; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |                 attr.value = tokens[i].text; | 
					
						
							| 
									
										
										
										
											2020-06-25 23:56:06 +02:00
										 |  |  |                 attr.endIndex = tokens[i].endIndex; | 
					
						
							| 
									
										
										
										
											2020-06-04 00:04:57 +02:00
										 |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             attrs.push(attr); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |         else if (text.startsWith('~')) { | 
					
						
							| 
									
										
										
										
											2020-08-17 23:54:18 +02:00
										 |  |  |             const relationName = text.substr(1); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             checkAttributeName(relationName); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 12:56:24 +02:00
										 |  |  |             const attr = { | 
					
						
							|  |  |  |                 type: 'relation', | 
					
						
							| 
									
										
										
										
											2020-08-17 23:54:18 +02:00
										 |  |  |                 name: relationName, | 
					
						
							| 
									
										
										
										
											2020-07-13 23:27:23 +02:00
										 |  |  |                 isInheritable: isInheritable(), | 
					
						
							| 
									
										
										
										
											2020-06-25 23:56:06 +02:00
										 |  |  |                 startIndex: startIndex, | 
					
						
							| 
									
										
										
										
											2020-07-13 23:27:23 +02:00
										 |  |  |                 endIndex: tokens[i].endIndex // i could be moved by isInheritable
 | 
					
						
							| 
									
										
										
										
											2020-06-06 12:56:24 +02:00
										 |  |  |             }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             attrs.push(attr); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |             if (i + 2 >= tokens.length || tokens[i + 1].text !== '=') { | 
					
						
							| 
									
										
										
										
											2020-06-06 12:56:24 +02:00
										 |  |  |                 if (allowEmptyRelations) { | 
					
						
							|  |  |  |                     break; | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 else { | 
					
						
							| 
									
										
										
										
											2020-06-20 09:39:44 +02:00
										 |  |  |                     throw new Error(`Relation "${text}" in ${context(i)} should point to a note.`); | 
					
						
							| 
									
										
										
										
											2020-06-06 12:56:24 +02:00
										 |  |  |                 } | 
					
						
							| 
									
										
										
										
											2020-06-04 00:04:57 +02:00
										 |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             i += 2; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 10:39:27 +02:00
										 |  |  |             let notePath = tokens[i].text; | 
					
						
							| 
									
										
										
										
											2020-06-05 17:25:14 +02:00
										 |  |  |             if (notePath.startsWith("#")) { | 
					
						
							|  |  |  |                 notePath = notePath.substr(1); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             const noteId = notePath.split('/').pop(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 12:56:24 +02:00
										 |  |  |             attr.value = noteId; | 
					
						
							| 
									
										
										
										
											2020-06-25 23:56:06 +02:00
										 |  |  |             attr.endIndex = tokens[i].endIndex; | 
					
						
							| 
									
										
										
										
											2020-06-04 00:04:57 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |         else { | 
					
						
							| 
									
										
										
										
											2020-07-23 00:19:50 +02:00
										 |  |  |             throw new Error(`Invalid attribute "${text}" in ${context(i)}`); | 
					
						
							| 
									
										
										
										
											2020-06-04 00:04:57 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return attrs; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-06 12:56:24 +02:00
										 |  |  | function lexAndParse(str, allowEmptyRelations = false) { | 
					
						
							| 
									
										
										
										
											2020-08-21 23:08:53 +02:00
										 |  |  |     const tokens = lex(str); | 
					
						
							| 
									
										
										
										
											2020-06-05 17:25:14 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-21 23:08:53 +02:00
										 |  |  |     return parse(tokens, str, allowEmptyRelations); | 
					
						
							| 
									
										
										
										
											2020-06-05 17:25:14 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-03 16:24:41 +02:00
										 |  |  | export default { | 
					
						
							| 
									
										
										
										
											2020-08-21 23:08:53 +02:00
										 |  |  |     lex, | 
					
						
							|  |  |  |     parse, | 
					
						
							| 
									
										
										
										
											2020-06-05 17:25:14 +02:00
										 |  |  |     lexAndParse | 
					
						
							| 
									
										
										
										
											2020-06-03 16:24:41 +02:00
										 |  |  | } |