lib.rs 13 KB

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