diff --git a/design/tutorial-scratchpad.graffle b/design/tutorial-scratchpad.graffle
new file mode 100644
index 000000000..6edd479cb
Binary files /dev/null and b/design/tutorial-scratchpad.graffle differ
diff --git a/scripts/sitemap b/scripts/sitemap
index 5986ae28e..6a7760f3b 100755
--- a/scripts/sitemap
+++ b/scripts/sitemap
@@ -16,6 +16,7 @@ fs = require 'fs'
modSlugs = {}
itemSlugs = {}
+tutorialSlugs = []
base = 'http://crafting-guide.com'
@@ -41,9 +42,15 @@ for modSlug, modData of DefaultMods
modVersion.eachItem (item)->
itemSlugs[item.slug] = item.slug
+ for tutorial in mod.tutorials
+ tutorialSlugs.push mod:mod.slug, tutorial:tutorial.slug
+
for modSlug in _.values(modSlugs).sort()
urls.push base + "/browse/#{modSlug}/"
+for tutorialSlug in tutorialSlugs
+ urls.push base + "/browse/#{tutorialSlug.mod}/tutorials/#{tutorialSlug.tutorial}/"
+
for key in _.keys(itemSlugs).sort()
itemSlug = itemSlugs[key]
urls.push base + "/browse/#{itemSlug.mod}/#{itemSlug.item}/"
diff --git a/src/coffee/constants.coffee b/src/coffee/constants.coffee
index bb5824ec2..8ab9e87fa 100644
--- a/src/coffee/constants.coffee
+++ b/src/coffee/constants.coffee
@@ -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'
diff --git a/src/coffee/controllers/markdown_section_controller.coffee b/src/coffee/controllers/markdown_section_controller.coffee
new file mode 100644
index 000000000..52c3139d6
--- /dev/null
+++ b/src/coffee/controllers/markdown_section_controller.coffee
@@ -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'
diff --git a/src/coffee/controllers/mod_page_controller.coffee b/src/coffee/controllers/mod_page_controller.coffee
index 634723043..600ca93fc 100644
--- a/src/coffee/controllers/mod_page_controller.coffee
+++ b/src/coffee/controllers/mod_page_controller.coffee
@@ -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?
diff --git a/src/coffee/controllers/tutorial_controller.coffee b/src/coffee/controllers/tutorial_controller.coffee
new file mode 100644
index 000000000..8c590d582
--- /dev/null
+++ b/src/coffee/controllers/tutorial_controller.coffee
@@ -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'
diff --git a/src/coffee/controllers/tutorial_page_controller.coffee b/src/coffee/controllers/tutorial_page_controller.coffee
new file mode 100644
index 000000000..f56c5202e
--- /dev/null
+++ b/src/coffee/controllers/tutorial_page_controller.coffee
@@ -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()
diff --git a/src/coffee/crafting_guide_router.coffee b/src/coffee/crafting_guide_router.coffee
index ec32b5044..6a5f3e06d 100644
--- a/src/coffee/crafting_guide_router.coffee
+++ b/src/coffee/crafting_guide_router.coffee
@@ -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
diff --git a/src/coffee/models/item_page.coffee b/src/coffee/models/item_page.coffee
index 4500a01b7..2638475a6 100644
--- a/src/coffee/models/item_page.coffee
+++ b/src/coffee/models/item_page.coffee
@@ -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: ->
diff --git a/src/coffee/models/mod.coffee b/src/coffee/models/mod.coffee
index fa0edea5b..7c7715c9a 100644
--- a/src/coffee/models/mod.coffee
+++ b/src/coffee/models/mod.coffee
@@ -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 ######################################################################
diff --git a/src/coffee/models/parser_versions/command_parser_version_base.coffee b/src/coffee/models/parser_versions/command_parser_version_base.coffee
index c9b29050d..f6a1efd32 100644
--- a/src/coffee/models/parser_versions/command_parser_version_base.coffee
+++ b/src/coffee/models/parser_versions/command_parser_version_base.coffee
@@ -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]
diff --git a/src/coffee/models/parser_versions/mod_parser_v1.coffee b/src/coffee/models/parser_versions/mod_parser_v1.coffee
index 3c2c25f50..f244fb301 100644
--- a/src/coffee/models/parser_versions/mod_parser_v1.coffee
+++ b/src/coffee/models/parser_versions/mod_parser_v1.coffee
@@ -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
diff --git a/src/coffee/models/parser_versions/tutorial_parser_v1.coffee b/src/coffee/models/parser_versions/tutorial_parser_v1.coffee
new file mode 100644
index 000000000..c5bf37df3
--- /dev/null
+++ b/src/coffee/models/parser_versions/tutorial_parser_v1.coffee
@@ -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
diff --git a/src/coffee/models/tutorial.coffee b/src/coffee/models/tutorial.coffee
new file mode 100644
index 000000000..1ca03a6d5
--- /dev/null
+++ b/src/coffee/models/tutorial.coffee
@@ -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
diff --git a/src/coffee/models/tutorial_parser.coffee b/src/coffee/models/tutorial_parser.coffee
new file mode 100644
index 000000000..21d83127b
--- /dev/null
+++ b/src/coffee/models/tutorial_parser.coffee
@@ -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
diff --git a/src/coffee/test/parser_versions/command_parser_version_base.test.coffee b/src/coffee/test/parser_versions/command_parser_version_base.test.coffee
index 6cfe148a7..08d50bee5 100644
--- a/src/coffee/test/parser_versions/command_parser_version_base.test.coffee
+++ b/src/coffee/test/parser_versions/command_parser_version_base.test.coffee
@@ -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'
diff --git a/src/coffee/underscore_mixins.coffee b/src/coffee/underscore_mixins.coffee
index 707227cca..ac789d648 100644
--- a/src/coffee/underscore_mixins.coffee
+++ b/src/coffee/underscore_mixins.coffee
@@ -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
diff --git a/src/jade/templates/markdown_section.jade b/src/jade/templates/markdown_section.jade
new file mode 100644
index 000000000..1e77bb761
--- /dev/null
+++ b/src/jade/templates/markdown_section.jade
@@ -0,0 +1,10 @@
+//-
+//- Crafting Guide - markdown_section.jade
+//-
+//- Copyright (c) 2015 by Redwood Labs
+//- All rights reserved.
+//-
+
+.view__markdown_section.section
+ h2
+ .panel.markdown
diff --git a/src/jade/templates/mod_page.jade b/src/jade/templates/mod_page.jade
index 3b950dce0..f0145fdc3 100644
--- a/src/jade/templates/mod_page.jade
+++ b/src/jade/templates/mod_page.jade
@@ -20,4 +20,8 @@
.byline: p
.description: p
+ .tutorials.section
+ h2 Tutorials
+ .panel
+
.itemGroups
diff --git a/src/jade/templates/tutorial.jade b/src/jade/templates/tutorial.jade
new file mode 100644
index 000000000..2ab96ca96
--- /dev/null
+++ b/src/jade/templates/tutorial.jade
@@ -0,0 +1,11 @@
+//-
+//- Crafting Guide - tutorial.jade
+//-
+//- Copyright (c) 2015 by Redwood Labs
+//- All rights reserved.
+//-
+
+.view__tutorial
+ a
+ img
+ .caption: p
\ No newline at end of file
diff --git a/src/jade/templates/tutorial_page.jade b/src/jade/templates/tutorial_page.jade
new file mode 100644
index 000000000..d3076229c
--- /dev/null
+++ b/src/jade/templates/tutorial_page.jade
@@ -0,0 +1,23 @@
+//-
+//- Crafting Guide - tutorial_page.jade
+//-
+//- Copyright (c) 2015 by Redwood Labs
+//- All rights reserved.
+//-
+
+.view__tutorial_page
+ .sidebar
+ .titleImage: a: img
+ a.officialPage.externalLink(target="new"): p Offical Documentation
+
+ .view__adsense
+
+ .mainBody
+ h1.name
+ .byline: p from
+
+ .sections
+
+ .videos.section
+ h2
+ .panel
diff --git a/src/scss/classes.scss b/src/scss/classes.scss
index 4ad3695ce..c797e935b 100644
--- a/src/scss/classes.scss
+++ b/src/scss/classes.scss
@@ -5,6 +5,16 @@ Copyright (C) 2015 by Redwood Labs
All rights reserved.
*/
+.byline {
+ margin: 0.25em 0 2em 0;
+
+ p {
+ font-family: $font-family-normal;
+ font-size: $font-size-normal;
+ font-style: italic;
+ }
+}
+
.centered {
text-align: center;
}
@@ -22,6 +32,20 @@ All rights reserved.
.error-new { background: $color-error-new !important; }
+.hidden {
+ max-height: 0;
+ opacity: 0;
+}
+
+.mainBody {
+ position: relative; width: 81.25%;
+
+ display: inline-block;
+ padding: 1em;
+ padding-top: 2em;
+ vertical-align: top;
+}
+
.sidebar {
position: relative; width: 18.75%;
@@ -53,15 +77,6 @@ All rights reserved.
}
}
-.mainBody {
- position: relative; width: 81.25%;
-
- display: inline-block;
- padding: 1em;
- padding-top: 2em;
- vertical-align: top;
-}
-
.section {
margin-top: 3em;
@@ -86,4 +101,12 @@ All rights reserved.
background: white;
padding: 1em;
}
+}
+
+.videos {
+ display: none;
+
+ .panel {
+ text-align: center;
+ }
}
\ No newline at end of file
diff --git a/src/scss/markdown.scss b/src/scss/markdown.scss
index 62e7763d9..1e89387a7 100644
--- a/src/scss/markdown.scss
+++ b/src/scss/markdown.scss
@@ -39,6 +39,19 @@ All rights reserved.
}
}
+ ol {
+ margin: 2em 2em 0 2em;
+
+ li {
+ font-family: $font-family-normal;
+ font-size: $font-size-normal;
+ line-height: $font-size-normal + 0.15em;
+ list-style: decimal;
+ margin: 0.66em 0 0 2em;
+ }
+ }
+
+
ul {
margin: 2em 2em 0 2em;
diff --git a/src/scss/tags.scss b/src/scss/tags.scss
index 3d1665d0e..93fbc2efe 100644
--- a/src/scss/tags.scss
+++ b/src/scss/tags.scss
@@ -48,6 +48,12 @@ h1 {
font-family: $font-family-header;
font-size: $font-size-xx-large;
}
+
+ &.name {
+ font-family: $font-family-header;
+ font-size: $font-size-x-large;
+ margin: 0;
+ }
}
h2 {
diff --git a/src/scss/templates/index.scss b/src/scss/templates/index.scss
index b72f3f7c7..19bc6da1e 100644
--- a/src/scss/templates/index.scss
+++ b/src/scss/templates/index.scss
@@ -23,4 +23,5 @@ All rights reserved.
@import 'mod_page';
@import 'mod_selector';
@import 'stack';
+@import 'tutorial';
@import 'video';
diff --git a/src/scss/templates/item_page.scss b/src/scss/templates/item_page.scss
index 3358a9d92..2780a09d7 100644
--- a/src/scss/templates/item_page.scss
+++ b/src/scss/templates/item_page.scss
@@ -24,22 +24,6 @@ All rights reserved.
}
.mainBody {
- h1.name {
- font-family: $font-family-header;
- font-size: $font-size-x-large;
- margin: 0;
- }
-
- .byline {
- display: none;
- margin: 0.25em 0 2em 0;
-
- p {
- font-family: $font-family-normal;
- font-size: $font-size-normal;
- font-style: italic;
- }
- }
.recipes {
display: none;
@@ -61,14 +45,6 @@ All rights reserved.
.usedToMake {
display: none;
}
-
- .videos {
- display: none;
-
- .panel {
- text-align: center;
- }
- }
}
.view__item {
diff --git a/src/scss/templates/mod_page.scss b/src/scss/templates/mod_page.scss
index b29173afb..5d2670b72 100644
--- a/src/scss/templates/mod_page.scss
+++ b/src/scss/templates/mod_page.scss
@@ -23,22 +23,6 @@ All rights reserved.
.mainBody {
- .name {
- font-family: $font-family-header;
- font-size: $font-size-x-large;
- margin: 0;
- }
-
- .byline {
- margin: 0.25em 0 2em 0;
-
- p {
- font-family: $font-family-normal;
- font-size: $font-size-normal;
- font-style: italic;
- }
- }
-
.description {
margin-bottom: 4em;
diff --git a/src/scss/templates/tutorial.scss b/src/scss/templates/tutorial.scss
new file mode 100644
index 000000000..2d4065e69
--- /dev/null
+++ b/src/scss/templates/tutorial.scss
@@ -0,0 +1,28 @@
+/*
+Crafting Guide - tutorial.scss
+
+Copyright (C) 2015 by Redwood Labs
+All rights reserved.
+*/
+
+.view__tutorial {
+ position: relative;
+ display: inline-block;
+ margin-left: 1em;
+ vertical-align: top;
+
+ &:first-child {
+ margin-left: 0;
+ }
+
+ .caption {
+ position: relative; width: 100%;
+ display: block;
+
+ p {
+ margin-top: 0.5em;
+ font-weight: bold;
+ text-align: center;
+ }
+ }
+}
\ No newline at end of file
diff --git a/static/browse/buildcraft/tutorials/gates_wires_and_chips/example_1_overview.png b/static/browse/buildcraft/tutorials/gates_wires_and_chips/example_1_overview.png
new file mode 100644
index 000000000..6d7e9094c
Binary files /dev/null and b/static/browse/buildcraft/tutorials/gates_wires_and_chips/example_1_overview.png differ
diff --git a/static/browse/buildcraft/tutorials/gates_wires_and_chips/icon.png b/static/browse/buildcraft/tutorials/gates_wires_and_chips/icon.png
new file mode 100644
index 000000000..4ee0debda
Binary files /dev/null and b/static/browse/buildcraft/tutorials/gates_wires_and_chips/icon.png differ
diff --git a/static/browse/buildcraft/tutorials/gates_wires_and_chips/index.html b/static/browse/buildcraft/tutorials/gates_wires_and_chips/index.html
new file mode 100644
index 000000000..f1a9bdbc5
--- /dev/null
+++ b/static/browse/buildcraft/tutorials/gates_wires_and_chips/index.html
@@ -0,0 +1,107 @@
+
+ The Ultimate Minecraft Crafting Guide
+
+
+
+
+
+
+
+
+
+
+
Gates, Wires, and Chips What do gates do? BuildCraft gates are devices which can be attached to a pipe to detect and interact with objects nearby. Gates
+can read a wide variety of conditions: that a nearby chest is full, or that a Combustion
+Engine is about to overheat. In reaction to these conditions, gates can
+emit various kinds of signals (including redstone signals) to control other gates and/or machines.
An Example: Controlling Engines Before diving into all the details, let's look at a realistic example of what you might do with gates. This is a
+small power plant driving a quarry.
+
+
+
+
Here are the important pieces of the build:
+
+
The on/off switch for the entire operation. Flipping the switch creates a redstone signal. A Basic Gate attached to a Structure Pipe reads the redstone signal and generates a red pipe wire signal. Red Pipe Wire follows the Cobblestone Kinesis Pipe , transmitting the signal throughout the rest of the build.Another Basic Gate receives the red pipe wire signal and converts it back to a redstone signal, thus turning
+the Combustion Engine s on. The red pipe wire signal is also received by Basic Gates next to the Redstone
+Engine s, turning them on as well, and causing both fuel and water to
+flow into the engines. Finally, the power generated by the Combusion Engines flows into the Quarry . Video VIDEO Buildcraft Gates Tutorial, Part 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/data/buildcraft/mod.cg b/static/data/buildcraft/mod.cg
index 078e9ef32..fe966fe3a 100644
--- a/static/data/buildcraft/mod.cg
+++ b/static/data/buildcraft/mod.cg
@@ -8,3 +8,5 @@ documentationUrl: http://www.mod-buildcraft.com/wiki/doku.php
downloadUrl: http://www.mod-buildcraft.com/download/
version: 6.2.6
+
+tutorial: Gates, Wires, and Chips
diff --git a/static/data/buildcraft/tutorials/gates_wires_and_chips.cg b/static/data/buildcraft/tutorials/gates_wires_and_chips.cg
new file mode 100644
index 000000000..e5a5a88d1
--- /dev/null
+++ b/static/data/buildcraft/tutorials/gates_wires_and_chips.cg
@@ -0,0 +1,35 @@
+schema: 1
+
+section:
+ title: What do gates do?
+ content: <<-END
+ BuildCraft gates are devices which can be attached to a pipe to detect and interact with objects nearby. Gates
+ can read a wide variety of conditions: that a nearby chest is full, or that a [Combustion
+ Engine](/browse/buildcraft/combustion_engine) is about to overheat. In reaction to these conditions, gates can
+ emit various kinds of signals (including redstone signals) to control other gates and/or machines.
+ END
+
+section:
+ title: An Example: Controlling Engines
+ content: <<-END
+ Before diving into all the details, let's look at a realistic example of what you might do with gates. This is a
+ small power plant driving a quarry.
+
+ 
+
+ Here are the important pieces of the build:
+
+ 1. The on/off switch for the entire operation. Flipping the switch creates a redstone signal.
+ 2. A [Basic Gate](/browse/buildcraft/basic_gate/) attached to a [Structure Pipe]
+ (/browse/buildcraft/structure_pipe) reads the redstone signal and generates a red pipe wire signal.
+ 3. [Red Pipe Wire](/browse/buildcraft/red_pipe_wire) follows the [Cobblestone Kinesis Pipe]
+ (/browse/buildcraft/cobblestone_kinesis_pipe), transmitting the signal throughout the rest of the build.
+ 4. Another Basic Gate receives the red pipe wire signal and converts it back to a redstone signal, thus turning
+ the [Combustion Engine](/browse/buildcraft/combustion_engine)s on.
+ 5. The red pipe wire signal is also received by Basic Gates next to the [Redstone
+ Engine](/browse/buildcraft/redstone_engine)s, turning them on as well, and causing both fuel and water to
+ flow into the engines.
+ 6. Finally, the power generated by the Combusion Engines flows into the [Quarry](/browse/buildcraft/quarry/).
+ END
+
+video: J6-VUApZGQs, Buildcraft Gates Tutorial, Part 1
diff --git a/static/sitemap.txt b/static/sitemap.txt
index d377d7415..c2f2986d5 100644
--- a/static/sitemap.txt
+++ b/static/sitemap.txt
@@ -13,6 +13,7 @@ http://crafting-guide.com/browse/minecraft/
http://crafting-guide.com/browse/railcraft/
http://crafting-guide.com/browse/thermal_dynamics/
http://crafting-guide.com/browse/thermal_expansion/
+http://crafting-guide.com/browse/buildcraft/tutorials/gates_wires_and_chips/
http://crafting-guide.com/browse/applied_energistics_2/128_spatial_component/
http://crafting-guide.com/browse/applied_energistics_2/128_spatial_storage_cell/
http://crafting-guide.com/browse/applied_energistics_2/16_spatial_component/