Move the document trait into a separate crate (#3035)
* add a default head method through eval * remove the old document trait * implement document for each platform * pull out document into a dedicated crate to cut down on shared dependencies --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
2b219c826c
commit
519ec9d294
91 files changed
+1113
-1044
No files matched your search
@@ -0,0 +1,156 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A context for the document
|
||||
pub type DocumentContext = Arc<dyn Document>;
|
||||
|
||||
fn format_string_for_js(s: &str) -> String {
|
||||
let escaped = s
|
||||
.replace('\\', "\\\\")
|
||||
.replace('\n', "\\n")
|
||||
.replace('\r', "\\r")
|
||||
.replace('"', "\\\"");
|
||||
format!("\"{escaped}\"")
|
||||
}
|
||||
|
||||
fn format_attributes(attributes: &[(&str, String)]) -> String {
|
||||
let mut formatted = String::from("[");
|
||||
for (key, value) in attributes {
|
||||
formatted.push_str(&format!(
|
||||
"[{}, {}],",
|
||||
format_string_for_js(key),
|
||||
format_string_for_js(value)
|
||||
));
|
||||
}
|
||||
if formatted.ends_with(',') {
|
||||
formatted.pop();
|
||||
}
|
||||
formatted.push(']');
|
||||
formatted
|
||||
}
|
||||
|
||||
fn create_element_in_head(
|
||||
tag: &str,
|
||||
attributes: &[(&str, String)],
|
||||
children: Option<String>,
|
||||
) -> String {
|
||||
let helpers = include_str!("./js/head.js");
|
||||
let attributes = format_attributes(attributes);
|
||||
let children = children
|
||||
.as_deref()
|
||||
.map(format_string_for_js)
|
||||
.unwrap_or("null".to_string());
|
||||
let tag = format_string_for_js(tag);
|
||||
format!(r#"{helpers};window.createElementInHead({tag}, {attributes}, {children});"#)
|
||||
}
|
||||
|
||||
/// A provider for document-related functionality.
|
||||
///
|
||||
/// Provides things like a history API, a title, a way to run JS, and some other basics/essentials used
|
||||
/// by nearly every platform.
|
||||
///
|
||||
/// An integration with some kind of navigation history.
|
||||
///
|
||||
/// Depending on your use case, your implementation may deviate from the described procedure. This
|
||||
/// is fine, as long as both `current_route` and `current_query` match the described format.
|
||||
///
|
||||
/// However, you should document all deviations. Also, make sure the navigation is user-friendly.
|
||||
/// The described behaviors are designed to mimic a web browser, which most users should already
|
||||
/// know. Deviations might confuse them.
|
||||
pub trait Document: 'static {
|
||||
/// Run `eval` against this document, returning an [`Eval`] that can be used to await the result.
|
||||
fn eval(&self, js: String) -> Eval;
|
||||
|
||||
/// Set the title of the document
|
||||
fn set_title(&self, title: String) {
|
||||
self.eval(format!("document.title = {title:?};"));
|
||||
}
|
||||
|
||||
/// Create a new element in the head
|
||||
fn create_head_element(
|
||||
&self,
|
||||
name: &str,
|
||||
attributes: &[(&str, String)],
|
||||
contents: Option<String>,
|
||||
) {
|
||||
self.eval(create_element_in_head(name, attributes, contents));
|
||||
}
|
||||
|
||||
/// Create a new meta tag in the head
|
||||
fn create_meta(&self, props: MetaProps) {
|
||||
let attributes = props.attributes();
|
||||
self.create_head_element("meta", &attributes, None);
|
||||
}
|
||||
|
||||
/// Create a new script tag in the head
|
||||
fn create_script(&self, props: ScriptProps) {
|
||||
let attributes = props.attributes();
|
||||
match (&props.src, props.script_contents()) {
|
||||
// The script has inline contents, render it as a script tag
|
||||
(_, Ok(contents)) => self.create_head_element("script", &attributes, Some(contents)),
|
||||
// The script has a src, render it as a script tag without a body
|
||||
(Some(_), _) => self.create_head_element("script", &attributes, None),
|
||||
// The script has neither contents nor src, log an error
|
||||
(None, Err(err)) => err.log("Script"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new style tag in the head
|
||||
fn create_style(&self, props: StyleProps) {
|
||||
let mut attributes = props.attributes();
|
||||
match (&props.href, props.style_contents()) {
|
||||
// The style has inline contents, render it as a style tag
|
||||
(_, Ok(contents)) => self.create_head_element("style", &attributes, Some(contents)),
|
||||
// The style has a src, render it as a link tag
|
||||
(Some(_), _) => {
|
||||
attributes.push(("type", "text/css".into()));
|
||||
self.create_head_element("link", &attributes, None)
|
||||
}
|
||||
// The style has neither contents nor src, log an error
|
||||
(None, Err(err)) => err.log("Style"),
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a new link tag in the head
|
||||
fn create_link(&self, props: LinkProps) {
|
||||
let attributes = props.attributes();
|
||||
self.create_head_element("link", &attributes, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// A document that does nothing
|
||||
#[derive(Default)]
|
||||
pub struct NoOpDocument;
|
||||
|
||||
impl Document for NoOpDocument {
|
||||
fn eval(&self, _: String) -> Eval {
|
||||
let owner = generational_box::Owner::default();
|
||||
let boxed = owner.insert(Box::new(NoOpEvaluator {}) as Box<dyn Evaluator + 'static>);
|
||||
Eval::new(boxed)
|
||||
}
|
||||
}
|
||||
|
||||
/// An evaluator that does nothing
|
||||
#[derive(Default)]
|
||||
pub struct NoOpEvaluator;
|
||||
|
||||
impl Evaluator for NoOpEvaluator {
|
||||
fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> {
|
||||
Err(EvalError::Unsupported)
|
||||
}
|
||||
|
||||
fn poll_recv(
|
||||
&mut self,
|
||||
_context: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
|
||||
std::task::Poll::Ready(Err(EvalError::Unsupported))
|
||||
}
|
||||
|
||||
fn poll_join(
|
||||
&mut self,
|
||||
_context: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
|
||||
std::task::Poll::Ready(Err(EvalError::Unsupported))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user