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