Skip to main content

doc_chunks/
literalset.rs

1pub use super::{TrimmedLiteral, TrimmedLiteralDisplay};
2
3use crate::{CheckableChunk, CommentVariant, Range};
4
5use std::fmt;
6
7/// A set of consecutive literals.
8///
9/// Provides means to render them as a code block
10#[derive(Clone, Default, Debug, Hash, PartialEq, Eq)]
11pub struct LiteralSet {
12    /// consecutive set of literals mapped by line number
13    literals: Vec<TrimmedLiteral>,
14    /// lines spanned (start, end) inclusive
15    pub coverage: (usize, usize),
16    /// Track what kind of comment the literals are
17    variant: CommentVariant,
18}
19
20impl LiteralSet {
21    /// Initiate a new set based on the first literal
22    pub fn from(literal: TrimmedLiteral) -> Self {
23        Self {
24            coverage: (literal.span().start.line, literal.span().end.line),
25            variant: literal.variant(),
26            literals: vec![literal],
27        }
28    }
29
30    /// Add a literal to a literal set, if the previous lines literal already
31    /// exists.
32    ///
33    /// Returns literal within the Err variant if not adjacent
34    pub fn add_adjacent(&mut self, literal: TrimmedLiteral) -> Result<(), TrimmedLiteral> {
35        if literal.variant().category() != self.variant.category() {
36            log::debug!(
37                "Adjacent literal is not the same comment variant: {:?} vs {:?}",
38                literal.variant().category(),
39                self.variant.category()
40            );
41            return Err(literal);
42        }
43        let previous_line = literal.span().end.line;
44        if previous_line == self.coverage.1 + 1 {
45            self.coverage.1 += 1;
46            self.literals.push(literal);
47            return Ok(());
48        }
49
50        let next_line = literal.span().start.line;
51        if next_line + 1 == self.coverage.0 {
52            self.literals.push(literal);
53            self.coverage.1 -= 1;
54            return Ok(());
55        }
56
57        Err(literal)
58    }
59
60    /// The set of trimmed literals that is covered.
61    pub fn literals(&self) -> Vec<&TrimmedLiteral> {
62        self.literals.iter().by_ref().collect()
63    }
64
65    /// The number of literals inside this set.
66    pub fn len(&self) -> usize {
67        self.literals.len()
68    }
69
70    pub fn is_empty(&self) -> bool {
71        self.literals.is_empty()
72    }
73
74    /// Convert to a checkable chunk.
75    ///
76    /// Creates the map from content ranges to source spans.
77    pub fn into_chunk(self) -> crate::CheckableChunk {
78        let n = self.len();
79        let mut source_mapping = indexmap::IndexMap::with_capacity(n);
80        let mut content = String::with_capacity(n * 120);
81        if n > 0 {
82            // cursor operates on characters
83            let mut cursor = 0usize;
84            // for use with `Range`
85            let mut start; // inclusive
86            let mut end; // exclusive
87            let mut it = self.literals.iter();
88            let mut next = it.next();
89            while let Some(literal) = next {
90                start = cursor;
91                cursor += literal.len_in_chars();
92                end = cursor;
93
94                let span = literal.span();
95                let range = Range { start, end };
96
97                // TODO this does not hold anymore for `#[doc=foo!(..)]`.
98                // TODO where the span is covering `foo!()`, but the
99                // TODO rendered length is 0.
100                if literal.variant() != CommentVariant::MacroDocEqMacro {
101                    if let Some(span_len) = span.one_line_len() {
102                        assert_eq!(range.len(), span_len);
103                    }
104                }
105                // keep zero length values too, to guarantee continuity
106                source_mapping.insert(range, span);
107                content.push_str(literal.as_str());
108                // the newline is _not_ covered by a span, after all it's inserted by us!
109                next = it.next();
110                if next.is_some() {
111                    // for the last, skip the newline
112                    content.push('\n');
113                    cursor += 1;
114                }
115            }
116        }
117        // all literals in a set have the same variant, so lets take the first one
118        let variant = if let Some(literal) = self.literals.first() {
119            literal.variant()
120        } else {
121            crate::CommentVariant::Unknown
122        };
123        CheckableChunk::from_string(content, source_mapping, variant)
124    }
125}
126
127impl fmt::Display for LiteralSet {
128    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129        let n = self.len();
130        if n > 0 {
131            for literal in self.literals.iter().take(n - 1) {
132                writeln!(formatter, "{}", literal.as_str())?;
133            }
134            if let Some(literal) = self.literals.last() {
135                write!(formatter, "{}", literal.as_str())?;
136            }
137        }
138        Ok(())
139    }
140}
141/// A debug helper to print concatenated length of all items.
142#[macro_export]
143macro_rules! chyrp_dbg {
144    ($first:literal $(, $( $line:literal ),+ )? $(,)? $(@ $prefix:literal)? ) => {
145        dbg!(concat!($first $( $(, "\n", $line )+ )?).len());
146        dbg!(concat!($first $( $(, "\n", $line )+ )?));
147    }
148}
149
150/// A helper macro creating valid doc string using the macro syntax
151/// `#[doc=r#"..."#]`.
152///
153/// Example:
154///
155/// ```rust
156/// # use doc_chunks::chyrp_up;
157/// let x = chyrp_up!(["some", "thing"]);
158/// let y = r##"#[doc=r#"some
159/// thing"#]
160/// struct ChyrpChyrp;"##;
161///
162/// assert_eq!(x,y);
163/// ```
164#[macro_export]
165macro_rules! chyrp_up {
166    ([ $( $line:literal ),+ $(,)? ] $(@ $prefix:literal)? ) => {
167        chyrp_up!( $( $line ),+ $(@ $prefix)? )
168    };
169    ($first:literal $(, $( $line:literal ),+ )? $(,)? $(@ $prefix:literal)? ) => {
170        concat!($( $prefix ,)? r##"#[doc=r#""##, $first $( $(, "\n", $line )+ )?, r##""#]"##, "\n", "struct ChyrpChyrp;")
171    };
172}
173
174/// A helper macro creating valid doc string using the macro syntax
175/// `/// ...`.
176///
177/// Example:
178///
179/// ```rust
180/// # use doc_chunks::fluff_up;
181/// let x = fluff_up!(["some", "thing"]);
182/// let y = r#"/// some
183/// /// thing
184/// struct Fluff;"#;
185///
186/// assert_eq!(x,y);
187/// ```
188#[macro_export]
189macro_rules! fluff_up {
190    ([ $( $line:literal ),+ $(,)?] $( @ $prefix:literal)?) => {
191        fluff_up!($( $line ),+ $(@ $prefix)?)
192    };
193    ($($line:literal ),+ $(,)? ) => {
194        fluff_up!($( $line ),+ @ "")
195    };
196    ($($line:literal ),+ $(,)? @ $prefix:literal ) => {
197        concat!("" $(, $prefix, "/// ", $line, "\n")+ , "struct Fluff;")
198    };
199}
200
201pub mod testhelper {
202    use super::*;
203    use crate::testcase::annotated_literals;
204
205    pub fn gen_literal_set(source: &str) -> LiteralSet {
206        let literals = dbg!(annotated_literals(dbg!(source)));
207
208        let mut iter = dbg!(literals).into_iter();
209        let literal = iter
210            .next()
211            .expect("Must have at least one item in laterals");
212        let mut cls = LiteralSet::from(literal);
213
214        for literal in iter {
215            assert!(cls.add_adjacent(literal).is_ok());
216        }
217        dbg!(cls)
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    use super::testhelper::gen_literal_set;
226    use crate::util::load_span_from;
227    use crate::util::sub_chars;
228
229    #[test]
230    fn fluff_one() {
231        const RAW: &str = fluff_up!(["a"]);
232        const EXPECT: &str = r#"/// a
233struct Fluff;"#;
234        assert_eq!(RAW, EXPECT);
235    }
236
237    #[test]
238    fn fluff_multi() {
239        const RAW: &str = fluff_up!(["a", "b", "c"]);
240        const EXPECT: &str = r#"/// a
241/// b
242/// c
243struct Fluff;"#;
244        assert_eq!(RAW, EXPECT);
245    }
246
247    // range within the literalset content string
248    const EXMALIBU_RANGE_START: usize = 9;
249    const EXMALIBU_RANGE_END: usize = EXMALIBU_RANGE_START + 8;
250    const EXMALIBU_RANGE: Range = EXMALIBU_RANGE_START..EXMALIBU_RANGE_END;
251    const RAW: &str = r#"/// Another exmalibu verification pass.
252/// 🚤w🌴x🌋y🍈z🍉0
253/// ♫ Boats float, ♫♫ don't they? ♫
254struct Vikings;
255"#;
256
257    const EXMALIBU_CHUNK_STR: &str = r#" Another exmalibu verification pass.
258 🚤w🌴x🌋y🍈z🍉0
259 ♫ Boats float, ♫♫ don't they? ♫"#;
260
261    #[test]
262    fn combine_literals() {
263        let _ = env_logger::builder()
264            .is_test(true)
265            .filter(None, log::LevelFilter::Trace)
266            .try_init();
267
268        let cls = gen_literal_set(RAW);
269
270        assert_eq!(cls.len(), 3);
271        assert_eq!(cls.to_string(), EXMALIBU_CHUNK_STR.to_owned());
272    }
273
274    #[test]
275    fn coverage() {
276        let _ = env_logger::builder()
277            .is_test(true)
278            .filter(None, log::LevelFilter::Trace)
279            .try_init();
280
281        let literal_set = gen_literal_set(RAW);
282        let chunk: CheckableChunk = literal_set.into_chunk();
283        let map_range_to_span = chunk.find_spans(EXMALIBU_RANGE);
284        let (_range, _span) = map_range_to_span
285            .first()
286            .expect("Must be at least one literal");
287
288        let range_for_raw_str = Range {
289            start: EXMALIBU_RANGE_START,
290            end: EXMALIBU_RANGE_END,
291        };
292
293        // check test integrity
294        assert_eq!("exmalibu", &EXMALIBU_CHUNK_STR[EXMALIBU_RANGE]);
295
296        // check actual result
297        assert_eq!(
298            &EXMALIBU_CHUNK_STR[EXMALIBU_RANGE],
299            &chunk.as_str()[range_for_raw_str.clone()]
300        );
301    }
302
303    macro_rules! test_raw {
304        ($test: ident, [ $($txt: literal),+ $(,)? ]; $range: expr, $expected: literal) => {
305            #[test]
306            fn $test() {
307                test_raw!([$($txt),+] ; $range, $expected);
308            }
309        };
310
311        ([$($txt:literal),+ $(,)?]; $range: expr, $expected: literal) => {
312            let _ = env_logger::builder()
313                .filter(None, log::LevelFilter::Trace)
314                .is_test(true)
315                .try_init();
316
317            let range: Range = $range;
318
319            const RAW: &str = fluff_up!($( $txt),+);
320            const START: usize = 3; // skip `///` which is the span we get from the literal
321            let _end: usize = START $( + $txt.len())+;
322            let literal_set = gen_literal_set(dbg!(RAW));
323
324
325            let chunk: CheckableChunk = dbg!(literal_set.into_chunk());
326            let map_range_to_span = chunk.find_spans(range.clone());
327
328            let mut iter = dbg!(map_range_to_span).into_iter();
329            let (range, _span) = iter.next().expect("Must be at least one literal");
330
331            // the range for raw str contains an offset of 3 when used with `///`
332            let range_for_raw_str = Range {
333                start: range.start + START,
334                end: range.end + START,
335            };
336
337            assert_eq!(&RAW[range_for_raw_str.clone()], &chunk.as_str()[range], "Testing range extract vs stringified chunk for integrity");
338            assert_eq!(&RAW[range_for_raw_str], $expected, "Testing range extract vs expected");
339        };
340    }
341
342    #[test]
343    fn first_line_extract_0() {
344        test_raw!(["livelyness", "yyy"] ; 2..6, "ivel");
345    }
346
347    #[test]
348    fn first_line_extract_1() {
349        test_raw!(["+ 12 + x0"] ; 9..10, "0");
350    }
351
352    #[test]
353    fn literal_set_into_chunk() {
354        let _ = env_logger::builder()
355            .filter(None, log::LevelFilter::Trace)
356            .is_test(true)
357            .try_init();
358
359        let literal_set = dbg!(gen_literal_set(RAW));
360
361        let chunk = dbg!(literal_set.clone().into_chunk());
362        let it = literal_set.literals();
363
364        for (range, span, s) in itertools::cons_tuples(chunk.iter().zip(it)) {
365            if range.len() == 0 {
366                continue;
367            }
368            assert_eq!(
369                load_span_from(RAW.as_bytes(), span.clone()).expect("Span extraction must work"),
370                sub_chars(chunk.as_str(), range.clone())
371            );
372
373            let r: Range = span.to_content_range(&chunk).expect("Should work");
374            // the range for raw str contains an offset of 3 when used with `///`
375            assert_eq!(
376                sub_chars(chunk.as_str(), range.clone()),
377                s.as_str().to_owned()
378            );
379            assert_eq!(&r, range);
380        }
381    }
382}