diff --git a/app/imports/parser/grammar.js b/app/imports/parser/grammar.js index 7ab4c9ed..c158d3ea 100644 --- a/app/imports/parser/grammar.js +++ b/app/imports/parser/grammar.js @@ -2,11 +2,16 @@ // http://github.com/Hardmath123/nearley function id(x) { return x[0]; } + import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; import CallNode from '/imports/parser/parseTree/CallNode.js'; import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - import IfNode from '/imports/parser/parseTree/IfNode.js'; + import IfNode from '/imports/parser/parseTree/IfNode.js'; + import IndexNode from '/imports/parser/parseTree/IndexNode.js'; import OperatorNode from '/imports/parser/parseTree/OperatorNode.js'; - import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; + import ParenthesisNode from '/imports/parser/parseTree/ParenthesisNode.js'; + import RollNode from '/imports/parser/parseTree/RollNode.js'; + import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; + import moo from 'moo'; const lexer = moo.compile({ @@ -25,7 +30,8 @@ function id(x) { return x[0]; } match: /\s+/, lineBreaks: true, }, - separators: [',', '.'], + separator: [',', ';'], + period: ['.'], multiplicativeOperator: ['*', '/'], exponentOperator: ['^'], additiveOperator: ['+', '-'], @@ -35,7 +41,7 @@ function id(x) { return x[0]; } stringDelimiters: ['\"', '\''], equalityOperator: ['=', '==', '===', '!=', '!=='], relationalOperator: ['>', '<', '>=', '<='], - brackets: ['(', ')', '{', '}'], + brackets: ['(', ')', '{', '}', '[', ']'], }); function nuller() { return null; } @@ -49,11 +55,11 @@ function id(x) { return x[0]; } } let Lexer = lexer; let ParserRules = [ - {"name": "ifStatement", "symbols": [{"literal":"if"}, "_", {"literal":"("}, "_", "expression", "_", {"literal":")"}, "_", "ifStatement", "_", {"literal":"else"}, "_", "ifStatement"], "postprocess": + {"name": "expression", "symbols": ["ifStatement"], "postprocess": d => d[0]}, + {"name": "ifStatement", "symbols": ["_", "expression", "_", {"literal":"?"}, "_", "expression", "_", {"literal":":"}, "_", "expression"], "postprocess": d => new IfNode({condition: d[4], consequent: d[8], alternative: d[12]}) }, - {"name": "ifStatement", "symbols": ["expression"], "postprocess": id}, - {"name": "expression", "symbols": ["equalityExpression"], "postprocess": d => d[0]}, + {"name": "ifStatement", "symbols": ["equalityExpression"], "postprocess": id}, {"name": "equalityExpression", "symbols": ["equalityExpression", "_", (lexer.has("equalityOperator") ? {type: "equalityOperator"} : equalityOperator), "_", "relationalExpression"], "postprocess": d => operator(d, 'equality')}, {"name": "equalityExpression", "symbols": ["relationalExpression"], "postprocess": id}, {"name": "relationalExpression", "symbols": ["relationalExpression", "_", (lexer.has("relationalOperator") ? {type: "relationalOperator"} : relationalOperator), "_", "orExpression"], "postprocess": d => operator(d, 'relation')}, @@ -66,33 +72,47 @@ let ParserRules = [ {"name": "additiveExpression", "symbols": ["multiplicativeExpression"], "postprocess": id}, {"name": "multiplicativeExpression", "symbols": ["multiplicativeExpression", "_", (lexer.has("multiplicativeOperator") ? {type: "multiplicativeOperator"} : multiplicativeOperator), "_", "rollExpression"], "postprocess": d => operator(d, 'multiply')}, {"name": "multiplicativeExpression", "symbols": ["rollExpression"], "postprocess": id}, - {"name": "rollExpression", "symbols": ["rollExpression", "_", {"literal":"d"}, "_", "exponentExpression"], "postprocess": d => operator(d, 'roll')}, - {"name": "rollExpression", "symbols": ["exponentExpression"], "postprocess": id}, + {"name": "rollExpression", "symbols": ["rollExpression", "_", {"literal":"d"}, "_", "exponentExpression"], "postprocess": d => new RollNode({left: d[0], right: d[4]})}, + {"name": "rollExpression", "symbols": ["singleRollExpression"], "postprocess": id}, + {"name": "singleRollExpression", "symbols": [{"literal":"d"}, "_", "exponentExpression"], "postprocess": d => new RollNode({left: new ConstantNode({value: 1, type: 'number'}), right: d[2]})}, + {"name": "singleRollExpression", "symbols": ["exponentExpression"], "postprocess": id}, {"name": "exponentExpression", "symbols": ["callExpression", "_", (lexer.has("exponentOperator") ? {type: "exponentOperator"} : exponentOperator), "_", "exponentExpression"], "postprocess": d => operator(d, 'exponent')}, {"name": "exponentExpression", "symbols": ["callExpression"], "postprocess": id}, {"name": "callExpression", "symbols": ["name", "_", "arguments"], "postprocess": d => new CallNode ({type: "call", fn: d[0], arguments: d[2]}) }, - {"name": "callExpression", "symbols": ["parenthesizedExpression"], "postprocess": id}, + {"name": "callExpression", "symbols": ["indexExpression"], "postprocess": id}, {"name": "arguments$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]}, {"name": "arguments$ebnf$1", "symbols": ["arguments$ebnf$1$subexpression$1"], "postprocess": id}, {"name": "arguments$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}}, {"name": "arguments$ebnf$2", "symbols": []}, - {"name": "arguments$ebnf$2$subexpression$1", "symbols": ["_", {"literal":","}, "_", "expression"], "postprocess": d => d[3]}, + {"name": "arguments$ebnf$2$subexpression$1", "symbols": ["_", (lexer.has("separator") ? {type: "separator"} : separator), "_", "expression"], "postprocess": d => d[3]}, {"name": "arguments$ebnf$2", "symbols": ["arguments$ebnf$2", "arguments$ebnf$2$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, {"name": "arguments", "symbols": [{"literal":"("}, "_", "arguments$ebnf$1", "arguments$ebnf$2", "_", {"literal":")"}], "postprocess": d => [d[2], ...d[3]] + }, + {"name": "indexExpression", "symbols": ["arrayExpression", {"literal":"["}, "_", "expression", "_", {"literal":"]"}], "postprocess": d => new IndexNode ({array: d[0], index: d[3]})}, + {"name": "indexExpression", "symbols": ["arrayExpression"], "postprocess": id}, + {"name": "arrayExpression$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]}, + {"name": "arrayExpression$ebnf$1", "symbols": ["arrayExpression$ebnf$1$subexpression$1"], "postprocess": id}, + {"name": "arrayExpression$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}}, + {"name": "arrayExpression$ebnf$2", "symbols": []}, + {"name": "arrayExpression$ebnf$2$subexpression$1", "symbols": ["_", (lexer.has("separator") ? {type: "separator"} : separator), "_", "expression"], "postprocess": d => d[3]}, + {"name": "arrayExpression$ebnf$2", "symbols": ["arrayExpression$ebnf$2", "arrayExpression$ebnf$2$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, + {"name": "arrayExpression", "symbols": [{"literal":"["}, "_", "arrayExpression$ebnf$1", "arrayExpression$ebnf$2", "_", {"literal":"]"}], "postprocess": + d => new ArrayNode({values: [d[2], ...d[3]]}) }, - {"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => d[2]}, + {"name": "arrayExpression", "symbols": ["parenthesizedExpression"], "postprocess": id}, + {"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => new ParenthesisNode({content: d[2]})}, {"name": "parenthesizedExpression", "symbols": ["valueExpression"], "postprocess": id}, {"name": "valueExpression", "symbols": ["name"], "postprocess": id}, {"name": "valueExpression", "symbols": ["number"], "postprocess": id}, {"name": "valueExpression", "symbols": ["string"], "postprocess": id}, - {"name": "number", "symbols": [(lexer.has("number") ? {type: "number"} : number)], "postprocess": d => new ConstantNode({value: d[0].value, type: 'number'})}, + {"name": "number", "symbols": [(lexer.has("number") ? {type: "number"} : number)], "postprocess": d => new ConstantNode({value: +d[0].value, type: 'number'})}, {"name": "name", "symbols": [(lexer.has("name") ? {type: "name"} : name)], "postprocess": d => new SymbolNode({name: d[0].value})}, {"name": "string", "symbols": [(lexer.has("string") ? {type: "string"} : string)], "postprocess": d => new ConstantNode({value: d[0].value, type: 'string'})}, {"name": "_", "symbols": []}, {"name": "_", "symbols": [(lexer.has("space") ? {type: "space"} : space)], "postprocess": nuller} ]; -let ParserStart = "ifStatement"; +let ParserStart = "expression"; export default { Lexer, ParserRules, ParserStart }; diff --git a/app/imports/parser/grammar.ne b/app/imports/parser/grammar.ne index 22563eb0..3f6c7fc6 100644 --- a/app/imports/parser/grammar.ne +++ b/app/imports/parser/grammar.ne @@ -1,10 +1,15 @@ @preprocessor esmodule @{% + import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; import CallNode from '/imports/parser/parseTree/CallNode.js'; import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - import IfNode from '/imports/parser/parseTree/IfNode.js'; + import IfNode from '/imports/parser/parseTree/IfNode.js'; + import IndexNode from '/imports/parser/parseTree/IndexNode.js'; import OperatorNode from '/imports/parser/parseTree/OperatorNode.js'; - import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; + import ParenthesisNode from '/imports/parser/parseTree/ParenthesisNode.js'; + import RollNode from '/imports/parser/parseTree/RollNode.js'; + import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; + import moo from 'moo'; const lexer = moo.compile({ @@ -23,7 +28,8 @@ match: /\s+/, lineBreaks: true, }, - separators: [',', '.'], + separator: [',', ';'], + period: ['.'], multiplicativeOperator: ['*', '/'], exponentOperator: ['^'], additiveOperator: ['+', '-'], @@ -33,7 +39,7 @@ stringDelimiters: ['\"', '\''], equalityOperator: ['=', '==', '===', '!=', '!=='], relationalOperator: ['>', '<', '>=', '<='], - brackets: ['(', ')', '{', '}'], + brackets: ['(', ')', '{', '}', '[', ']'], }); function nuller() { return null; } @@ -50,14 +56,14 @@ # Use the Moo lexer @lexer lexer +expression -> + ifStatement {% d => d[0] %} + ifStatement -> - "if" _ "(" _ expression _ ")" _ ifStatement _ "else" _ ifStatement {% + _ expression _ "?" _ expression _ ":" _ expression {% d => new IfNode({condition: d[4], consequent: d[8], alternative: d[12]}) %} -| expression {% id %} - -expression -> - equalityExpression {% d => d[0] %} +| equalityExpression {% id %} equalityExpression -> equalityExpression _ %equalityOperator _ relationalExpression {% d => operator(d, 'equality') %} @@ -84,7 +90,11 @@ multiplicativeExpression -> | rollExpression {% id %} rollExpression -> - rollExpression _ "d" _ exponentExpression {% d => operator(d, 'roll') %} + rollExpression _ "d" _ exponentExpression {% d => new RollNode({left: d[0], right: d[4]}) %} +| singleRollExpression {% id %} + +singleRollExpression -> + "d" _ exponentExpression {% d => new RollNode({left: new ConstantNode({value: 1, type: 'number'}), right: d[2]}) %} | exponentExpression {% id %} exponentExpression -> @@ -95,15 +105,25 @@ callExpression -> name _ arguments {% d => new CallNode ({type: "call", fn: d[0], arguments: d[2]}) %} -| parenthesizedExpression {% id %} +| indexExpression {% id %} arguments -> - "(" _ (expression {% d => d[0] %}):? ( _ "," _ expression {% d => d[3] %} ):* _ ")" {% - d => [d[2], ...d[3]] +"(" _ (expression {% d => d[0] %}):? ( _ %separator _ expression {% d => d[3] %} ):* _ ")" {% + d => [d[2], ...d[3]] %} +indexExpression -> + arrayExpression "[" _ expression _ "]" {% d => new IndexNode ({array: d[0], index: d[3]}) %} +| arrayExpression {% id %} + +arrayExpression -> + "[" _ (expression {% d => d[0] %}):? ( _ %separator _ expression {% d => d[3] %} ):* _ "]" {% + d => new ArrayNode({values: [d[2], ...d[3]]}) + %} +| parenthesizedExpression {% id %} + parenthesizedExpression -> - "(" _ expression _ ")" {% d => d[2] %} + "(" _ expression _ ")" {% d => new ParenthesisNode({content: d[2]}) %} | valueExpression {% id %} valueExpression -> @@ -113,7 +133,7 @@ valueExpression -> # A number or a function of a number number -> - %number {% d => new ConstantNode({value: d[0].value, type: 'number'}) %} + %number {% d => new ConstantNode({value: +d[0].value, type: 'number'}) %} name -> %name {% d => new SymbolNode({name: d[0].value}) %} diff --git a/app/imports/parser/parseTree/ArrayNode.js b/app/imports/parser/parseTree/ArrayNode.js new file mode 100644 index 00000000..43f592ad --- /dev/null +++ b/app/imports/parser/parseTree/ArrayNode.js @@ -0,0 +1,15 @@ +import ParseNode from '/imports/parser/parseTree/ParseNode.js'; + +export default class ArrayNode extends ParseNode { + constructor({values}) { + super(); + this.values = values; + } + compile(){ + let values = this.values.map(node => node.compile()); + return new ArrayNode({values}); + } + toString(){ + return `[${this.values.map(node => node.toString()).join(', ')}]`; + } +} diff --git a/app/imports/parser/parseTree/ConstantNode.js b/app/imports/parser/parseTree/ConstantNode.js index caa910f0..4d3198fd 100644 --- a/app/imports/parser/parseTree/ConstantNode.js +++ b/app/imports/parser/parseTree/ConstantNode.js @@ -11,11 +11,13 @@ export default class ConstantNode extends ParseNode { compile(){ return this; } - reduce(){ - if (this.type === 'numberArray'){ - return this.value.reduce((total, num) => total + num, 0); - } else { - return this; - } + toString(){ + return `${this.value}`; + } + get isNumber(){ + return this.type === 'number'; + } + get isInteger(){ + return this.isNumberNode && Number.isInteger(this.value); } } diff --git a/app/imports/parser/parseTree/ErrorNode.js b/app/imports/parser/parseTree/ErrorNode.js new file mode 100644 index 00000000..bbef94c3 --- /dev/null +++ b/app/imports/parser/parseTree/ErrorNode.js @@ -0,0 +1,15 @@ +import ParseNode from '/imports/parser/parseTree/ParseNode.js'; + +export default class ErrorNode extends ParseNode { + constructor({node, error}) { + super(); + this.node = node; + this.error = error; + } + compile(){ + return this; + } + toString(){ + return '###'; + } +} diff --git a/app/imports/parser/parseTree/IfNode.js b/app/imports/parser/parseTree/IfNode.js index 29bca520..a801e4bd 100644 --- a/app/imports/parser/parseTree/IfNode.js +++ b/app/imports/parser/parseTree/IfNode.js @@ -8,6 +8,10 @@ export default class IfNode extends ParseNode { this.consequent = consequent; this.alternative = alternative; } + toString(){ + let {condition, consequent, alternative} = this; + return `${condition.toString()} ? ${consequent.toString()} : ${alternative.toString()}` + } compile(){ let condition = this.condition.compile(); let consequent = this.consequent.compile(); @@ -19,7 +23,7 @@ export default class IfNode extends ParseNode { ){ // Handle unresolved condition return new ConstantNode({ - value: `if (${condition.value}) ${consequent.value} else ${alternative.value}`, + value: `${condition.value}) ${consequent.value} else ${alternative.value}`, type: 'uncompiledNode', errors: [ ...condition.errors, diff --git a/app/imports/parser/parseTree/IndexNode.js b/app/imports/parser/parseTree/IndexNode.js new file mode 100644 index 00000000..158da8c6 --- /dev/null +++ b/app/imports/parser/parseTree/IndexNode.js @@ -0,0 +1,25 @@ +import ParseNode from '/imports/parser/parseTree/ParseNode.js'; + +export default class IndexNode extends ParseNode { + constructor({array, index}) { + super(); + this.array = array; + this.index = index; + } + compile(){ + let index = this.index.compile(); + if (index.constructor.name === 'ConstantNode' && index.type === 'number'){ + let selection = this.array.values[index.value]; + if (selection){ + return selection.compile(); + } + } + return new IndexNode({ + array: this.array.compile(), + index: this.index.compile(), + }); + } + toString(){ + return `${this.array.toString()}[${this.index.toString()}]`; + } +} diff --git a/app/imports/parser/parseTree/OperatorNode.js b/app/imports/parser/parseTree/OperatorNode.js index cd4d9146..fa75f315 100644 --- a/app/imports/parser/parseTree/OperatorNode.js +++ b/app/imports/parser/parseTree/OperatorNode.js @@ -1,4 +1,5 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js'; +import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; export default class OperatorNode extends ParseNode { constructor({left, right, operator, fn}) { @@ -8,4 +9,45 @@ export default class OperatorNode extends ParseNode { this.fn = fn; this.operator = operator; } + compile(){ + let leftNode = this.left.compile(); + let rightNode = this.right.compile(); + let left, right; + if (leftNode.type !== 'number' || rightNode.type !== 'number'){ + return new OperatorNode({ + left: leftNode, + right: rightNode, + operator: this.operator, + fn: this.fn + }); + } else { + left = leftNode.value; + right = rightNode.value; + } + let result; + switch(this.operator){ + case '+': result = left + right; break; + case '-': result = left - right; break; + case '*': result = left * right; break; + case '/': result = left / right; break; + case '^': result = Math.pow(left, right); break; + case '&': + case '&&': result = left && right; break; + case '|': + case '||': result = left || right; break; + case '=': + case '==': + case '===': result = left == right; break; + case '!=': + case '>': result = left > right; break; + case '<': result = left < right; break; + case '>=': result = left >= right; break; + case '<=': result = left <= right; break; + } + return new ConstantNode({value: result, type: typeof result}); + } + toString(){ + let {left, right, operator} = this; + return `${left.toString()} ${operator} ${right.toString()}`; + } } diff --git a/app/imports/parser/parseTree/ParenthesisNode.js b/app/imports/parser/parseTree/ParenthesisNode.js new file mode 100644 index 00000000..22da9c4b --- /dev/null +++ b/app/imports/parser/parseTree/ParenthesisNode.js @@ -0,0 +1,19 @@ +import ParseNode from '/imports/parser/parseTree/ParseNode.js'; + +export default class ParenthesisNode extends ParseNode { + constructor({content}) { + super(); + this.content = content; + } + compile(){ + let content = this.content.compile(); + if (content.constructor.name === 'ConstantNode'){ + return content; + } else { + return new ParenthesisNode({content}); + } + } + toString(){ + return `(${this.content.toString()})`; + } +} diff --git a/app/imports/parser/parseTree/ParseNode.js b/app/imports/parser/parseTree/ParseNode.js index 0e213587..0d8686d3 100644 --- a/app/imports/parser/parseTree/ParseNode.js +++ b/app/imports/parser/parseTree/ParseNode.js @@ -1,7 +1,10 @@ export default class ParseNode { - // Compiling a node must return a ConstantNode compile(){ - throw new Meteor.Error('Compile not implemented on ' + this); + // Returns a ParseNode, a ConstantNode if possible + throw new Meteor.Error('Compile not implemented on ' + this.constructor.name); + } + toString(){ + throw new Meteor.Error('toString not implemented on ' + this.constructor.name); } // Compile, but turn rolls into arrays roll(){ @@ -9,6 +12,6 @@ export default class ParseNode { } // Compile, turn rolls into arrays, and reduce those arrays into single values reduce(){ - return this.compileAndRoll() + return this.roll(); } } diff --git a/app/imports/parser/parseTree/RollArrayNode.js b/app/imports/parser/parseTree/RollArrayNode.js new file mode 100644 index 00000000..d8d60236 --- /dev/null +++ b/app/imports/parser/parseTree/RollArrayNode.js @@ -0,0 +1,17 @@ +import ParseNode from '/imports/parser/parseTree/ParseNode.js'; + +export default class RollArrayNode extends ParseNode { + constructor({values}) { + super(); + this.values = values; + } + compile(){ + return this; + } + toString(){ + return `[${this.values.join(', ')}]`; + } + reduce(){ + //TODO sum and return values + } +} diff --git a/app/imports/parser/parseTree/RollNode.js b/app/imports/parser/parseTree/RollNode.js index e69de29b..d2f99379 100644 --- a/app/imports/parser/parseTree/RollNode.js +++ b/app/imports/parser/parseTree/RollNode.js @@ -0,0 +1,57 @@ +import ParseNode from '/imports/parser/parseTree/ParseNode.js'; +import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; +import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; + +export default class RollNode extends ParseNode { + constructor({left, right}) { + super(); + this.left = left; + this.right = right; + } + compile(){ + let left = this.left.compile(); + let right = this.right.compile(); + return new RollNode({left, right}); + } + toString(){ + if ( + this.left.isNumberNode && this.left.value === 1 + ){ + return `d${this.right.toString()}`; + } else { + return `${this.left.toString()}d${this.right.toString()}`; + } + } + roll(){ + let left = this.left.reduce(); + let right = this.right.reduce(); + if (!left.isInteger){ + return new ErrorNode({ + node: this, + error: 'Number of dice is not an integer' + }); + } + if (!right.isInteger){ + return new ErrorNode({ + node: this, + error: 'Dice size is not an integer' + }); + } + let number = left.value; + if (number > 100) return new ErrorNode({ + node: this, + error: 'Can\'t roll more than 100 dice at once' + }); + let diceSize = right.value; + let randomSrc = DDP.randomStream('diceRoller'); + let rolls = []; + for (let i = 0; i < number; i++){ + let roll = ~~(randomSrc.fraction() * diceSize) + 1 + rolls.push(roll); + } + return new ArrayNode({values: rolls}); + } + reduce(){ + this.roll().reduce(); + } +} diff --git a/app/imports/parser/parseTree/SymbolNode.js b/app/imports/parser/parseTree/SymbolNode.js index 52ce13fa..cb855b72 100644 --- a/app/imports/parser/parseTree/SymbolNode.js +++ b/app/imports/parser/parseTree/SymbolNode.js @@ -6,6 +6,9 @@ export default class SymbolNode extends ParseNode { super(); this.name = name; } + toString(){ + return `${this.name}` + } compile(scope){ let value = scope && scope[this.name]; let type = typeof value; diff --git a/app/imports/ui/pages/Parser.vue b/app/imports/ui/pages/Parser.vue new file mode 100644 index 00000000..abcc4e4b --- /dev/null +++ b/app/imports/ui/pages/Parser.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/app/imports/ui/router.js b/app/imports/ui/router.js index 8e0fc7c5..89c7d0e3 100644 --- a/app/imports/ui/router.js +++ b/app/imports/ui/router.js @@ -23,6 +23,7 @@ import PatreonLevelTooLow from '/imports/ui/pages/PatreonLevelTooLow.vue'; import Tabletops from '/imports/ui/pages/Tabletops.vue'; import Tabletop from '/imports/ui/pages/Tabletop.vue'; import TabletopToolbar from '/imports/ui/tabletop/TabletopToolbar.vue'; +import Parser from '/imports/ui/pages/Parser.vue'; let userSubscription = Meteor.subscribe('user'); @@ -109,6 +110,14 @@ RouterFactory.configure(factory => { title: 'Character List', }, beforeEnter: ensureLoggedIn, + },{ + path: '/parser', + components: { + default: Parser, + }, + meta: { + title: 'Parser', + }, },{ path: '/library', components: {