1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
use crate::page_data::PageData;
use std::collections::HashMap;
use std::env;
/// Initializes the HTML shell by interpolating necessary scripts into it, as well as by adding the render configuration.
pub fn prep_html_shell(
html_shell: String,
render_cfg: &HashMap<String, String>,
path_prefix: &str,
) -> String {
// Define the script that will load the Wasm bundle (inlined to avoid unnecessary extra requests)
let load_script = format!(
r#"<script type="module">
import init, {{ run }} from "{path_prefix}/.perseus/bundle.js";
async function main() {{
await init("{path_prefix}/.perseus/bundle.wasm");
run();
}}
main();
</script>"#,
path_prefix = path_prefix
);
// We inject a script that defines the render config as a global variable, which we put just before the close of the head
// We also inject a delimiter comment that will be used to wall off the constant document head from the interpolated document head
// We also inject the above script to load the Wasm bundle (avoids extra trips)
// We also inject a global variable to identify that we're testing if we are (picked up by app shell to trigger helper DOM events)
// We also inject a `<base>` tag with the base path of the app, which allows us to serve at relative paths just from an environment variable
let prepared = html_shell.replace(
"</head>",
// It's safe to assume that something we just deserialized will serialize again in this case
&format!(
"<script>window.__PERSEUS_RENDER_CFG = '{}';{testing_var}</script>\n{}\n<!--PERSEUS_INTERPOLATED_HEAD_BEGINS-->\n</head>",
serde_json::to_string(&render_cfg).unwrap(),
load_script,
testing_var=if env::var("PERSEUS_TESTING").is_ok() {
"window.__PERSEUS_TESTING = true;"
} else {
""
}
),
);
// Add in the `<base>` element at the very top so that it applies to everything in the HTML shell
// Otherwise any stylesheets loaded before it won't work properly
let prepared_with_base = prepared.replace(
"<head>",
&format!(
"<head>\n<base href=\"{path_prefix}\" />",
// We add a trailing `/` to the base URL (https://stackoverflow.com/a/26043021)
// Note that it's already had any pre-existing ones stripped away
path_prefix = format!("{}/", path_prefix)
),
);
prepared_with_base
}
/// Interpolates content, metadata, and state into the HTML shell, ready to be sent to the user for initial loads. This should be passed
/// an HTMl shell prepared with `prep_html_shell`. This also takes the HTML `id` of the element in the shell to interpolate content
/// into.
pub fn interpolate_page_data(html_shell: &str, page_data: &PageData, root_id: &str) -> String {
// Interpolate the document `<head>`
let html_with_head = html_shell.replace(
"<!--PERSEUS_INTERPOLATED_HEAD_BEGINS-->",
&format!("<!--PERSEUS_INTERPOLATED_HEAD_BEGINS-->{}", &page_data.head),
);
// Interpolate a global variable of the state so the app shell doesn't have to make any more trips
// The app shell will unset this after usage so it doesn't contaminate later non-initial loads
// Error pages (above) will set this to `error`
let state_var = format!("<script>window.__PERSEUS_INITIAL_STATE = `{}`;</script>", {
if let Some(state) = &page_data.state {
state
// We escape any backslashes to prevent their interfering with JSON delimiters
.replace(r#"\"#, r#"\\"#)
// We escape any backticks, which would interfere with JS's raw strings system
.replace(r#"`"#, r#"\`"#)
// We escape any interpolations into JS's raw string system
.replace(r#"${"#, r#"\${"#)
} else {
"None".to_string()
}
});
// We put this at the very end of the head (after the delimiter comment) because it doesn't matter if it's expunged on subsequent loads
let html_with_state = html_with_head.replace("</head>", &format!("{}\n</head>", state_var));
// Figure out exactly what we're interpolating in terms of content
// The user MUST place have a `<div>` of this exact form (documented explicitly)
// We permit either double or single quotes
let html_to_replace_double = format!("<div id=\"{}\">", root_id);
let html_to_replace_single = format!("<div id='{}'>", root_id);
let html_replacement = format!(
// We give the content a specific ID so that it can be deleted if an error page needs to be rendered on the client-side
"{}<div id=\"__perseus_content_initial\" class=\"__perseus_content\">{}</div>",
&html_to_replace_double,
&page_data.content
);
// Now interpolate that HTML into the HTML shell
html_with_state
.replace(&html_to_replace_double, &html_replacement)
.replace(&html_to_replace_single, &html_replacement)
}
/// Intepolates a fallback for locale redirection pages such that, even if JavaScript is disabled, the user will still be redirected to the default locale.
/// From there, Perseus' inbuilt progressive enhancement can occur, but without this a user directed to an unlocalized page with JS disabled would see a
/// blank screen, which is terrible UX. Note that this also includes a fallback for if JS is enabled but Wasm is disabled. Note that the redirect URL
/// is expected to be generated with a path prefix inbuilt.
///
/// This also adds a `__perseus_initial_state` `<div>` in case it's needed (for Wasm redirections).
pub fn interpolate_locale_redirection_fallback(
html_shell: &str,
redirect_url: &str,
root_id: &str,
) -> String {
// This will be used if JavaScript is completely disabled (it's then the site's responsibility to show a further message)
let dumb_redirector = format!(
r#"<noscript>
<meta http-equiv="refresh" content="0; url={}" />
</noscript>"#,
redirect_url
);
// This will be used if JS is enabled, but Wasm is disabled or not supported (it's then the site's responsibility to show a further message)
// Wasm support detector courtesy https://stackoverflow.com/a/47880734
let js_redirector = format!(
r#"<script>
function wasmSupported() {{
try {{
if (typeof WebAssembly === "object"
&& typeof WebAssembly.instantiate === "function") {{
const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
if (module instanceof WebAssembly.Module) {{
return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
}}
}}
}} catch (e) {{}}
return false;
}}
if (!wasmSupported()) {{
window.location.replace("{}");
}}
</script>"#,
redirect_url
);
let html = html_shell.replace(
"</head>",
&format!("{}\n{}\n</head>", js_redirector, dumb_redirector),
);
// The user MUST place have a `<div>` of this exact form (documented explicitly)
// We permit either double or single quotes
let html_to_replace_double = format!("<div id=\"{}\">", root_id);
let html_to_replace_single = format!("<div id='{}'>", root_id);
let html_replacement = format!(
// We give the content a specific ID so that it can be deleted if an error page needs to be rendered on the client-side
"{}<div id=\"__perseus_content_initial\" class=\"__perseus_content\"></div>",
&html_to_replace_double,
);
// Now interpolate that HTML into the HTML shell
html.replace(&html_to_replace_double, &html_replacement)
.replace(&html_to_replace_single, &html_replacement)
}
