ftml/parsing/paragraph/mod.rs
1/*
2 * parsing/paragraph/mod.rs
3 *
4 * ftml - Library to parse Wikidot text
5 * Copyright (C) 2019-2026 Wikijump Team
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21mod stack;
22
23pub use self::stack::ParagraphStack;
24
25use super::consume::consume;
26use super::parser::Parser;
27use super::prelude::*;
28use super::rule::Rule;
29use super::token::Token;
30
31/// Wrapper type to satisfy the issue with generic closure types.
32///
33/// Because `None` does not specify the type for `F`, we need to
34/// tell the compiler it has a concrete type.
35///
36/// But since it's just `None`, it's not actually pointing to a function,
37/// it's just clarifying what the `_` in `Option<_>` is.
38pub const NO_CLOSE_CONDITION: Option<CloseConditionFn> = None;
39
40type CloseConditionFn = fn(&mut Parser) -> Result<bool, ParseError>;
41
42/// Function to iterate over tokens to produce elements in paragraphs.
43///
44/// Originally in `parse()`, but was moved out to allow paragraph
45/// extraction deeper in code, such as in the `try_paragraph`
46/// collection helper.
47///
48/// This does not necessarily produce a paragraph container.
49/// It may produce multiple or none. Instead the logic iterates
50/// and produces paragraphs or child elements as needed.
51pub fn gather_paragraphs<'r, 't, F>(
52 parser: &mut Parser<'r, 't>,
53 rule: Rule,
54 mut close_condition_fn: Option<F>,
55) -> ParseResult<'r, 't, Vec<Element<'t>>>
56where
57 'r: 't,
58 F: FnMut(&mut Parser<'r, 't>) -> Result<bool, ParseError>,
59{
60 debug!("Gathering paragraphs until ending");
61
62 // Update parser rule
63 parser.set_rule(rule);
64
65 // Create paragraph stack
66 let mut stack = ParagraphStack::new();
67
68 loop {
69 let (elements, mut errors, paragraph_safe) = match parser.current().token {
70 Token::InputEnd => {
71 if close_condition_fn.is_some() {
72 // There was a close condition, but it was not satisfied
73 // before the end of input.
74 //
75 // Pass an error up the chain
76
77 warn!("Hit the end of input, producing an error");
78 return Err(parser.make_err(ParseErrorKind::EndOfInput));
79 } else {
80 // Avoid an unnecessary Element::Null and just exit
81 // If there's no close condition, then this is not an error
82
83 warn!("Hit the end of input, terminating token iteration");
84 break;
85 }
86 }
87
88 // If we've hit a paragraph break, then finish the current paragraph
89 Token::ParagraphBreak => {
90 debug!("Hit a paragraph break, creating a new paragraph container");
91
92 // Paragraph break -- end the paragraph and start a new one!
93 stack.end_paragraph();
94
95 // We must manually bump up this pointer because
96 // we 'continue' here, skipping the usual pointer update.
97 parser.step()?;
98 continue;
99 }
100
101 // Determine if we're ending the paragraph here,
102 // or continuing with another element
103 _ => {
104 if let Some(ref mut close_condition_fn) = close_condition_fn &&
105 close_condition_fn(parser).unwrap_or(false) {
106 debug!("Hit closing condition for paragraphs, terminating token iteration");
107 break;
108 }
109
110 // Otherwise, produce consumption from this token pointer
111 trace!("Trying to consume tokens to produce element");
112 consume(parser)
113 }
114 }?
115 .into();
116
117 trace!("Tokens consumed to produce element");
118
119 // Add new elements to the list
120 push_elements(&mut stack, elements, paragraph_safe);
121
122 // Process errors
123 stack.push_errors(&mut errors);
124 }
125
126 stack.into_result()
127}
128
129fn push_elements<'t>(
130 stack: &mut ParagraphStack<'t>,
131 elements: Elements<'t>,
132 paragraph_safe: bool,
133) {
134 stack.reserve_elements(elements.len());
135
136 for element in elements {
137 // Don't add a line break if the paragraph is otherwise empty
138 if stack.current_empty() && element == Element::LineBreak {
139 continue;
140 }
141
142 stack.push_element(element, paragraph_safe);
143 }
144}