WebSurfaceView now parsing player code through virtual DOM into iframe

This commit is contained in:
Nick Winter 2016-07-14 18:07:36 -07:00
parent 5b16da099a
commit ed320a8d9e
14 changed files with 207 additions and 22 deletions

View file

@ -0,0 +1,45 @@
// TODO: don't serve this script from codecombat.com; serve it from a harmless extra domain we don't have yet.
window.addEventListener("message", receiveMessage, false);
var concreteDOM;
var virtualDOM;
function receiveMessage(event) {
var origin = event.origin || event.originalEvent.origin; // For Chrome, the origin property is in the event.originalEvent object.
if (origin != 'https://codecombat.com' && origin != 'http://localhost:3000') {
console.log("Bad origin:", origin);
}
//console.log(event);
switch (event.data.type) {
case 'create':
case 'update':
if (virtualDOM)
update(event.data.dom);
else
create(event.data.dom);
break;
case 'log':
console.log(event.data.text);
break;
default:
console.log('Unknown message type:', event.data.type);
}
//event.source.postMessage("hi there yourself! the secret response is: rheeeeet!", event.origin);
}
function create(dom) {
concreteDOM = deku.dom.create(event.data.dom);
virtualDOM = event.data.dom;
// TODO: target the actual HTML tag and combine our initial structure for styles/scripts/tags with theirs
$('body').empty().append(concreteDOM);
}
function update(dom) {
function dispatch() {} // Might want to do something here in the future
var context = {}; // Might want to use this to send shared state to every component
var changes = deku.diff.diffNode(virtualDOM, event.data.dom);
changes.reduce(deku.dom.update(dispatch, context), concreteDOM) // Rerender
virtualDOM = event.data.dom;
}

View file

@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>My CodeCombat Website</title>
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<script src="/javascripts/web-dev-listener.js"></script>
<script src="/javascripts/app/vendor/aether-html.js"></script>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
</head>
<body>
<h1>Loading...</h1>
</body>
</html>

View file

@ -146,3 +146,6 @@ module.exports =
lineOffsetPx: {type: ['number', 'undefined']}
'tome:hide-problem-alert': c.object {title: 'Hide Problem Alert'}
'tome:jiggle-problem-alert': c.object {title: 'Jiggle Problem Alert'}
'tome:html-updated': c.object {title: 'HTML Updated', required: ['html']},
html: {type: 'string'}

View file

@ -0,0 +1,53 @@
#web-surface-view
background-color: white
iframe
width: 100%
height: 100%
//
// body
// background-color: initial
// color: initial
//
// html, body, div, span, applet, object, iframe,
// h1, h2, h3, h4, h5, h6, p, blockquote, pre,
// a, abbr, acronym, address, big, cite, code,
// del, dfn, em, img, ins, kbd, q, s, samp,
// small, strike, strong, sub, sup, tt, var,
// b, u, i, center,
// dl, dt, dd, ol, ul, li,
// fieldset, form, label, legend,
// table, caption, tbody, tfoot, thead, tr, th, td,
// article, aside, canvas, details, embed,
// figure, figcaption, footer, header, hgroup,
// menu, nav, output, ruby, section, summary,
// time, mark, audio, video
// margin: initial
// padding: initial
// border: initial
// font-size: innitial
// font: initial
// vertical-align: baseline
//
// // HTML5 display-role reset for older browsers
// article, aside, details, figcaption, figure,
// footer, header, hgroup, menu, nav, section
// display: block
//
// body
// line-height: 1
//
// ol, ul
// list-style: none
//
// blockquote, q
// quotes: none
//
// blockquote:before, blockquote:after, q:before, q:after
// content: ''
// content: none
//
// table
// border-collapse: collapse
// border-spacing: 0
//

View file

@ -288,9 +288,12 @@ $level-resize-transition-time: 0.5s
#canvas-wrapper canvas
display: none
#web-surface
width: 100%
height: 100%
#web-surface-view
position: absolute
top: 0
right: 0
left: 0
bottom: 0
html.fullscreen-editor
#level-view

View file

@ -0,0 +1 @@
iframe(src="/web-dev-iframe.html")

View file

@ -25,7 +25,7 @@ if view.showAds()
canvas(width=924, height=589)#webgl-surface
canvas(width=924, height=589)#normal-surface
#web-surface
#web-surface-view
#ascii-surface
#canvas-left-gradient.gradient
#canvas-top-gradient.gradient

View file

@ -37,6 +37,7 @@ require 'vendor/aether-python'
require 'vendor/aether-coffeescript'
require 'vendor/aether-lua'
require 'vendor/aether-java'
require 'vendor/aether-html'
module.exports = class LevelEditView extends RootView
id: 'editor-level-view'

