Skip to content

Commit d16d4d0

Browse files
committed
One-way iframe communication
1 parent a0c4de2 commit d16d4d0

File tree

10 files changed

+141
-95
lines changed

10 files changed

+141
-95
lines changed

client/config/dev/Try.Config.purs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ compileUrl :: String
77
compileUrl = "http://localhost:8081"
88

99
mainGist :: String
10-
mainGist = "7ad2b2eef11ac7dcfd14aa1585dd8f69"
10+
mainGist = "005a86e5c843d3738c1fdd95cc278144"

client/config/prod/Try.Config.purs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ compileUrl :: String
77
compileUrl = "https://compile.purescript.org"
88

99
mainGist :: String
10-
mainGist = "7ad2b2eef11ac7dcfd14aa1585dd8f69"
10+
mainGist = "005a86e5c843d3738c1fdd95cc278144"

client/public/frame-error.html

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE HTML>
2+
<html>
3+
<head>
4+
<title>Try PureScript iFrame</title>
5+
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
6+
<meta content="utf-8" http-equiv="encoding">
7+
</head>
8+
<body>
9+
<p>Your browser is missing <a href=https://caniuse.com/#feat=iframe-srcdoc>srcdoc</a> support</p>
10+
</body>
11+
</html>

client/public/index.html

+1-35
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<link rel="icon" type="image/png" href="./img/favicon_clear-32.png" sizes="32x32">
1111
<link rel="icon" type="image/png" href="./img/favicon_clear-256.png" sizes="256x256">
1212

13+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js"></script>
1314
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
1415
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
1516
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/ace/1.1.01/ace.js" charset="utf-8"></script>
@@ -157,41 +158,6 @@
157158
};
158159
})(marker));
159160
}
160-
161-
function setupIFrame($ctr, data) {
162-
var $iframe = $('<iframe sandbox="allow-scripts" id="output-iframe" src="frame.html">');
163-
164-
$ctr
165-
.empty()
166-
.append($iframe);
167-
168-
var tries = 0;
169-
var sendSources = setInterval(function() {
170-
// Stop after 10 seconds
171-
if (tries >= 100) {
172-
return clearInterval(sendSources);
173-
}
174-
tries++;
175-
var iframe = $iframe.get(0).contentWindow;
176-
if (iframe) {
177-
iframe.postMessage(data, "*");
178-
} else {
179-
console.warn("Frame is not available");
180-
}
181-
}, 100);
182-
183-
window.addEventListener("message", function() {
184-
clearInterval(sendSources);
185-
}, { once: true });
186-
187-
window.addEventListener("message", function(event) {
188-
if (event.data && event.data.gistId && /^[0-9a-f]+$/.test(event.data.gistId)) {
189-
window.location.search = "gist=" + event.data.gistId;
190-
}
191-
});
192-
193-
return $iframe;
194-
}
195161
</script>
196162
<script type="text/javascript" src="js/index.js"></script>
197163
</body>

client/public/js/frame.js

