rstml/node/
raw_text.rs

1use std::marker::PhantomData;
2
3use derive_where::derive_where;
4use proc_macro2::{Span, TokenStream, TokenTree};
5use quote::ToTokens;
6use syn::{parse::ParseStream, spanned::Spanned, token::Brace, LitStr, Token};
7
8use super::{CustomNode, Infallible, Node};
9use crate::recoverable::ParseRecoverable;
10
11/// Raw unquoted text
12///
13/// Internally it is valid `TokenStream` (stream of rust code tokens).
14/// So, it has few limitations:
15/// 1. It cant contain any unclosed branches, braces or parens.
16/// 2. Some tokens like ' ` can be treated as invalid, because in rust it only
17/// allowed in certain contexts.
18///
19/// Can be formatted to a string using `to_source_text`,
20/// `to_token_stream_string` or `to_string_best` methods.
21///
22/// Note:
23/// It use `Span::source_text` to retrieve source text with spaces
24/// source_text method is not available in `quote!` context, or in context where
25/// input is generated by another macro. In still can return default formatting
26/// for TokenStream.
27#[derive_where(Clone, Debug)]
28pub struct RawText<C = Infallible> {
29    token_stream: TokenStream,
30    // Span that started before previous token, and after next.
31    context_span: Option<(Span, Span)>,
32    #[cfg(feature = "rawtext-stable-hack-module")]
33    recovered_text: Option<String>,
34    // Use type parameter to make it possible to find custom nodes in the raw_node.
35    _c: PhantomData<C>,
36}
37
38impl<C> Default for RawText<C> {
39    fn default() -> Self {
40        Self {
41            token_stream: Default::default(),
42            context_span: Default::default(),
43            #[cfg(feature = "rawtext-stable-hack-module")]
44            recovered_text: Default::default(),
45            _c: PhantomData,
46        }
47    }
48}
49
50impl<C> RawText<C> {
51    /// Custom node type parameter is used only for parsing, so it can be
52    /// changed during usage.
53    pub fn convert_custom<U>(self) -> RawText<U> {
54        RawText {
55            token_stream: self.token_stream,
56            context_span: self.context_span,
57            #[cfg(feature = "rawtext-stable-hack-module")]
58            recovered_text: self.recovered_text,
59            _c: PhantomData,
60        }
61    }
62    pub(crate) fn set_tag_spans(&mut self, before: impl Spanned, after: impl Spanned) {
63        // todo: use span.after/before when it will be available in proc_macro2
64        // for now just join full span an remove tokens from it.
65        self.context_span = Some((before.span(), after.span()));
66    }
67
68    /// Convert to string using Display implementation of inner token stream.
69    pub fn to_token_stream_string(&self) -> String {
70        self.token_stream.to_string()
71    }
72
73    /// Try to get source text of the token stream.
74    /// Internally uses `Span::source_text` and `Span::join`, so it can be not
75    /// available.
76    ///
77    /// Optionally including whitespaces.
78    /// Whitespaces can be recovered only if before and after `RawText` was
79    /// other valid `Node`.
80    pub fn to_source_text(&self, with_whitespaces: bool) -> Option<String> {
81        if with_whitespaces {
82            let (start, end) = self.context_span?;
83            let full = start.join(end)?;
84            let full_text = full.source_text()?;
85            let start_text = start.source_text()?;
86            let end_text = end.source_text()?;
87            debug_assert!(full_text.ends_with(&end_text));
88            debug_assert!(full_text.starts_with(&start_text));
89            Some(full_text[start_text.len()..(full_text.len() - end_text.len())].to_string())
90        } else {
91            self.join_spans()?.source_text()
92        }
93    }
94
95    /// Return Spans for all unquoted text or nothing.
96    /// Usefull to detect is `Span::join` is available or not.
97    pub fn join_spans(&self) -> Option<Span> {
98        let mut span: Option<Span> = None;
99        for tt in self.token_stream.clone().into_iter() {
100            let joined = if let Some(span) = span {
101                span.join(tt.span())?
102            } else {
103                tt.span()
104            };
105            span = Some(joined);
106        }
107        span
108    }
109
110    pub fn is_empty(&self) -> bool {
111        self.token_stream.is_empty()
112    }
113
114    pub(crate) fn vec_set_context(
115        open_tag_end: Span,
116        close_tag_start: Option<Span>,
117        mut children: Vec<Node<C>>,
118    ) -> Vec<Node<C>>
119    where
120        C: CustomNode,
121    {
122        let spans: Vec<Span> = Some(open_tag_end)
123            .into_iter()
124            .chain(children.iter().map(|n| n.span()))
125            .chain(close_tag_start)
126            .collect();
127
128        for (spans, children) in spans.windows(3).zip(&mut children) {
129            if let Node::RawText(t) = children {
130                t.set_tag_spans(spans[0], spans[2])
131            }
132        }
133        children
134    }
135
136    /// Trying to return best string representation available:
137    /// 1. calls `to_source_text_hack()`.
138    /// 2. calls `to_source_text(true)`
139    /// 3. calls `to_source_text(false)`
140    /// 4. as fallback calls `to_token_stream_string()`
141    pub fn to_string_best(&self) -> String {
142        #[cfg(feature = "rawtext-stable-hack-module")]
143        if let Some(recovered) = &self.recovered_text {
144            return recovered.clone();
145        }
146        self.to_source_text(true)
147            .or_else(|| self.to_source_text(false))
148            .unwrap_or_else(|| self.to_token_stream_string())
149    }
150
151    // Returns text recovered using recover_space_hack.
152    // If feature "rawtext-stable-hack-module" wasn't activated returns None.
153    //
154    // Recovered text, tries to save whitespaces if possible.
155    pub fn to_source_text_hack(&self) -> Option<String> {
156        #[cfg(feature = "rawtext-stable-hack-module")]
157        {
158            return self.recovered_text.clone();
159        }
160        #[cfg(not(feature = "rawtext-stable-hack-module"))]
161        None
162    }
163
164    #[cfg(feature = "rawtext-stable-hack-module")]
165    pub(crate) fn recover_space(&mut self, other: &Self) {
166        self.recovered_text = Some(
167            other
168                .to_source_text(self.context_span.is_some())
169                .expect("Cannot recover space in this context"),
170        )
171    }
172    #[cfg(feature = "rawtext-stable-hack-module")]
173    pub(crate) fn init_recover_space(&mut self, init: String) {
174        self.recovered_text = Some(init)
175    }
176}
177
178impl RawText {
179    /// Returns true if we on nightly rust and join_spans available
180    pub fn is_source_text_available() -> bool {
181        // TODO: Add feature join_spans check.
182        cfg!(rstml_signal_nightly)
183    }
184}
185
186impl<C: CustomNode> ParseRecoverable for RawText<C> {
187    fn parse_recoverable(
188        parser: &mut crate::recoverable::RecoverableContext,
189        input: ParseStream,
190    ) -> Option<Self> {
191        let mut token_stream = TokenStream::new();
192        let any_node = |input: ParseStream| {
193            input.peek(Token![<])
194                || input.peek(Brace)
195                || input.peek(LitStr)
196                || C::peek_element(&input.fork())
197        };
198        // Parse any input until catching any node.
199        // Fail only on eof.
200        while !any_node(input) && !input.is_empty() {
201            token_stream.extend([parser.save_diagnostics(input.parse::<TokenTree>())?])
202        }
203        Some(Self {
204            token_stream,
205            context_span: None,
206            #[cfg(feature = "rawtext-stable-hack-module")]
207            recovered_text: None,
208            _c: PhantomData,
209        })
210    }
211}
212
213impl<C: CustomNode> ToTokens for RawText<C> {
214    fn to_tokens(&self, tokens: &mut TokenStream) {
215        self.token_stream.to_tokens(tokens)
216    }
217}
218
219impl<C: CustomNode> From<TokenStream> for RawText<C> {
220    fn from(token_stream: TokenStream) -> Self {
221        Self {
222            token_stream,
223            context_span: None,
224            #[cfg(feature = "rawtext-stable-hack-module")]
225            recovered_text: None,
226            _c: PhantomData,
227        }
228    }
229}