Merge branch 'andrewminer-tutorial-pages' into andrewminer-thermal-dynamics

This commit is contained in:
Andrew Miner
2015-03-20 19:21:25 -07:00
34 changed files with 759 additions and 97 deletions
+3
View File
@@ -69,6 +69,9 @@ Url.mod = _.template "/browse/<%= modSlug %>/"
Url.modData = _.template "/data/<%= modSlug %>/mod.cg"
Url.modIcon = _.template "/browse/<%= modSlug %>/icon.png"
Url.modVersionData = _.template "/data/<%= modSlug %>/<%= modVersion %>/mod-version.cg"
Url.tutorial = _.template "/browse/<%= modSlug %>/tutorials/<%= tutorialSlug %>/"
Url.tutorialData = _.template "/data/<%= modSlug %>/tutorials/<%= tutorialSlug %>.cg"
Url.tutorialIcon = _.template "/browse/<%= modSlug %>/tutorials/<%= tutorialSlug %>/icon.png"
exports.UrlParam = UrlParam = {}
UrlParam.quantity = 'count'
@@ -0,0 +1,40 @@
###
Crafting Guide - markdown_section_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
########################################################################################################################
module.exports = class MarkdownSectionController extends BaseController
constructor: (options={})->
options.model ?= ''
options.title ?= 'Description'
options.templateName = 'markdown_section'
super options
@title = options.title
# BaseController Overrides #####################################################################
onDidRender: ->
@$title = @$('h2')
@$markdownPanel = @$('.markdown')
super
refresh: ->
@$title.html @title
@$markdownPanel.empty()
@$markdownPanel.html _.parseMarkdown @model
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click .markdown': 'routeLinkClick'
@@ -11,6 +11,7 @@ ItemGroupController = require './item_group_controller'
Mod = require '../models/mod'
ModPack = require '../models/mod_pack'
PageController = require './page_controller'
TutorialController = require './tutorial_controller'
{Duration} = require '../constants'
{Text} = require '../constants'
{Url} = require '../constants'
@@ -55,15 +56,17 @@ module.exports = class ModPageController extends PageController
onDidRender: ->
@adsenseController = @addChild AdsenseController, '.view__adsense', model:'sidebar_skyscraper'
@$name = @$('.name')
@$byline = @$('.byline p')
@$description = @$('.description p')
@$documentationLink = @$('.documentation')
@$downloadLink = @$('.download')
@$homePageLink = @$('.homePage')
@$groupContainer = @$('.itemGroups')
@$titleImage = @$('.titleImage img')
@$versionSelector = @$('select.version')
@$name = @$('.name')
@$byline = @$('.byline p')
@$description = @$('.description p')
@$documentationLink = @$('.documentation')
@$downloadLink = @$('.download')
@$homePageLink = @$('.homePage')
@$groupContainer = @$('.itemGroups')
@$titleImage = @$('.titleImage img')
@$tutorialsSection = @$('.tutorials')
@$tutorialsContainer = @$('.tutorials .panel')
@$versionSelector = @$('select.version')
super
@@ -83,6 +86,7 @@ module.exports = class ModPageController extends PageController
@_refreshLink @$downloadLink, @model.downloadUrl
@_refreshItemGroups()
@_refreshTutorials()
@_refreshVersions()
super
@@ -128,7 +132,7 @@ module.exports = class ModPageController extends PageController
while @_groupControllers.length > groupIndex + 1
controller = @_groupControllers.pop()
controller.$el.slideUp duration:Duration.normal, -> @remove()
controller.$el.slideUp duration:Duration.normal, complete:-> @remove()
_refreshLink: ($link, url)->
if url?
@@ -137,6 +141,32 @@ module.exports = class ModPageController extends PageController
else
$link.slideUp duration:Duration.normal
_refreshTutorials: ->
@_tutorialControllers ?= []
tutorials = @model.tutorials
if tutorials.length is 0
@$tutorialsSection.addClass 'hidden'
else
@$tutorialsSection.removeClass 'hidden'
index = 0
for tutorial in tutorials
controller = @_tutorialControllers[index]
if not controller?
controller = new TutorialController model:tutorial
controller.render()
@$tutorialsContainer.append controller.$el
@_tutorialControllers.push controller
else
controller.model = model
index += 1
while @_tutorialControllers.length > index
controller = @_tutorialControllers.pop()
controller.$el.slideUp duration:Duration.normal, complete:-> @remove()
_refreshVersions: ->
@$versionSelector.empty()
return unless @model?
@@ -0,0 +1,39 @@
###
Crafting Guide - tutorial_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
{Url} = require '../constants'
########################################################################################################################
module.exports = class TutorialController extends BaseController
constructor: (options={})->
if not options.model? then throw new Error 'options.model is required'
options.templateName = 'tutorial'
super options
# BaseController Overrides #####################################################################
onDidRender: ->
@$icon = @$('img')
@$link = @$('a')
@$title = @$('p')
super
refresh: ->
templateData = modSlug:@model.modSlug, tutorialSlug:@model.slug
@$icon.attr 'src', Url.tutorialIcon templateData
@$link.attr 'href', Url.tutorial templateData
@$title.html @model.name
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click a': 'routeLinkClick'
@@ -0,0 +1,159 @@
###
Crafting Guide - tutorial_page_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
AdsenseController = require './adsense_controller'
BaseController = require './base_controller'
MarkdownSectionController = require './markdown_section_controller'
VideoController = require './video_controller'
{Duration} = require '../constants'
{Event} = require '../constants'
{Url} = require '../constants'
########################################################################################################################
module.exports = class TutorialPageController extends BaseController
constructor: (options={})->
if not options.modSlug? then throw new Error 'options.modSlug is required'
if not options.tutorialSlug? then throw new Error 'options.tutorialSlug is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.model ?= null
options.templateName = 'tutorial_page'
super options
@modPack = options.modPack
@imageLoader = options.imageLoader
@modSlug = options.modSlug
@tutorialSlug = options.tutorialSlug
@modPack.on Event.change, => @_resolveTutorial()
@_resolveTutorial()
# BaseController Overrides #####################################################################
onDidRender: ->
@adsenseController = @addChild AdsenseController, '.view__adsense', model:'sidebar_skyscraper'
@$byline = @$('.byline')
@$bylineLink = @$('.byline a')
@$name = @$('.name')
@$officialLink = @$('a.officialPage')
@$sectionsContainer = @$('.sections')
@$title = @$('h1.name')
@$titleImage = @$('.titleImage img')
@$videosSection = @$('.videos')
@$videosSectionTitle = @$('.videos h2')
@$videosSectionPanel = @$('.videos .panel')
super
refresh: ->
if @model? and @model.isLoaded
@$el.removeClass 'hidden'
else
@$el.addClass 'hidden'
@_refreshByline()
@_refreshOfficialUrl()
@_refreshSections()
@_refreshTitle()
@_refreshVideos()
super
# Private Methods ##############################################################################
_refreshByline: ->
if @model?
@$byline.removeClass 'hidden'
@$bylineLink.html @modPack.getMod(@modSlug).name
@$bylineLink.attr 'href', Url.mod modSlug:@modSlug
else
@$byline.addClass 'hidden'
_refreshOfficialUrl: ->
if @model?.officialUrl?
@$officialLink.attr 'href', @model.officialUrl
@$officialLink.removeClass 'hidden'
else
@$officialLink.addClass 'hidden'
_refreshTitle: ->
if @model?
@$title.html @model.name
@$titleImage.attr 'src', Url.tutorialIcon modSlug:@modSlug, tutorialSlug:@tutorialSlug
else
@$title.empty()
@$titleImage.removeAttr 'src'
_refreshSections: ->
@_sectionControllers ?= []
index = 0
if @model?
for section in @model.sections
controller = @_sectionControllers[index]
if not controller?
controller = new MarkdownSectionController title:section.title, model:section.content
controller.render()
@$sectionsContainer.append controller.$el
@_sectionControllers.push controller
else
controller.title = section.title
controller.model = section.model
controller.refresh()
index += 1
while @_sectionControllers.length > index
controller = @_sectionController.pop()
controller.$el.fadeOut duration:Duration.normal, complete:-> @remove()
_refreshVideos: ->
@_videoControllers ?= []
index = 0
videos = @model?.videos or []
if videos? and videos.length > 0
@$videosSection.slideDown duration:Duration.normal
@$videosSectionTitle.html if videos.length is 1 then 'Video' else 'Videos'
for video in videos
controller = @_videoControllers[index]
if not controller?
controller = new VideoController model:video
@_videoControllers.push controller
controller.render()
@$videosSectionPanel.append controller.$el
else
controller.model = video
index++
else
@$videosSection.slideUp duration:Duration.normal
while @_videoControllers.length > index
controller = @_videoControllers.pop()
controller.$el.slideUp duration:Duration.normal, complete:-> controller.$el.remove()
_resolveTutorial: ->
return if @model?
mod = @modPack.getMod @modSlug
if not mod?
logger.error => "cannot find the mod for: #{@modSlug}"
router.navigate '/', trigger:true
return
if not mod.isLoaded
mod.fetch()
return
@model = mod.getTutorial @tutorialSlug
if not @model?
logger.error => "mod #{@modSlug} doesn't have a tutorial for: #{@tutorialSlug}"
return
@model.fetch()
+13 -7
View File
@@ -16,6 +16,7 @@ ItemSlug = require './models/item_slug'
Mod = require './models/mod'
ModPack = require './models/mod_pack'
ModPageController = require './controllers/mod_page_controller'
TutorialPageController = require './controllers/tutorial_page_controller'
Storage = require './models/storage'
UrlParams = require './url_params'
{ProductionEnvs} = require './constants'
@@ -64,13 +65,14 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
@_recordPageView()
routes:
'(/)': 'route__home'
'browse(/)': 'route__browse'
'browse/:modSlug(/)': 'route__browseMod'
'browse/:modSlug/:itemSlug(/)': 'route__browseModItem'
'configure(/)': 'route__configure'
'craft(/)': 'route__craft'
'craft/:text': 'route__craft'
'(/)': 'route__home'
'browse(/)': 'route__browse'
'browse/:modSlug(/)': 'route__browseMod'
'browse/:modSlug/:itemSlug(/)': 'route__browseModItem'
'browse/:modSlug/tutorials/:tutorialSlug(/)': 'route__browseTutorial'
'configure(/)': 'route__configure'
'craft(/)': 'route__craft'
'craft/:text': 'route__craft'
'item/:itemSlug': 'deprecated__item'
'crafting/(:text)': 'deprecated__crafting'
@@ -100,6 +102,10 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
controller = new ItemPageController _.extend {itemSlug:slug}, @_defaultOptions
@_setPage 'browseModItem', controller
route__browseTutorial: (modSlug, tutorialSlug)->
controller = new TutorialPageController _.extend {modSlug:modSlug, tutorialSlug:tutorialSlug}, @_defaultOptions
@_setPage 'browseTutorial', controller
route__configure: ->
@_setPage 'configure', new ConfigurePageController _.extend {}, @_defaultOptions
+1 -1
View File
@@ -22,7 +22,7 @@ module.exports = class ItemPage extends BaseModel
compileDescription: ->
return null unless @item?.description?
description = markdown.parse @item.description, 'Maruku'
description = _.parseMarkdown @item.description
return description
findComponentInItems: ->
+41 -21
View File
@@ -16,6 +16,7 @@ module.exports = class Mod extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.slug? then throw new Error 'attributes.slug is required'
attributes.author ?= ''
attributes.description ?= ''
attributes.documentationUrl ?= null
@@ -23,11 +24,13 @@ module.exports = class Mod extends BaseModel
attributes.homePageUrl ?= null
attributes.modPack ?= null
attributes.name ?= ''
super attributes, options
@_activeModVersion = null
@_activeVersion = null
@_modVersions = []
@_tutorials = []
Object.defineProperties this,
'activeModVersion': { get:-> @_activeModVersion }
@@ -107,6 +110,31 @@ module.exports = class Mod extends BaseModel
# Property Methods #############################################################################
getActiveVersion: ->
return @_activeVersion
setActiveVersion: (version)->
return if version is @_activeVersion
version ?= Mod.Version.None
if version is Mod.Version.Latest then version = _.last(@_modVersions).version
if version is Mod.Version.None
@_activeVersion = version
@_activateModVersion null
@trigger Event.change + ':activeVersion', this, @_activeVersion
@trigger Event.change, this
else
for modVersion in @_modVersions
if version is modVersion.version
@_activateModVersion modVersion
break
@_activeVersion = version
@trigger Event.change + ':activeVersion', this, @_activeVersion
@trigger Event.change, this
addModVersion: (modVersion)->
return unless modVersion?
return if @_modVersions.indexOf(modVersion) isnt -1
@@ -136,30 +164,22 @@ module.exports = class Mod extends BaseModel
return null
getActiveVersion: ->
return @_activeVersion
addTutorial: (tutorial)->
return unless tutorial?
if @getTutorial(tutorial.slug)? then throw new Error "duplicate tutorial: #{tutorial.name}"
@_tutorials.push tutorial
tutorial.modSlug = @slug
setActiveVersion: (version)->
return if version is @_activeVersion
getAllTutorials: ->
return @_tutorials[..]
version ?= Mod.Version.None
if version is Mod.Version.Latest then version = _.last(@_modVersions).version
getTutorial: (tutorialSlug)->
for tutorial in @_tutorials
return tutorial if tutorial.slug is tutorialSlug
return null
if version is Mod.Version.None
@_activeVersion = version
@_activateModVersion null
@trigger Event.change + ':activeVersion', this, @_activeVersion
@trigger Event.change, this
else
for modVersion in @_modVersions
if version is modVersion.version
@_activateModVersion modVersion
break
@_activeVersion = version
@trigger Event.change + ':activeVersion', this, @_activeVersion
@trigger Event.change, this
Object.defineProperties @prototype,
tutorials: { get:@prototype.getAllTutorials }
# Backbone.View Overrides ######################################################################
@@ -125,6 +125,7 @@ module.exports = class CommandParserVersionBase
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]
@@ -8,6 +8,7 @@ All rights reserved.
CommandParserVersionBase = require './command_parser_version_base'
Mod = require '../mod'
ModVersion = require '../mod_version'
Tutorial = require '../tutorial'
########################################################################################################################
@@ -47,17 +48,23 @@ module.exports = class ModParserV1 extends CommandParserVersionBase
if downloadUrl.length is 0 then throw new Error 'downloadUrl cannot be empty (omit it instead)'
@_rawData.downloadUrl = downloadUrl
_command_name: (name)->
if @_rawData.name? then throw new Error 'duplicate declaration of "name"'
if name.length is 0 then throw new Error '"name" cannot be empty'
@_rawData.name = name
_command_homePageUrl: (homePageUrl='')->
if @_rawData.homePageUrl? then throw new Error 'duplicate declaration of "homePageUrl"'
if homePageUrl.length is 0 then throw new Error 'homePageUrl cannot be empty'
@_rawData.homePageUrl = homePageUrl
_command_name: (name)->
if @_rawData.name? then throw new Error 'duplicate declaration of "name"'
if name.length is 0 then throw new Error '"name" cannot be empty'
@_rawData.name = name
_command_tutorial: (nameParts...)->
name = nameParts.join(', ').trim()
if name.length is 0 then throw new Error '"name" cannot be empty'
@_rawData.tutorialNames ?= []
@_rawData.tutorialNames.push name
_command_version: (version='')->
if version.length is 0 then throw new Error 'version cannot be empty'
@@ -78,5 +85,9 @@ module.exports = class ModParserV1 extends CommandParserVersionBase
model.name = rawData.name
model.homePageUrl = rawData.homePageUrl
if rawData.tutorialNames?
for tutorialName in rawData.tutorialNames
model.addTutorial new Tutorial name:tutorialName
for version in rawData.versions
model.addModVersion new ModVersion modSlug:model.slug, version:version
@@ -0,0 +1,65 @@
###
Crafting Guide - tutorial_parser_v1.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
CommandParserVersionBase = require './command_parser_version_base'
Tutorial = require '../tutorial'
########################################################################################################################
module.exports = class TutorialParserV1 extends CommandParserVersionBase
# CommandParserVersionBase Overrides ###########################################################
_buildModel: (rawData, model)->
@_buildTutorial rawData, model
_unparseModel: (builder, model)->
@_unparseTutorial builder, model
# Command Methods ##############################################################################
_command_content: (contentParts...)->
if not @_rawData.currentSection? then throw new Error 'cannot declare "title" before "section"'
if @_rawData.currentSection.content? then throw new Error 'duplicate declaration of content'
content = contentParts.join(', ').trim()
if not content.length > 0 then throw new Error 'content cannot be empty'
@_rawData.currentSection.content = content
_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'
@_rawData.officialUrl = officialUrl
_command_section: (textParts...)->
@_rawData.sections ?= []
@_rawData.sections.push @_rawData.currentSection = {}
_command_title: (titleParts...)->
if not @_rawData.currentSection? then throw new Error 'cannot declare "title" before "section"'
if @_rawData.currentSection.title? then throw new Error 'duplicate declaration of title'
title = titleParts.join(', ').trim()
if not title.length > 0 then throw new Error 'title cannot be empty'
@_rawData.currentSection.title = title
_command_video: (youTubeId, nameParts...)->
if not youTubeId?.length then throw new Error 'video declaration requires a YouTubeID'
name = nameParts.join ', '
if not name?.length then throw new Error 'video declaration requires a name'
@_rawData.videos ?= []
@_rawData.videos.push youTubeId:youTubeId, name:name
# Object Building Methods ######################################################################
_buildTutorial: (rawData, model)->
if not rawData.sections? then throw new Error 'the "section" declaration is required'
model.officialUrl = rawData.officialUrl
model.videos = rawData.videos
model.sections = rawData.sections
+35
View File
@@ -0,0 +1,35 @@
###
Crafting Guide - tutorial.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
TutorialParser = require './tutorial_parser'
{Url} = require '../constants'
########################################################################################################################
module.exports = class Tutorial extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.name?.length > 0 then throw new Error "attributes.name cannot be empty"
attributes.modSlug ?= null
attributes.officialUrl ?= null
attributes.sections ?= []
attributes.slug ?= _.slugify attributes.name
attributes.videos ?= []
super attributes, options
# Backbone.Model Overrides #####################################################################
parse: (text)->
TutorialParser = require './tutorial_parser' # to avoid require cycles
@_parser ?= new TutorialParser model:this
@_parser.parse text
return null # prevent calling `set`
url: ->
return Url.tutorialData modSlug:@modSlug, tutorialSlug:@slug
+19
View File
@@ -0,0 +1,19 @@
###
Crafting Guide - tutorial_parser.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
VersionedParserBase = require './versioned_parser_base'
TutorialParserV1 = require './parser_versions/tutorial_parser_v1'
########################################################################################################################
module.exports = class TutorialParser extends VersionedParserBase
# VersionedParserBase Overrides ################################################################
_createParsers: (options)->
return result =
'1': new TutorialParserV1 options
@@ -40,9 +40,9 @@ describe 'command_parser_version_base.coffee', ->
expect(result[1]).to.be.null
it 'trims smallest leading whitespace', ->
parser._lines = ['command: <<-END', ' alpha', ' bravo', ' charlie', 'END', 'command1: arg2']
parser._lines = ['command: <<-END', ' alpha', ' bravo', '', ' charlie', 'END', 'command1: arg2']
parser._lineNumber = 1
result = parser._parseHereDoc parser._lines[0]
result[0].should.equal 'command: '
result[1].should.equal 'alpha\n bravo\ncharlie'
result[1].should.equal 'alpha\n bravo\n\ncharlie'
+5 -2
View File
@@ -1,5 +1,5 @@
###
Crafting Guide - underscore.coffee
Crafting Guide - underscore_mixins.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
@@ -7,12 +7,15 @@ All rights reserved.
_.mixin
parseMarkdown: (text)->
return markdown.parse text, 'Maruku'
slugify: (text)->
return null unless text?
result = text.toLowerCase()
result = result.replace /[^a-zA-Z0-9_]/g, '_'
result = result.replace /__+/, '_'
result = result.replace /__+/g, '_'
result = result.replace /^_/, ''
result = result.replace /_$/, ''
return result