1#![deny(unused_crate_dependencies)]
17
18pub mod span;
20pub mod testcase;
21pub use self::span::Span;
22pub use proc_macro2::LineColumn;
23
24pub mod util;
25use self::util::{load_span_from, sub_char_range};
26
27use indexmap::IndexMap;
28use proc_macro2::TokenTree;
29use rayon::prelude::*;
30use serde::Deserialize;
31use std::path::PathBuf;
32use toml::Spanned;
33
34pub type Range = core::ops::Range<usize>;
36
37pub fn apply_offset(range: &mut Range, offset: usize) {
39 range.start = range.start.saturating_add(offset);
40 range.end = range.end.saturating_add(offset);
41}
42
43pub mod chunk;
44pub mod cluster;
45mod developer;
46pub mod errors;
47pub mod literal;
48pub mod literalset;
49pub mod markdown;
50
51pub use chunk::*;
52pub use cluster::*;
53pub use errors::*;
54pub use literal::*;
55pub use literalset::*;
56pub use markdown::*;
57
58#[derive(Debug, Clone)]
60pub struct Documentation {
61 index: IndexMap<ContentOrigin, Vec<CheckableChunk>>,
63}
64
65impl Default for Documentation {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl Documentation {
72 pub fn new() -> Self {
74 Self {
75 index: IndexMap::with_capacity(64),
76 }
77 }
78
79 pub fn contains_key(&self, key: &ContentOrigin) -> bool {
81 self.index.contains_key(key)
82 }
83
84 #[inline(always)]
86 pub fn is_empty(&self) -> bool {
87 self.index.is_empty()
88 }
89
90 #[inline(always)]
92 pub fn iter(&self) -> impl Iterator<Item = (&ContentOrigin, &Vec<CheckableChunk>)> {
93 self.index.iter()
94 }
95
96 pub fn par_iter(&self) -> impl ParallelIterator<Item = (&ContentOrigin, &Vec<CheckableChunk>)> {
98 self.index.par_iter()
99 }
100
101 pub fn into_par_iter(
103 self,
104 ) -> impl ParallelIterator<Item = (ContentOrigin, Vec<CheckableChunk>)> {
105 self.index.into_par_iter()
106 }
107
108 pub fn extend<I, J>(&mut self, other: I)
110 where
111 I: IntoIterator<Item = (ContentOrigin, Vec<CheckableChunk>), IntoIter = J>,
112 J: Iterator<Item = (ContentOrigin, Vec<CheckableChunk>)>,
113 {
114 other
115 .into_iter()
116 .for_each(|(origin, chunks): (_, Vec<CheckableChunk>)| {
117 self.add_inner(origin, chunks);
118 });
119 }
120
121 pub fn add_inner(&mut self, origin: ContentOrigin, mut chunks: Vec<CheckableChunk>) {
123 self.index
124 .entry(origin)
125 .and_modify(|acc: &mut Vec<CheckableChunk>| {
126 acc.append(&mut chunks);
127 })
128 .or_insert_with(|| chunks);
129 }
131
132 pub fn add_rust(
134 &mut self,
135 origin: ContentOrigin,
136 content: &str,
137 doc_comments: bool,
138 dev_comments: bool,
139 ) -> Result<()> {
140 let cluster = Clusters::load_from_str(content, doc_comments, dev_comments)?;
141
142 let chunks = Vec::<CheckableChunk>::from(cluster);
143 self.add_inner(origin, chunks);
144 Ok(())
145 }
146
147 pub fn add_cargo_manifest_description(
150 &mut self,
151 path: PathBuf,
152 manifest_content: &str,
153 ) -> Result<()> {
154 fn extract_range_of_description(manifest_content: &str) -> Result<Range> {
155 #[derive(Deserialize, Debug)]
156 struct Manifest {
157 package: Spanned<Package>,
158 }
159
160 #[derive(Deserialize, Debug)]
161 struct Package {
162 description: Spanned<String>,
163 }
164
165 let value: Manifest = toml::from_str(manifest_content)?;
166 let d = value.package.into_inner().description;
167 let range = d.span();
168 Ok(range)
169 }
170
171 let mut range = extract_range_of_description(manifest_content)?;
172 let description = sub_char_range(manifest_content, range.clone());
173
174 let description = if range.len() > 6 {
178 if description.starts_with("\"\"\"") {
179 range.start += 3;
180 range.end -= 3;
181 assert!(!range.is_empty());
182 }
183 dbg!(&description[3..(description.len()) - 3])
184 } else {
185 description
186 };
187
188 fn convert_range_to_span(content: &str, range: Range) -> Option<Span> {
189 let mut line = 0_usize;
190 let mut column = 0_usize;
191 let mut prev = '\n';
192 let mut start = None;
193 for (offset, c) in content.chars().enumerate() {
194 if prev == '\n' {
195 column = 0;
196 line += 1;
197 }
198 prev = c;
199
200 if offset == range.start {
201 start = Some(LineColumn { line, column });
202 continue;
203 }
204 if offset + 1 == range.end {
206 let end = LineColumn { line, column };
207 return Some(Span {
208 start: start.unwrap(),
209 end,
210 });
211 }
212 column += 1;
213 }
214 None
215 }
216
217 let span = convert_range_to_span(manifest_content, range.clone()).expect(
218 "Description is part of the manifest since it was parsed from the same source. qed",
219 );
220 let origin = ContentOrigin::CargoManifestDescription(path);
221 let source_mapping = dbg!(indexmap::indexmap! {
222 range => span
223 });
224 self.add_inner(
225 origin,
226 vec![CheckableChunk::from_str(
227 description,
228 source_mapping,
229 CommentVariant::TomlEntry,
230 )],
231 );
232 Ok(())
233 }
234
235 pub fn add_commonmark(&mut self, origin: ContentOrigin, content: &str) -> Result<()> {
237 let start = LineColumn { line: 1, column: 0 };
239 let end = content
240 .lines()
241 .enumerate()
242 .last()
243 .map(|(idx, linecontent)| (idx + 1, linecontent))
244 .map(|(linenumber, linecontent)| LineColumn {
245 line: linenumber,
246 column: linecontent.chars().count().saturating_sub(1),
247 })
248 .ok_or_else(|| {
249 Error::Span(
250 "Common mark / markdown file does not contain a single line".to_string(),
251 )
252 })?;
253
254 let span = Span { start, end };
255 let source_mapping = indexmap::indexmap! {
256 0..content.chars().count() => span
257 };
258 self.add_inner(
259 origin,
260 vec![CheckableChunk::from_str(
261 content,
262 source_mapping,
263 CommentVariant::CommonMark,
264 )],
265 );
266 Ok(())
267 }
268
269 #[inline(always)]
271 pub fn get(&self, origin: &ContentOrigin) -> Option<&[CheckableChunk]> {
272 self.index.get(origin).map(AsRef::as_ref)
273 }
274
275 #[inline(always)]
277 pub fn entry_count(&self) -> usize {
278 self.index.len()
279 }
280
281 pub fn load_from_str(
283 origin: ContentOrigin,
284 content: &str,
285 doc_comments: bool,
286 dev_comments: bool,
287 ) -> Self {
288 let mut docs = Documentation::new();
289
290 match origin.clone() {
291 ContentOrigin::RustDocTest(_path, span) => {
292 if let Ok(excerpt) = load_span_from(&mut content.as_bytes(), span) {
293 docs.add_rust(origin.clone(), excerpt.as_str(), doc_comments, dev_comments)
294 } else {
295 Ok(())
297 }
298 }
299 origin @ ContentOrigin::RustSourceFile(_) => {
300 docs.add_rust(origin, content, doc_comments, dev_comments)
301 }
302 ContentOrigin::CargoManifestDescription(path) => {
303 docs.add_cargo_manifest_description(path, content)
304 }
305 origin @ ContentOrigin::CommonMarkFile(_) => docs.add_commonmark(origin, content),
306 origin @ ContentOrigin::TestEntityRust => {
307 docs.add_rust(origin, content, doc_comments, dev_comments)
308 }
309 origin @ ContentOrigin::TestEntityCommonMark => docs.add_commonmark(origin, content),
310 }
311 .unwrap_or_else(move |e| {
312 log::warn!(
313 "BUG: Failed to load content from {origin} (dev_comments={dev_comments:?}): {e:?}",
314 );
315 });
316 docs
317 }
318
319 pub fn len(&self) -> usize {
320 self.index.len()
321 }
322}
323
324impl IntoIterator for Documentation {
325 type Item = (ContentOrigin, Vec<CheckableChunk>);
326 type IntoIter = indexmap::map::IntoIter<ContentOrigin, Vec<CheckableChunk>>;
327
328 fn into_iter(self) -> Self::IntoIter {
329 self.index.into_iter()
330 }
331}