even better sandbox
This commit is contained in:
parent
0580db6c68
commit
668e9a050e
|
@ -111,6 +111,27 @@ scripts = ["components/literate-programming.js"]
|
||||||
- this API wraps a lower-level message-passing API which is used to communicate with the main page, to let it set things like the sandbox `<iframe>`'s size (as well as make it visible).
|
- this API wraps a lower-level message-passing API which is used to communicate with the main page, to let it set things like the sandbox `<iframe>`'s size (as well as make it visible).
|
||||||
see the source code for details.
|
see the source code for details.
|
||||||
|
|
||||||
|
% id = "01HPXYH05CWEAKZ406ZNA919TS"
|
||||||
|
- it's also possible to use ordinary DOM elements, *however* instead of `document.body` you should use `treehouse/sandbox.js`'s `body()`.
|
||||||
|
|
||||||
|
there's also sugar for `body().appendChild()` in form of `addElement()`:
|
||||||
|
|
||||||
|
```javascript dom-output
|
||||||
|
import { addElement } from "treehouse/sandbox.js";
|
||||||
|
|
||||||
|
let slider = document.createElement("input");
|
||||||
|
slider.type = "range";
|
||||||
|
addElement(slider);
|
||||||
|
```
|
||||||
|
|
||||||
|
<th-literate-program data-mode="graphics" data-program="dom-output"></th-literate-program>
|
||||||
|
|
||||||
|
% id = "01HPXYH05C3VC96N214D8VQGND"
|
||||||
|
- do note however that this isn't used on the site right now due to a lack of CSS in the sandbox, therefore rendering the sandbox's theme unreadable in dark mode.
|
||||||
|
|
||||||
|
% id = "01HPXYH05C75CGRW5GN1K9W8GY"
|
||||||
|
- technically you *can* use `document.body`, but its content will be replaced with `body()`'s once the script finishes running, so in the end it's quite useless
|
||||||
|
|
||||||
% id = "01HPWJB4Y5H9DKZT2ZA8PWNV99"
|
% id = "01HPWJB4Y5H9DKZT2ZA8PWNV99"
|
||||||
+ ### known issues
|
+ ### known issues
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ enum TableState {
|
||||||
struct HtmlWriter<'a, I, W> {
|
struct HtmlWriter<'a, I, W> {
|
||||||
treehouse: &'a Treehouse,
|
treehouse: &'a Treehouse,
|
||||||
config: &'a Config,
|
config: &'a Config,
|
||||||
|
page_id: &'a str,
|
||||||
|
|
||||||
/// Iterator supplying events.
|
/// Iterator supplying events.
|
||||||
iter: I,
|
iter: I,
|
||||||
|
@ -64,10 +65,17 @@ where
|
||||||
I: Iterator<Item = Event<'a>>,
|
I: Iterator<Item = Event<'a>>,
|
||||||
W: StrWrite,
|
W: StrWrite,
|
||||||
{
|
{
|
||||||
fn new(treehouse: &'a Treehouse, config: &'a Config, iter: I, writer: W) -> Self {
|
fn new(
|
||||||
|
treehouse: &'a Treehouse,
|
||||||
|
config: &'a Config,
|
||||||
|
page_id: &'a str,
|
||||||
|
iter: I,
|
||||||
|
writer: W,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
treehouse,
|
treehouse,
|
||||||
config,
|
config,
|
||||||
|
page_id,
|
||||||
iter,
|
iter,
|
||||||
writer,
|
writer,
|
||||||
end_newline: true,
|
end_newline: true,
|
||||||
|
@ -248,6 +256,8 @@ where
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
self.write("data-program=\"")?;
|
self.write("data-program=\"")?;
|
||||||
|
escape_href(&mut self.writer, self.page_id)?;
|
||||||
|
self.write(":")?;
|
||||||
escape_html(&mut self.writer, program_name)?;
|
escape_html(&mut self.writer, program_name)?;
|
||||||
self.write("\" data-language=\"")?;
|
self.write("\" data-language=\"")?;
|
||||||
escape_html(&mut self.writer, language)?;
|
escape_html(&mut self.writer, language)?;
|
||||||
|
@ -606,9 +616,16 @@ impl<'a> CodeBlockMode<'a> {
|
||||||
/// </ul>
|
/// </ul>
|
||||||
/// "#);
|
/// "#);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn push_html<'a, I>(s: &mut String, treehouse: &'a Treehouse, config: &'a Config, iter: I)
|
pub fn push_html<'a, I>(
|
||||||
where
|
s: &mut String,
|
||||||
|
treehouse: &'a Treehouse,
|
||||||
|
config: &'a Config,
|
||||||
|
page_id: &'a str,
|
||||||
|
iter: I,
|
||||||
|
) where
|
||||||
I: Iterator<Item = Event<'a>>,
|
I: Iterator<Item = Event<'a>>,
|
||||||
{
|
{
|
||||||
HtmlWriter::new(treehouse, config, iter, s).run().unwrap();
|
HtmlWriter::new(treehouse, config, page_id, iter, s)
|
||||||
|
.run()
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,7 +130,13 @@ pub fn branch_to_html(
|
||||||
Some(broken_link_callback),
|
Some(broken_link_callback),
|
||||||
);
|
);
|
||||||
s.push_str("<th-bc>");
|
s.push_str("<th-bc>");
|
||||||
markdown::push_html(s, treehouse, config, markdown_parser);
|
markdown::push_html(
|
||||||
|
s,
|
||||||
|
treehouse,
|
||||||
|
config,
|
||||||
|
treehouse.tree_path(file_id).expect(".tree file expected"),
|
||||||
|
markdown_parser,
|
||||||
|
);
|
||||||
if let Content::Link(link) = &branch.attributes.content {
|
if let Content::Link(link) = &branch.attributes.content {
|
||||||
write!(
|
write!(
|
||||||
s,
|
s,
|
||||||
|
|
|
@ -537,6 +537,16 @@ img[is="th-emoji"] {
|
||||||
|
|
||||||
/* Literate programming support */
|
/* Literate programming support */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--error-color: #d94141;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--error-color: #e39393;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
th-literate-program[data-mode="input"] {
|
th-literate-program[data-mode="input"] {
|
||||||
/* Override the cursor with an I-beam, because the editor captures clicks and does not bubble
|
/* Override the cursor with an I-beam, because the editor captures clicks and does not bubble
|
||||||
them back up to the caller */
|
them back up to the caller */
|
||||||
|
@ -551,7 +561,7 @@ th-literate-program[data-mode="output"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
& code.error {
|
& code.error {
|
||||||
color: #e39393;
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
@ -570,6 +580,7 @@ th-literate-program[data-mode="output"] {
|
||||||
th-literate-program[data-mode="graphics"] {
|
th-literate-program[data-mode="graphics"] {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: none;
|
background: none;
|
||||||
|
border: none;
|
||||||
|
|
||||||
& iframe {
|
& iframe {
|
||||||
border-style: none;
|
border-style: none;
|
||||||
|
@ -581,6 +592,28 @@ th-literate-program[data-mode="graphics"] {
|
||||||
& iframe.hidden {
|
& iframe.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& pre.error {
|
||||||
|
color: var(--error-color);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: 'Error';
|
||||||
|
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Syntax highlighting */
|
/* Syntax highlighting */
|
||||||
|
|
|
@ -200,12 +200,25 @@ class OutputMode {
|
||||||
clearResults() {
|
clearResults() {
|
||||||
this.frame.replaceChildren();
|
this.frame.replaceChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static messageOutputArrayToString(output) {
|
||||||
|
return output
|
||||||
|
.map(x => {
|
||||||
|
if (typeof x === "object") return JSON.stringify(x);
|
||||||
|
else return x + "";
|
||||||
|
})
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GraphicsMode {
|
class GraphicsMode {
|
||||||
constructor(frame) {
|
constructor(frame) {
|
||||||
this.frame = frame;
|
this.frame = frame;
|
||||||
|
|
||||||
|
this.error = document.createElement("pre");
|
||||||
|
this.error.classList.add("error");
|
||||||
|
this.frame.appendChild(this.error);
|
||||||
|
|
||||||
this.iframe = document.createElement("iframe");
|
this.iframe = document.createElement("iframe");
|
||||||
this.iframe.classList.add("hidden");
|
this.iframe.classList.add("hidden");
|
||||||
this.iframe.src = import.meta.resolve("../../html/sandbox.html");
|
this.iframe.src = import.meta.resolve("../../html/sandbox.html");
|
||||||
|
@ -213,15 +226,20 @@ class GraphicsMode {
|
||||||
|
|
||||||
this.iframe.contentWindow.addEventListener("message", event => {
|
this.iframe.contentWindow.addEventListener("message", event => {
|
||||||
let message = event.data;
|
let message = event.data;
|
||||||
if (message.kind == "resize") {
|
if (message.kind == "ready") {
|
||||||
|
this.evaluate();
|
||||||
|
}
|
||||||
|
else if (message.kind == "resize") {
|
||||||
this.resize(message);
|
this.resize(message);
|
||||||
|
} else if (message.kind == "output" && message.output.kind == "error") {
|
||||||
|
this.error.textContent = OutputMode.messageOutputArrayToString(message.output.message);
|
||||||
|
this.iframe.classList.add("hidden");
|
||||||
|
} else if (message.kind == "evalComplete") {
|
||||||
|
this.error.textContent = "";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.iframe.contentWindow.addEventListener("DOMContentLoaded", () => this.evaluate());
|
|
||||||
this.frame.program.onChanged.push(_ => this.evaluate());
|
this.frame.program.onChanged.push(_ => this.evaluate());
|
||||||
|
|
||||||
this.evaluate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate() {
|
evaluate() {
|
||||||
|
|
|
@ -18,7 +18,22 @@ async function withTemporaryGlobalScope(callback) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function evaluate(commands) {
|
let evaluationComplete = null;
|
||||||
|
|
||||||
|
export async function evaluate(commands, { start, success, error }) {
|
||||||
|
if (evaluationComplete != null) {
|
||||||
|
await evaluationComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start != null) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
let signalEvaluationComplete;
|
||||||
|
evaluationComplete = new Promise((resolve, _reject) => {
|
||||||
|
signalEvaluationComplete = resolve;
|
||||||
|
})
|
||||||
|
|
||||||
outputIndex = 0;
|
outputIndex = 0;
|
||||||
try {
|
try {
|
||||||
await withTemporaryGlobalScope(async scope => {
|
await withTemporaryGlobalScope(async scope => {
|
||||||
|
@ -34,15 +49,25 @@ export async function evaluate(commands) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
if (success != null) {
|
||||||
|
success();
|
||||||
|
}
|
||||||
|
postMessage({
|
||||||
|
kind: "evalComplete",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
postMessage({
|
postMessage({
|
||||||
kind: "output",
|
kind: "output",
|
||||||
output: {
|
output: {
|
||||||
kind: "error",
|
kind: "error",
|
||||||
message: [error.toString()],
|
message: [err.toString()],
|
||||||
},
|
},
|
||||||
outputIndex,
|
outputIndex,
|
||||||
});
|
});
|
||||||
|
if (error != null) {
|
||||||
|
error();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
signalEvaluationComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,6 @@ globalThis.console = {
|
||||||
addEventListener("message", async event => {
|
addEventListener("message", async event => {
|
||||||
let message = event.data;
|
let message = event.data;
|
||||||
if (message.action == "eval") {
|
if (message.action == "eval") {
|
||||||
evaluate(message.input);
|
evaluate(message.input, {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,15 @@
|
||||||
|
export const internals = {
|
||||||
|
body: document.createElement("body"),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function body() {
|
||||||
|
return internals.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addElement(element) {
|
||||||
|
body().appendChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
export class Sketch {
|
export class Sketch {
|
||||||
constructor(width, height) {
|
constructor(width, height) {
|
||||||
this.canvas = document.createElement("canvas");
|
this.canvas = document.createElement("canvas");
|
||||||
|
@ -5,6 +17,6 @@ export class Sketch {
|
||||||
this.canvas.height = height;
|
this.canvas.height = height;
|
||||||
this.ctx = this.canvas.getContext("2d");
|
this.ctx = this.canvas.getContext("2d");
|
||||||
|
|
||||||
document.body.appendChild(this.canvas);
|
addElement(this.canvas);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
static/js/vendor/codejar.js
vendored
4
static/js/vendor/codejar.js
vendored
|
@ -306,8 +306,8 @@ export function CodeJar(editor, highlight, opt = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function handleSelfClosingCharacters(event) {
|
function handleSelfClosingCharacters(event) {
|
||||||
const open = `([{'"`;
|
const open = `([{'"\``;
|
||||||
const close = `)]}'"`;
|
const close = `)]}'"\``;
|
||||||
if (open.includes(event.key)) {
|
if (open.includes(event.key)) {
|
||||||
preventDefault(event);
|
preventDefault(event);
|
||||||
const pos = save();
|
const pos = save();
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { evaluate } from "treehouse/components/literate-programming/eval.js";
|
import { evaluate } from "treehouse/components/literate-programming/eval.js";
|
||||||
|
import { internals as sandboxInternals } from "treehouse/sandbox.js";
|
||||||
|
|
||||||
// I'm aware there's also ResizeObserver but it didn't seem to fire off any events when a
|
// I'm aware there's also ResizeObserver but it didn't seem to fire off any events when a
|
||||||
// canvas was added, rendering it pretty much useless.
|
// canvas was added, rendering it pretty much useless.
|
||||||
|
@ -35,11 +36,28 @@
|
||||||
addEventListener("message", async event => {
|
addEventListener("message", async event => {
|
||||||
let message = event.data;
|
let message = event.data;
|
||||||
if (message.action == "eval") {
|
if (message.action == "eval") {
|
||||||
document.body.replaceChildren();
|
evaluate(message.input, {
|
||||||
evaluate(message.input);
|
success() {
|
||||||
|
// A double buffered approach for flickerless code modifications.
|
||||||
|
document.body.replaceChildren(...sandboxInternals.body.childNodes);
|
||||||
|
sandboxInternals.body.replaceChildren();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
postMessage({
|
||||||
|
kind: "resize",
|
||||||
|
width: document.body.scrollWidth,
|
||||||
|
height: document.body.scrollHeight,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
error() {
|
||||||
|
sandboxInternals.body.replaceChildren();
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
postMessage({ kind: "ready" });
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue