Author SHA1 Message Date
Andrew Miner 9ddbd5cda7 Add remaining commands 2016-04-15 21:22:10 -07:00
Andrew Miner 31f6e12844 wip 2016-04-13 22:31:11 -07:00
Andrew Miner 14278c81df 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.
2016-04-10 13:37:17 -07:00
21 changed files with 1655 additions and 0 deletions
@@ -0,0 +1,143 @@
#
# Crafting Guide - lexical_analyzer.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserCommand = require './parser_command'
########################################################################################################################
module.exports = class LexicalAnalyzer
constructor: (fileName, rawText)->
if not fileName? then throw new Error 'fileName is required'
if not rawText? then throw new Error 'rawText is required'
@_commands = []
@_fileName = fileName
@_rawText = rawText
@_lines = rawText.split '\n'
@_lineIndex = -1
@_lineNumber = 0
# Class Methods ################################################################################
@::COMMAND = /\ *([^:]*):?(.*)/
@::COMMENT = /([^\\]?)#.*/
@::FILE = /^#FILE +(.*)/
# Public Methods ###############################################################################
next: ->
command = @peek()
@_commands.shift()
return command
peek: ->
while @_commands.length is 0
break if @_lineNumber > @_lines.length
@_parseLine()
return null unless @_commands.length > 0
return @_commands[0]
push: (command)->
@_commands.unshift command
# Property Methods #############################################################################
getIsFinished: ->
return @peek() is null
Object.defineProperties @prototype,
isFinished: { get:@::getIsFinished }
# Private Methods ##############################################################################
_currentLine: ->
return null if @_lineIndex >= @_lines.length
return @_lines[@_lineIndex]
_nextLine: ->
if @_lineIndex < @_lines.length
while true
@_lineIndex += 1
@_lineNumber += 1
break if @_lineIndex >= @_lines.length
line = @_lines[@_lineIndex]
fileMatch = line.match @FILE, '$1'
if fileMatch?
@_fileName = fileMatch[1]
@_lineNumber = 0
else
line = line.replace @COMMENT, '$1'
@_lines[@_lineIndex] = line
break if line.trim().length > 0
return @_currentLine()
_parseHereDoc: ->
line = @_currentLine()
hereDocIndex = line.indexOf '<<-'
return [line, null] unless hereDocIndex isnt -1
hereDocStopText = line[hereDocIndex+3...line.length]
line = line[0...hereDocIndex]
hereDocLines = []
while true
nextLine = @_nextLine()
break unless nextLine?
break if nextLine.trim() is hereDocStopText
hereDocLines.push nextLine
shortestIndent = Number.MAX_VALUE
for hereDocLine in hereDocLines
continue if hereDocLine.trim().length is 0
shortestIndent = Math.min hereDocLine.match(/( *).*/)[1].length, shortestIndent
for i in [0...hereDocLines.length]
hereDocLines[i] = hereDocLines[i][shortestIndent..]
return [line, null] unless hereDocLines.length > 0
return [line, hereDocLines.join('\n')]
_parseLine: ->
line = @_nextLine()
return unless line?
startingLineNumber = @_lineNumber
[line, lineHereDoc] = @_parseHereDoc()
lineParts = (part.trim() for part in line.split(';'))
for linePart, index in lineParts
continue if linePart.length is 0
hereDoc = lineHereDoc if index is lineParts.length - 1
match = @COMMAND.exec linePart
if not match? then throw new Error "Expected <command>: <args>, but found: \"#{linePart}\""
argText = if match[2] then match[2].trim() else ''
args = (arg.trim() for arg in argText.split(','))
args = (arg for arg in args when arg.length > 0)
if hereDoc?
if argText.length > 0 then argText += ', '
argText += hereDoc
args.push hereDoc
@_commands.push new ParserCommand
argText: argText
args: args
fileName: @_fileName
hereDoc: hereDoc
lineNumber: startingLineNumber
name: match[1]
@@ -0,0 +1,121 @@
#
# Crafting Guide - lexical_analyzer.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
LexicalAnalyzer = require './lexical_analyzer'
########################################################################################################################
lexer = null
createLexer = (text)->
return new LexicalAnalyzer 'file.cg', text
########################################################################################################################
describe 'lexical_analyzer.coffee', ->
describe 'a document with', ->
it 'no content returns null immediately', ->
lexer = createLexer ''
expect(lexer.next()).to.be.null
it 'only whitespace returns null immediately', ->
lexer = createLexer '\n\n \n \n\n'
expect(lexer.next()).to.be.null
it 'a single command returns only that command', ->
lexer = createLexer 'alpha: bravo'
lexer.next().name.should.equal 'alpha'
expect(lexer.next()).to.be.null
it 'multiple commands on a single line returns all commands', ->
lexer = createLexer 'alpha: bravo; charlie: delta; echo: foxtrot'
lexer.next().name.should.equal 'alpha'
lexer.next().name.should.equal 'charlie'
lexer.next().name.should.equal 'echo'
expect(lexer.next()).to.be.null
it 'multiple commands on separate lines returns all commands', ->
lexer = createLexer 'alpha: bravo \n charlie: delta \n echo: foxtrot'
lexer.next().name.should.equal 'alpha'
lexer.next().name.should.equal 'charlie'
lexer.next().name.should.equal 'echo'
expect(lexer.next()).to.be.null
describe 'a command with', ->
it 'only a name has no arguments', ->
command = createLexer('alpha:').next()
command.argText.should.equal ''
command.args.should.eql []
it 'one argument has only that argument', ->
command = createLexer('alpha: bravo').next()
command.argText.should.equal 'bravo'
command.args.should.eql ['bravo']
it 'multiple arguments has all of them', ->
command = createLexer('alpha: bravo, charlie, delta').next()
command.argText.should.equal 'bravo, charlie, delta'
command.args.should.eql ['bravo', 'charlie', 'delta']
it 'only a heredoc has it recorded in the right places', ->
command = createLexer('alpha: <<-END\nbravo charlie\nEND').next()
command.hereDoc.should.equal 'bravo charlie'
command.args.should.eql ['bravo charlie']
command.argText.should.equal 'bravo charlie'
it 'multiple args and a hereDoc is built correctly', ->
command = createLexer('alpha: bravo, charlie <<-END\ndelta echo\nfoxtrot\nEND').next()
command.hereDoc.should.equal 'delta echo\nfoxtrot'
command.argText.should.equal 'bravo, charlie, delta echo\nfoxtrot'
command.args.should.eql ['bravo', 'charlie', 'delta echo\nfoxtrot']
describe 'file name is assigned correctly when', ->
it 'no file markers are given', ->
lexer = createLexer 'alpha: bravo\ncharlie: delta'
lexer.next().fileName.should.equal 'file.cg'
lexer.next().fileName.should.equal 'file.cg'
it 'commands appear before the first file marker', ->
lexer = createLexer 'alpha: bravo\n#FILE file2.cg\ncharlie: delta'
lexer.next().fileName.should.equal 'file.cg'
lexer.next().fileName.should.equal 'file2.cg'
it 'multiple file markers are used', ->
lexer = createLexer '#FILE file1.cg\na: b\n#FILE file2.cg\nc: d\n#FILE file3.cg\ne: f'
lexer.next().fileName.should.equal 'file1.cg'
lexer.next().fileName.should.equal 'file2.cg'
lexer.next().fileName.should.equal 'file3.cg'
describe 'line numbers are assigned correctly when', ->
it 'commands are listed one after another', ->
lexer = createLexer 'alpha: bravo\ncharlie: delta\necho: foxtrot'
lexer.next().lineNumber.should.equal 1
lexer.next().lineNumber.should.equal 2
lexer.next().lineNumber.should.equal 3
it 'commands are separated by blank lines and comments', ->
lexer = createLexer '# foo\n\nalpha: bravo\n\ncharlie: delta\n\n# bar\n\necho: foxtrot'
lexer.next().lineNumber.should.equal 3
lexer.next().lineNumber.should.equal 5
lexer.next().lineNumber.should.equal 9
it 'commands include hereDocs', ->
lexer = createLexer 'alpha: <<-END\nbravo charlie\nEND\ndelta: \n\necho: '
lexer.next().lineNumber.should.equal 1
lexer.next().lineNumber.should.equal 4
lexer.next().lineNumber.should.equal 6
it 'file markers are used', ->
lexer = createLexer '#FILE file1.cg\nalpha: \n#FILE file2.cg\n\nbravo: \n\n#FILE file3.cg\n\n\ncharlie: '
lexer.next().lineNumber.should.equal 1
lexer.next().lineNumber.should.equal 2
lexer.next().lineNumber.should.equal 3
+112
View File
@@ -0,0 +1,112 @@
#
# Crafting Guide - parser.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
LexicalAnalyzer = require './lexical_analyzer'
ModPack = require '../game/mod_pack'
ParserData = require './parser_data'
########################################################################################################################
module.exports = class Parser
constructor: (modPack, fileName, rawText)->
@_extensions = []
@_fileName = fileName
@_lexer = new LexicalAnalyzer fileName, rawText
@_modPack = new ModPack
@_data = new ParserData
modPackData = @_data.create {}, 'modPack'
modPackData.instance = @_modPack
@_loadSchema()
# Public Methods ###############################################################################
next: ->
if not @_lexer.isFinished
@_processNextCommand()
else if not @_data.isValidated
@_validateNextEntry()
else if not @_data.isBuilt
@_buildNextEntry()
else if not @_data.isAssembled
@_assembleNextEntry()
# Property Methods #############################################################################
getIsFinished: ->
return @_lexer.isFinished and @_data.isValidated and @_data.isBuilt and @_data.isAssembled
getData: ->
return @_data
getModPack: ->
return @_modPack
Object.defineProperties @prototype,
data: { get:@::getData }
isFinished: { get:@::getIsFinished }
modPack: { get:@::getModPack }
# Private Methods ##############################################################################
_assembleNextEntry: ->
entry = @_data.popNextToAssemble()
return unless entry?
for extensions in @_extensions
continue unless extension.canAssemble entry
extension.assemble entry
return
_buildNextEntry: ->
entry = @_data.popNextToBuild()
return unless entry?
for extensions in @_extensions
continue unless extension.canBuild entry
extension.build entry
return
_loadSchema: ->
command = @_lexer.next()
if command.name isnt 'schema' then throw new Error "#{@_fileName} must start with the \"schema\" command"
schema = command.argText
if schema is '1'
@_use new require './parser_extensions/current_pe_v1'
@_use new require './parser_extensions/item_pe_v1'
@_use new require './parser_extensions/mod_pe_v1'
@_use new require './parser_extensions/mod_version_pe_v1'
@_use new require './parser_extensions/recipe_pe_v1'
else
throw new Error "unknown schema version: #{schema}"
_processNextCommand: ->
command = @_lexer.next()
return unless command?
for extension in @_extensions
continue unless extension.canExecute command
extension.execute command
return
@_data.addError command, "unknown command: #{command.name}"
_use: (parserExtension)->
parserExtension.state = @_state
@_extensions.push parserExtension
_validateNextEntry: ->
entry = @_data.popNextToValidate()
return unless entry?
for extensions in @_extensions
continue unless extension.canValidate entry
extension.validate entry
return
@@ -0,0 +1,33 @@
#
# Crafting Guide - parser_command.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class ParserCommand
constructor: (options={})->
if not options.name? then throw 'options.name is required'
if not options.fileName? then throw new 'options.fileName is required'
if not options.lineNumber? then throw 'options.lineNumber is required'
{@name, @fileName, @lineNumber, @argText, @args, @hereDoc} = options
# Property Methods #############################################################################
getLocation: ->
return "#{@fileName}:#{@lineNumber}"
Object.defineProperties @prototype,
location: { get:@::getLocation }
# Object Overrides #############################################################################
toString: ->
result = "#{@fileName}:#{@lineNumber} #{@name}: #{@argText}"
if @hereDoc?
result += " <<- END\n#{@hereDoc}\nEND"
return result
@@ -0,0 +1,134 @@
#
# Crafting Guide - parser_data.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class ParserData
constructor: ->
@clear()
@_toAssemble = []
@_toBuild = []
@_toValidate = []
# Public Methods ###############################################################################
addError: (command, message)->
if not command? then throw new Error 'command is required'
message ?= 'is not valid'
@_errors.push command:command, message:message
create: (command, type, id=null)->
id ?= _.uniqueId "#{type}-"
@clearCurrent type
typeData = @_findOrCreateType type
itemData = @_current = typeData['current'] = typeData[id] = id:id, command:command, type:type
@_toAssemble.push itemData
@_toBuild.push itemData
@_toValidate.push itemData
return itemData
clear: ->
@_current = null
@_data =
mod: {}
modVersion: {}
tutorial: {}
itemGroup: {}
item: {}
multiblock: {}
recipe: {}
@_errors = []
clearCurrent: (type)->
switch type
when 'mod'
delete @_data.mod.current
delete @_data.modVersion.current
delete @_data.tutorial.current
delete @_data.itemGroup.current
delete @_data.item.current
delete @_data.multiblock.current
delete @_data.recipe.current
when 'modVersion'
delete @_data.modVersion.current
delete @_data.tutorial.current
delete @_data.itemGroup.current
delete @_data.item.current
delete @_data.multiblock.current
delete @_data.recipe.current
when 'tutorial'
delete @_data.tutorial.current
when 'itemGroup'
delete @_data.itemGroup.current
delete @_data.item.current
delete @_data.multiblock.current
delete @_data.recipe.current
when 'item'
delete @_data.item.current
delete @_data.multiblock.current
delete @_data.recipe.current
when 'multiblock'
delete @_data.multiblock.current
when 'recipe'
delete @_data.recipe.current
get: (type, id)->
typeData = @_findOrCreateType type
itemData = typeData[id] or null
return itemData
getCurrent: (type)->
return @_current unless type?
typeData = @_findOrCreateType type
current = typeData['current'] or null
return current
popNextToAssemble: ->
return @_toAssemble.pop() or null
popNextToBuild: ->
return @_toBuild.pop() or null
popNextToValidate: ->
return @_toValidate.pop() or null
# Property Methods #############################################################################
getErrors: ->
return @_errors[..]
getIsAssembled: ->
return @_toAssemble.length > 0
getIsBuilt: ->
return @_toBuild.length > 0
getIsValidated: ->
return @_toValidate.length > 0
Object.defineProperties @prototype,
errors: { get:@::getErrors }
isAssembled: { get:@::getIsAssembled }
isBuilt: { get:@::getIsBuilt }
isValidated: { get:@::getIsValidated }
# Private Methods ##############################################################################
_findOrCreateType: (type)->
typeData = @_data[type]
if not typeData?
typeData = @_data[type] = {}
typeData.type = type
return typeData
@@ -0,0 +1,55 @@
#
# Crafting Guide - parser_data.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserCommand = require './parser_command'
ParserData = require './parser_data'
########################################################################################################################
command = state = null
########################################################################################################################
describe 'parser_data.coffee', ->
beforeEach ->
state = new ParserData
command = new ParserCommand name:'alpha', fileName:'file.cg', lineNumber:1
describe 'create', ->
it 'returns an object with the given id', ->
alpha = state.create command, 'alpha', 0
alpha.id.should.equal 0
alpha.command.location.should.equal 'file.cg:1'
it 'returns an object with a newly created id when none is given', ->
alpha = state.create command, 'alpha'
alpha.id.should.not.be.null
alpha.id.should.match /^alpha-[0-9]+$/
alpha.command.location.should.equal 'file.cg:1'
describe 'get', ->
it 'returns a previously created item', ->
alpha1 = state.create command, 'alpha', 0
alpha2 = state.get 'alpha', 0
alpha1.should.equal alpha2
it 'returns null when no such item exists', ->
alpha = state.create command, 'alpha', 0
expect(state.get 'alpha', 1).to.be.null
expect(state.get 'bravo', 0).to.be.null
describe 'getCurrent', ->
it 'returns the new item each time one is created', ->
alpha1 = state.create command, 'alpha', 0
state.getCurrent('alpha').id.should.equal 0
alpha2 = state.create command, 'alpha', 1
state.getCurrent('alpha').id.should.equal 1
@@ -0,0 +1,165 @@
#
# Crafting Guide - parser_extension.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class ParserExtension
constructor: (data, type, commandFunctions)->
@data = data
@_commandFunctions = commandFunctions or {}
@_type = type or null
# Public Methods ###############################################################################
canAssemble: (entry)->
return entry.type is @_type
canBuild: (entry)->
return entry.type is @_type
canExecute: (command)->
_.isFunction @_commandFunctions[command.name]
canValidate: (entry)->
return entry.type is @_type
execute: (command)->
if not @_data? then throw new Error '@data must be defined before executing commands'
func = @_commandFunctions[command.name]
if not func then throw new Error "#{@constructor.name} cannot handle command: #{command.name}"
func.call this, command
return this
# Error Checking Helpers #######################################################################
alreadyExists: (command, type, id)->
obj = @data.get(type, id)
if obj?
@data.addError command, "a #{type} called #{id} has already been declared"
return true
return false
duplicateField: (command, obj, field)->
if obj[field]?
@data.addError command, "duplicate #{field}"
return true
return false
incompatibleField: (command, obj, field, otherCommandName)->
otherCommandName ?= field
if obj[field]?
@data.addError "#{command.name} cannot be used with #{otherCommandName}"
return true
return false
missingCurrent: (command, type)->
obj = @data.getCurrent type
if not obj?
@data.addError command, "you must declare a #{type} before using #{command.name}"
return true
return false
missingArgs: (command)->
if not command.args or command.args.length is 0
@data.addError command, "#{command.name} requires at least one item"
return true
return false
missingArgText: (command)->
if command.argText.length is 0
@data.addError command, "#{command.name} cannot be empty"
return true
return false
invalidVerb: (command, verb)->
if not verb in ['mod', 'item']
@data.addError command, "#{command.name} only applies to either `mod` or `item`"
return true
return false
tooFewArguments: (command, minimum)->
if command.args.length < minimum
@data.addError command, "#{command.name} requires at least #{minimum} arguments"
return true
return false
# Parsing Helpers ##############################################################################
parseBoolean: (command, text)->
return null unless text?
switch text.toLowerCase()
when "yes" then return true
when "no" then return false
@data.addError command, "#{command.name} requires \"yes\" or \"no\""
return null
parseInt: (command, text)->
return null unless text?
value = Number.parseInt text
if isNaN value
@addError command, "#{command.name} requires a number"
return null
return value
# Property Methods #############################################################################
getData: ->
return @_data
setData: (data)->
if not data? then throw new Error 'data cannot be undefined'
@_data = data
Object.defineProperties @prototype,
data: { get:@::getData, set:@::setData }
# Overridable Methods ##########################################################################
assmeble: (entry)->
throw new Error "#{@constructor.name} must override the `assemble` method"
build: (entry)->
throw new Error "#{@constructor.name} must override the `build` method"
validate: (entry)->
throw new Error "#{@constructor.name} must override the `validate` method"
# Private Methods ##############################################################################
_findBuildMethod: (entry)->
methodName = "_build_#{entry.type}"
method = this[methodName]
return null unless _.isFunction method
return method
_findCommandMethod: (command)->
methodName = "_command_#{command.name}"
method = this[methodName]
return null unless _.isFunction method
return method
_findValidateMethod: (command)->
methodName = "_validate_#{command.name}"
method = this[methodName]
return null unless _.isFunction method
return method
@@ -0,0 +1,50 @@
#
# 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
constructor: (data)->
super data, null,
description: @_command_description
name: @_command_name
officialUrl: @_command_officialUrl
video: @_command_video
# Command Methods ##############################################################################
_command_description: (command)->
current = @data.getCurrent()
return if @duplicateField command, current, 'description'
return if @missingArgText command
current.description = command.argText
_command_name: (command)->
current = @data.getCurrent()
return if @duplicateField command, current, 'name'
return if @missingArgText command
current.name = command.argText
_command_officialUrl: (command)->
current = @data.getCurrent()
return if @duplicateField command, current, 'officialUrl'
return if @missingArgText command
current.officialUrl = command.argText
_command_video: (command)->
current = @data.getCurrent()
return if @missingArgs command
return if @tooFewArguments command, 2
current.videos ?= []
current.videos.push youTubeId:command.args[0], caption:command.args[1..].join(', ')
@@ -0,0 +1,59 @@
#
# Crafting Guide - current_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CurrentParserExtensionV1 = require './current_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = state = null
########################################################################################################################
describe 'current_pe_v1.coffee', ->
beforeEach ->
state = new ParserData
parser = new CurrentParserExtensionV1 state
state.create {}, 'alpha'
describe 'description', ->
it 'assigns to the current object', ->
parser.execute name:'description', argText:'bravo'
state.getCurrent().description.should.equal 'bravo'
state.errors.should.eql []
describe 'name', ->
it 'assigns to the current object', ->
parser.execute name:'name', argText:'bravo'
state.getCurrent().name.should.equal 'bravo'
state.errors.should.eql []
describe 'officialUrl', ->
it 'assigns to the current object', ->
parser.execute name:'officialUrl', argText:'bravo'
state.getCurrent().officialUrl.should.equal 'bravo'
state.errors.should.eql []
describe 'video', ->
it 'assigns to the current object', ->
parser.execute name:'video', args:['alpha', 'bravo']
videos = state.getCurrent().videos
videos[0].youTubeId.should.equal 'alpha'
videos[0].caption.should.equal 'bravo'
state.errors.should.eql []
it 'can assign multiple videos', ->
parser.execute name:'video', args:['alpha', 'bravo']
parser.execute name:'video', args:['charlie', 'delta']
state.getCurrent().videos.length.should.equal 2
state.errors.should.eql []
@@ -0,0 +1,67 @@
#
# 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
constructor: (data)->
super data, 'item',
gatherable: @_command_gatherable
item: @_command_item
update: @_command_update
# ParserExtensions Overrides ###################################################################
assmeble: (entry)->
throw new Error "#{@constructor.name} must override the `assemble` method"
build: (entry)->
entry.instance = new
throw new Error "#{@constructor.name} must override the `build` method"
validate: (entry)-> # do nothing
# Command Methods ##############################################################################
_command_gatherable: (command)->
return if @missingArgText command
return if @missingCurrent command, 'item'
item = @data.getCurrent 'item'
return if @duplicateField command, item, 'gatherable'
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 = @data.create command, 'item', command.argText
item.name = command.argText
item.isUpdate = false
group = @data.getCurrent 'itemGroup'
if group? then item.group = group
item.modVersion = @data.getCurrent 'modVersion'
_command_update: (command)->
return if @missingArgText command
return if @missingCurrent command, 'modVersion'
return if @alreadyExists command, 'item', command.argText
item = @data.create command, 'item', command.argText
item.name = command.argText
item.isUpdate = true
item.modVersion = @data.getCurrent 'modVersion'
@@ -0,0 +1,54 @@
#
# Crafting Guide - item_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ItemParserExtensionV1 = require './item_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = data = null
########################################################################################################################
describe 'item_pe_v1.coffee', ->
beforeEach ->
data = new ParserData
parser = new ItemParserExtensionV1 data
data.create {}, 'modVersion', 0
describe 'gatherable', ->
it 'assigns to the current item', ->
data.create {}, 'item', 1
parser.execute name:'gatherable', argText:'yes'
data.getCurrent('item').gatherable.should.be.true
data.errors.should.eql []
describe 'item', ->
it 'creates a new item', ->
data.create {}, 'itemGroup', 2
parser.execute name:'item', argText:'alpha'
item = data.getCurrent 'item'
item.id.should.equal 'alpha'
item.isUpdate.should.equal false
item.name.should.equal 'alpha'
item.modVersion.id.should.equal 0
item.group.id.should.equal 2
data.getErrors().should.eql []
describe 'update', ->
it 'creates an item update', ->
parser.execute name:'update', argText:'alpha'
item = data.getCurrent 'item'
item.isUpdate.should.equal true
data.getErrors().should.eql []
@@ -0,0 +1,62 @@
#
# 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
constructor: (data)->
super data, 'mod',
author: @_command_author
documentationUrl: @_command_documentationUrl
downloadUrl: @_command_downloadUrl
homePageUrl: @_command_homePageUrl
mod: @_command_mod
# Command Methods ##############################################################################
_command_author: (command)->
return if @missingCurrent command, 'mod'
mod = @data.getCurrent 'mod'
return if @duplicateField command, mod, 'author'
return if @missingArgText command
mod.author = command.argText
_command_documentationUrl: (command)->
current = @data.getCurrent()
return if @duplicateField command, current, 'documentationUrl'
return if @missingArgText command
current.documentationUrl = command.argText
_command_downloadUrl: (command)->
return if @missingCurrent command, 'mod'
mod = @data.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 = @data.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
@data.create command, 'mod', command.argText
@@ -0,0 +1,58 @@
#
# Crafting Guide - mod_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ModParserExtensionV1 = require './mod_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = data = null
########################################################################################################################
describe 'mod_pe_v1.coffee', ->
beforeEach ->
data = new ParserData
parser = new ModParserExtensionV1 data
data.create {}, 'mod', 0
describe 'author', ->
it 'assigns to the current mod', ->
parser.execute name:'author', argText:'alpha'
data.getCurrent('mod').author.should.equal 'alpha'
data.errors.should.eql []
describe 'documentationUrl', ->
it 'assigns to the current mod', ->
parser.execute name:'documentationUrl', argText:'alpha'
data.getCurrent('mod').documentationUrl.should.equal 'alpha'
data.errors.should.eql []
describe 'downloadUrl', ->
it 'assigns to the current mod', ->
parser.execute name:'downloadUrl', argText:'alpha'
data.getCurrent('mod').downloadUrl.should.equal 'alpha'
data.errors.should.eql []
describe 'homePageUrl', ->
it 'assigns to the current mod', ->
parser.execute name:'homePageUrl', argText:'alpha'
data.getCurrent('mod').homePageUrl.should.equal 'alpha'
data.errors.should.eql []
describe 'mod', ->
it 'creates a new mod', ->
parser.execute name:'mod', argText:'alpha'
mod = data.getCurrent 'mod'
mod.id.should.equal 'alpha'
@@ -0,0 +1,33 @@
#
# 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
constructor: (data)->
super data, 'modVersion',
group: @_command_group
version: @_command_version
# Command Methods ##############################################################################
_command_group: (command)->
return if @missingArgText command
return if @missingCurrent command, 'modVersion'
group = @data.create command, 'itemGroup', command.argText
group.modVersion = @data.getCurrent 'modVersion'
_command_version: (command)->
return if @missingArgText command
return if @missingCurrent command, 'mod'
modVersion = @data.create command, 'modVersion', command.argText
modVersion.mod = @data.getCurrent 'mod'
@@ -0,0 +1,41 @@
#
# Crafting Guide - mod_version_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ModVersionParserExtensionV1 = require './mod_version_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = data = null
########################################################################################################################
describe 'mod_version_pe_v1.coffee', ->
beforeEach ->
data = new ParserData
parser = new ModVersionParserExtensionV1 data
data.create {}, 'mod', 0
describe 'group', ->
it 'creates a new item group', ->
data.create {}, 'modVersion', 1
parser.execute name:'group', argText:'alpha'
itemGroup = data.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'
modVersion = data.getCurrent 'modVersion'
modVersion.id.should.equal 'alpha'
modVersion.mod.id.should.equal 0
@@ -0,0 +1,38 @@
#
# Crafting Guide - multiblock_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class MultiblockParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, 'multiblock',
layer: @_command_layer
multiblock: @_command_multiblock
# Command Methods ##############################################################################
_command_layer: (command)->
return if @missingCurrent command, 'multiblock'
return if @missingArgText command
multiblock = @data.getCurrent 'multiblock'
multiblock.layers ?= []
multiblock.layers.push command.argText
_command_multiblock: (command)->
return if @missingCurrent command, 'item'
item = @data.getCurrent 'item'
return if @duplicateField command, item, 'multiblock'
return if @incompatibleField command, item, 'recipe'
return if @incompatibleField command, item, 'gatherable'
multiblock = @data.create command, 'multiblock'
multiblock.item = item
@@ -0,0 +1,48 @@
#
# Crafting Guide - multiblock_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
MultiblockParserExtensionV1 = require './multiblock_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = data = null
########################################################################################################################
describe 'multiblock_pe_v1.coffee', ->
beforeEach ->
data = new ParserData
parser = new MultiblockParserExtensionV1 data
data.create {}, 'item', 1
describe 'layer', ->
it 'creates a new layer in the current multiblock', ->
data.create {}, 'multiblock', 2
parser.execute name:'layer', argText:'alpha'
multiblock = data.getCurrent 'multiblock'
multiblock.layers.length.should.equal 1
multiblock.layers[0].should.equal 'alpha'
it 'can create multiple layers', ->
data.create {}, 'multiblock', 2
parser.execute name:'layer', argText:'alpha'
parser.execute name:'layer', argText:'bravo'
multiblock = data.getCurrent 'multiblock'
multiblock.layers.should.eql ['alpha', 'bravo']
describe 'multiblock', ->
it 'creates a new multiblock', ->
parser.execute name:'multiblock'
multiblock = data.getCurrent 'multiblock'
multiblock.item.id.should.equal 1
@@ -0,0 +1,143 @@
#
# 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
constructor: (data)->
super data, 'recipe',
extras: @_command_extras
ignoreDuringCrafting: @_command_ignoreDuringCrafting
input: @_command_input
onlyIf: @_command_onlyIf
pattern: @_command_pattern
recipe: @_command_recipe
quantity: @_command_quantity
tools: @_command_tools
# Class Methods ################################################################################
@::PATTERN = /[0-9. ]+/
@::STACK = /^([0-9]+) +(.+)$/
# Error Checking Helpers #######################################################################
invalidPattern: (command)->
match = command.argText.match @PATTERN
if not match?
@data.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 = @data.getCurrent 'recipe'
return if @duplicateField command, 'extras', recipe
recipe.extras = command.args
_command_ignoreDuringCrafting: (command)->
return if @missingArgText command
return if @missingCurrent command, 'recipe'
recipe = @data.getCurrent 'recipe'
return if @duplicateField command, recipe, 'ignoreDuringCrafting'
ignoreDuringCrafting = @parseBoolean command, command.argText
return unless ignoreDuringCrafting?
recipe.ignoreDuringCrafting = ignoreDuringCrafting
_command_input: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgs command
recipe = @data.getCurrent 'recipe'
multiblock = @data.getCurrent 'multiblock'
target = if multiblock? then multiblock else recipe
return if @duplicateField command, 'input', target
target.input = (@_parseStack(arg) for arg in command.args)
_command_onlyIf: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgText command
recipe = @data.getCurrent 'recipe'
return if @duplicateField command, recipe, 'condition'
words = command.argText.split ' '
inverted = false
if words[0] is 'not'
inverted = true
words.shift()
verb = words[0]
noun = words[1..].join ' '
return if @invalidVerb command, verb
recipe.condition = verb:verb, noun:noun, inverted:inverted
_command_pattern: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgText command
return if @invalidPattern command
recipe = @data.getCurrent 'recipe'
return if @duplicateField command, 'pattern', recipe
recipe.pattern = command.argText
_command_quantity: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgText command
value = @parseInt command, command.argText
return unless value?
recipe = @data.getCurrent 'recipe'
return if @duplicateField command, 'quantity', recipe
recipe.quantity = value
_command_recipe: (command)->
return if @missingCurrent command, 'item'
recipe = @data.create command, 'recipe'
recipe.item = @data.getCurrent 'item'
_command_tools: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgs command
recipe = @data.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,125 @@
#
# Crafting Guide - recipe_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
RecipeParserExtensionV1 = require './recipe_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = data = null
########################################################################################################################
describe 'recipe_pe_v1.coffee', ->
beforeEach ->
data = new ParserData
parser = new RecipeParserExtensionV1 data
data.create {}, 'item', 0
describe 'extras', ->
it 'assigns to the current recipe', ->
data.create {}, 'recipe'
parser.execute name:'extras', args:['bravo']
recipe = data.getCurrent 'recipe'
recipe.extras.should.eql ['bravo']
data.errors.should.eql []
describe 'ignoreDuringCrafting', ->
it 'assigns to the current recipe', ->
data.create {}, 'recipe'
parser.execute name:'ignoreDuringCrafting', argText:'yes'
recipe = data.getCurrent 'recipe'
recipe.ignoreDuringCrafting.should.equal true
data.errors.should.eql []
describe 'input', ->
it 'correctly reads a single item', ->
data.create {}, 'recipe'
parser.execute name:'input', args:['bravo']
recipe = data.getCurrent 'recipe'
recipe.input.should.eql [name:'bravo', quantity:1]
data.errors.should.eql []
it 'correctly reads multiple items', ->
data.create {}, 'recipe'
parser.execute name:'input', args:['bravo', 'charlie']
recipe = data.getCurrent 'recipe'
recipe.input.should.eql [{name:'bravo', quantity:1}, {name:'charlie', quantity:1}]
data.errors.should.eql []
it 'correctly reads a stack', ->
data.create {}, 'recipe'
parser.execute name:'input', args:['10 bravo']
recipe = data.getCurrent 'recipe'
recipe.input.should.eql [name:'bravo', quantity:10]
data.errors.should.eql []
describe 'onlyIf', ->
it 'assigns to the current recipe', ->
data.create {}, 'recipe'
parser.execute name:'onlyIf', argText:'not mod Alpha'
recipe = data.getCurrent 'recipe'
recipe.condition.should.eql verb:'mod', noun:'Alpha', inverted:true
data.errors.should.eql []
describe 'pattern', ->
it 'assigns to the current recipe', ->
data.create {}, 'recipe'
parser.execute name:'pattern', argText:'00. 00. ...'
recipe = data.getCurrent 'recipe'
recipe.pattern.should.equal '00. 00. ...'
data.errors.should.eql []
it 'rejects an invalid pattern', ->
data.create {}, 'recipe'
parser.execute name:'pattern', argText:'alpha'
recipe = data.getCurrent 'recipe'
expect(recipe.pattern).to.be.undefined
(e.message for e in data.errors).should.eql ['invalid pattern: "alpha"']
describe 'quantity', ->
it 'assigns to the current recipe', ->
data.create {}, 'recipe'
parser.execute name:'quantity', argText:'42'
recipe = data.getCurrent 'recipe'
recipe.quantity.should.equal 42
data.errors.should.eql []
describe 'recipe', ->
it 'creates a new recipe', ->
parser.execute name:'recipe'
recipe = data.getCurrent 'recipe'
recipe.item.id.should.equal 0
data.errors.should.eql []
describe 'tools', ->
it 'assigns to the current recipe', ->
data.create {}, 'recipe'
parser.execute name:'tools', args:['bravo']
recipe = data.getCurrent 'recipe'
recipe.tools.should.eql [name:'bravo', quantity:1]
data.errors.should.eql []
@@ -0,0 +1,51 @@
#
# Crafting Guide - tutorial_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class TutorialParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, 'tutorial',
content: @_command_content
section: @_command_section
title: @_command_title
tutorial: @_command_tutorial
# Command Methods ##############################################################################
_command_content: (command)->
return if @missingCurrent command, 'tutorial'
return if @missingArgText command
section = @data.getCurrent 'section'
section.content = command.argText
_command_section: (command)->
return if @missingCurrent command, 'tutorial'
section = @data.create command, 'section'
section.tutorial = @data.getCurrent 'tutorial'
_command_title: (command)->
return if @missingCurrent command, 'section'
return if @missingArgText command
section = @data.getCurrent 'section'
return if @duplicateField command, section, 'title'
section.title = command.argText
_command_tutorial: (command)->
return if @missingCurrent command, 'modVersion'
return if @missingArgText command
tutorial = @data.create command, 'tutorial', command.argText
tutorial.name = command.argText
tutorial.modVersion = @data.getCurrent 'modVersion'
@@ -0,0 +1,63 @@
#
# Crafting Guide - tutorial_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
TutorialParserExtensionV1 = require './tutorial_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = data = null
########################################################################################################################
describe 'tutorial_pe_v1.coffee', ->
beforeEach ->
data = new ParserData
parser = new TutorialParserExtensionV1 data
data.create {}, 'modVersion', 1
describe 'content', ->
it 'assigns content to the current section', ->
data.create {}, 'tutorial', 2
data.create {}, 'section', 3
parser.execute name:'content', argText:'alpha'
data.errors.should.eql []
section = data.getCurrent 'section'
section.content.should.equal 'alpha'
describe 'section', ->
it 'creates a section in the current tutorial', ->
data.create {}, 'tutorial', 2
parser.execute name:'section'
data.errors.should.eql []
section = data.getCurrent 'section'
section.tutorial.id.should.equal 2
describe 'title', ->
it 'assigns a title to the current tutorial', ->
data.create {}, 'tutorial', 2
data.create {}, 'section', 3
parser.execute name:'title', argText:'alpha'
data.errors.should.eql []
section = data.getCurrent 'section'
section.title.should.equal 'alpha'
describe 'tutorial', ->
it 'creates a new tutorial', ->
parser.execute name:'tutorial', argText:'alpha'
data.errors.should.eql []
tutorial = data.getCurrent 'tutorial'
tutorial.name.should.equal 'alpha'