View file

@ -44,6 +44,7 @@ LevelSetupManager = require 'lib/LevelSetupManager'
ContactModal = require 'views/core/ContactModal'
HintsView = require './HintsView'
HintsState = require './HintsState'
WebSurfaceView = require './WebSurfaceView'
PROFILE_ME = false
@ -268,6 +269,7 @@ module.exports = class PlayLevelView extends RootView
@insertSubView new DuelStatsView level: @level, session: @session, otherSession: @otherSession, supermodel: @supermodel, thangs: @world.thangs if @level.isType('hero-ladder', 'course-ladder')
@insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel, courseID: @courseID, courseInstanceID: @courseInstanceID}
@insertSubView @hintsView = new HintsView({ @session, @level, @hintsState }), @$('.hints-view')
@insertSubView @webSurface = new WebSurfaceView level: @level if @level.isType('web-dev')
#_.delay (=> Backbone.Mediator.publish('level:set-debug', debug: true)), 5000 if @isIPadApp() # if me.displayName() is 'Nick'
initVolume: ->
@ -324,7 +326,7 @@ module.exports = class PlayLevelView extends RootView
@levelLoader.destroy()
@levelLoader = null
if @level.isType('web-dev')
@initWebSurface()
Backbone.Mediator.publish 'level:started', {}
else
@initSurface()
@ -369,7 +371,6 @@ module.exports = class PlayLevelView extends RootView
if window.currentModal and not window.currentModal.destroyed and window.currentModal.constructor isnt VictoryModal
return Backbone.Mediator.subscribeOnce 'modal:closed', @onLevelStarted, @
@surface?.showLevel()
@webSurface?.showLevel()
Backbone.Mediator.publish 'level:set-time', time: 0
if (@isEditorPreview or @observing) and not @getQueryVariable('intro')
@loadingView.startUnveiling()
@ -671,11 +672,3 @@ module.exports = class PlayLevelView extends RootView
@setupManager?.destroy()
@setupManager = new LevelSetupManager({supermodel: @supermodel, level: @level, levelID: @levelID, parent: @, session: @session, hadEverChosenHero: true})
@setupManager.open()
# web-dev levels
initWebSurface: ->
@webSurface = showLevel: =>
# TODO: make a real WebSurface class
@$('#web-surface').css('background-color', 'red')
Backbone.Mediator.publish 'level:started', {}

View file

@ -0,0 +1,43 @@
CocoView = require 'views/core/CocoView'
State = require 'models/State'
template = require 'templates/play/level/web-surface-view'
module.exports = class WebSurfaceView extends CocoView
id: 'web-surface-view'
template: template
subscriptions:
'tome:html-updated': 'onHTMLUpdated'
initialize: (options) ->
@state = new State
blah: 'blah'
super(options)
afterRender: ->
super()
@iframe = @$('iframe')[0]
$(@iframe).on 'load', (e) =>
@iframe.contentWindow.postMessage {type: 'log', text: 'Player HTML iframe is ready.'}, "*"
@iframeLoaded = true
@onIframeLoaded?()
@onIframeLoaded = null
onHTMLUpdated: (e) ->
unless @iframeLoaded
return @onIframeLoaded = => @onHTMLUpdated e unless @destroyed
dom = htmlparser2.parseDOM e.html, {}
body = _.find(dom, name: 'body') ? {name: 'body', attribs: null, children: dom}
html = _.find(dom, name: 'html') ? {name: 'html', attribs: null, children: [body]}
# TODO: pull out the actual scripts, styles, and body/elements they are doing so we can merge them with our initial structure on the other side
virtualDOM = @dekuify html
messageType = if @virtualDOM then 'update' else 'create'
@iframe.contentWindow.postMessage {type: messageType, dom: virtualDOM}, '*'
dekuify: (elem) ->
return elem.data if elem.type is 'text'
return null if elem.type is 'comment' # TODO: figure out how to make a comment in virtual dom
unless elem.name
console.log("Failed to dekuify", elem)
return elem.type
deku.element(elem.name, elem.attribs, (@dekuify(c) for c in elem.children ? []))

View file

