Skip to content
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

Support wasm-pack frontend in mdBook #1501

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
6 changes: 6 additions & 0 deletions src/book/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ impl BookBuilder {
let mut highlight_js = File::create(themedir.join("highlight.js"))?;
highlight_js.write_all(theme::HIGHLIGHT_JS)?;

let mut iframe = File::create(themedir.join("iframe.html"))?;
iframe.write_all(theme::IFRAME)?;

let mut wasm_entry_js = File::create(themedir.join("wasm-entry.mjs"))?;
wasm_entry_js.write_all(theme::WASM_ENTRY_MJS)?;

Ok(())
}

Expand Down
2 changes: 2 additions & 0 deletions src/renderer/html_handlebars/hbs_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ impl HtmlHandlebars {
write_file(destination, "CNAME", format!("{}\n", cname).as_bytes())?;
}

write_file(destination, "iframe.html", &theme.iframe_html)?;
write_file(destination, "wasm-entry.mjs", &theme.wasm_entry_mjs)?;
write_file(destination, "book.js", &theme.js)?;
write_file(destination, "css/general.css", &theme.general_css)?;
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
Expand Down
146 changes: 131 additions & 15 deletions src/theme/book.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ function playground_text(playground) {
method: 'POST',
mode: 'cors',
})
.then(response => response.json())
.then(response => {
// get list of crates available in the rust playground
let playground_crates = response.crates.map(item => item["id"]);
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
});
.then(response => response.json())
.then(response => {
// get list of crates available in the rust playground
let playground_crates = response.crates.map(item => item["id"]);
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
});
}

function handle_crate_list_update(playground_block, playground_crates) {
Expand All @@ -59,7 +59,13 @@ function playground_text(playground) {
win: "Ctrl-Enter",
mac: "Ctrl-Enter"
},
exec: _editor => run_rust_code(playground_block)
exec: _editor => {
if (code_block.classList.contains("wasm")) {
run_wasm_pack_code(playground_block);
} else {
run_rust_code(playground_block);
}
}
});
}
}
Expand Down Expand Up @@ -132,9 +138,115 @@ function playground_text(playground) {
mode: 'cors',
body: JSON.stringify(params)
})
.then(response => response.json())
.then(response => result_block.innerText = response.result)
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
.then(response => response.json())
.then(response => result_block.innerText = response.result)
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
}

function run_wasm_pack_code(code_block) {
var result_block = code_block.querySelector(".result");
if (!result_block) {
result_block = document.createElement('code');
result_block.className = 'result hljs language-bash';

code_block.append(result_block);
}

let text = playground_text(code_block);

var params = {
code: text,
};

result_block.innerText = "Running...";

prepareSandbox(params)
.then(src => processHTML(src))
.then(html => {
result_block.innerText = "";
var iframe = createIFrame(html);
result_block.appendChild(iframe);
})
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
}

// Greatly inspired from WebAssemblyStudio
async function prepareSandbox(params) {
var wasmResult = fetch_with_timeout("https://playground.titaneric.com/wasm-pack", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
body: JSON.stringify(params)
})
.then(response => response.json())
.then(({ wasm_js, wasm_bg, success, stderr }) => {
if (!success) {
throw new Error(stderr);
}

return {
wasm_js: atob(wasm_js),
wasm_bg: base64ToByteArray(wasm_bg)
}
})

var htmlSrc = fetch(new Request("iframe.html"))
.then(response => response.text());
var jsSrc = fetch(new Request("wasm-entry.mjs"))
.then(response => response.text());

return Promise.all([htmlSrc, jsSrc, wasmResult])
}

function base64ToByteArray(src) {
var decode = atob(src);
const byteNumbers = new Array(decode.length);
for (let i = 0; i < decode.length; i++) {
byteNumbers[i] = decode.charCodeAt(i);
}
return new Uint8Array(byteNumbers);
}

async function processHTML([htmlSrc, jsSrc, { wasm_js, wasm_bg }]) {
var src = rewriteJS(jsSrc, wasm_js, wasm_bg);
var jsBlob = createObjectURL(src, "application/javascript");
return htmlSrc.replace(/\bsrc\s*=\s*['"](.+?)['"]/g, (all, path) => {
return `src="${jsBlob}"`;
});
}

