noosphere_into/transcluder/
content.rs

1use std::marker::PhantomData;
2
3use crate::{ResolvedLink, TextTransclude, Transclude, Transcluder};
4use anyhow::Result;
5use async_trait::async_trait;
6use noosphere_core::context::{HasSphereContext, SphereContentRead};
7use noosphere_core::data::Header;
8use noosphere_storage::Storage;
9use subtext::{block::Block, primitive::Entity, Peer};
10use tokio_stream::StreamExt;
11
12/// A [Transcluder] implementation that uses [HasSphereContext] to resolve the content
13/// being transcluded.
14#[derive(Clone)]
15pub struct SphereContentTranscluder<R, S>
16where
17    R: HasSphereContext<S> + Clone,
18    S: Storage + 'static,
19{
20    content: R,
21    storage_type: PhantomData<S>,
22}
23
24impl<R, S> SphereContentTranscluder<R, S>
25where
26    R: HasSphereContext<S> + Clone,
27    S: Storage + 'static,
28{
29    pub fn new(content: R) -> Self {
30        SphereContentTranscluder {
31            content,
32            storage_type: PhantomData,
33        }
34    }
35}
36
37#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
38#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
39impl<R, S> Transcluder for SphereContentTranscluder<R, S>
40where
41    R: HasSphereContext<S> + Clone,
42    S: Storage + 'static,
43{
44    async fn transclude(&self, link: &ResolvedLink) -> Result<Option<Transclude>> {
45        match link {
46            ResolvedLink::Hyperlink { .. } => {
47                // TODO(#50): Support hyperlinks
48                Ok(None)
49            }
50            ResolvedLink::Slashlink { link, href } => {
51                match link.peer {
52                    Peer::None => {
53                        // TODO(#49): Perhaps this should be sensitive to external content
54                        // e.g., from other spheres
55                    }
56                    _ => return Ok(None),
57                };
58
59                let slug = match &link.slug {
60                    Some(slug) => slug.to_owned(),
61                    None => return Ok(None),
62                };
63                // TODO(#50): Support content types other than Subtext
64
65                Ok(match self.content.read(&slug).await? {
66                    Some(file) => {
67                        // TODO(#52): Maybe fall back to first heading if present and use
68                        // that as a stand-in for title...
69                        let title = file.memo.get_first_header(&Header::Title);
70
71                        let subtext_ast_stream =
72                            subtext::stream::<Block<Entity>, _, _>(file.contents).await;
73
74                        tokio::pin!(subtext_ast_stream);
75
76                        let mut excerpt = None;
77
78                        while let Some(Ok(block)) = subtext_ast_stream.next().await {
79                            match block {
80                                Block::Blank(_) => continue,
81                                any_other => {
82                                    excerpt = Some(any_other.to_text_content());
83                                    break;
84                                }
85                            }
86                        }
87
88                        Some(Transclude::Text(TextTransclude {
89                            title,
90                            excerpt,
91                            link_text: format!("/{slug}"),
92                            href: href.to_owned(),
93                        }))
94                    }
95                    None => {
96                        // TODO(#53): Figure out how to treat "dead" links for HTML generation
97                        // purposes; it may be that we want some dynamic widget that
98                        // determines the liveness of a transclude at render time
99                        Some(Transclude::Text(TextTransclude {
100                            title: None,
101                            excerpt: None,
102                            link_text: format!("/{slug}"),
103                            href: href.to_owned(),
104                        }))
105                    }
106                })
107            }
108        }
109    }
110}