-
Notifications
You must be signed in to change notification settings - Fork 50
One-way iframe communication #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<!DOCTYPE HTML> | ||
<html> | ||
<head> | ||
<title>Try PureScript iFrame</title> | ||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> | ||
<meta content="utf-8" http-equiv="encoding"> | ||
</head> | ||
<body> | ||
<p>Your browser is missing <a href=https://caniuse.com/#feat=iframe-srcdoc>srcdoc</a> support</p> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
<link rel="icon" type="image/png" href="./img/favicon_clear-32.png" sizes="32x32"> | ||
<link rel="icon" type="image/png" href="./img/favicon_clear-256.png" sizes="256x256"> | ||
|
||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js"></script> | ||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script> | ||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.js"></script> | ||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/ace/1.1.01/ace.js" charset="utf-8"></script> | ||
|
@@ -157,41 +158,6 @@ | |
}; | ||
})(marker)); | ||
} | ||
|
||
function setupIFrame($ctr, data) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clearing out JS from index.html is a major goal. The final version ends up being very concise. |
||
var $iframe = $('<iframe sandbox="allow-scripts" id="output-iframe" src="frame.html">'); | ||
|
||
$ctr | ||
.empty() | ||
.append($iframe); | ||
|
||
var tries = 0; | ||
var sendSources = setInterval(function() { | ||
// Stop after 10 seconds | ||
if (tries >= 100) { | ||
return clearInterval(sendSources); | ||
} | ||
tries++; | ||
var iframe = $iframe.get(0).contentWindow; | ||
if (iframe) { | ||
iframe.postMessage(data, "*"); | ||
} else { | ||
console.warn("Frame is not available"); | ||
} | ||
}, 100); | ||
|
||
window.addEventListener("message", function() { | ||
clearInterval(sendSources); | ||
}, { once: true }); | ||
|
||
window.addEventListener("message", function(event) { | ||
if (event.data && event.data.gistId && /^[0-9a-f]+$/.test(event.data.gistId)) { | ||
window.location.search = "gist=" + event.data.gistId; | ||
} | ||
}); | ||
|
||
return $iframe; | ||
} | ||
</script> | ||
<script type="text/javascript" src="js/index.js"></script> | ||
</body> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,51 +1,87 @@ | ||
(function() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file is mostly the same, but with additional comments and less function nesting. Actual differences (described in more detail later) are:
|
||
function evalSources(sources) { | ||
var modules = {}; | ||
function dirname(str) { | ||
var ix = str.lastIndexOf("/"); | ||
return ix < 0 ? "" : str.slice(0, ix); | ||
/* | ||
This script executes the JS files returned by PS compilation. | ||
*/ | ||
|
||
// Get directory name of path | ||
function dirname(str) { | ||
let ix = str.lastIndexOf("/"); | ||
return ix < 0 ? "" : str.slice(0, ix); | ||
}; | ||
|
||
// Concatenates paths together | ||
function resolvePath(a, b) { | ||
// `b` relative to current directory with `./` | ||
if (b[0] === "." && b[1] === "/") { | ||
return dirname(a) + b.slice(1); | ||
} | ||
// `b` relative to `a` parent directory with `../` | ||
if (b[0] === "." && b[1] === "." && b[2] === "/") { | ||
return dirname(dirname(a)) + b.slice(2); | ||
} | ||
// `b` is either shim or path from root | ||
return b; | ||
}; | ||
|
||
// Executes JS source and all dependencies. | ||
// Maintains cache of previously-executed sources. | ||
function evalSources(sources) { | ||
// Cache all modules | ||
var modules = {}; | ||
// Executes module source, or returns cached exports. | ||
return function load(name) { | ||
// Check if module is already cached | ||
if (modules[name]) { | ||
return modules[name].exports; | ||
} | ||
function resolvePath(a, b) { | ||
if (b[0] === "." && b[1] === "/") { | ||
return dirname(a) + b.slice(1); | ||
} | ||
if (b[0] === "." && b[1] === "." && b[2] === "/") { | ||
return dirname(dirname(a)) + b.slice(2); | ||
} | ||
return b; | ||
// Not cached, so execute contents. | ||
// Provide custom `require`, `module`, and `exports`. | ||
// Custom `require` which executes file contents, as well as any dependencies. | ||
function require(path) { | ||
return load(resolvePath(name, path)); | ||
} | ||
return function load(name) { | ||
if (modules[name]) { | ||
return modules[name].exports; | ||
} | ||
function require(path) { | ||
return load(resolvePath(name, path)); | ||
} | ||
var module = modules[name] = { exports: {} }; | ||
new Function("module", "exports", "require", sources[name])(module, module.exports, require); | ||
return module.exports; | ||
}; | ||
// Provide empty exports, which will be set, and then returned. | ||
var module = modules[name] = { exports: {} }; | ||
// Create a function from the module's file contents, | ||
// and execute this function with our substitutions. | ||
new Function("module", "exports", "require", sources[name])(module, module.exports, require); | ||
return module.exports; | ||
}; | ||
}; | ||
|
||
function loadFrame(str) { | ||
// Convert JSON string back to object. | ||
// keys: file paths | ||
// values: compressed JS source | ||
obj = JSON.parse(str); | ||
|
||
// Decompress values back to JS source | ||
Object.keys(obj).forEach(function (key) { | ||
obj[key] = LZString.decompressFromEncodedURIComponent(obj[key]); | ||
}); | ||
Comment on lines
+57
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New decompression step |
||
|
||
// Execute all sources, and save returned `exports` from `<file>`. | ||
// Expecting a `exports.main` entry point. | ||
let file = evalSources(obj)("<file>"); | ||
|
||
// Check if `main` can be launched | ||
if (!file.main) { | ||
console.log('Missing "main"'); | ||
} else if (typeof file.main !== "function") { | ||
console.log('"main" is not a function'); | ||
} else { | ||
// Launch entry point | ||
file.main(); | ||
} | ||
}; | ||
|
||
var parent; | ||
|
||
document.addEventListener("DOMContentLoaded", function() { | ||
window.addEventListener("message", function(event) { | ||
parent = event.source; | ||
parent.postMessage("trypurescript", "*"); | ||
var file = evalSources(event.data)("<file>"); | ||
if (file.main && typeof file.main === "function") { | ||
file.main(); | ||
} | ||
}, { once: true }); | ||
}, { once: true }); | ||
|
||
document.addEventListener("click", function(event) { | ||
if (parent && event.target.nodeName === "A" && event.target.hostname === "gist.github.com") { | ||
event.preventDefault(); | ||
parent.postMessage({ | ||
gistId: event.target.pathname.split("/").slice(-1)[0] | ||
}, "*"); | ||
} | ||
}, false); | ||
Comment on lines
-30
to
-50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed event listeners |
||
})(); | ||
// Call script tag contents when frame loads. | ||
// Expects a call to loadFrame, passing JS sources. | ||
window.onload = function() { | ||
// https://stackoverflow.com/a/8677590 | ||
//grab the last script tag in the DOM | ||
//this will always be the one that is currently evaluating during load | ||
let tags = document.getElementsByTagName('script'); | ||
let tag = tags[tags.length -1]; | ||
//force evaluation of the contents | ||
eval( tag.innerHTML ); | ||
}; | ||
Comment on lines
+77
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New code to execute the iframe contents |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
"use strict"; | ||
|
||
exports.compressToEncodedURIComponent = function (input) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are used for the later Code-compressed-in-URL feature too. |
||
return LZString.compressToEncodedURIComponent(input); | ||
} | ||
|
||
exports.decompressFromEncodedURIComponent = function (input) { | ||
let result = LZString.decompressFromEncodedURIComponent(input); | ||
return result || "Failed to decompress URI"; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module LzString where | ||
|
||
foreign import compressToEncodedURIComponent :: String -> String | ||
|
||
foreign import decompressFromEncodedURIComponent :: String -> String |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -4,12 +4,14 @@ import Prelude | |||
|
||||
import Control.Monad.Cont.Trans (ContT(..), runContT) | ||||
import Control.Monad.Except.Trans (runExceptT) | ||||
import Data.Argonaut (encodeJson, stringify) | ||||
import Data.Array (mapMaybe) | ||||
import Data.Array as Array | ||||
import Data.Either (Either(..)) | ||||
import Data.Foldable (elem, fold, for_, intercalate, traverse_) | ||||
import Data.FoldableWithIndex (forWithIndex_) | ||||
import Data.Maybe (Maybe(..), fromMaybe) | ||||
import Data.Newtype (unwrap) | ||||
import Effect (Effect) | ||||
import Effect.Console (error) | ||||
import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn5, mkEffectFn1, runEffectFn1, runEffectFn2, runEffectFn5) | ||||
|
@@ -18,6 +20,7 @@ import Foreign.Object (Object) | |||
import Foreign.Object as Object | ||||
import JQuery as JQuery | ||||
import JQuery.Extras as JQueryExtras | ||||
import LzString (compressToEncodedURIComponent) | ||||
import Try.API (CompileError(..), CompileResult(..), CompileWarning(..), CompilerError(..), ErrorPosition(..), FailedResult(..), SuccessResult(..)) | ||||
import Try.API as API | ||||
import Try.Config as Config | ||||
|
@@ -114,13 +117,6 @@ foreign import setAnnotations :: EffectFn1 (Array Annotation) Unit | |||
clearAnnotations :: Effect Unit | ||||
clearAnnotations = runEffectFn1 setAnnotations [] | ||||
|
||||
-- | Set up a fresh iframe in the specified container, and use it | ||||
-- | to execute the provided JavaScript code. | ||||
foreign import setupIFrame | ||||
:: EffectFn2 JQuery.JQuery | ||||
(Object JS) | ||||
Unit | ||||
|
||||
loader :: Loader | ||||
loader = makeLoader Config.loaderUrl | ||||
|
||||
|
@@ -188,9 +184,31 @@ compile = do | |||
-- | Execute the compiled code in a new iframe. | ||||
execute :: JS -> Object JS -> Effect Unit | ||||
execute js modules = do | ||||
let eventData = Object.insert "<file>" js modules | ||||
let | ||||
-- Add JS entry file, labeled with '<file>' key. | ||||
eventData = Object.insert "<file>" js modules | ||||
-- Convert object of JS files to JSON string. | ||||
-- Compress each file with LZString to avoid | ||||
-- non-trivial HTML + JSON string escaping. | ||||
jsonStr | ||||
= stringify | ||||
$ encodeJson | ||||
$ map (unwrap >>> compressToEncodedURIComponent) eventData | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The JS files are compressed with lz-string because I couldn't figure out how to escape the contents correctly to pass to the iframe. It would be nice to escape these a bit more efficiently, although I don't think this compression/decompression step is a major performance issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you mean by "escape the contents correctly"? What issues were you having? This step does not seem necessary to me, but I would need to know what you were doing, what outputs you were expecting, and what outputs you actually received. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The issue occurs a few lines after this comment in: "<script src='js/frame.js'> loadFrame('" <> jsonStr <> "'); </script>" I tried a few different schemes for escaping |
||||
-- HTML for iframe srcdoc attribute. | ||||
-- We pass the above JSON string to `loadFrame`, which is defined in frame.js. | ||||
-- Note that we must also point to lz-string cdn for decompressing contents. | ||||
iframeSrcDoc | ||||
= "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js\"></script>" | ||||
<> "<script src='js/frame.js'> loadFrame('" <> jsonStr <> "'); </script>" | ||||
|
||||
-- Set up an iframe and use it to execute the provided JavaScript code. | ||||
column2 <- JQuery.select "#column2" | ||||
runEffectFn2 setupIFrame column2 eventData | ||||
iframe <- JQuery.create "<iframe>" | ||||
-- allow-top-navigation-by-user-activation lets us click on links to other examples | ||||
JQuery.setAttr "sandbox" "allow-scripts allow-top-navigation-by-user-activation" iframe | ||||
JQuery.setAttr "src" "frame-error.html" iframe | ||||
JQuery.setAttr "srcdoc" iframeSrcDoc iframe | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One issue with this, and part of the reason for the old code, is just going to be browser support. This only works in the current versions of Edge that have switched to Blink. I am personally somewhat ambivalent on old Edge/IE support, but I know @hdgarrood has requested it in the past. That is, if there is a way to keep a secure and featureful experience in not-to-old browsers, we should make the effort. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like there's fairly widespread support, but in case there's a compatibility issue, this error is displayed:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general I’m less worried about compatibility for tools which are mainly meant for use by developers than I am for actual compiler output or FFI in libraries (things which will affect people who use apps written in purescript, not just purescript devs themselves.) That said, this is not a good enough reason to drop IE11 support for me. 5% is still quite a lot of people. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be fair to @milesfrain, I'm not sure if any of us can confirm that trypurs works as-is in IE11. I've certainly never tested it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah right, yes, that’s a good point. I haven’t run Windows on any of my machines since before I started contributing to PureScript. |
||||
JQuery.append iframe column2 | ||||
|
||||
-- | Setup the editor component and some event handlers. | ||||
setupEditor :: forall r. { code :: String | r } -> Effect Unit | ||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The difference between these gists is that
The original creates a link to the gist:
"https://gist.github.com/" <> gist
This version creates links to TPS with the gist query param.
"https://try.purescript.org/?gist=" <> gist
This allows us to remove the gist click event listener from the old
setupIFrame
.