plantuml_parser/dsl/content.rs
1use crate::Error;
2use crate::ParseContainer;
3use crate::PathResolver;
4use crate::PlantUmlFileData;
5use crate::{
6 FooterLine, HeaderLine, IncludeToken, PlantUmlBlock, PlantUmlBlockKind, PlantUmlLine,
7 PlantUmlLineKind, StartLine, TitleLine,
8};
9use std::collections::HashMap;
10use std::path::PathBuf;
11use std::sync::Arc;
12
13/// A token sequence that is a single PlantUML diagram, that is, from the [`StartLine`] to the [`EndLine`][`crate::EndLine`] (lines inclusive).
14///
15/// # Examples
16///
17/// ```
18/// use plantuml_parser::{IncludesCollections, PlantUmlContent, PlantUmlFileData};
19/// # use std::collections::HashMap;
20///
21/// # fn main() -> anyhow::Result<()> {
22/// let data_for_include = r#"
23/// @startuml
24/// title Example Title Included
25/// Bob -> Alice: Hi
26/// @enduml
27/// "#;
28///
29/// let data = r#"
30/// @startuml
31/// title Example Title
32/// Alice -> Bob: Hello
33/// !include foo.puml!0
34/// @enduml
35/// "#;
36///
37/// let filedata = PlantUmlFileData::parse_from_str(data)?;
38/// let content: &PlantUmlContent = filedata.get(0).unwrap();
39///
40/// // Parsed and collected `!include`, `title`, `header`, `footer`
41/// let include_0 = content.includes().get(0).unwrap();
42/// assert!(content.includes().get(1).is_none());
43/// assert_eq!(content.includes().len(), 1);
44/// assert_eq!(include_0.filepath(), "foo.puml");
45/// assert_eq!(include_0.index(), Some(0));
46/// assert_eq!(include_0.id(), Some("0"));
47///
48/// let title_0 = content.titles().get(0).unwrap();
49/// assert!(content.titles().get(1).is_none());
50/// assert_eq!(content.titles().len(), 1);
51/// assert_eq!(title_0.title(), "Example Title");
52///
53/// // `construct()`
54/// let filedata_for_include = PlantUmlFileData::parse_from_str(data_for_include)?;
55/// let includes = IncludesCollections::new(HashMap::from([
56/// ("bar/foo.puml".into(), filedata_for_include),
57/// ]));
58/// let constructed = content.construct("bar/x.puml".into(), &includes)?;
59/// assert_eq!(
60/// constructed,
61/// concat!(
62/// "@startuml\n",
63/// "title Example Title\n",
64/// "Alice -> Bob: Hello\n",
65/// "title Example Title Included\n",
66/// "Bob -> Alice: Hi\n",
67/// "@enduml\n",
68/// ),
69/// );
70///
71/// // Increased the element of titles in `constructed`
72/// let filedata = PlantUmlFileData::parse_from_str(constructed)?;
73/// let content: &PlantUmlContent = filedata.get(0).unwrap();
74/// let title_0 = content.titles().get(0).unwrap();
75/// let title_1 = content.titles().get(1).unwrap();
76/// assert!(content.titles().get(2).is_none());
77/// assert_eq!(content.titles().len(), 2);
78/// assert_eq!(title_0.title(), "Example Title");
79/// assert_eq!(title_1.title(), "Example Title Included");
80/// # Ok(())
81/// # }
82/// ```
83#[derive(Clone, Debug)]
84pub struct PlantUmlContent {
85 lines: Vec<Arc<PlantUmlLine>>,
86 blocks: Vec<PlantUmlBlock>,
87 includes: Vec<IncludeToken>,
88 titles: Vec<TitleLine>,
89 headers: Vec<HeaderLine>,
90 footers: Vec<FooterLine>,
91}
92
93/// Data collected on the file path and PlantUML diagrams in the file for the include process.
94#[derive(Clone, Debug)]
95pub struct IncludesCollections(HashMap<PathBuf, PlantUmlFileData>);
96
97impl PlantUmlContent {
98 /// Tries to parse a single PlantUML diagram, that is, from the [`StartLine`] to the [`EndLine`][`crate::EndLine`] (lines inclusive).
99 pub(crate) fn parse(input: ParseContainer) -> Result<(ParseContainer, Self), Error> {
100 let mut lines = vec![];
101 let mut blocks = vec![];
102 let mut includes = vec![];
103 let mut titles = vec![];
104 let mut headers = vec![];
105 let mut footers = vec![];
106
107 let (mut rest, (_, mut line)) = PlantUmlLine::parse(input.clone())?;
108 if line.start().is_none() {
109 StartLine::parse(input.clone())?;
110 // unreachable
111 return Err(Error::Unreachable("The first line is not `StartLine` provided to `PlantUmlContent::parse()`, but `PlantUmlContent::parse()` is `pub(crate)`.".into()));
112 }
113 let start_line = line.clone();
114 let diagram_kind = start_line.diagram_kind().unwrap();
115 lines.push(Arc::new(line.clone()));
116
117 while line.end().is_none() && !rest.is_empty() {
118 // TODO: refactor: too specialized to treat `BlockComment`
119 match PlantUmlBlock::parse(rest.clone()) {
120 Ok((tmp_rest, (_, block))) => {
121 rest = tmp_rest;
122 blocks.push(block.clone());
123
124 match block.kind() {
125 PlantUmlBlockKind::BlockComment(comment) => {
126 let comment_lines = comment.lines().to_vec();
127 lines.extend(comment_lines);
128 continue;
129 }
130 }
131 }
132 _ => { // do nothing
133 }
134 }
135
136 let (tmp_rest, (_, tmp_line)) = PlantUmlLine::parse(rest)?;
137 (rest, line) = (tmp_rest, tmp_line);
138 if line.end().map(|x| x.eq_diagram_kind(diagram_kind)) == Some(false) {
139 let start_line = start_line.start().cloned().unwrap();
140 let end_line = line.end().cloned().unwrap();
141 return Err(Error::DiagramKindNotMatch(start_line, end_line));
142 }
143
144 match line.kind() {
145 PlantUmlLineKind::Include(line) => {
146 includes.push(line.token().clone());
147 }
148
149 PlantUmlLineKind::Title(line) => {
150 titles.push(line.clone());
151 }
152
153 PlantUmlLineKind::Header(line) => {
154 headers.push(line.clone());
155 }
156
157 PlantUmlLineKind::Footer(line) => {
158 footers.push(line.clone());
159 }
160
161 _ => {}
162 }
163
164 lines.push(Arc::new(line.clone()));
165 }
166
167 if rest.is_empty() && line.end().is_none() {
168 Err(Error::ContentUnclosed("".into()))
169 } else {
170 let ret = Self {
171 lines,
172 blocks,
173 includes,
174 titles,
175 headers,
176 footers,
177 };
178
179 Ok((rest, ret))
180 }
181 }
182
183 /// Returns the ID of the diagram.
184 pub fn id(&self) -> Option<&str> {
185 self.lines[0].start().unwrap().id()
186 }
187
188 /// Returns the string after the include process of [`PlantUmlContent`].
189 ///
190 /// * `base` - A base path of `self`.
191 /// * `includes` - A pre read [`PlantUmlFileData`] collection for the include process.
192 ///
193 /// # Examples
194 ///
195 /// ```
196 /// use plantuml_parser::{IncludesCollections, PlantUmlContent, PlantUmlFileData};
197 /// # use std::collections::HashMap;
198 ///
199 /// # fn main() -> anyhow::Result<()> {
200 /// let data_for_include = r#"
201 /// @startuml
202 /// Bob -> Alice: Hi
203 /// @enduml
204 /// "#;
205 ///
206 /// let data = r#"
207 /// @startuml
208 /// Alice -> Bob: Hello
209 /// !include foo.puml!0
210 /// @enduml
211 /// "#;
212 ///
213 /// let filedata_for_include = PlantUmlFileData::parse_from_str(data_for_include)?;
214 /// let includes = IncludesCollections::new(HashMap::from([
215 /// ("bar/foo.puml".into(), filedata_for_include),
216 /// ]));
217 ///
218 /// let filedata = PlantUmlFileData::parse_from_str(data)?;
219 /// let content: &PlantUmlContent = filedata.get(0).unwrap();
220 /// let constructed = content.construct("bar/x.puml".into(), &includes)?;
221 /// assert_eq!(
222 /// constructed,
223 /// concat!(
224 /// "@startuml\n",
225 /// "Alice -> Bob: Hello\n",
226 /// "Bob -> Alice: Hi\n",
227 /// "@enduml\n",
228 /// ),
229 /// );
230 ///
231 /// // include paths are not matched
232 /// let constructed = content.construct("bar.puml".into(), &includes)?;
233 /// assert_eq!(
234 /// constructed,
235 /// concat!(
236 /// "@startuml\n",
237 /// "Alice -> Bob: Hello\n",
238 /// "!include foo.puml!0\n",
239 /// "@enduml\n",
240 /// ),
241 /// );
242 /// # Ok(())
243 /// # }
244 /// ```
245 pub fn construct(
246 &self,
247 base: PathBuf,
248 includes: &IncludesCollections,
249 ) -> Result<String, Error> {
250 let constructed = [
251 self.lines[0].raw_str(),
252 &self.construct_inner(base, includes)?,
253 self.lines[self.lines.len() - 1].raw_str(),
254 ]
255 .join("");
256 Ok(constructed)
257 }
258
259 fn construct_inner(
260 &self,
261 base: PathBuf,
262 includes: &IncludesCollections,
263 ) -> Result<String, Error> {
264 let resolver = PathResolver::new(base);
265
266 let constructed = self.lines[1..self.lines.len() - 1]
267 .iter()
268 .map(|x| {
269 let Some(include) = x.include() else {
270 // line is not `!include`
271 return Ok(x.raw_str().to_string());
272 };
273
274 let mut inner_resolver = resolver.clone();
275 inner_resolver.add(include.filepath().into());
276 let path = inner_resolver.build()?;
277
278 let Some(data) = includes.0.get(&path) else {
279 tracing::info!("file not loaded: path = {path:?}");
280 // file not found
281 return Ok(x.raw_str().to_string());
282 };
283
284 let Some(content) = data.get_by_token(include.token()) else {
285 tracing::info!(
286 "specified content not found in file: path = {path:?}, token.id = {:?}, token.index = {:?}",
287 include.token().id(),
288 include.token().index(),
289 );
290 // specified `PlantUmlContent` not found
291 return Ok(x.raw_str().to_string());
292 };
293
294 // recursive construct
295 content.construct_inner(path, includes)
296 })
297 .collect::<Result<Vec<_>, Error>>()?
298 .join("");
299
300 Ok(constructed)
301 }
302
303 /// Returns the string in the [`PlantUmlContent`] without [`StartLine`] and [`EndLine`][`crate::EndLine`].
304 ///
305 /// # Examples
306 ///
307 /// ```
308 /// use plantuml_parser::{PlantUmlContent, PlantUmlFileData};
309 ///
310 /// # fn main() -> anyhow::Result<()> {
311 /// let data = r#"
312 /// @startuml
313 /// Alice -> Bob: Hello
314 /// @enduml
315 /// "#;
316 ///
317 /// let filedata = PlantUmlFileData::parse_from_str(data)?;
318 /// let content: &PlantUmlContent = filedata.get(0).unwrap();
319 /// assert_eq!(content.inner(), "Alice -> Bob: Hello\n");
320 /// # Ok(())
321 /// # }
322 /// ```
323 pub fn inner(&self) -> String {
324 self.lines[1..self.lines.len() - 1]
325 .iter()
326 .map(|x| x.raw_str())
327 .collect::<Vec<_>>()
328 .join("")
329 }
330
331 /// Returns the list of [`PlantUmlBlock`] in the [`PlantUmlContent`].
332 pub fn blocks(&self) -> &[PlantUmlBlock] {
333 &self.blocks
334 }
335
336 /// Returns the includes in the [`PlantUmlContent`].
337 pub fn includes(&self) -> &[IncludeToken] {
338 &self.includes
339 }
340
341 /// Returns the titles in the [`PlantUmlContent`].
342 pub fn titles(&self) -> &[TitleLine] {
343 &self.titles
344 }
345
346 /// Returns the headers in the [`PlantUmlContent`].
347 pub fn headers(&self) -> &[HeaderLine] {
348 &self.headers
349 }
350
351 /// Returns the footers in the [`PlantUmlContent`].
352 pub fn footers(&self) -> &[FooterLine] {
353 &self.footers
354 }
355}
356
357impl IncludesCollections {
358 /// Creates a new [`IncludesCollections`].
359 pub fn new(inner: HashMap<PathBuf, PlantUmlFileData>) -> Self {
360 Self(inner)
361 }
362}
363
364impl From<HashMap<PathBuf, PlantUmlFileData>> for IncludesCollections {
365 fn from(inner: HashMap<PathBuf, PlantUmlFileData>) -> Self {
366 Self(inner)
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn test_parse_plant_uml_content() -> anyhow::Result<()> {
376 // OK
377 let testdata = r#"@startuml
378 @enduml"#;
379 let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
380 assert_eq!(rest, "");
381 assert_eq!(parsed.lines.len(), 2);
382 assert_eq!(parsed.inner(), "");
383
384 // OK
385 let testdata = r#"@startuml
386 a -> b
387 @enduml"#;
388 let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
389 assert_eq!(rest, "");
390 assert_eq!(parsed.lines.len(), 3);
391 assert_eq!(parsed.inner(), " a -> b\n");
392
393 // OK (with id)
394 let testdata = r#"@startuml aaa
395 a -> b
396 @enduml"#;
397 let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
398 assert_eq!(rest, "");
399 assert_eq!(parsed.lines.len(), 3);
400 assert_eq!(parsed.inner(), " a -> b\n");
401 assert_eq!(parsed.id(), Some("aaa"));
402
403 // OK (with id 2)
404 let testdata = r#"@startuml(id=bbb)
405 a -> b
406 @enduml"#;
407 let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
408 assert_eq!(rest, "");
409 assert_eq!(parsed.lines.len(), 3);
410 assert_eq!(parsed.inner(), " a -> b\n");
411 assert_eq!(parsed.id(), Some("bbb"));
412
413 // NG. diagram_kind not match
414 let testdata = r#"@startuml
415 a -> b
416 @endfoo"#;
417 assert!(PlantUmlContent::parse(testdata.into()).is_err());
418
419 // OK
420 let testdata = r#"@startuml(id=ccc)
421 a -> b
422 !include shared.iuml
423 a -> b
424 !include functions.puml!aaa
425 a -> b
426 @enduml"#;
427 let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
428 assert_eq!(rest, "");
429 assert_eq!(parsed.lines.len(), 7);
430 assert_eq!(
431 parsed.inner(),
432 " a -> b\n !include shared.iuml\n a -> b\n !include functions.puml!aaa\n a -> b\n"
433 );
434 assert_eq!(parsed.id(), Some("ccc"));
435 let mut includes = parsed.includes().iter();
436 let include = includes.next().unwrap();
437 assert_eq!(include.filepath(), "shared.iuml");
438 assert_eq!(include.index(), None);
439 assert_eq!(include.id(), None);
440 let include = includes.next().unwrap();
441 assert_eq!(include.filepath(), "functions.puml");
442 assert_eq!(include.index(), None);
443 assert_eq!(include.id(), Some("aaa"));
444 assert!(includes.next().is_none());
445
446 Ok(())
447 }
448
449 #[test]
450 fn test_parse_block_comment() -> anyhow::Result<()> {
451 // OK
452 let testdata = r#"@startuml(id=ccc)
453 a -> b: 1
454 /' begin
455 !include shared.iuml
456 end '/
457 a -> b: 2
458 !include functions.puml!aaa
459 a -> b: 3
460 @enduml"#;
461 let expected_inner = r#" a -> b: 1
462 /' begin
463 !include shared.iuml
464 end '/
465 a -> b: 2
466 !include functions.puml!aaa
467 a -> b: 3
468"#;
469 let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
470 assert_eq!(rest, "");
471 println!("parsed.blocks() = {:?}", parsed.blocks());
472 for line in parsed.lines.iter() {
473 println!("parsed.lines[].kind() = {:?}", line.kind());
474 println!("parsed.lines[].raw_str() = {}", line.raw_str());
475 }
476 assert_eq!(parsed.lines.len(), 9);
477 assert_eq!(parsed.inner(), expected_inner);
478 assert_eq!(parsed.id(), Some("ccc"));
479 let mut includes = parsed.includes().iter();
480 let include = includes.next().unwrap();
481 assert_eq!(include.filepath(), "functions.puml");
482 assert_eq!(include.index(), None);
483 assert_eq!(include.id(), Some("aaa"));
484 assert!(includes.next().is_none());
485
486 Ok(())
487 }
488}