even better sandbox
This commit is contained in:
		
							parent
							
								
									0580db6c68
								
							
						
					
					
						commit
						668e9a050e
					
				
					 10 changed files with 169 additions and 19 deletions
				
			
		| 
						 | 
				
			
			@ -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).
 | 
			
		||||
        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"
 | 
			
		||||
+ ### known issues
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,6 +41,7 @@ enum TableState {
 | 
			
		|||
struct HtmlWriter<'a, I, W> {
 | 
			
		||||
    treehouse: &'a Treehouse,
 | 
			
		||||
    config: &'a Config,
 | 
			
		||||
    page_id: &'a str,
 | 
			
		||||
 | 
			
		||||
    /// Iterator supplying events.
 | 
			
		||||
    iter: I,
 | 
			
		||||
| 
						 | 
				
			
			@ -64,10 +65,17 @@ where
 | 
			
		|||
    I: Iterator<Item = Event<'a>>,
 | 
			
		||||
    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 {
 | 
			
		||||
            treehouse,
 | 
			
		||||
            config,
 | 
			
		||||
            page_id,
 | 
			
		||||
            iter,
 | 
			
		||||
            writer,
 | 
			
		||||
            end_newline: true,
 | 
			
		||||
| 
						 | 
				
			
			@ -248,6 +256,8 @@ where
 | 
			
		|||
                                }
 | 
			
		||||
                            })?;
 | 
			
		||||
                            self.write("data-program=\"")?;
 | 
			
		||||
                            escape_href(&mut self.writer, self.page_id)?;
 | 
			
		||||
                            self.write(":")?;
 | 
			
		||||
                            escape_html(&mut self.writer, program_name)?;
 | 
			
		||||
                            self.write("\" data-language=\"")?;
 | 
			
		||||
                            escape_html(&mut self.writer, language)?;
 | 
			
		||||
| 
						 | 
				
			
			@ -606,9 +616,16 @@ impl<'a> CodeBlockMode<'a> {
 | 
			
		|||
/// </ul>
 | 
			
		||||
/// "#);
 | 
			
		||||
/// ```
 | 
			
		||||
pub fn push_html<'a, I>(s: &mut String, treehouse: &'a Treehouse, config: &'a Config, iter: I)
 | 
			
		||||
where
 | 
			
		||||
pub fn push_html<'a, I>(
 | 
			
		||||
    s: &mut String,
 | 
			
		||||
    treehouse: &'a Treehouse,
 | 
			
		||||
    config: &'a Config,
 | 
			
		||||
    page_id: &'a str,
 | 
			
		||||
    iter: I,
 | 
			
		||||
) where
 | 
			
		||||
    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),
 | 
			
		||||
        );
 | 
			
		||||
        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 {
 | 
			
		||||
            write!(
 | 
			
		||||
                s,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -537,6 +537,16 @@ img[is="th-emoji"] {
 | 
			
		|||
 | 
			
		||||
/* Literate programming support */
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
    --error-color: #d94141;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
    :root {
 | 
			
		||||
        --error-color: #e39393;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
th-literate-program[data-mode="input"] {
 | 
			
		||||
    /* Override the cursor with an I-beam, because the editor captures clicks and does not bubble
 | 
			
		||||
       them back up to the caller */
 | 
			
		||||
| 
						 | 
				
			
			@ -551,7 +561,7 @@ th-literate-program[data-mode="output"] {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    & code.error {
 | 
			
		||||
        color: #e39393;
 | 
			
		||||
        color: var(--error-color);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &::after {
 | 
			
		||||
| 
						 | 
				
			
			@ -570,6 +580,7 @@ th-literate-program[data-mode="output"] {
 | 
			
		|||
th-literate-program[data-mode="graphics"] {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    background: none;
 | 
			
		||||
    border: none;
 | 
			
		||||
 | 
			
		||||
    & iframe {
 | 
			
		||||
        border-style: none;
 | 
			
		||||
| 
						 | 
				
			
			@ -581,6 +592,28 @@ th-literate-program[data-mode="graphics"] {
 | 
			
		|||
    & iframe.hidden {
 | 
			
		||||
        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 */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -200,12 +200,25 @@ class OutputMode {
 | 
			
		|||
    clearResults() {
 | 
			
		||||
        this.frame.replaceChildren();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static messageOutputArrayToString(output) {
 | 
			
		||||
        return output
 | 
			
		||||
            .map(x => {
 | 
			
		||||
                if (typeof x === "object") return JSON.stringify(x);
 | 
			
		||||
                else return x + "";
 | 
			
		||||
            })
 | 
			
		||||
            .join(" ");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class GraphicsMode {
 | 
			
		||||
    constructor(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.classList.add("hidden");
 | 
			
		||||
        this.iframe.src = import.meta.resolve("../../html/sandbox.html");
 | 
			
		||||
| 
						 | 
				
			
			@ -213,15 +226,20 @@ class GraphicsMode {
 | 
			
		|||
 | 
			
		||||
        this.iframe.contentWindow.addEventListener("message", event => {
 | 
			
		||||
            let message = event.data;
 | 
			
		||||
            if (message.kind == "resize") {
 | 
			
		||||
            if (message.kind == "ready") {
 | 
			
		||||
                this.evaluate();
 | 
			
		||||
            }
 | 
			
		||||
            else if (message.kind == "resize") {
 | 
			
		||||
                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.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;
 | 
			
		||||
    try {
 | 
			
		||||
        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({
 | 
			
		||||
            kind: "output",
 | 
			
		||||
            output: {
 | 
			
		||||
                kind: "error",
 | 
			
		||||
                message: [error.toString()],
 | 
			
		||||
                message: [err.toString()],
 | 
			
		||||
            },
 | 
			
		||||
            outputIndex,
 | 
			
		||||
        });
 | 
			
		||||
        if (error != null) {
 | 
			
		||||
            error();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    signalEvaluationComplete();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,6 @@ globalThis.console = {
 | 
			
		|||
addEventListener("message", async event => {
 | 
			
		||||
    let message = event.data;
 | 
			
		||||
    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 {
 | 
			
		||||
    constructor(width, height) {
 | 
			
		||||
        this.canvas = document.createElement("canvas");
 | 
			
		||||
| 
						 | 
				
			
			@ -5,6 +17,6 @@ export class Sketch {
 | 
			
		|||
        this.canvas.height = height;
 | 
			
		||||
        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) {
 | 
			
		||||
        const open = `([{'"`;
 | 
			
		||||
        const close = `)]}'"`;
 | 
			
		||||
        const open = `([{'"\``;
 | 
			
		||||
        const close = `)]}'"\``;
 | 
			
		||||
        if (open.includes(event.key)) {
 | 
			
		||||
            preventDefault(event);
 | 
			
		||||
            const pos = save();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@
 | 
			
		|||
 | 
			
		||||
    <script type="module">
 | 
			
		||||
        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
 | 
			
		||||
        // canvas was added, rendering it pretty much useless.
 | 
			
		||||
| 
						 | 
				
			
			@ -35,11 +36,28 @@
 | 
			
		|||
        addEventListener("message", async event => {
 | 
			
		||||
            let message = event.data;
 | 
			
		||||
            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>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue