Add first stage of a new single-file parser

This first stage consists of the general framework for the parser as
a whole, a lexical analyzers to converts text into commands, and the
semantic anlyzer to convert commands into a complete data structure
representing the cumulative effect of running the commands.
This commit is contained in:
Andrew Miner
2016-04-10 13:37:17 -07:00
parent 29315f03f7
commit 14278c81df
17 changed files with 1203 additions and 0 deletions
@@ -0,0 +1,35 @@
#
# Crafting Guide - current_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class CurrentParserExtensionV1 extends ParserExtension
# Command Methods ##############################################################################
_command_documentationUrl: (command)->
current = @state.getCurrent()
return if @duplicateField command, current, 'documentationUrl'
return if @missingArgText command
current.documentationUrl = command.argText
_command_description: (command)->
current = @state.getCurrent()
return if @duplicateField command, current, 'description'
return if @missingArgText command
current.description = command.argText
_command_name: (command)->
current = @state.getCurrent()
return if @duplicateField command, current, 'name'
return if @missingArgText command
current.name = command.argText
@@ -0,0 +1,47 @@
#
# Crafting Guide - current_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CurrentParserExtensionV1 = require './current_pe_v1'
ParserState = require '../parser_state'
########################################################################################################################
parser = state = null
########################################################################################################################
describe 'current_pe_v1.coffee', ->
beforeEach ->
state = new ParserState
parser = new CurrentParserExtensionV1 state
state.create {}, 'alpha'
describe 'documentationUrl', ->
it 'assigns to the current object', ->
parser.execute name:'documentationUrl', argText:'bravo'
.then ->
state.getCurrent().documentationUrl.should.equal 'bravo'
state.errors.should.eql []
describe 'description', ->
it 'assigns to the current object', ->
parser.execute name:'description', argText:'bravo'
.then ->
state.getCurrent().description.should.equal 'bravo'
state.errors.should.eql []
describe 'name', ->
it 'assigns to the current object', ->
parser.execute name:'name', argText:'bravo'
.then ->
state.getCurrent().name.should.equal 'bravo'
state.errors.should.eql []
@@ -0,0 +1,40 @@
#
# Crafting Guide - item_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class ItemParserExtensionV1 extends ParserExtension
# Command Methods ##############################################################################
_command_gatherable: (command)->
return if @missingArgText command
return if @missingCurrent command, 'item'
item = @state.getCurrent 'item'
return if @duplicateField command, 'gatherable', item
gatherable = @parseBoolean command, command.argText
return unless gatherable?
item.gatherable = gatherable
_command_item: (command)->
return if @missingArgText command
return if @missingCurrent command, 'modVersion'
return if @alreadyExists command, 'item', command.argText
item = @state.create command, 'item', command.argText
item.name = command.argText
group = @state.getCurrent 'itemGroup'
if group? then item.group = group
item.modVersion = @state.getCurrent 'modVersion'
@@ -0,0 +1,46 @@
#
# Crafting Guide - item_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ItemParserExtensionV1 = require './item_pe_v1'
ParserState = require '../parser_state'
########################################################################################################################
parser = state = null
########################################################################################################################
describe 'item_pe_v1.coffee', ->
beforeEach ->
state = new ParserState
parser = new ItemParserExtensionV1 state
state.create {}, 'modVersion', 0
describe 'gatherable', ->
it 'assigns to the current item', ->
state.create {}, 'item', 1
parser.execute name:'gatherable', argText:'yes'
.then ->
state.getCurrent('item').gatherable.should.be.true
state.errors.should.eql []
describe 'item', ->
it 'creates a new item', ->
state.create {}, 'itemGroup', 2
parser.execute name:'item', argText:'alpha'
.then ->
item = state.getCurrent 'item'
item.id.should.equal 'alpha'
item.name.should.equal 'alpha'
item.modVersion.id.should.equal 0
item.group.id.should.equal 2
@@ -0,0 +1,47 @@
#
# Crafting Guide - mod_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class ModParserExtensionV1 extends ParserExtension
# Command Methods ##############################################################################
_command_author: (command)->
return if @missingCurrent command, 'mod'
mod = @state.getCurrent 'mod'
return if @duplicateField command, mod, 'author'
return if @missingArgText command
mod.author = command.argText
_command_downloadUrl: (command)->
return if @missingCurrent command, 'mod'
mod = @state.getCurrent 'mod'
return if @duplicateField command, mod, 'downloadUrl'
return if @missingArgText command
mod.downloadUrl = command.argText
_command_homePageUrl: (command)->
return if @missingCurrent command, 'mod'
mod = @state.getCurrent 'mod'
return if @duplicateField command, mod, 'homePageUrl'
return if @missingArgText command
mod.homePageUrl = command.argText
_command_mod: (command)->
return if @missingArgText command
return if @alreadyExists command, 'mod', command.argText
@state.create command, 'mod', command.argText
@@ -0,0 +1,55 @@
#
# Crafting Guide - mod_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ModParserExtensionV1 = require './mod_pe_v1'
ParserState = require '../parser_state'
########################################################################################################################
parser = state = null
########################################################################################################################
describe 'mod_pe_v1.coffee', ->
beforeEach ->
state = new ParserState
parser = new ModParserExtensionV1 state
state.create {}, 'mod', 0
describe 'author', ->
it 'assigns to the current mod', ->
parser.execute name:'author', argText:'alpha'
.then ->
state.getCurrent('mod').author.should.equal 'alpha'
state.errors.should.eql []
describe 'downloadUrl', ->
it 'assigns to the current mod', ->
parser.execute name:'downloadUrl', argText:'alpha'
.then ->
state.getCurrent('mod').downloadUrl.should.equal 'alpha'
state.errors.should.eql []
describe 'homePageUrl', ->
it 'assigns to the current mod', ->
parser.execute name:'homePageUrl', argText:'alpha'
.then ->
state.getCurrent('mod').homePageUrl.should.equal 'alpha'
state.errors.should.eql []
describe 'mod', ->
it 'creates a new mod', ->
parser.execute name:'mod', argText:'alpha'
.then ->
mod = state.getCurrent 'mod'
mod.id.should.equal 'alpha'
@@ -0,0 +1,28 @@
#
# Crafting Guide - mod_version_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class ModVersionParserExtensionV1 extends ParserExtension
# Command Methods ##############################################################################
_command_group: (command)->
return if @missingArgText command
return if @missingCurrent command, 'modVersion'
group = @state.create command, 'itemGroup', command.argText
group.modVersion = @state.getCurrent 'modVersion'
_command_version: (command)->
return if @missingArgText command
return if @missingCurrent command, 'mod'
modVersion = @state.create command, 'modVersion', command.argText
modVersion.mod = @state.getCurrent 'mod'
@@ -0,0 +1,43 @@
#
# Crafting Guide - mod_version_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ModVersionParserExtensionV1 = require './mod_version_pe_v1'
ParserState = require '../parser_state'
########################################################################################################################
parser = state = null
########################################################################################################################
describe 'mod_version_pe_v1.coffee', ->
beforeEach ->
state = new ParserState
parser = new ModVersionParserExtensionV1 state
state.create {}, 'mod', 0
describe 'group', ->
it 'creates a new item group', ->
state.create {}, 'modVersion', 1
parser.execute name:'group', argText:'alpha'
.then ->
itemGroup = state.getCurrent('itemGroup')
itemGroup.id.should.equal 'alpha'
itemGroup.modVersion.id.should.equal 1
describe 'version', ->
it 'creates a new mod version', ->
parser.execute name:'version', argText:'alpha'
.then ->
modVersion = state.getCurrent 'modVersion'
modVersion.id.should.equal 'alpha'
modVersion.mod.id.should.equal 0
@@ -0,0 +1,96 @@
#
# Crafting Guide - recipe_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class RecipeParserExtensionV1 extends ParserExtension
@::PATTERN = /[0-9. ]+/
@::STACK = /^([0-9]+) +(.+)$/
# Error Checking Helpers #######################################################################
invalidPattern: (command)->
match = command.argText.match @PATTERN
if not match?
@state.addError command, "invalid pattern: \"#{command.argText}\""
return true
return false
# Command Methods ##############################################################################
_command_extras: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgs command
recipe = @state.getCurrent 'recipe'
return if @duplicateField command, 'extras', recipe
recipe.extras = command.args
_command_input: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgs command
recipe = @state.getCurrent 'recipe'
return if @duplicateField command, 'input', recipe
recipe.input = (@_parseStack(arg) for arg in command.args)
_command_pattern: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgText command
return if @invalidPattern command
recipe = @state.getCurrent 'recipe'
return if @duplicateField command, 'pattern', recipe
recipe.pattern = command.argText
_command_recipe: (command)->
return if @missingCurrent command, 'item'
recipe = @state.create command, 'recipe'
recipe.item = @state.getCurrent 'item'
_command_quantity: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgText command
value = @parseInt command, command.argText
return unless value?
recipe = @state.getCurrent 'recipe'
return if @duplicateField command, 'quantity', recipe
recipe.quantity = value
_command_tools: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgs command
recipe = @state.getCurrent 'recipe'
return if @duplicateField command, 'tools', recipe
recipe.tools = (@_parseStack(arg) for arg in command.args)
# Custom Parsers ###############################################################################
_parseStack: (text)->
match = text.match @STACK
return name:text, quantity:1 unless match?
quantity = parseFloat match[1]
if isNaN quantity then quantity = 1
name = match[2]
return name:name, quantity:quantity
@@ -0,0 +1,114 @@
#
# Crafting Guide - recipe_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
RecipeParserExtensionV1 = require './recipe_pe_v1'
ParserState = require '../parser_state'
########################################################################################################################
parser = state = null
########################################################################################################################
describe 'recipe_pe_v1.coffee', ->
beforeEach ->
state = new ParserState
parser = new RecipeParserExtensionV1 state
state.create {}, 'item', 0
describe 'extras', ->
it 'assigns to the current recipe', ->
state.create {}, 'recipe'
parser.execute name:'extras', args:['bravo']
.then ->
recipe = state.getCurrent 'recipe'
recipe.extras.should.eql ['bravo']
state.errors.should.eql []
describe 'input', ->
it 'correctly reads a single item', ->
state.create {}, 'recipe'
parser.execute name:'input', args:['bravo']
.then ->
recipe = state.getCurrent 'recipe'
recipe.input.should.eql [name:'bravo', quantity:1]
state.errors.should.eql []
it 'correctly reads multiple items', ->
state.create {}, 'recipe'
parser.execute name:'input', args:['bravo', 'charlie']
.then ->
recipe = state.getCurrent 'recipe'
recipe.input.should.eql [{name:'bravo', quantity:1}, {name:'charlie', quantity:1}]
state.errors.should.eql []
it 'correctly reads a stack', ->
state.create {}, 'recipe'
parser.execute name:'input', args:['10 bravo']
.then ->
recipe = state.getCurrent 'recipe'
recipe.input.should.eql [name:'bravo', quantity:10]
state.errors.should.eql []
describe 'pattern', ->
it 'assigns to the current recipe', ->
state.create {}, 'recipe'
parser.execute name:'pattern', argText:'00. 00. ...'
.then ->
recipe = state.getCurrent 'recipe'
recipe.pattern.should.equal '00. 00. ...'
state.errors.should.eql []
it 'rejects an invalid pattern', ->
state.create {}, 'recipe'
parser.execute name:'pattern', argText:'alpha'
.then ->
recipe = state.getCurrent 'recipe'
expect(recipe.pattern).to.be.undefined
(e.message for e in state.errors).should.eql ['invalid pattern: "alpha"']
describe 'recipe', ->
it 'creates a new recipe', ->
parser.execute name:'recipe'
.then ->
recipe = state.getCurrent 'recipe'
recipe.item.id.should.equal 0
state.errors.should.eql []
describe 'quantity', ->
it 'assigns to the current recipe', ->
state.create {}, 'recipe'
parser.execute name:'quantity', argText:'42'
.then ->
recipe = state.getCurrent 'recipe'
recipe.quantity.should.equal 42
state.errors.should.eql []
describe 'tools', ->
it 'assigns to the current recipe', ->
state.create {}, 'recipe'
parser.execute name:'tools', args:['bravo']
.then ->
recipe = state.getCurrent 'recipe'
recipe.tools.should.eql [name:'bravo', quantity:1]
state.errors.should.eql []