ftml/parsing/rule/impls/
blockquote.rs

1/*
2 * parsing/rule/impls/blockquote.rs
3 *
4 * ftml - Library to parse Wikidot text
5 * Copyright (C) 2019-2025 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
21use super::prelude::*;
22use crate::parsing::paragraph::ParagraphStack;
23use crate::parsing::{process_depths, DepthItem, DepthList};
24use crate::tree::{AttributeMap, Container, ContainerType};
25
26const MAX_BLOCKQUOTE_DEPTH: usize = 30;
27
28pub const RULE_BLOCKQUOTE: Rule = Rule {
29    name: "blockquote",
30    position: LineRequirement::StartOfLine,
31    try_consume_fn,
32};
33
34fn try_consume_fn<'r, 't>(
35    parser: &mut Parser<'r, 't>,
36) -> ParseResult<'r, 't, Elements<'t>> {
37    debug!("Parsing nested native blockquotes");
38
39    // Context variables
40    let mut depths = Vec::new();
41    let mut errors = Vec::new();
42
43    // Produce a depth list with elements
44    loop {
45        let current = parser.current();
46        let depth = match current.token {
47            // 1 or more ">"s in one token. Return ASCII length.
48            Token::Quote => current.slice.len(),
49
50            // Invalid token, bail
51            _ => {
52                warn!("Didn't find blockquote token, ending list iteration");
53                break;
54            }
55        };
56        parser.step()?;
57        parser.get_optional_space()?; // allow whitespace after ">"
58
59        // Check that the depth isn't obscenely deep, to avoid DOS attacks via stack overflow.
60        if depth > MAX_BLOCKQUOTE_DEPTH {
61            debug!("Native blockquote has a depth ({depth}) greater than the maximum ({MAX_BLOCKQUOTE_DEPTH})! Failing");
62            return Err(parser.make_err(ParseErrorKind::BlockquoteDepthExceeded));
63        }
64
65        // Parse elements until we hit the end of the line
66        let mut paragraph_safe = true;
67        let mut elements = collect_consume(
68            parser,
69            RULE_BLOCKQUOTE,
70            &[
71                ParseCondition::current(Token::LineBreak),
72                ParseCondition::current(Token::ParagraphBreak),
73                ParseCondition::current(Token::InputEnd),
74            ],
75            &[],
76            None,
77        )?
78        .chain(&mut errors, &mut paragraph_safe);
79
80        // Add a line break for the end of the line
81        elements.push(Element::LineBreak);
82
83        // Append blockquote line
84        //
85        // Depth lists expect zero-based list depths, but tokens are one-based.
86        // So, we subtract one.
87        //
88        // This will not overflow because Token::Quote requires at least one ">".
89        depths.push((depth - 1, (), (elements, paragraph_safe)))
90    }
91
92    // This blockquote has no rows, so the rule fails
93    if depths.is_empty() {
94        return Err(parser.make_err(ParseErrorKind::RuleFailed));
95    }
96
97    let depth_lists = process_depths((), depths);
98    let elements: Vec<Element> = depth_lists
99        .into_iter()
100        .map(|(_, depth_list)| build_blockquote_element(depth_list))
101        .collect();
102
103    ok!(false; elements, errors)
104}
105
106fn build_blockquote_element(list: DepthList<(), (Vec<Element>, bool)>) -> Element {
107    let mut stack = ParagraphStack::new();
108
109    // Convert depth list into a list of elements
110    for item in list {
111        match item {
112            DepthItem::Item((elements, paragraph_safe)) => {
113                for element in elements {
114                    stack.push_element(element, paragraph_safe);
115                }
116            }
117            DepthItem::List(_, list) => {
118                let blockquote = build_blockquote_element(list);
119                stack.pop_line_break();
120                stack.push_element(blockquote, false);
121            }
122        }
123    }
124
125    stack.pop_line_break();
126
127    Element::Container(Container::new(
128        ContainerType::Blockquote,
129        stack.into_elements(),
130        AttributeMap::new(),
131    ))
132}