1use crate::{
2 HasSpan, Parser, Span,
3 attributes::Attrlist,
4 blocks::{
5 CompoundDelimitedBlock, ContentModel, IsBlock, RawDelimitedBlock, metadata::BlockMetadata,
6 },
7 content::{Content, SubstitutionGroup},
8 span::MatchedItem,
9 strings::CowStr,
10};
11
12#[derive(Clone, Copy, Eq, PartialEq)]
14pub enum SimpleBlockStyle {
15 Paragraph,
17
18 Literal,
20
21 Listing,
29
30 Source,
34}
35
36impl std::fmt::Debug for SimpleBlockStyle {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 SimpleBlockStyle::Paragraph => write!(f, "SimpleBlockStyle::Paragraph"),
40 SimpleBlockStyle::Literal => write!(f, "SimpleBlockStyle::Literal"),
41 SimpleBlockStyle::Listing => write!(f, "SimpleBlockStyle::Listing"),
42 SimpleBlockStyle::Source => write!(f, "SimpleBlockStyle::Source"),
43 }
44 }
45}
46
47#[derive(Clone, Debug, Eq, PartialEq)]
50pub struct SimpleBlock<'src> {
51 content: Content<'src>,
52 source: Span<'src>,
53 style: SimpleBlockStyle,
54 title_source: Option<Span<'src>>,
55 title: Option<String>,
56 anchor: Option<Span<'src>>,
57 anchor_reftext: Option<Span<'src>>,
58 attrlist: Option<Attrlist<'src>>,
59}
60
61impl<'src> SimpleBlock<'src> {
62 pub(crate) fn parse(
63 metadata: &BlockMetadata<'src>,
64 parser: &mut Parser,
65 ) -> Option<MatchedItem<'src, Self>> {
66 let MatchedItem {
67 item: (content, style),
68 after,
69 } = parse_lines(metadata.block_start, &metadata.attrlist, parser)?;
70
71 Some(MatchedItem {
72 item: Self {
73 content,
74 source: metadata
75 .source
76 .trim_remainder(after)
77 .trim_trailing_whitespace(),
78 style,
79 title_source: metadata.title_source,
80 title: metadata.title.clone(),
81 anchor: metadata.anchor,
82 anchor_reftext: metadata.anchor_reftext,
83 attrlist: metadata.attrlist.clone(),
84 },
85 after: after.discard_empty_lines(),
86 })
87 }
88
89 pub(crate) fn parse_fast(
90 source: Span<'src>,
91 parser: &Parser,
92 ) -> Option<MatchedItem<'src, Self>> {
93 let MatchedItem {
94 item: (content, style),
95 after,
96 } = parse_lines(source, &None, parser)?;
97
98 let source = content.original();
99
100 Some(MatchedItem {
101 item: Self {
102 content,
103 source,
104 style,
105 title_source: None,
106 title: None,
107 anchor: None,
108 anchor_reftext: None,
109 attrlist: None,
110 },
111 after: after.discard_empty_lines(),
112 })
113 }
114
115 pub fn content(&self) -> &Content<'src> {
117 &self.content
118 }
119
120 pub fn style(&self) -> SimpleBlockStyle {
122 self.style
123 }
124}
125
126fn parse_lines<'src>(
128 source: Span<'src>,
129 attrlist: &Option<Attrlist<'src>>,
130 parser: &Parser,
131) -> Option<MatchedItem<'src, (Content<'src>, SimpleBlockStyle)>> {
132 let source_after_whitespace = source.discard_whitespace();
133 let strip_indent = source_after_whitespace.col() - 1;
134
135 let mut style = if source_after_whitespace.col() == source.col() {
136 SimpleBlockStyle::Paragraph
137 } else {
138 SimpleBlockStyle::Literal
139 };
140
141 if let Some(attrlist) = attrlist {
144 match attrlist.block_style() {
145 Some("normal") => {
146 style = SimpleBlockStyle::Paragraph;
147 }
148
149 Some("literal") => {
150 style = SimpleBlockStyle::Literal;
151 }
152
153 Some("listing") => {
154 style = SimpleBlockStyle::Listing;
155 }
156
157 Some("source") => {
158 style = SimpleBlockStyle::Source;
159 }
160
161 _ => {}
162 }
163 }
164
165 let mut next = source;
166 let mut filtered_lines: Vec<&'src str> = vec![];
167
168 while let Some(line_mi) = next.take_non_empty_line() {
169 let mut line = line_mi.item;
170
171 if !filtered_lines.is_empty() {
176 if line.data() == "+" {
177 break;
178 }
179
180 if line.starts_with('[') && line.ends_with(']') {
181 break;
182 }
183
184 if (line.starts_with('/')
185 || line.starts_with('-')
186 || line.starts_with('.')
187 || line.starts_with('+')
188 || line.starts_with('=')
189 || line.starts_with('*')
190 || line.starts_with('_'))
191 && (RawDelimitedBlock::is_valid_delimiter(&line)
192 || CompoundDelimitedBlock::is_valid_delimiter(&line))
193 {
194 break;
195 }
196 }
197
198 next = line_mi.after;
199
200 if line.starts_with("//") && !line.starts_with("///") {
201 continue;
202 }
203
204 if strip_indent > 0
207 && let Some(n) = line.position(|c| c != ' ' && c != '\t')
208 {
209 line = line.into_parse_result(n.min(strip_indent)).after;
210 };
211
212 filtered_lines.push(line.trim_trailing_whitespace().data());
213 }
214
215 let source = source.trim_remainder(next).trim_trailing_whitespace();
216 if source.is_empty() {
217 return None;
218 }
219
220 let filtered_lines = filtered_lines.join("\n");
221 let mut content: Content<'src> = Content::from_filtered(source, filtered_lines);
222
223 SubstitutionGroup::Normal
224 .override_via_attrlist(attrlist.as_ref())
225 .apply(&mut content, parser, attrlist.as_ref());
226
227 Some(MatchedItem {
228 item: (content, style),
229 after: next.discard_empty_lines(),
230 })
231}
232
233impl<'src> IsBlock<'src> for SimpleBlock<'src> {
234 fn content_model(&self) -> ContentModel {
235 ContentModel::Simple
236 }
237
238 fn rendered_content(&self) -> Option<&str> {
239 Some(self.content.rendered())
240 }
241
242 fn raw_context(&self) -> CowStr<'src> {
243 "paragraph".into()
244 }
245
246 fn title_source(&'src self) -> Option<Span<'src>> {
247 self.title_source
248 }
249
250 fn title(&self) -> Option<&str> {
251 self.title.as_deref()
252 }
253
254 fn anchor(&'src self) -> Option<Span<'src>> {
255 self.anchor
256 }
257
258 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
259 self.anchor_reftext
260 }
261
262 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
263 self.attrlist.as_ref()
264 }
265}
266
267impl<'src> HasSpan<'src> for SimpleBlock<'src> {
268 fn span(&self) -> Span<'src> {
269 self.source
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 #![allow(clippy::unwrap_used)]
276
277 use std::ops::Deref;
278
279 use pretty_assertions_sorted::assert_eq;
280
281 use crate::{
282 Parser,
283 blocks::{ContentModel, IsBlock, SimpleBlockStyle, metadata::BlockMetadata},
284 content::SubstitutionGroup,
285 tests::prelude::*,
286 };
287
288 #[test]
289 fn impl_clone() {
290 let mut parser = Parser::default();
292
293 let b1 =
294 crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
295
296 let b2 = b1.item.clone();
297 assert_eq!(b1.item, b2);
298 }
299
300 #[test]
301 fn empty_source() {
302 let mut parser = Parser::default();
303 assert!(crate::blocks::SimpleBlock::parse(&BlockMetadata::new(""), &mut parser).is_none());
304 }
305
306 #[test]
307 fn only_spaces() {
308 let mut parser = Parser::default();
309 assert!(
310 crate::blocks::SimpleBlock::parse(&BlockMetadata::new(" "), &mut parser).is_none()
311 );
312 }
313
314 #[test]
315 fn single_line() {
316 let mut parser = Parser::default();
317 let mi =
318 crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
319
320 assert_eq!(
321 mi.item,
322 SimpleBlock {
323 content: Content {
324 original: Span {
325 data: "abc",
326 line: 1,
327 col: 1,
328 offset: 0,
329 },
330 rendered: "abc",
331 },
332 source: Span {
333 data: "abc",
334 line: 1,
335 col: 1,
336 offset: 0,
337 },
338 style: SimpleBlockStyle::Paragraph,
339 title_source: None,
340 title: None,
341 anchor: None,
342 anchor_reftext: None,
343 attrlist: None,
344 },
345 );
346
347 assert_eq!(mi.item.content_model(), ContentModel::Simple);
348 assert_eq!(mi.item.rendered_content().unwrap(), "abc");
349 assert_eq!(mi.item.raw_context().deref(), "paragraph");
350 assert_eq!(mi.item.resolved_context().deref(), "paragraph");
351 assert!(mi.item.declared_style().is_none());
352 assert!(mi.item.id().is_none());
353 assert!(mi.item.roles().is_empty());
354 assert!(mi.item.options().is_empty());
355 assert!(mi.item.title_source().is_none());
356 assert!(mi.item.title().is_none());
357 assert!(mi.item.anchor().is_none());
358 assert!(mi.item.anchor_reftext().is_none());
359 assert!(mi.item.attrlist().is_none());
360 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
361
362 assert_eq!(
363 mi.after,
364 Span {
365 data: "",
366 line: 1,
367 col: 4,
368 offset: 3
369 }
370 );
371 }
372
373 #[test]
374 fn multiple_lines() {
375 let mut parser = Parser::default();
376 let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\ndef"), &mut parser)
377 .unwrap();
378
379 assert_eq!(
380 mi.item,
381 SimpleBlock {
382 content: Content {
383 original: Span {
384 data: "abc\ndef",
385 line: 1,
386 col: 1,
387 offset: 0,
388 },
389 rendered: "abc\ndef",
390 },
391 source: Span {
392 data: "abc\ndef",
393 line: 1,
394 col: 1,
395 offset: 0,
396 },
397 style: SimpleBlockStyle::Paragraph,
398 title_source: None,
399 title: None,
400 anchor: None,
401 anchor_reftext: None,
402 attrlist: None,
403 }
404 );
405
406 assert_eq!(
407 mi.after,
408 Span {
409 data: "",
410 line: 2,
411 col: 4,
412 offset: 7
413 }
414 );
415
416 assert_eq!(mi.item.rendered_content().unwrap(), "abc\ndef");
417 }
418
419 #[test]
420 fn consumes_blank_lines_after() {
421 let mut parser = Parser::default();
422 let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\n\ndef"), &mut parser)
423 .unwrap();
424
425 assert_eq!(
426 mi.item,
427 SimpleBlock {
428 content: Content {
429 original: Span {
430 data: "abc",
431 line: 1,
432 col: 1,
433 offset: 0,
434 },
435 rendered: "abc",
436 },
437 source: Span {
438 data: "abc",
439 line: 1,
440 col: 1,
441 offset: 0,
442 },
443 style: SimpleBlockStyle::Paragraph,
444 title_source: None,
445 title: None,
446 anchor: None,
447 anchor_reftext: None,
448 attrlist: None,
449 }
450 );
451
452 assert_eq!(
453 mi.after,
454 Span {
455 data: "def",
456 line: 3,
457 col: 1,
458 offset: 5
459 }
460 );
461 }
462
463 #[test]
464 fn overrides_sub_group_via_subs_attribute() {
465 let mut parser = Parser::default();
466 let mi = crate::blocks::SimpleBlock::parse(
467 &BlockMetadata::new("[subs=quotes]\na<b>c *bold*\n\ndef"),
468 &mut parser,
469 )
470 .unwrap();
471
472 assert_eq!(
473 mi.item,
474 SimpleBlock {
475 content: Content {
476 original: Span {
477 data: "a<b>c *bold*",
478 line: 2,
479 col: 1,
480 offset: 14,
481 },
482 rendered: "a<b>c <strong>bold</strong>",
483 },
484 source: Span {
485 data: "[subs=quotes]\na<b>c *bold*",
486 line: 1,
487 col: 1,
488 offset: 0,
489 },
490 style: SimpleBlockStyle::Paragraph,
491 title_source: None,
492 title: None,
493 anchor: None,
494 anchor_reftext: None,
495 attrlist: Some(Attrlist {
496 attributes: &[ElementAttribute {
497 name: Some("subs"),
498 value: "quotes",
499 shorthand_items: &[],
500 },],
501 anchor: None,
502 source: Span {
503 data: "subs=quotes",
504 line: 1,
505 col: 2,
506 offset: 1,
507 },
508 },),
509 }
510 );
511
512 assert_eq!(
513 mi.after,
514 Span {
515 data: "def",
516 line: 4,
517 col: 1,
518 offset: 28
519 }
520 );
521
522 assert_eq!(
523 mi.item.rendered_content().unwrap(),
524 "a<b>c <strong>bold</strong>"
525 );
526 }
527}