function rewriteJS(src, wasmJS, bgWasm) {
var wasmJSBlob = createObjectURL(wasmJS, "application/javascript");
var bgWasmBlob = createObjectURL(bgWasm, "application/wasm");

// replace wasm.js
src = src.replace(/\bfrom\s+['"](.+?)['"](\s*[;\n])/g, (all, path, sep) => {
return `from "${wasmJSBlob}"${sep}`;
})
// replace `input` of init to object URL
src = src.replace(/\(['"](.+?)['"]\)/g, (all, url) => {
return `("${bgWasmBlob}")`;
})
return src
}

function createObjectURL(src, mime) {
return URL.createObjectURL(new Blob([src], { type: mime }));
}

function createIFrame(src) {
var iframe = document.createElement('iframe');
iframe.scrolling = 'no';
iframe.style.height = "100%";
iframe.style.width = "100%";
iframe.style.padding = 0;
iframe.style.margin = 0;
iframe.style.border = 0;
iframe.style.overflow = "hidden";
iframe.src = createObjectURL(src, "text/html");
return iframe
}

// Syntax highlighting Configuration
Expand All @@ -146,7 +258,7 @@ function playground_text(playground) {
let code_nodes = Array
.from(document.querySelectorAll('code'))
// Don't highlight `inline code` blocks in headers.
.filter(function (node) {return !node.parentElement.classList.contains("header"); });
.filter(function (node) { return !node.parentElement.classList.contains("header"); });

if (window.ace) {
// language-rust class needs to be removed for editable
Expand Down Expand Up @@ -240,7 +352,11 @@ function playground_text(playground) {

buttons.insertBefore(runCodeButton, buttons.firstChild);
runCodeButton.addEventListener('click', function (e) {
run_rust_code(pre_block);
if (code_block.classList.contains("wasm")) {
run_wasm_pack_code(pre_block);
} else {
run_rust_code(pre_block);
}
});

if (window.playground_copyable) {
Expand Down Expand Up @@ -363,15 +479,15 @@ function playground_text(playground) {
set_theme(theme);
});

themePopup.addEventListener('focusout', function(e) {
themePopup.addEventListener('focusout', function (e) {
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) {
hideThemes();
}
});

// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628
document.addEventListener('click', function(e) {
document.addEventListener('click', function (e) {
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
hideThemes();
}
Expand Down Expand Up @@ -593,7 +709,7 @@ function playground_text(playground) {
});
})();

(function scrollToTop () {
(function scrollToTop() {
var menuTitle = document.querySelector('.menu-title');

menuTitle.addEventListener('click', function () {
Expand Down
18 changes: 18 additions & 0 deletions src/theme/iframe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<style>
body {
background-color: rgb(255, 255, 255);
}
</style>
</head>

<body>
<span id="container"></span>
<script src="wasm-entry.mjs" type="module"></script>
</body>

</html>
12 changes: 12 additions & 0 deletions src/theme/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use std::path::Path;

use crate::errors::*;

pub static IFRAME: &[u8] = include_bytes!("iframe.html");
pub static WASM_ENTRY_MJS: &[u8] = include_bytes!("wasm-entry.mjs");
pub static INDEX: &[u8] = include_bytes!("index.hbs");
pub static HEAD: &[u8] = include_bytes!("head.hbs");
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
Expand Down Expand Up @@ -62,6 +64,8 @@ pub struct Theme {
pub ayu_highlight_css: Vec<u8>,
pub highlight_js: Vec<u8>,
pub clipboard_js: Vec<u8>,
pub iframe_html: Vec<u8>,
pub wasm_entry_mjs: Vec<u8>,
}

impl Theme {
Expand All @@ -79,6 +83,8 @@ impl Theme {
// Check for individual files, if they exist copy them across
{
let files = vec![
(theme_dir.join("iframe.html"), &mut theme.iframe_html),
(theme_dir.join("wasm-entry.mjs"), &mut theme.wasm_entry_mjs),
(theme_dir.join("index.hbs"), &mut theme.index),
(theme_dir.join("head.hbs"), &mut theme.head),
(theme_dir.join("redirect.hbs"), &mut theme.redirect),
Expand Down Expand Up @@ -161,6 +167,8 @@ impl Default for Theme {
ayu_highlight_css: AYU_HIGHLIGHT_CSS.to_owned(),
highlight_js: HIGHLIGHT_JS.to_owned(),
clipboard_js: CLIPBOARD_JS.to_owned(),
iframe_html: IFRAME.to_owned(),
wasm_entry_mjs: WASM_ENTRY_MJS.to_owned(),
}
}
}
Expand Down Expand Up @@ -219,6 +227,8 @@ mod tests {
"highlight.css",
"ayu-highlight.css",
"clipboard.min.js",
"iframe.html",
"wasm-entry.mjs",
];

let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
Expand Down Expand Up @@ -248,6 +258,8 @@ mod tests {
ayu_highlight_css: Vec::new(),
highlight_js: Vec::new(),
clipboard_js: Vec::new(),
iframe_html: Vec::new(),
wasm_entry_mjs: Vec::new(),
};

assert_eq!(got, empty);
Expand Down
3 changes: 3 additions & 0 deletions src/theme/wasm-entry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import init from './wasm.js';

await init("WASM_URL");
2 changes: 2 additions & 0 deletions tests/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ fn copy_theme() {
"favicon.svg",
"highlight.css",
"highlight.js",
"iframe.html",
"index.hbs",
"wasm-entry.mjs",
];
let theme_dir = temp.path().join("theme");
let mut actual: Vec<_> = walkdir::WalkDir::new(&theme_dir)
Expand Down