+83-47
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,87 @@
1-
(function() {
2-
function evalSources(sources) {
3-
var modules = {};
4-
function dirname(str) {
5-
var ix = str.lastIndexOf("/");
6-
return ix < 0 ? "" : str.slice(0, ix);
1+
/*
2+
This script executes the JS files returned by PS compilation.
3+
*/
4+
5+
// Get directory name of path
6+
function dirname(str) {
7+
let ix = str.lastIndexOf("/");
8+
return ix < 0 ? "" : str.slice(0, ix);
9+
};
10+
11+
// Concatenates paths together
12+
function resolvePath(a, b) {
13+
// `b` relative to current directory with `./`
14+
if (b[0] === "." && b[1] === "/") {
15+
return dirname(a) + b.slice(1);
16+
}
17+
// `b` relative to `a` parent directory with `../`
18+
if (b[0] === "." && b[1] === "." && b[2] === "/") {
19+
return dirname(dirname(a)) + b.slice(2);
20+
}
21+
// `b` is either shim or path from root
22+
return b;
23+
};
24+
25+
// Executes JS source and all dependencies.
26+
// Maintains cache of previously-executed sources.
27+
function evalSources(sources) {
28+
// Cache all modules
29+
var modules = {};
30+
// Executes module source, or returns cached exports.
31+
return function load(name) {
32+
// Check if module is already cached
33+
if (modules[name]) {
34+
return modules[name].exports;
735
}
8-
function resolvePath(a, b) {
9-
if (b[0] === "." && b[1] === "/") {
10-
return dirname(a) + b.slice(1);
11-
}
12-
if (b[0] === "." && b[1] === "." && b[2] === "/") {
13-
return dirname(dirname(a)) + b.slice(2);
14-
}
15-
return b;
36+
// Not cached, so execute contents.
37+
// Provide custom `require`, `module`, and `exports`.
38+
// Custom `require` which executes file contents, as well as any dependencies.
39+
function require(path) {
40+
return load(resolvePath(name, path));
1641
}
17-
return function load(name) {
18-
if (modules[name]) {
19-
return modules[name].exports;
20-
}
21-
function require(path) {
22-
return load(resolvePath(name, path));
23-
}
24-
var module = modules[name] = { exports: {} };
25-
new Function("module", "exports", "require", sources[name])(module, module.exports, require);
26-
return module.exports;
27-
};
42+
// Provide empty exports, which will be set, and then returned.
43+
var module = modules[name] = { exports: {} };
44+
// Create a function from the module's file contents,
45+
// and execute this function with our substitutions.
46+
new Function("module", "exports", "require", sources[name])(module, module.exports, require);
47+
return module.exports;
48+
};
49+
};
50+
51+
function loadFrame(str) {
52+
// Convert JSON string back to object.
53+
// keys: file paths
54+
// values: compressed JS source
55+
obj = JSON.parse(str);
56+
57+
// Decompress values back to JS source
58+
Object.keys(obj).forEach(function (key) {
59+
obj[key] = LZString.decompressFromEncodedURIComponent(obj[key]);
60+
});
61+
62+
// Execute all sources, and save returned `exports` from `<file>`.
63+
// Expecting a `exports.main` entry point.
64+
let file = evalSources(obj)("<file>");
65+
66+
// Check if `main` can be launched
67+
if (!file.main) {
68+
console.log('Missing "main"');
69+
} else if (typeof file.main !== "function") {
70+
console.log('"main" is not a function');
71+
} else {
72+
// Launch entry point
73+
file.main();
2874
}
75+
};
2976

30-
var parent;
31-
32-
document.addEventListener("DOMContentLoaded", function() {
33-
window.addEventListener("message", function(event) {
34-
parent = event.source;
35-
parent.postMessage("trypurescript", "*");
36-
var file = evalSources(event.data)("<file>");
37-
if (file.main && typeof file.main === "function") {
38-
file.main();
39-
}
40-
}, { once: true });
41-
}, { once: true });
42-
43-
document.addEventListener("click", function(event) {
44-
if (parent && event.target.nodeName === "A" && event.target.hostname === "gist.github.com") {
45-
event.preventDefault();
46-
parent.postMessage({
47-
gistId: event.target.pathname.split("/").slice(-1)[0]
48-
}, "*");
49-
}
50-
}, false);
51-
})();
77+
// Call script tag contents when frame loads.
78+
// Expects a call to loadFrame, passing JS sources.
79+
window.onload = function() {
80+
// https://stackoverflow.com/a/8677590
81+
//grab the last script tag in the DOM
82+
//this will always be the one that is currently evaluating during load
83+
let tags = document.getElementsByTagName('script');
84+
let tag = tags[tags.length -1];
85+
//force evaluation of the contents
86+
eval( tag.innerHTML );
87+
};

client/spago.dhall

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ You can edit this file as you like.
44
-}
55
{ name = "try-purescript"
66
, dependencies =
7-
[ "arrays"
7+
[ "argonaut"
8+
, "arrays"
89
, "bifunctors"
910
, "console"
1011
, "const"

client/src/LzString.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"use strict";
2+
3+
exports.compressToEncodedURIComponent = function (input) {
4+
return LZString.compressToEncodedURIComponent(input);
5+
}
6+
7+
exports.decompressFromEncodedURIComponent = function (input) {
8+
let result = LZString.decompressFromEncodedURIComponent(input);
9+
return result || "Failed to decompress URI";
10+
}

client/src/LzString.purs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module LzString where
2+
3+
foreign import compressToEncodedURIComponent :: String -> String
4+
5+
foreign import decompressFromEncodedURIComponent :: String -> String

client/src/Main.js

-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@ exports.onEditorChanged = onEditorChanged;
55
exports.cleanUpMarkers = cleanUpMarkers;
66
exports.addMarker = addMarker;
77
exports.setAnnotations = setAnnotations;
8-
exports.setupIFrame = setupIFrame;

client/src/Main.purs

+27-9
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import Prelude
44

55
import Control.Monad.Cont.Trans (ContT(..), runContT)
66
import Control.Monad.Except.Trans (runExceptT)
7+
import Data.Argonaut (encodeJson, stringify)
78
import Data.Array (mapMaybe)
89
import Data.Array as Array
910
import Data.Either (Either(..))
1011
import Data.Foldable (elem, fold, for_, intercalate, traverse_)
1112
import Data.FoldableWithIndex (forWithIndex_)
1213
import Data.Maybe (Maybe(..), fromMaybe)
14+
import Data.Newtype (unwrap)
1315
import Effect (Effect)
1416
import Effect.Console (error)
1517
import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn5, mkEffectFn1, runEffectFn1, runEffectFn2, runEffectFn5)
@@ -18,6 +20,7 @@ import Foreign.Object (Object)
1820
import Foreign.Object as Object
1921
import JQuery as JQuery
2022
import JQuery.Extras as JQueryExtras
23+
import LzString (compressToEncodedURIComponent)
2124
import Try.API (CompileError(..), CompileResult(..), CompileWarning(..), CompilerError(..), ErrorPosition(..), FailedResult(..), SuccessResult(..))
2225
import Try.API as API
2326
import Try.Config as Config
@@ -114,13 +117,6 @@ foreign import setAnnotations :: EffectFn1 (Array Annotation) Unit
114117
clearAnnotations :: Effect Unit
115118
clearAnnotations = runEffectFn1 setAnnotations []
116119

117-
-- | Set up a fresh iframe in the specified container, and use it
118-
-- | to execute the provided JavaScript code.
119-
foreign import setupIFrame
120-
:: EffectFn2 JQuery.JQuery
121-
(Object JS)
122-
Unit
123-
124120
loader :: Loader
125121
loader = makeLoader Config.loaderUrl
126122

@@ -188,9 +184,31 @@ compile = do
188184
-- | Execute the compiled code in a new iframe.
189185
execute :: JS -> Object JS -> Effect Unit
190186
execute js modules = do
191-
let eventData = Object.insert "<file>" js modules
187+
let
188+
-- Add JS entry file, labeled with '<file>' key.
189+
eventData = Object.insert "<file>" js modules
190+
-- Convert object of JS files to JSON string.
191+
-- Compress each file with LZString to avoid
192+
-- non-trivial HTML + JSON string escaping.
193+
jsonStr
194+
= stringify
195+
$ encodeJson
196+
$ map (unwrap >>> compressToEncodedURIComponent) eventData
197+
-- HTML for iframe srcdoc attribute.
198+
-- We pass the above JSON string to `loadFrame`, which is defined in frame.js.
199+
-- Note that we must also point to lz-string cdn for decompressing contents.
200+
iframeSrcDoc
201+
= "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js\"></script>"
202+
<> "<script src='js/frame.js'> loadFrame('" <> jsonStr <> "'); </script>"
203+
204+
-- Set up an iframe and use it to execute the provided JavaScript code.
192205
column2 <- JQuery.select "#column2"
193-
runEffectFn2 setupIFrame column2 eventData
206+
iframe <- JQuery.create "<iframe>"
207+
-- allow-top-navigation lets us click on links to other examples
208+
JQuery.setAttr "sandbox" "allow-scripts allow-top-navigation" iframe
209+
JQuery.setAttr "src" "frame-error.html" iframe
210+
JQuery.setAttr "srcdoc" iframeSrcDoc iframe
211+
JQuery.append iframe column2
194212

195213
-- | Setup the editor component and some event handlers.
196214
setupEditor :: forall r. { code :: String | r } -> Effect Unit

0 commit comments

Comments
 (0)