1
0

document.rs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. use std::sync::Arc;
  2. use super::*;
  3. /// A context for the document
  4. pub type DocumentContext = Arc<dyn Document>;
  5. fn format_string_for_js(s: &str) -> String {
  6. let escaped = s
  7. .replace('\\', "\\\\")
  8. .replace('\n', "\\n")
  9. .replace('\r', "\\r")
  10. .replace('"', "\\\"");
  11. format!("\"{escaped}\"")
  12. }
  13. fn format_attributes(attributes: &[(&str, String)]) -> String {
  14. let mut formatted = String::from("[");
  15. for (key, value) in attributes {
  16. formatted.push_str(&format!(
  17. "[{}, {}],",
  18. format_string_for_js(key),
  19. format_string_for_js(value)
  20. ));
  21. }
  22. if formatted.ends_with(',') {
  23. formatted.pop();
  24. }
  25. formatted.push(']');
  26. formatted
  27. }
  28. fn create_element_in_head(
  29. tag: &str,
  30. attributes: &[(&str, String)],
  31. children: Option<String>,
  32. ) -> String {
  33. let helpers = include_str!("./js/head.js");
  34. let attributes = format_attributes(attributes);
  35. let children = children
  36. .as_deref()
  37. .map(format_string_for_js)
  38. .unwrap_or("null".to_string());
  39. let tag = format_string_for_js(tag);
  40. format!(r#"{helpers};window.createElementInHead({tag}, {attributes}, {children});"#)
  41. }
  42. /// A provider for document-related functionality.
  43. ///
  44. /// Provides things like a history API, a title, a way to run JS, and some other basics/essentials used
  45. /// by nearly every platform.
  46. ///
  47. /// An integration with some kind of navigation history.
  48. ///
  49. /// Depending on your use case, your implementation may deviate from the described procedure. This
  50. /// is fine, as long as both `current_route` and `current_query` match the described format.
  51. ///
  52. /// However, you should document all deviations. Also, make sure the navigation is user-friendly.
  53. /// The described behaviors are designed to mimic a web browser, which most users should already
  54. /// know. Deviations might confuse them.
  55. pub trait Document: 'static {
  56. /// Run `eval` against this document, returning an [`Eval`] that can be used to await the result.
  57. fn eval(&self, js: String) -> Eval;
  58. /// Set the title of the document
  59. fn set_title(&self, title: String) {
  60. self.eval(format!("document.title = {title:?};"));
  61. }
  62. /// Create a new element in the head
  63. fn create_head_element(
  64. &self,
  65. name: &str,
  66. attributes: &[(&str, String)],
  67. contents: Option<String>,
  68. ) {
  69. self.eval(create_element_in_head(name, attributes, contents));
  70. }
  71. /// Create a new meta tag in the head
  72. fn create_meta(&self, props: MetaProps) {
  73. let attributes = props.attributes();
  74. self.create_head_element("meta", &attributes, None);
  75. }
  76. /// Create a new script tag in the head
  77. fn create_script(&self, props: ScriptProps) {
  78. let attributes = props.attributes();
  79. match (&props.src, props.script_contents()) {
  80. // The script has inline contents, render it as a script tag
  81. (_, Ok(contents)) => self.create_head_element("script", &attributes, Some(contents)),
  82. // The script has a src, render it as a script tag without a body
  83. (Some(_), _) => self.create_head_element("script", &attributes, None),
  84. // The script has neither contents nor src, log an error
  85. (None, Err(err)) => err.log("Script"),
  86. }
  87. }
  88. /// Create a new style tag in the head
  89. fn create_style(&self, props: StyleProps) {
  90. let mut attributes = props.attributes();
  91. match (&props.href, props.style_contents()) {
  92. // The style has inline contents, render it as a style tag
  93. (_, Ok(contents)) => self.create_head_element("style", &attributes, Some(contents)),
  94. // The style has a src, render it as a link tag
  95. (Some(_), _) => {
  96. attributes.push(("type", "text/css".into()));
  97. self.create_head_element("link", &attributes, None)
  98. }
  99. // The style has neither contents nor src, log an error
  100. (None, Err(err)) => err.log("Style"),
  101. };
  102. }
  103. /// Create a new link tag in the head
  104. fn create_link(&self, props: LinkProps) {
  105. let attributes = props.attributes();
  106. self.create_head_element("link", &attributes, None);
  107. }
  108. }
  109. /// A document that does nothing
  110. #[derive(Default)]
  111. pub struct NoOpDocument;
  112. impl Document for NoOpDocument {
  113. fn eval(&self, _: String) -> Eval {
  114. let owner = generational_box::Owner::default();
  115. let boxed = owner.insert(Box::new(NoOpEvaluator {}) as Box<dyn Evaluator + 'static>);
  116. Eval::new(boxed)
  117. }
  118. }
  119. /// An evaluator that does nothing
  120. #[derive(Default)]
  121. pub struct NoOpEvaluator;
  122. impl Evaluator for NoOpEvaluator {
  123. fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> {
  124. Err(EvalError::Unsupported)
  125. }
  126. fn poll_recv(
  127. &mut self,
  128. _context: &mut std::task::Context<'_>,
  129. ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
  130. std::task::Poll::Ready(Err(EvalError::Unsupported))
  131. }
  132. fn poll_join(
  133. &mut self,
  134. _context: &mut std::task::Context<'_>,
  135. ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
  136. std::task::Poll::Ready(Err(EvalError::Unsupported))
  137. }
  138. }