diff --git a/Gruntfile.coffee b/Gruntfile.coffee index a468c0a8b..4be0962df 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -133,17 +133,18 @@ module.exports = (grunt)-> tasks: ['browserify:dev', 'exorcise'] jade: files: ['./src/**/*.jade'] - tasks: ['jade:pages', 'jade:templates', 'browserify:dev', 'exorcise'] + tasks: ['jade:pages', 'jade:templates'] sass: files: ['./src/**/*.scss'] tasks: ['sass', 'copy:styles'] + test: + files: ['./src/**/*.coffee', './src/**/*.js', './test/**/*.coffee'] + tasks: ['test'] grunt.registerTask 'default', 'build' grunt.registerTask 'build', ['rsync', 'sass:build', 'jade', 'copy', 'browserify:dev', 'exorcise'] - grunt.registerTask 'clean-watch', ['clean', 'build', 'watch'] - grunt.registerTask 'dist', ['rsync', 'sass:dist', 'jade', 'copy', 'browserify:prod', 'uglify'] grunt.registerTask 'prepublish', ['clean', 'coffee'] @@ -155,6 +156,10 @@ module.exports = (grunt)-> grunt.registerTask 'test', ['mochaTest'] + grunt.registerTask 'watch-dev', ['clean', 'build', 'watch'] + + grunt.registerTask 'watch-test', ['clean', 'build', 'watch:test'] + args = process.argv[..] while args.length > 0 switch args[0] diff --git a/design/pages.graffle b/design/pages.graffle index a73a2d942..6516509e2 100644 Binary files a/design/pages.graffle and b/design/pages.graffle differ diff --git a/src/coffee/constants.coffee b/src/coffee/constants.coffee index c3a28ea18..7dcf4a3bb 100644 --- a/src/coffee/constants.coffee +++ b/src/coffee/constants.coffee @@ -66,7 +66,7 @@ Event.route = 'route' Event.sort = 'sort' Event.sync = 'sync' # model, response Event.transitionEnd = (-> - return unless document? + return 'transitionend' unless document? transitions = 'WebkitTransition': 'webkitTransitionEnd' 'MozTransition': 'transitionend' @@ -88,6 +88,10 @@ Key.Escape = 27 Key.UpArrow = 38 Key.DownArrow = 40 +exports.GitHub = GitHub = {} +GitHub.file = {} +GitHub.file.itemDescription = _.template "/data/<%= modSlug %>/items/<%= itemSlug %>/item.cg" + exports.Login = Login = {} Login.authorizeUrl = _.template "https://github.com/login/oauth/authorize" + "?client_id=<%= clientId %>&scope=public_repo&state=<%= state %>" diff --git a/src/coffee/controllers/base_controller.coffee b/src/coffee/controllers/base_controller.coffee index 7208e93ed..54876e7c0 100644 --- a/src/coffee/controllers/base_controller.coffee +++ b/src/coffee/controllers/base_controller.coffee @@ -49,14 +49,17 @@ module.exports = class BaseController extends backbone.View if not ($el.hasClass('hiding') or $el.hasClass('hidden')) logger.verbose => "#{this} is hiding \"#{$el.selector}\"" - @_onAnimationComplete $el, Duration.normal, => - $el.addClass 'hidden' - $el.removeClass 'hiding' - logger.verbose => "#{this} is finished hiding #{$el.selector}" - @trigger Event.animate.hide.finish, this, $el - $el.addClass 'hiding' @trigger Event.animate.hide.start, this, $el + @_onAnimationComplete $el, Duration.normal, => @hide $el + return + + if $el.hasClass('hiding') and not $el.hasClass('hidden') + $el.removeClass 'hiding' + $el.addClass 'hidden' + logger.verbose => "#{this} is finished hiding #{$el.selector}" + + @trigger Event.animate.hide.finish, this, $el refresh: -> logger.verbose => "#{this} refreshing" @@ -86,20 +89,27 @@ module.exports = class BaseController extends backbone.View show: ($el)-> $el ?= @$el - $el.addClass 'hideable' - if $el.hasClass('hidden') or $el.hasClass('hiding') + if not $el.hasClass 'hideable' + $el.addClass 'hideable' + _.defer => @show $el + return + + if $el.hasClass 'hidden' logger.verbose => "#{this} is showing \"#{$el.selector}\"" - - @_onAnimationComplete $el, Duration.normal, => - logger.verbose => "#{this} is finished showing #{$el.selector}" - @trigger Event.animate.show.finish, this, $el - - $el.addClass 'hiding' $el.removeClass 'hidden' - _.defer => - $el.removeClass 'hiding' - @trigger Event.animate.show.start, this, $el + $el.addClass 'hiding' + @trigger Event.animate.show.start, this, $el + _.defer => @show $el + return + + if $el.hasClass 'hiding' + $el.removeClass 'hiding' + @_onAnimationComplete $el, Duration.normal, => @show $el + return + + logger.verbose => "#{this} is finished showing #{$el.selector}" + @trigger Event.animate.show.finish, this, $el unrender: -> @undelegateEvents() @@ -153,9 +163,9 @@ module.exports = class BaseController extends backbone.View @tryRefresh() Object.defineProperties @prototype, - model: {get:@prototype.getModel, set:@prototype.setModel} + model: {get:@prototype.getModel, set:@prototype.setModel} rendered: {get:@prototype.isRendered } - user: {get:@prototype.user, set:@prototype.setUser} + user: {get:@prototype.getUser, set:@prototype.setUser} # Backbone.View Overrides ###################################################################### diff --git a/src/coffee/controllers/item_page_controller.coffee b/src/coffee/controllers/item_page_controller.coffee index 25c1f0f5c..d45a43b5b 100644 --- a/src/coffee/controllers/item_page_controller.coffee +++ b/src/coffee/controllers/item_page_controller.coffee @@ -16,8 +16,10 @@ MarkdownSectionController = require './markdown_section_controller' PageController = require './page_controller' VideoController = require './video_controller' _ = require 'underscore' +w = require 'when' {Duration} = require '../constants' {Event} = require '../constants' +{GitHub} = require '../constants' {Text} = require '../constants' {Url} = require '../constants' @@ -26,6 +28,7 @@ _ = require 'underscore' module.exports = class ItemPageController extends PageController constructor: (options={})-> + if not options.client? then throw new Error 'options.client is required' if not options.itemSlug? then throw new Error 'options.itemSlug is required' if not options.imageLoader? then throw new Error 'options.imageLoader is required' if not options.modPack? then throw new Error 'options.modPack is required' @@ -35,9 +38,12 @@ module.exports = class ItemPageController extends PageController super options + @client = options.client @imageLoader = options.imageLoader @modPack = options.modPack - @_itemSlug = options.itemSlug + + @_editingFile = null + @_itemSlug = options.itemSlug @modPack.on Event.change, => @tryRefresh() @@ -59,11 +65,16 @@ module.exports = class ItemPageController extends PageController @adsenseController = @addChild AdsenseController, '.view__adsense', model:'sidebar_skyscraper' options = imageLoader:@imageLoader, modPack:@modPack, show:false - @_descriptionController = @addChild MarkdownSectionController, '.section.description', options @_similarItemsController = @addChild ItemGroupController, '.view__item_group.similar', options @_usedAsToolToMakeController = @addChild ItemGroupController, '.view__item_group.usedAsToolToMake', options @_usedToMakeController = @addChild ItemGroupController, '.view__item_group.usedToMake', options + @_descriptionController = @addChild MarkdownSectionController, '.section.description', + editable: true + modPack: @modPack + beginEditing: => @_beginEditingDescription() + endEditing: => @_endEditingDescription() + @$byline = @$('.byline') @$bylineLink = @$('.byline a') @$descriptionSection = @$('.description') @@ -111,17 +122,54 @@ module.exports = class ItemPageController extends PageController super + setUser: (user)-> + super user + @_descriptionController.user = user if @_descriptionController? + # Backbone.View Overrides ###################################################################### events: -> return _.extend super, - 'click a.craftingPlan': 'routeLinkClick' - 'click .byline a': 'routeLinkClick' - 'click .markdown a': 'routeLinkClick' - 'click button': 'craftingPlanButtonClicked' + 'click a.craftingPlan': 'routeLinkClick' + 'click .byline a': 'routeLinkClick' + 'click .markdown a': 'routeLinkClick' + 'click button.craftingPlan': 'craftingPlanButtonClicked' # Private Methods ############################################################################## + _beginEditingDescription: -> + if not global.router.user? + global.router.login() + return w.reject new Error 'must be logged in to edit' + + if not @model.item? + return w.reject new Error 'must have an item' + + @client.fetchFile path:GitHub.file.itemDescription modSlug:@_itemSlug.mod, itemSlug:@_itemSlug.item + .then (response)=> + @_editingFile = response.json.data + if @_editingFile.content.length > 0 + @model.item.parse @_editingFile.content + else + @model.item.description = '' + + @_descriptionController.model = @model.item.description + + _endEditingDescription: -> + oldDescription = @model.item.description + @model.item.description = @_descriptionController.model + + args = + content: @model.item.unparse() + message: "User-submitted text for #{@model.item.name}" + path: GitHub.file.itemDescription modSlug:@_itemSlug.mod, itemSlug:@_itemSlug.item + sha: @_editingFile.sha + + @client.updateFile args + .catch (e)=> + @model.item.description = oldDescription + throw e + _refreshByline: -> mod = @model.item?.modVersion?.mod if mod?.name?.length > 0 @@ -136,6 +184,8 @@ module.exports = class ItemPageController extends PageController if @model.item?.description?.length > 0 @_descriptionController.model = @model.item.description @show @$descriptionSection + else if @user? + @show @$descriptionSection else @hide @$descriptionSection diff --git a/src/coffee/controllers/login_page_controller.coffee b/src/coffee/controllers/login_page_controller.coffee index 235d31069..1e3fdb9b6 100644 --- a/src/coffee/controllers/login_page_controller.coffee +++ b/src/coffee/controllers/login_page_controller.coffee @@ -77,7 +77,7 @@ module.exports = class LoginPageController extends PageController attributes = response.json.data.user if attributes? router.user = new User attributes - router.navigate Url.root(), trigger:true + router.resumeAfterLogin() .catch (error)-> logger.error "Failed to get access token: #{error}" @_accessTokenLookupFailed = true @@ -120,9 +120,9 @@ module.exports = class LoginPageController extends PageController @client.completeGitHubLogin code:code .then (response)=> router.user = new User response.json.user - .catch (error)-> + .catch (error)=> @_accessTokenLookupFailed = true - @refresh + @refresh() .done() _redirectToGitHub: -> diff --git a/src/coffee/controllers/markdown_section_controller.coffee b/src/coffee/controllers/markdown_section_controller.coffee index 2226ab5bc..166d8f20b 100644 --- a/src/coffee/controllers/markdown_section_controller.coffee +++ b/src/coffee/controllers/markdown_section_controller.coffee @@ -8,48 +8,153 @@ All rights reserved. BaseController = require './base_controller' convertMarkdown = require 'marked' _ = require 'underscore' +{Event} = require '../constants' {Url} = require '../constants' +w = require 'when' ######################################################################################################################## module.exports = class MarkdownSectionController extends BaseController + @State = State = + appologizing: 'applogizing' + confirming: 'confirming' + creating: 'creating' + editing: 'editing' + previewing: 'previewing' + viewing: 'viewing' + waiting: 'waiting' + constructor: (options={})-> if not options.modPack? then throw new Error 'options.modPack is required' - options.imageBase ?= '' - options.model ?= '' - options.title ?= 'Description' + options.model ?= null options.templateName = 'markdown_section' + @_state = State.waiting super options - @imageBase = options.imageBase - @modPack = options.modPack - @title = options.title + @on Event.animate.hide.finish, => @_updateStateVisibility() + @on Event.animate.show.finish, => @_updateStateVisibility() + + @confirmactionMessage = options.confirmationMessage + @confirmDuration = options.confirmationDuration ?= 5000 + @imageBase = options.imageBase ?= '' + @modPack = options.modPack + @title = options.title ?= 'Description' + + # @_beginEditing must be a function returning a promise which resolves when all actions necessary prior to an + # editing session have been completed (e.g., loading the latest content from a remote server). The promise must + # reject if an error occured which should prevent editing. + @_beginEditing = options.beginEditing + + # @_endEditing must be a function returning a promise which resolves when the model of this controller has been + # saved however is appropriate. The promise must reject if saving failed for some reason. + @_endEditing = options.endEditing + + # Event Methods ################################################################################ + + onCancelClicked: (event)-> + event.preventDefault() + @model = @_originalModel + @_resetToDefaultState() + + onEditClicked: (event)-> + event.preventDefault() + return unless @editable + + @_originalModel = @model + @state = State.waiting + @_beginEditing() + .then => + @state = State.editing + .catch (e)=> + logger.warning "cannot begin editing: #{e.stack}" + @state = State.appologizing + w(true).delay(@confirmDuration).then => @_resetToDefaultState() + + onPreviewClicked: (event)-> + event.preventDefault() + @state = State.previewing + + onReturnClicked: (event)-> + event.preventDefault() + @state = State.editing + + onSaveClicked: (event)-> + event.preventDefault() + + @state = State.waiting + @_endEditing() + .then => + @state = State.confirming + .catch (e)=> + logger.error "failed to end editing: #{e}" + @state = State.appologizing + .delay @confirmDuration + .then => + @state = State.viewing + + onTextChanged: (event)-> + event.preventDefault() + @model = @$textarea.val() + + # Property Methods ############################################################################# + + isEditable: -> + return _.isFunction(@_beginEditing) and _.isFunction(@_endEditing) + + getState: -> + return @_state + + setState: (newState)-> + oldState = @_state + return if oldState is newState + @_state = newState + + logger.verbose => "MarkdownSectionController.#{@cid} changed state from #{oldState} to #{newState}" + @trigger Event.change + ':state', this, oldState, newState + @_updateStateVisibility() + + Object.defineProperties @prototype, + editable: {get:@prototype.isEditable} + state: {get:@prototype.getState, set:@prototype.setState} # BaseController Overrides ##################################################################### onDidRender: -> - @$title = @$('h2') + @$errorText = @$('.error p') @$markdownPanel = @$('.markdown') + @$sizer = @$('.sizer') + @$textarea = @$('textarea') + @$title = @$('h2') + + @_resetToDefaultState() super refresh: -> @$title.html @title - text = @model - text = @_convertWikiLinks text - text = @_convertImageLinks text - - @$markdownPanel.html convertMarkdown text - + @_updateSizer() + @_updatePreview() super + onWillChangeModel: (oldModel, newModel)-> + result = super oldModel, newModel + @$textarea.val newModel + + return result + # Backbone.View Overrides ###################################################################### events: -> return _.extend super, - 'click .markdown a': 'routeLinkClick' + 'click .markdown a': 'routeLinkClick' + 'click button.cancel': 'onCancelClicked' + 'click button.edit': 'onEditClicked' + 'click button.preview': 'onPreviewClicked' + 'click button.return': 'onReturnClicked' + 'click button.save': 'onSaveClicked' + 'input textarea': 'onTextChanged' # Private Methods ############################################################################## @@ -66,3 +171,81 @@ module.exports = class MarkdownSectionController extends BaseController result = "[#{name}](#{display.itemUrl})" return result + + _updateSizer: -> + text = @model + if @model? + text = text.replace /\n/g, '
' + + @$sizer.html text + + _updatePreview: -> + text = @model + if @model? + text = @_convertWikiLinks text + text = @_convertImageLinks text + text = convertMarkdown text + + @$markdownPanel.html text + + _resetToDefaultState: -> + if @model? + @state = State.viewing + else + if @editable? + @state = State.creating + else + @state = State.waiting + + + _updateStateVisibility: -> + return if @_lastUpdatedState is @state + @_lastUpdatedState = @state + + elements = + appologizingPanel: @$('.appologizing') + buttonPanel: @$('.buttons') + cancelButton: @$('button.cancel') + confirmingPanel: @$('.confirming') + creatingPanel: @$('.creating') + editButton: @$('button.edit') + editorPanel: @$('.editor') + errorPanel: @$('.error') + markdownPanel: @$markdownPanel + previewButton: @$('button.preview') + returnButton: @$('button.return') + saveButton: @$('button.save') + waitingPanel: @$('.waiting') + + visible = {} + errorText = '' + + if @state is State.appologizing + visible = appologizingPanel:true + else if @state is State.confirming + visible = confirmingPanel:true + else if @state is State.creating + visible = buttonPanel:true, creatingPanel:true, editButton:true + else if @state is State.editing + visible = buttonPanel:true, cancelButton:true, editorPanel:true, previewButton:true, saveButton:true + else if @state is State.previewing + visible = buttonPanel:true, errorPanel:true, markdownPanel:true, returnButton:true + errorText = "remember: your changes aren't saved yet!" + else if @state is State.viewing + visible = markdownPanel:true + if @editable then _.extend visible, {buttonPanel:true, editButton:true} + else # assume any unknown state is the same as "waiting" + visible = waitingPanel:true + + toHide = ($el for name, $el of elements when not visible[name]) + toShow = ($el for name, $el of elements when visible[name]) + + if toHide.length > 0 + @hide $el for $el in toHide + if toShow.length > 0 + @once Event.animate.hide.finish, => + @show $el for $el in toShow + else if toShow.length > 0 + @show $el for $el in toShow + + @$errorText.html errorText diff --git a/src/coffee/crafting_guide_router.coffee b/src/coffee/crafting_guide_router.coffee index a05ef9479..64ff616d4 100644 --- a/src/coffee/crafting_guide_router.coffee +++ b/src/coffee/crafting_guide_router.coffee @@ -36,9 +36,10 @@ backbone = require 'backbone' module.exports = class CraftingGuideRouter extends backbone.Router constructor: (options={})-> + @_lastReported = null @_page = null @_pageControllers = {} - @_lastReported = null + @_postLoginUrl = Url.root @_user = null super options @@ -80,6 +81,16 @@ module.exports = class CraftingGuideRouter extends backbone.Router logger.info "User is not logged in" .done() + login: -> + @storage.store 'post-login-url', window.location.pathname + @navigate Url.login(), trigger:true + + resumeAfterLogin: -> + url = @storage.load 'post-login-url' + url ?= Url.root() + @navigate url, trigger:true + @storage.store 'post-login-url', null + # Property Methods ############################################################################# getUser: -> diff --git a/src/coffee/models/item.coffee b/src/coffee/models/item.coffee index 444962f77..590adb456 100644 --- a/src/coffee/models/item.coffee +++ b/src/coffee/models/item.coffee @@ -45,6 +45,11 @@ module.exports = class Item extends BaseModel return if this.name < that.name then -1 else +1 return 0 + unparse: -> + ItemParser = require './item_parser' # to avoid require cycles + @_parser ?= new ItemParser model:this + return @_parser.unparse() + # Property Methods ############################################################################# getIsCraftable: -> diff --git a/src/coffee/models/parser_versions/item_parser_v1.coffee b/src/coffee/models/parser_versions/item_parser_v1.coffee index 251bce482..6cc05e8c2 100644 --- a/src/coffee/models/parser_versions/item_parser_v1.coffee +++ b/src/coffee/models/parser_versions/item_parser_v1.coffee @@ -17,17 +17,23 @@ module.exports = class ItemParserV1 extends CommandParserVersionBase @_buildItem rawData, model _unparseModel: (builder, model)-> + builder.line 'schema: ', 1 + builder.line() + @_unparseItem builder, model # Command Methods ############################################################################## _command_description: (textParts...)-> - @_rawData.description ?= '' + if not @_rawData.description? + @_rawData.description = '' + else + @_rawData.description += '\n' @_rawData.description += textParts.join ', ' _command_officialUrl: (officialUrl)-> if @_rawData.officialUrl? then throw new Error 'duplicate declaration of "officialUrl"' - if officialUrl.length is 0 then throw new Error 'officialUrl cannot be empty' + if not officialUrl? or (officialUrl.length is 0) then throw new Error 'officialUrl cannot be empty' @_rawData.officialUrl = officialUrl _command_video: (youTubeId, nameParts...)-> @@ -41,6 +47,26 @@ module.exports = class ItemParserV1 extends CommandParserVersionBase # Object Building Methods ###################################################################### _buildItem: (rawData, model)-> - model.description = rawData.description if rawData.description - model.officialUrl = rawData.officialUrl if rawData.officialUrl - model.videos = rawData.videos if rawData.videos + model.description = rawData.description if rawData.description? + model.officialUrl = rawData.officialUrl if rawData.officialUrl? + model.videos = rawData.videos if rawData.videos? + + # Un-parsing Methods ########################################################################### + + _unparseItem: (builder, model)-> + if model.officialUrl? + builder.line 'officialUrl: ', model.officialUrl + builder.line() + + if model.description? + if model.description.indexOf('\n') isnt -1 + builder.line 'description: <<-END' + builder.line model.description + builder.line 'END' + else + builder.line 'description: ', model.description + builder.line() + + for video in model.videos + builder.line 'video: ', video.youTubeId, ', ', video.name + builder.line() diff --git a/src/jade/templates/markdown_section.jade b/src/jade/templates/markdown_section.jade index 4d2f8f476..b06fe439f 100644 --- a/src/jade/templates/markdown_section.jade +++ b/src/jade/templates/markdown_section.jade @@ -7,4 +7,24 @@ .view__markdown_section.section h2 - .panel.markdown \ No newline at end of file + .panel + .waiting.hideable: img(src='/images/wait.gif') + .markdown.hideable.hidden + .editor.hideable.hidden + textarea + .sizer + .creating.hideable.hidden + p Please help by adding a description! + .buttons.hideable.hidden + .error.hideable.hidden: p + button.return.hideable.hidden return + button.cancel.hideable.hidden cancel + button.preview.hideable.hidden preview + button.save.hideable.hidden save + button.edit.hideable edit + .confirming.hideable.hidden + p Thanks for your help! + p Your changes will appear on the site in a few minutes. + .appologizing.hideable.hidden + p Sorry, but something went wrong. + p Please try again after a few moments. diff --git a/src/scss/classes.scss b/src/scss/classes.scss index 48960bc5b..40b57a9cd 100644 --- a/src/scss/classes.scss +++ b/src/scss/classes.scss @@ -32,9 +32,9 @@ All rights reserved. width: 100%; } -.error { background: $color-error !important; } +.error { background: $color-error; } -.error-new { background: $color-error-new !important; } +.error-new { background: $color-error-new; } .float-right { float: right; diff --git a/src/scss/templates/markdown_section.scss b/src/scss/templates/markdown_section.scss index 9f49252c5..9fbdfcd91 100644 --- a/src/scss/templates/markdown_section.scss +++ b/src/scss/templates/markdown_section.scss @@ -6,7 +6,102 @@ All rights reserved. */ .view__markdown_section { + + .appologizing, .confirming { + width: 100%; + + p { + width: 100%; + + margin: 1em auto; + text-align: center; + + &:first-child { + font-size: $font-size-x-large; + margin-top: 2em; + } + + &:last-child { + margin-bottom: 4em; + } + } + } + + .buttons { + position: relative; width: 100%; + + text-align: right; + + button { + margin: 0.66em 0 0 0.66em; + min-width: 6.66em; + padding: 0.25em 0.5em; + } + + .error { + + height: 100%; + + background: none; + display: inline-block; + + p { + color: $color-error-new; + font-weight: bold; + } + } + } + + .creating { + width: 100%; + text-align: center; + + p { + color: $color-gray-light; + font-size: $font-size-x-large; + margin: 1em; + } + } + .editor { width: 100%; + + position: relative; + min-height: 10em; + + textarea, .sizer { + width: 100%; + border: 1px solid $color-gray-light; + font-family: $font-family-input; + font-size: $font-size-normal; + } + + textarea { + position: absolute; height: 100%; width: 100%; + + background: $color-gray-faint; + resize: none; + z-index: $layer-normal; + + &:focus { + background: $color-background-panel; + border: 1px solid $color-gray-medium; + } + } + + .sizer { + padding-bottom: 1.5em; + visibility: hidden; + z-index: $layer-normal - 1; + } + } + + .waiting { + position: relative; min-height: 20em; width: 100%; + + img { + position: absolute; top: 50%; left: 50%; + @include transform(translate(-50%, -50%)); + } } } diff --git a/src/scss/views.scss b/src/scss/views.scss index be9588c28..0ab6b691d 100644 --- a/src/scss/views.scss +++ b/src/scss/views.scss @@ -57,8 +57,6 @@ All rights reserved. position: absolute; bottom: 5em; height: 3.2em; width: 100%; .login { - display: none; // Just until there's a feature which needs it. - position: absolute; top: 50%; right: 2em; @include transform(translateY(-50%)); diff --git a/static/images/wait.gif b/static/images/wait.gif new file mode 100644 index 000000000..27f20ed91 Binary files /dev/null and b/static/images/wait.gif differ diff --git a/test/parser_versions/item_version_parser_v1.test.coffee b/test/parser_versions/item_version_parser_v1.test.coffee new file mode 100644 index 000000000..386aaf272 --- /dev/null +++ b/test/parser_versions/item_version_parser_v1.test.coffee @@ -0,0 +1,100 @@ +### +Crafting Guide - item_parser_v1.test.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +Item = require '../../src/coffee/models/item' +ItemParserV1 = require '../../src/coffee/models/parser_versions/item_parser_v1' +_ = require 'underscore' + +######################################################################################################################## + +baseText = item = parser = null + +######################################################################################################################## + +describe 'item_parser_v1.coffee', -> + + beforeEach -> + item = new Item name:'alpha' + parser = new ItemParserV1 model:item + + describe 'officialUrl', -> + + it 'may be omitted', -> + parser.parse 'schema: 1\nvideo: youtubeid, Video Alpha\ndescription: Bravo, Charlie' + expect(item.officialUrl).to.be.null + + it 'is assigned properly when given', -> + parser.parse 'schema: 1\nofficialUrl: http://testurl.com' + item.officialUrl.should.equal 'http://testurl.com' + + it 'does not allow duplicate declarations', -> + func = -> parser.parse 'schema: 1\nofficialUrl: http://testurl.com\nofficialUrl: http://testurl2.com' + expect(func).to.throw Error, 'duplicate' + + it 'does not allow an empty value if given', -> + func = -> parser.parse 'schema: 1\nofficialUrl:' + expect(func).to.throw Error, 'empty' + + describe 'description', -> + + it 'may be omitted', -> + parser.parse 'schema: 1\nvideo: youTubeId, Video Alpha\nofficialUrl: http://testurl.com' + expect(item.description).to.be.null + + it 'is assigned properly when given', -> + parser.parse 'schema: 1\ndescription: Alpha Bravo Charlie' + item.description.should.equal 'Alpha Bravo Charlie' + + it 'can be a heredoc', -> + parser.parse 'schema: 1\ndescription: <<-END\nAlpha\nBravo\nCharlie\nEND' + item.description.should.equal 'Alpha\nBravo\nCharlie' + + it 'concatenates multiple declarations', -> + parser.parse 'schema: 1\ndescription: Alpha\ndescription: Bravo' + item.description.should.equal 'Alpha\nBravo' + + describe 'video', -> + + it 'may be omitted', -> + parser.parse 'schema: 1\nofficialUrl: http://testurl.com\ndescription: Alpha Bravo Charlie' + item.videos.should.eql [] + + it 'is assigned properly when given', -> + parser.parse 'schema: 1\nvideo: youtubeid, Alpha Bravo' + item.videos[0].should.eql youTubeId:'youtubeid', name:'Alpha Bravo' + item.videos.length.should.equal 1 + + it 'may be included multiple times', -> + parser.parse 'schema: 1\nvideo: youtubeid1, Alpha\nvideo: youtubeid2, Bravo' + item.videos[0].should.eql youTubeId:'youtubeid1', name:'Alpha' + item.videos[1].should.eql youTubeId:'youtubeid2', name:'Bravo' + item.videos.length.should.equal 2 + + it 'requires a YouTubeId and name', -> + func = -> parser.parse 'schema: 1\nvideo: alpha' + expect(func).to.throw Error, 'requires a name' + + describe 'unparsing', -> + + it 'can round-trip a fully described item', -> + text = """ + schema: 1 + + officialUrl: http://testurl.com + + description: <<-END + Alpha + Bravo + END + + video: youtubeid1, Alpha Bravo + video: youtubeid2, Charlie Delta + + + """ + parser.parse text + parser.unparse().should.equal text