lib.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. #![doc = include_str!("../README.md")]
  2. use std::fmt::{Display, Formatter};
  3. use dioxus_core::exports::bumpalo;
  4. use dioxus_core::exports::bumpalo::Bump;
  5. use dioxus_core::IntoVNode;
  6. use dioxus_core::*;
  7. pub struct SsrRenderer {
  8. inner: bumpalo::Bump,
  9. cfg: SsrConfig,
  10. }
  11. impl SsrRenderer {
  12. pub fn new(cfg: impl FnOnce(SsrConfig) -> SsrConfig) -> Self {
  13. Self {
  14. cfg: cfg(SsrConfig::default()),
  15. inner: bumpalo::Bump::new(),
  16. }
  17. }
  18. pub fn render_lazy<'a>(&'a mut self, f: LazyNodes<'a, '_>) -> String {
  19. let bump = &mut self.inner as *mut _;
  20. let s = self.render_inner(f);
  21. // reuse the bump's memory
  22. unsafe { (&mut *bump as &mut bumpalo::Bump).reset() };
  23. s
  24. }
  25. fn render_inner<'a>(&'a self, f: LazyNodes<'a, '_>) -> String {
  26. let factory = NodeFactory::new(&self.inner);
  27. let root = f.into_vnode(factory);
  28. format!(
  29. "{:}",
  30. TextRenderer {
  31. cfg: self.cfg.clone(),
  32. root: &root,
  33. vdom: None
  34. }
  35. )
  36. }
  37. }
  38. pub fn render_lazy<'a>(f: LazyNodes<'a, '_>) -> String {
  39. let bump = bumpalo::Bump::new();
  40. let borrowed = &bump;
  41. // Safety
  42. //
  43. // The lifetimes bounds on LazyNodes are really complicated - they need to support the nesting restrictions in
  44. // regular component usage. The <'a> lifetime is used to enforce that all calls of IntoVnode use the same allocator.
  45. //
  46. // When LazyNodes are provided, they are FnOnce, but do not come with a allocator selected to borrow from. The <'a>
  47. // lifetime is therefore longer than the lifetime of the allocator which doesn't exist... yet.
  48. //
  49. // Therefore, we cast our local bump alloactor into right lifetime. This is okay because our usage of the bump arena
  50. // is *definitely* shorter than the <'a> lifetime, and we return *owned* data - not borrowed data.
  51. let _b = unsafe { std::mem::transmute::<&Bump, &'a Bump>(borrowed) };
  52. let root = f.into_vnode(NodeFactory::new(_b));
  53. format!(
  54. "{:}",
  55. TextRenderer {
  56. cfg: SsrConfig::default(),
  57. root: &root,
  58. vdom: None
  59. }
  60. )
  61. }
  62. pub fn render_vdom(dom: &VirtualDom) -> String {
  63. format!("{:}", TextRenderer::from_vdom(dom, SsrConfig::default()))
  64. }
  65. pub fn render_vdom_cfg(dom: &VirtualDom, cfg: impl FnOnce(SsrConfig) -> SsrConfig) -> String {
  66. format!(
  67. "{:}",
  68. TextRenderer::from_vdom(dom, cfg(SsrConfig::default()))
  69. )
  70. }
  71. pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
  72. Some(format!(
  73. "{:}",
  74. TextRenderer {
  75. cfg: SsrConfig::default(),
  76. root: vdom.get_scope(scope).unwrap().root_node(),
  77. vdom: Some(vdom)
  78. }
  79. ))
  80. }
  81. /// A configurable text renderer for the Dioxus VirtualDOM.
  82. ///
  83. ///
  84. /// ## Details
  85. ///
  86. /// This uses the `Formatter` infrastructure so you can write into anything that supports `write_fmt`. We can't accept
  87. /// any generic writer, so you need to "Display" the text renderer. This is done through `format!` or `format_args!`
  88. ///
  89. /// ## Example
  90. /// ```ignore
  91. /// static App: Component = |cx| cx.render(rsx!(div { "hello world" }));
  92. /// let mut vdom = VirtualDom::new(App);
  93. /// vdom.rebuild();
  94. ///
  95. /// let renderer = TextRenderer::new(&vdom);
  96. /// let output = format!("{}", renderer);
  97. /// assert_eq!(output, "<div>hello world</div>");
  98. /// ```
  99. pub struct TextRenderer<'a, 'b> {
  100. vdom: Option<&'a VirtualDom>,
  101. root: &'b VNode<'a>,
  102. cfg: SsrConfig,
  103. }
  104. impl Display for TextRenderer<'_, '_> {
  105. fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
  106. self.html_render(self.root, f, 0)
  107. }
  108. }
  109. impl<'a> TextRenderer<'a, '_> {
  110. pub fn from_vdom(vdom: &'a VirtualDom, cfg: SsrConfig) -> Self {
  111. Self {
  112. cfg,
  113. root: vdom.base_scope().root_node(),
  114. vdom: Some(vdom),
  115. }
  116. }
  117. fn html_render(&self, node: &VNode, f: &mut std::fmt::Formatter, il: u16) -> std::fmt::Result {
  118. match &node {
  119. VNode::Text(text) => {
  120. if self.cfg.indent {
  121. for _ in 0..il {
  122. write!(f, " ")?;
  123. }
  124. }
  125. write!(f, "{}", text.text)?
  126. }
  127. VNode::Placeholder(_anchor) => {
  128. //
  129. if self.cfg.indent {
  130. for _ in 0..il {
  131. write!(f, " ")?;
  132. }
  133. }
  134. write!(f, "<!-- -->")?;
  135. }
  136. VNode::Element(el) => {
  137. if self.cfg.indent {
  138. for _ in 0..il {
  139. write!(f, " ")?;
  140. }
  141. }
  142. write!(f, "<{}", el.tag)?;
  143. let mut inner_html = None;
  144. let mut attr_iter = el.attributes.iter().peekable();
  145. while let Some(attr) = attr_iter.next() {
  146. match attr.namespace {
  147. None => match attr.name {
  148. "dangerous_inner_html" => inner_html = Some(attr.value),
  149. _ => write!(f, " {}=\"{}\"", attr.name, attr.value)?,
  150. },
  151. Some(ns) => {
  152. // write the opening tag
  153. write!(f, " {}=\"", ns)?;
  154. let mut cur_ns_el = attr;
  155. 'ns_parse: loop {
  156. write!(f, "{}:{};", cur_ns_el.name, cur_ns_el.value)?;
  157. match attr_iter.peek() {
  158. Some(next_attr) if next_attr.namespace == Some(ns) => {
  159. cur_ns_el = attr_iter.next().unwrap();
  160. }
  161. _ => break 'ns_parse,
  162. }
  163. }
  164. // write the closing tag
  165. write!(f, "\"")?;
  166. }
  167. }
  168. }
  169. // we write the element's id as a data attribute
  170. //
  171. // when the page is loaded, the `querySelectorAll` will be used to collect all the nodes, and then add
  172. // them interpreter's stack
  173. if let (true, Some(id)) = (self.cfg.pre_render, node.try_mounted_id()) {
  174. write!(f, " dioxus-id=\"{}\"", id)?;
  175. for _listener in el.listeners {
  176. // todo: write the listeners
  177. }
  178. }
  179. match self.cfg.newline {
  180. true => writeln!(f, ">")?,
  181. false => write!(f, ">")?,
  182. }
  183. if let Some(inner_html) = inner_html {
  184. write!(f, "{}", inner_html)?;
  185. } else {
  186. for child in el.children {
  187. self.html_render(child, f, il + 1)?;
  188. }
  189. }
  190. if self.cfg.newline {
  191. writeln!(f)?;
  192. }
  193. if self.cfg.indent {
  194. for _ in 0..il {
  195. write!(f, " ")?;
  196. }
  197. }
  198. write!(f, "</{}>", el.tag)?;
  199. if self.cfg.newline {
  200. writeln!(f)?;
  201. }
  202. }
  203. VNode::Fragment(frag) => {
  204. for child in frag.children {
  205. self.html_render(child, f, il + 1)?;
  206. }
  207. }
  208. VNode::Component(vcomp) => {
  209. let idx = vcomp.scope.get().unwrap();
  210. if let (Some(vdom), false) = (self.vdom, self.cfg.skip_components) {
  211. let new_node = vdom.get_scope(idx).unwrap().root_node();
  212. self.html_render(new_node, f, il + 1)?;
  213. } else {
  214. }
  215. }
  216. }
  217. Ok(())
  218. }
  219. }
  220. #[derive(Clone, Debug, Default)]
  221. pub struct SsrConfig {
  222. /// currently not supported - control if we indent the HTML output
  223. indent: bool,
  224. /// Control if elements are written onto a new line
  225. newline: bool,
  226. /// Choose to write ElementIDs into elements so the page can be re-hydrated later on
  227. pre_render: bool,
  228. // Currently not implemented
  229. // Don't proceed onto new components. Instead, put the name of the component.
  230. // TODO: components don't have names :(
  231. skip_components: bool,
  232. }
  233. impl SsrConfig {
  234. pub fn indent(mut self, a: bool) -> Self {
  235. self.indent = a;
  236. self
  237. }
  238. pub fn newline(mut self, a: bool) -> Self {
  239. self.newline = a;
  240. self
  241. }
  242. pub fn pre_render(mut self, a: bool) -> Self {
  243. self.pre_render = a;
  244. self
  245. }
  246. pub fn skip_components(mut self, a: bool) -> Self {
  247. self.skip_components = a;
  248. self
  249. }
  250. }
  251. #[cfg(test)]
  252. mod tests {
  253. use super::*;
  254. use dioxus_core::prelude::*;
  255. use dioxus_core_macro::*;
  256. use dioxus_html as dioxus_elements;
  257. static SIMPLE_APP: Component = |cx| {
  258. cx.render(rsx!(div {
  259. "hello world!"
  260. }))
  261. };
  262. static SLIGHTLY_MORE_COMPLEX: Component = |cx| {
  263. cx.render(rsx! {
  264. div { title: "About W3Schools",
  265. (0..20).map(|f| rsx!{
  266. div {
  267. title: "About W3Schools",
  268. style: "color:blue;text-align:center",
  269. class: "About W3Schools",
  270. p {
  271. title: "About W3Schools",
  272. "Hello world!: {f}"
  273. }
  274. }
  275. })
  276. }
  277. })
  278. };
  279. static NESTED_APP: Component = |cx| {
  280. cx.render(rsx!(
  281. div {
  282. SIMPLE_APP {}
  283. }
  284. ))
  285. };
  286. static FRAGMENT_APP: Component = |cx| {
  287. cx.render(rsx!(
  288. div { "f1" }
  289. div { "f2" }
  290. div { "f3" }
  291. div { "f4" }
  292. ))
  293. };
  294. #[test]
  295. fn to_string_works() {
  296. let mut dom = VirtualDom::new(SIMPLE_APP);
  297. dom.rebuild();
  298. dbg!(render_vdom(&dom));
  299. }
  300. #[test]
  301. fn hydration() {
  302. let mut dom = VirtualDom::new(NESTED_APP);
  303. dom.rebuild();
  304. dbg!(render_vdom_cfg(&dom, |c| c.pre_render(true)));
  305. }
  306. #[test]
  307. fn nested() {
  308. let mut dom = VirtualDom::new(NESTED_APP);
  309. dom.rebuild();
  310. dbg!(render_vdom(&dom));
  311. }
  312. #[test]
  313. fn fragment_app() {
  314. let mut dom = VirtualDom::new(FRAGMENT_APP);
  315. dom.rebuild();
  316. dbg!(render_vdom(&dom));
  317. }
  318. #[test]
  319. fn write_to_file() {
  320. use std::fs::File;
  321. use std::io::Write;
  322. let mut file = File::create("index.html").unwrap();
  323. let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX);
  324. dom.rebuild();
  325. file.write_fmt(format_args!(
  326. "{}",
  327. TextRenderer::from_vdom(&dom, SsrConfig::default())
  328. ))
  329. .unwrap();
  330. }
  331. #[test]
  332. fn styles() {
  333. static STLYE_APP: Component = |cx| {
  334. cx.render(rsx! {
  335. div { color: "blue", font_size: "46px" }
  336. })
  337. };
  338. let mut dom = VirtualDom::new(STLYE_APP);
  339. dom.rebuild();
  340. dbg!(render_vdom(&dom));
  341. }
  342. #[test]
  343. fn lazy() {
  344. let p1 = SsrRenderer::new(|c| c).render_lazy(rsx! {
  345. div { "ello" }
  346. });
  347. let p2 = render_lazy(rsx! {
  348. div {
  349. "ello"
  350. }
  351. });
  352. assert_eq!(p1, p2);
  353. }
  354. #[test]
  355. fn big_lazy() {
  356. let s = render_lazy(rsx! {
  357. div {
  358. div {
  359. div {
  360. h1 { "ello world" }
  361. h1 { "ello world" }
  362. h1 { "ello world" }
  363. h1 { "ello world" }
  364. h1 { "ello world" }
  365. }
  366. }
  367. }
  368. });
  369. dbg!(s);
  370. }
  371. #[test]
  372. fn inner_html() {
  373. let s = render_lazy(rsx! {
  374. div {
  375. dangerous_inner_html: "<div> ack </div>"
  376. }
  377. });
  378. dbg!(s);
  379. }
  380. }