@ -70,6 +70,14 @@ module.exports = class Spell
@originalSource = @languages[@language] ? @languages.javascript
@originalSource = @addPicoCTFProblem() if window.serverConfig.picoCTF
if @level.isType('web-dev')
playerCode = @originalSource.match(/<playercode>\n([\s\S]*)\n *<\/playercode>/)[1]
playerCodeLines = playerCode.split('\n')
indentation = playerCodeLines[0].length - playerCodeLines[0].trim().length
playerCode = (line.substr(indentation) for line in playerCodeLines).join('\n')
@wrapperCode = @originalSource.replace /<playercode>[\s\S]*<\/playercode>/, ''
@originalSource = playerCode
# Translate comments chosen spoken language.
return unless @commentContext
context = $.extend true, {}, @commentContext
@ -95,6 +103,9 @@ module.exports = class Spell
when 'coffeescript' then @originalSource
else @originalSource
constructHTML: (source) ->
@wrapperCode.replace '', source
addPicoCTFProblem: ->
return @originalSource unless problem = @level.picoCTFProblem
description = """

View file

@ -539,9 +539,10 @@ module.exports = class SpellView extends CocoView
Backbone.Mediator.publish 'tome:spell-loaded', spell: @spell
@eventsSuppressed = false # Now that the initial change is in, we can start running any changed code
@createToolbarView()
@updateHTML() if @options.level.isType('web-dev')
createDebugView: ->
return if @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev') # We'll turn this on later, maybe, but not yet.
return if @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') # We'll turn this on later, maybe, but not yet.
@debugView = new SpellDebugView ace: @ace, thang: @thang, spell:@spell
@$el.append @debugView.render().$el.hide()
@ -712,6 +713,8 @@ module.exports = class SpellView extends CocoView
]
onSignificantChange.push _.debounce @checkRequiredCode, 750 if @options.level.get 'requiredCode'
onSignificantChange.push _.debounce @checkSuspectCode, 750 if @options.level.get 'suspectCode'
onAnyChange.push _.throttle @updateHTML, 10 if @options.level.isType('web-dev')
@onCodeChangeMetaHandler = =>
return if @eventsSuppressed
#@playSound 'code-change', volume: 0.5 # Currently not using this sound.
@ -724,6 +727,9 @@ module.exports = class SpellView extends CocoView
onCursorActivity: => # Used to refresh autocast delay; doesn't do anything at the moment.
updateHTML: =>
Backbone.Mediator.publish 'tome:html-updated', html: @spell.constructHTML @getSource()
# Design for a simpler system?
# * Keep Aether linting, debounced, on any significant change
# - All problems just vanish when you make any change to the code
@ -865,9 +871,7 @@ module.exports = class SpellView extends CocoView
beginningOfLine = not currentLine.substr(0, cursorPosition.column).trim().length # uncommenting code, for example
incompleteThis = /^(s|se|sel|self|t|th|thi|this)$/.test currentLine.trim()
#console.log "finished=#{valid and (endOfLine or beginningOfLine) and not incompleteThis}", valid, endOfLine, beginningOfLine, incompleteThis, cursorPosition, currentLine.length, aether, new Date() - 0, currentLine
if @options.level.isType('web-dev') and valid
console.log 'Update it!'
else if valid and (endOfLine or beginningOfLine) and not incompleteThis
if valid and (endOfLine or beginningOfLine) and not incompleteThis
@preload()
singleLineCommentRegex: ->
@ -896,6 +900,7 @@ module.exports = class SpellView extends CocoView
return if @spell.source.indexOf('while') isnt -1 # If they're working with while-loops, it's more likely to be an incomplete infinite loop, so don't preload.
return if @spell.source.length > 500 # Only preload on really short methods
return if @spellThang?.castAether?.metrics?.statementsExecuted > 2000 # Don't preload if they are running significant amounts of user code
return if @options.level.isType('web-dev')
oldSource = @spell.source
oldSpellThangAethers = {}
for thangID, spellThang of @spell.thangs

View file

@ -107,13 +107,12 @@
"aether": {
"main": [
"build/aether.js",
"build/clojure.js",
"build/coffeescript.js",
"build/io.js",
"build/javascript.js",
"build/lua.js",
"build/python.js",
"build/java.js"
"build/java.js",
"build/html.js"
]
}
},

View file

@ -115,7 +115,7 @@ exports.config =
'javascripts/app/vendor/aether-lua.js': 'bower_components/aether/build/lua.js'
'javascripts/app/vendor/aether-java.js': 'bower_components/aether/build/java.js'
'javascripts/app/vendor/aether-python.js': 'bower_components/aether/build/python.js'
'javascripts/app/vendor/aether-java.js': 'bower_components/aether/build/java.js'
'javascripts/app/vendor/aether-html.js': 'bower_components/aether/build/html.js'
# Any vendor libraries we don't want the client to load immediately
'javascripts/app/vendor/d3.js': regJoin('^bower_components/d3')