Skip to main content

cortexkit_rich_ui_syntax/
parser.rs

1use crate::expression::{literal, reply_value, ExprParser, Result};
2use crate::DiagnosticCode as C;
3use crate::*;
4use std::collections::{HashMap, HashSet};
5
6pub(crate) struct Engine<'s> {
7    pub source: &'s str,
8    pub mode: Mode,
9    nodes: usize,
10    pub blocks: usize,
11    tags: usize,
12    declarations: usize,
13    names: HashSet<String>,
14    remaining_work: usize,
15    diagnostics: Vec<Diagnostic>,
16    suppressed: usize,
17    pub fatal: bool,
18    body_depth: usize,
19    pub expression_frames: usize,
20    structural_failures: usize,
21    /// Detail for the failure most recently returned as an error code: a
22    /// narrower span and the subject name. Consumed by the next report of the
23    /// same code, discarded by a report of any other code.
24    failure: Option<(DiagnosticCode, Option<Span>, Option<String>)>,
25}
26
27impl<'s> Engine<'s> {
28    /// An engine over `source[..end]` with the allowance documented for
29    /// adversarial input: `max(1024, 128 * end)` units of work.
30    pub fn new(source: &'s str, end: usize, mode: Mode) -> Self {
31        Engine {
32            source,
33            mode,
34            nodes: 0,
35            blocks: 0,
36            tags: 0,
37            declarations: 0,
38            names: HashSet::new(),
39            remaining_work: end.saturating_mul(128).max(1024),
40            diagnostics: vec![],
41            suppressed: 0,
42            fatal: false,
43            body_depth: 0,
44            expression_frames: 0,
45            structural_failures: 0,
46            failure: None,
47        }
48    }
49    /// Records detail for a failure about to be returned as `code`.
50    pub fn fail(
51        &mut self,
52        code: DiagnosticCode,
53        span: Option<Span>,
54        subject: Option<String>,
55    ) -> DiagnosticCode {
56        self.failure = Some((code, span, subject));
57        code
58    }
59    pub fn work(&mut self, count: usize) -> Result<()> {
60        if count > self.remaining_work {
61            self.fatal = true;
62            return Err(C::ParseWork);
63        }
64        self.remaining_work -= count;
65        Ok(())
66    }
67    pub fn report(&mut self, code: DiagnosticCode, span: Span) {
68        let (span, subject) = match self.failure.take() {
69            Some((failed, detail, subject)) if failed == code => (detail.unwrap_or(span), subject),
70            _ => (span, None),
71        };
72        self.push_diagnostic(code, span, subject);
73    }
74    fn report_about(&mut self, code: DiagnosticCode, span: Span, subject: &str) {
75        self.failure = None;
76        self.push_diagnostic(code, span, Some(subject.to_owned()));
77    }
78    fn push_diagnostic(&mut self, code: DiagnosticCode, span: Span, subject: Option<String>) {
79        if self.diagnostics.len() < DIAGNOSTICS {
80            self.diagnostics.push(Diagnostic {
81                code,
82                span,
83                subject,
84            });
85        } else {
86            self.suppressed += 1;
87        }
88    }
89    pub fn node(&mut self, span: Span, kind: Kind, children: Vec<Node>) -> Result<Node> {
90        self.nodes += 1;
91        if self.nodes > AST_NODES {
92            self.fatal = true;
93            return Err(C::LimitAst);
94        }
95        Ok(Node {
96            span,
97            kind,
98            children,
99        })
100    }
101    fn opaque(&mut self, start: usize, end: usize) -> Result<Node> {
102        self.node(
103            Span::new(start, end),
104            Kind::Literal(self.source[start..end].replace('\0', "\u{fffd}")),
105            vec![],
106        )
107    }
108    fn expr(&mut self, start: usize, end: usize, tag_depth: usize) -> Result<(Node, usize)> {
109        let mut p = ExprParser {
110            engine: self,
111            pos: start,
112            end,
113            tag_depth,
114        };
115        let node = p.expression(0, 0)?;
116        p.ws();
117        Ok((node, p.pos))
118    }
119    /// Opening-tag errors have no component span. Keep just the candidate prefix
120    /// opaque, stopping at the first blank line so later paragraphs remain live.
121    fn candidate_end(&mut self, start: usize, end: usize) -> usize {
122        let mut pos = start;
123        let mut quote = None;
124        let mut escaped = false;
125        let mut braces = 0;
126        while pos < end {
127            if self.work(1).is_err() {
128                return pos;
129            }
130            if at_line_start(self.source, pos)
131                && self.source[pos..line_content_end(self.source, pos, end)]
132                    .trim()
133                    .is_empty()
134            {
135                return pos;
136            }
137            let c = self.source.as_bytes()[pos];
138            if escaped {
139                escaped = false;
140            } else if c == b'\\' {
141                escaped = true;
142            } else if let Some(q) = quote {
143                if c == q {
144                    quote = None;
145                }
146            } else if matches!(c, b'\'' | b'"') {
147                quote = Some(c);
148            } else if c == b'{' {
149                braces += 1;
150            } else if c == b'}' {
151                braces = (braces - 1).max(0);
152            } else if c == b'>' && braces == 0 {
153                return pos + 1;
154            }
155            pos += self.source[pos..end]
156                .chars()
157                .next()
158                .map_or(1, char::len_utf8);
159        }
160        end
161    }
162    pub fn tag(
163        &mut self,
164        start: usize,
165        end: usize,
166        depth: usize,
167        block_candidate: bool,
168    ) -> Result<(Node, usize, bool)> {
169        let structural_before = self.structural_failures;
170        let opening = self.open_tag(start, end, depth);
171        let (name, props, open_end, self_closing) = match opening {
172            Ok(value) => value,
173            Err(code) => {
174                if code == C::Nesting {
175                    self.structural_failures += 1;
176                }
177                let stop = self.candidate_end(start, end).max(start + 1);
178                self.report(code, Span::new(start, stop));
179                return Ok((self.opaque(start, stop)?, stop, true));
180            }
181        };
182        self.tags += 1;
183        let over_tags = self.mode == Mode::Reply && self.tags > 64;
184        if self.tags == 65 && self.mode == Mode::Reply {
185            self.report_about(C::LimitTags, Span::new(start, open_end), &name);
186        }
187        let block = block_candidate
188            && self.source[open_end..line_content_end(self.source, open_end, end)]
189                .trim()
190                .is_empty();
191        let mut stop = open_end;
192        let mut children = vec![];
193        let mut broken = false;
194        if !self_closing {
195            let (content, content_end, close) =
196                self.body(open_end, end, depth, !block, true, true)?;
197            children = content;
198            stop = content_end;
199            match close {
200                Some((close_name, close_end)) => {
201                    stop = close_end;
202                    if close_name != name {
203                        broken = true;
204                        self.report_about(C::MismatchedClose, Span::new(start, stop), &name);
205                    }
206                    if block {
207                        let tail = line_content_end(self.source, close_end, end);
208                        if !block_position(self.source, content_end)
209                            || !self.source[close_end..tail].trim().is_empty()
210                        {
211                            broken = true;
212                            self.report_about(C::MismatchedClose, Span::new(start, stop), &name);
213                        }
214                    }
215                }
216                None => {
217                    broken = true;
218                    self.report_about(C::UnclosedTag, Span::new(start, stop), &name);
219                }
220            }
221        }
222        if depth > TAG_DEPTH {
223            broken = true;
224            self.report_about(C::Nesting, Span::new(start, stop), &name);
225        }
226        if broken {
227            self.structural_failures += 1;
228        }
229        if !children.is_empty() && props.iter().any(|p| p.name == "children") {
230            broken = true;
231            self.report_about(C::DuplicateProp, Span::new(start, stop), "children");
232        }
233        if self.mode == Mode::Reply {
234            if let Some(prop) = props.iter().find(|p| !reply_value(&p.value)) {
235                broken = true;
236                let prop = prop.name.clone();
237                self.report_about(C::ReplyForbidden, Span::new(start, stop), &prop);
238            }
239        }
240        // A crossed or missing close invalidates the outer component span. A
241        // child's forbidden prop, by contrast, is a local literal-text failure.
242        if self.structural_failures > structural_before {
243            broken = true;
244        }
245        if broken || over_tags {
246            return Ok((self.opaque(start, stop)?, stop, true));
247        }
248        let node = self.node(
249            Span::new(start, stop),
250            Kind::Tag { name, block, props },
251            children,
252        )?;
253        Ok((node, stop, false))
254    }
255    fn open_tag(
256        &mut self,
257        start: usize,
258        end: usize,
259        depth: usize,
260    ) -> Result<(String, Vec<Prop>, usize, bool)> {
261        let bytes = self.source.as_bytes();
262        if depth > TAG_DEPTH {
263            let name = self.source[start..end]
264                .strip_prefix('<')
265                .filter(|rest| rest.starts_with(|c: char| c.is_ascii_uppercase()))
266                .map(|rest| {
267                    rest.chars()
268                        .take_while(char::is_ascii_alphanumeric)
269                        .collect::<String>()
270                });
271            return Err(self.fail(C::Nesting, None, name));
272        }
273        if bytes.get(start) != Some(&b'<')
274            || !bytes.get(start + 1).is_some_and(u8::is_ascii_uppercase)
275        {
276            return Err(C::Syntax);
277        }
278        let mut pos = start + 2;
279        while pos < end && bytes[pos].is_ascii_alphanumeric() {
280            pos += 1;
281        }
282        let name = self.source[start + 1..pos].to_owned();
283        let mut props = Vec::new();
284        let mut unique = HashSet::new();
285        loop {
286            self.work(1)?;
287            let before = pos;
288            while pos < end && bytes[pos].is_ascii_whitespace() {
289                pos += 1;
290            }
291            if has_blank_line(&self.source[before..pos]) {
292                return Err(C::Syntax);
293            }
294            if self.source[pos..end].starts_with("/>") {
295                return Ok((name, props, pos + 2, true));
296            }
297            if self.source[pos..end].starts_with('>') {
298                return Ok((name, props, pos + 1, false));
299            }
300            if before == pos {
301                return Err(C::Syntax);
302            }
303            let prop_start = pos;
304            let mut p = ExprParser {
305                engine: self,
306                pos,
307                end,
308                tag_depth: depth,
309            };
310            let prop_name = p.identifier()?;
311            if !unique.insert(prop_name.clone()) {
312                return Err(p.engine.fail(C::DuplicateProp, None, Some(prop_name)));
313            }
314            let name_end = p.pos;
315            p.ws();
316            let value = if p.eat("=") {
317                p.ws();
318                if p.rest_is_string() {
319                    let start = p.pos;
320                    let text = p.string()?;
321                    p.engine.node(
322                        Span::new(start, p.pos),
323                        Kind::Expression(Expression::String(text)),
324                        vec![],
325                    )?
326                } else {
327                    if !p.eat("{") {
328                        return Err(C::Syntax);
329                    }
330                    let node = p.expression(0, 0)?;
331                    if !p.eat("}") {
332                        return Err(C::Syntax);
333                    }
334                    node
335                }
336            } else {
337                p.pos = name_end;
338                p.engine.node(
339                    Span::new(prop_start, p.pos),
340                    Kind::Expression(Expression::Boolean(true)),
341                    vec![],
342                )?
343            };
344            pos = p.pos;
345            if has_blank_line(&self.source[prop_start..pos]) {
346                return Err(C::Syntax);
347            }
348            props.push(Prop {
349                name: prop_name,
350                span: Span::new(prop_start, pos),
351                value,
352            });
353        }
354    }
355    fn declaration_end(&mut self, start: usize, end: usize) -> Result<usize> {
356        let mut stop = line_content_end(self.source, start, end);
357        while stop < end {
358            self.work(stop - start + 1)?;
359            let next_start = line_end(self.source, stop, end);
360            let next_end = line_content_end(self.source, next_start, end);
361            if self.source[next_start..next_end].trim().is_empty() {
362                break;
363            }
364            let text = self.source[start..stop].trim_end();
365            let next = self.source[next_start..next_end].trim_start();
366            let continuation = declaration_continues(text)
367                || ["?", ":", "&&", "||", "??", "."]
368                    .iter()
369                    .any(|x| next.starts_with(x));
370            if !continuation {
371                break;
372            }
373            stop = next_end;
374        }
375        Ok(stop)
376    }
377    fn declaration(
378        &mut self,
379        start: usize,
380        stop: usize,
381        tag_depth: usize,
382    ) -> Result<(Node, usize)> {
383        self.declarations += 1;
384        if self.declarations > DECLARATIONS {
385            return Err(C::LimitDeclarations);
386        }
387        let mut p = ExprParser {
388            engine: self,
389            pos: start + 6,
390            end: stop,
391            tag_depth,
392        };
393        let state = if p.eat("let") {
394            true
395        } else if p.eat("const") {
396            false
397        } else {
398            return Err(C::Syntax);
399        };
400        let before = p.pos;
401        p.ws();
402        if p.pos == before {
403            return Err(C::Syntax);
404        }
405        if state && !p.eat("$") {
406            return Err(C::Syntax);
407        }
408        if state
409            && p.engine
410                .source
411                .as_bytes()
412                .get(p.pos)
413                .is_some_and(u8::is_ascii_whitespace)
414        {
415            return Err(C::Syntax);
416        }
417        let name = p.word()?;
418        if name.as_bytes()[0].is_ascii_uppercase()
419            || crate::expression::KEYWORDS.contains(&name.as_str())
420            || matches!(name.as_str(), "data" | "input" | "view" | "now" | "zone")
421        {
422            return Err(p.engine.fail(C::ReservedName, None, Some(name)));
423        }
424        if !p.engine.names.insert(name.clone()) {
425            return Err(p.engine.fail(C::DuplicateName, None, Some(name)));
426        }
427        if !p.eat("=") {
428            return Err(C::Syntax);
429        }
430        let child = p.expression(0, 0).map_err(|code| {
431            if code == C::Syntax {
432                C::DeclarationIncomplete
433            } else {
434                code
435            }
436        })?;
437        p.ws();
438        if p.pos != stop {
439            return Err(C::Syntax);
440        }
441        if state && !literal(&child) {
442            return Err(p.engine.fail(C::LiteralRequired, None, Some(name)));
443        }
444        Ok((
445            p.engine.node(
446                Span::new(start, stop),
447                Kind::Declaration { name, state },
448                vec![child],
449            )?,
450            stop,
451        ))
452    }
453    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
454    pub fn body(
455        &mut self,
456        start: usize,
457        end: usize,
458        depth: usize,
459        inline: bool,
460        allow_tags: bool,
461        stop_at_close: bool,
462    ) -> Result<(Vec<Node>, usize, Option<(String, usize)>)> {
463        self.body_depth += 1;
464        if self.body_depth > 64 {
465            self.fatal = true;
466            return Err(C::Nesting);
467        }
468        self.work(end - start)?;
469        let contexts = markdown_contexts(self.source, start, end);
470        let code_matches = &contexts.code;
471        let mut specials = Vec::new();
472        let mut pos = start;
473        let mut close = None;
474        while pos < end {
475            self.work(1)?;
476            let rest = &self.source[pos..end];
477            if inline
478                && at_line_start(self.source, pos)
479                && self.source[pos..line_content_end(self.source, pos, end)]
480                    .trim()
481                    .is_empty()
482            {
483                break;
484            }
485            if let Some(stop) = thematic(self.source, pos, end) {
486                specials.push(self.node(
487                    Span::new(pos, stop),
488                    Kind::Markdown(Markdown::ThematicBreak),
489                    vec![],
490                )?);
491                pos = stop;
492                continue;
493            }
494            if let Some((language, fence_end, text)) =
495                contexts.fences.get(&pos).cloned().or_else(|| {
496                    fence(self.source, pos, end).map(|(language, stop)| {
497                        (language, stop, fence_content(self.source, pos, stop))
498                    })
499                })
500            {
501                let node = self.node(
502                    Span::new(pos, fence_end),
503                    Kind::Markdown(Markdown::CodeBlock { language, text }),
504                    vec![],
505                )?;
506                specials.push(node);
507                pos = fence_end;
508                continue;
509            }
510            if let Some(tail) = rest.strip_prefix('\\') {
511                if let Some(c) = tail.chars().next() {
512                    if c.is_ascii_punctuation() || c == '\n' || c == '\r' {
513                        pos += 1 + c.len_utf8();
514                        continue;
515                    }
516                }
517            }
518            if rest.starts_with('`') {
519                if let Some(&stop) = code_matches.get(&pos) {
520                    specials.push(self.node(
521                        Span::new(pos, stop),
522                        Kind::Markdown(Markdown::Code(code_text(&self.source[pos..stop]))),
523                        vec![],
524                    )?);
525                    pos = stop;
526                    continue;
527                }
528                pos += rest.bytes().take_while(|b| *b == b'`').count();
529                continue;
530            }
531            if stop_at_close && rest.starts_with("</") {
532                if let Some((name, stop)) = close_tag(self.source, pos, end) {
533                    close = Some((name, stop));
534                    break;
535                }
536            }
537            let link_start = if rest.starts_with("![") {
538                Some(pos + 1)
539            } else if rest.starts_with('[') {
540                Some(pos)
541            } else {
542                None
543            };
544            if let Some(bracket) = link_start.filter(|_| contexts.links.contains_key(&pos)) {
545                if let Some((label_end, stop, _, _)) = self.link(bracket, end, code_matches)? {
546                    let (children, _, _) =
547                        self.body(bracket + 1, label_end, depth, true, false, false)?;
548                    specials.push(self.node(
549                        Span::new(pos, stop),
550                        Kind::Markdown(Markdown::Link {
551                            destination: contexts.links[&pos].0.clone(),
552                            title: contexts.links[&pos].1.clone(),
553                            image: bracket != pos,
554                        }),
555                        children,
556                    )?);
557                    pos = stop;
558                    continue;
559                }
560            }
561            if rest.starts_with("<http://") || rest.starts_with("<https://") {
562                let found = rest.find('>');
563                self.work(found.unwrap_or(rest.len()))?;
564                if let Some(offset) = found {
565                    let stop = pos + offset + 1;
566                    specials.push(self.node(
567                        Span::new(pos, stop),
568                        Kind::Markdown(Markdown::Link {
569                            destination: rest[1..offset].to_owned(),
570                            title: String::new(),
571                            image: false,
572                        }),
573                        vec![],
574                    )?);
575                    pos = stop;
576                    continue;
577                }
578            }
579            if rest.starts_with("https://") || rest.starts_with("http://") {
580                let len = rest
581                    .find(|c: char| c.is_whitespace() || matches!(c, '<' | '>'))
582                    .unwrap_or(rest.len());
583                let stop = pos + len;
584                specials.push(self.node(
585                    Span::new(pos, stop),
586                    Kind::Markdown(Markdown::Link {
587                        destination: self.source[pos..stop].to_owned(),
588                        title: String::new(),
589                        image: false,
590                    }),
591                    vec![],
592                )?);
593                pos = stop;
594                continue;
595            }
596            if allow_tags
597                && rest.starts_with('<')
598                && rest.as_bytes().get(1).is_some_and(u8::is_ascii_uppercase)
599            {
600                let line_start = self.source[..pos].rfind(['\r', '\n']).map_or(0, |i| i + 1);
601                self.work(pos - line_start)?;
602                let block = !inline && block_position(self.source, pos);
603                let (node, stop, _) = self.tag(pos, end, depth + 1, block)?;
604                specials.push(node);
605                pos = stop;
606                continue;
607            }
608            if at_line_start(self.source, pos) && declaration_start(rest) {
609                let stop = self.declaration_end(pos, end)?;
610                if self.mode == Mode::Reply {
611                    specials.push(self.opaque(pos, stop)?);
612                    pos = stop;
613                    continue;
614                }
615                match self.declaration(pos, stop, depth) {
616                    Ok((node, stop)) => {
617                        specials.push(node);
618                        pos = stop;
619                        continue;
620                    }
621                    Err(code) => {
622                        self.report(code, Span::new(pos, stop));
623                        specials.push(self.opaque(pos, stop)?);
624                        pos = stop;
625                        continue;
626                    }
627                }
628            }
629            if self.mode == Mode::Reply && rest.starts_with('{') {
630                let stop = self.literal_braces(pos, end)?;
631                specials.push(self.opaque(pos, stop)?);
632                pos = stop;
633                continue;
634            }
635            if self.mode == Mode::Program && rest.starts_with('{') {
636                match self.expr(pos + 1, end, depth) {
637                    Ok((node, next))
638                        if self.source[next..end].starts_with('}')
639                            && !has_blank_line(&self.source[pos..next]) =>
640                    {
641                        let stop = next + 1;
642                        let block = !inline
643                            && block_position(self.source, pos)
644                            && self.source[stop..line_content_end(self.source, stop, end)]
645                                .trim()
646                                .is_empty();
647                        specials.push(self.node(
648                            Span::new(pos, stop),
649                            Kind::Interpolation { block },
650                            vec![node],
651                        )?);
652                        pos = stop;
653                        continue;
654                    }
655                    Err(code) => self.report(code, Span::new(pos, pos + 1)),
656                    _ => self.report(C::Syntax, Span::new(pos, pos + 1)),
657                }
658                let stop = self.literal_braces(pos, end)?;
659                specials.push(self.opaque(pos, stop)?);
660                pos = stop;
661                continue;
662            }
663            if let Some(stop) = reference_end(&self.source[..end], pos) {
664                specials.push(self.node(
665                    Span::new(pos, stop),
666                    Kind::Reference {
667                        n: self.source[pos + 5..stop - 1].to_owned(),
668                    },
669                    vec![],
670                )?);
671                pos = stop;
672                continue;
673            }
674            pos += rest.chars().next().map_or(1, char::len_utf8);
675        }
676        let nodes = crate::markdown::assemble(self, start, pos, specials, inline)?;
677        self.body_depth -= 1;
678        Ok((nodes, pos, close))
679    }
680    fn literal_braces(&mut self, start: usize, end: usize) -> Result<usize> {
681        let mut depth = 1;
682        let mut pos = start + 1;
683        while pos < end {
684            self.work(1)?;
685            if at_line_start(self.source, pos)
686                && self.source[pos..line_content_end(self.source, pos, end)]
687                    .trim()
688                    .is_empty()
689            {
690                break;
691            }
692            match self.source.as_bytes()[pos] {
693                b'\\' => {
694                    pos = next_char(self.source, pos + 1, end);
695                    continue;
696                }
697                b'{' => depth += 1,
698                b'}' => {
699                    depth -= 1;
700                    if depth == 0 {
701                        return Ok(pos + 1);
702                    }
703                }
704                _ => {}
705            }
706            pos = next_char(self.source, pos, end);
707        }
708        Ok(pos)
709    }
710    fn link(
711        &mut self,
712        start: usize,
713        end: usize,
714        code: &HashMap<usize, usize>,
715    ) -> Result<Option<(usize, usize, String, String)>> {
716        let mut pos = start + 1;
717        let mut depth = 1;
718        while pos < end {
719            self.work(1)?;
720            if let Some(stop) = code.get(&pos) {
721                pos = *stop;
722                continue;
723            }
724            let c = self.source.as_bytes()[pos];
725            if c == b'\\' {
726                pos = next_char(self.source, pos + 1, end);
727                continue;
728            }
729            if c == b'[' {
730                depth += 1;
731            }
732            if c == b']' {
733                depth -= 1;
734                if depth == 0 {
735                    break;
736                }
737            }
738            pos = next_char(self.source, pos, end);
739        }
740        if pos >= end || !self.source[pos..end].starts_with("](") {
741            return Ok(None);
742        }
743        let label_end = pos;
744        pos += 2;
745        let dest_start = pos;
746        let mut nesting = 1;
747        let mut quote = None;
748        while pos < end {
749            self.work(1)?;
750            let c = self.source.as_bytes()[pos];
751            if c == b'\\' {
752                pos = next_char(self.source, pos + 1, end);
753                continue;
754            }
755            if let Some(q) = quote {
756                if c == q {
757                    quote = None;
758                }
759            } else if matches!(c, b'\'' | b'"') {
760                quote = Some(c);
761            } else if c == b'(' {
762                nesting += 1;
763            } else if c == b')' {
764                nesting -= 1;
765                if nesting == 0 {
766                    break;
767                }
768            }
769            pos = next_char(self.source, pos, end);
770        }
771        if pos == end || has_blank_line(&self.source[dest_start..pos]) {
772            return Ok(None);
773        }
774        let raw = self.source[dest_start..pos].trim();
775        let split = raw.find(char::is_whitespace).unwrap_or(raw.len());
776        Ok(Some((
777            label_end,
778            pos + 1,
779            raw[..split].trim_matches(['<', '>']).to_owned(),
780            raw[split..].trim().trim_matches(['\'', '"']).to_owned(),
781        )))
782    }
783}
784
785impl ExprParser<'_, '_> {
786    fn rest_is_string(&self) -> bool {
787        self.engine.source[self.pos..self.end].starts_with(['\'', '"'])
788    }
789}
790
791fn declaration_start(rest: &str) -> bool {
792    rest.strip_prefix("export")
793        .filter(|s| s.starts_with([' ', '\t']))
794        .is_some_and(|s| {
795            let s = s.trim_start_matches([' ', '\t']);
796            ["let", "const"].iter().any(|k| {
797                s.strip_prefix(k)
798                    .is_some_and(|tail| tail.starts_with([' ', '\t']))
799            })
800        })
801}
802fn declaration_continues(text: &str) -> bool {
803    let mut depth = 0_i32;
804    let mut tags = 0_i32;
805    let mut header = false;
806    let mut closing = false;
807    let mut header_braces = 0_i32;
808    let mut quote = None;
809    let mut escaped = false;
810    let mut operand = true;
811    let mut previous = '\0';
812    let mut chars = text.chars().peekable();
813    while let Some(c) = chars.next() {
814        if escaped {
815            escaped = false;
816            continue;
817        }
818        if c == '\\' {
819            escaped = true;
820            continue;
821        }
822        if let Some(q) = quote {
823            if c == q {
824                quote = None;
825                operand = false;
826            }
827            continue;
828        }
829        if (header || tags == 0) && matches!(c, '\'' | '"') {
830            quote = Some(c);
831            continue;
832        }
833        if c == '<'
834            && !header
835            && (tags > 0 || operand)
836            && chars
837                .peek()
838                .is_some_and(|c| c.is_ascii_uppercase() || *c == '/')
839        {
840            closing = chars.peek() == Some(&'/');
841            tags += if closing { -1 } else { 1 };
842            header = true;
843            header_braces = 0;
844        } else if header {
845            if c == '{' {
846                header_braces += 1;
847            }
848            if c == '}' {
849                header_braces -= 1;
850            }
851            if c == '>' && header_braces == 0 {
852                if !closing && previous == '/' {
853                    tags -= 1;
854                }
855                header = false;
856                operand = false;
857            }
858        } else if tags == 0 {
859            match c {
860                '(' | '[' | '{' => {
861                    depth += 1;
862                    operand = true;
863                }
864                ')' | ']' | '}' => {
865                    depth -= 1;
866                    operand = false;
867                }
868                '+' | '-' | '*' | '/' | '%' | '?' | ':' | ',' | '=' | '!' | '<' | '>' | '&'
869                | '|' => operand = true,
870                c if !c.is_whitespace() => operand = false,
871                _ => {}
872            }
873        }
874        if !c.is_whitespace() {
875            previous = c;
876        }
877    }
878    depth > 0 || tags > 0 || header || operand
879}
880pub(crate) fn line_content_end(source: &str, pos: usize, end: usize) -> usize {
881    source[pos..end].find(['\r', '\n']).map_or(end, |i| pos + i)
882}
883fn line_end(source: &str, pos: usize, end: usize) -> usize {
884    let stop = line_content_end(source, pos, end);
885    if stop == end {
886        end
887    } else if source[stop..end].starts_with("\r\n") {
888        stop + 2
889    } else {
890        stop + 1
891    }
892}
893fn at_line_start(source: &str, pos: usize) -> bool {
894    pos == 0
895        || (pos == 3 && source.starts_with('\u{feff}'))
896        || source
897            .as_bytes()
898            .get(pos - 1)
899            .is_some_and(|c| matches!(c, b'\n' | b'\r'))
900}
901fn block_position(source: &str, pos: usize) -> bool {
902    let start = source[..pos]
903        .rfind(['\r', '\n'])
904        .map_or(if source.starts_with('\u{feff}') { 3 } else { 0 }, |p| {
905            p + 1
906        });
907    let mut prefix = &source[start..pos];
908    for _ in 0..=MARKDOWN_DEPTH {
909        let trimmed = prefix.trim_start_matches(' ');
910        if prefix.len() - trimmed.len() > 3 {
911            return false;
912        }
913        prefix = trimmed;
914        if prefix.is_empty() {
915            return true;
916        }
917        if let Some(tail) = prefix.strip_prefix('>') {
918            prefix = tail.strip_prefix(' ').unwrap_or(tail);
919            continue;
920        }
921        let marker = if prefix.starts_with(['-', '+', '*']) {
922            1
923        } else {
924            let digits = prefix.bytes().take_while(u8::is_ascii_digit).count();
925            if digits == 0 || digits > 9 || !prefix[digits..].starts_with(['.', ')']) {
926                return false;
927            }
928            digits + 1
929        };
930        if !prefix[marker..].starts_with([' ', '\t']) {
931            return false;
932        }
933        prefix = prefix[marker..].trim_start_matches([' ', '\t']);
934    }
935    false
936}
937pub(crate) fn has_blank_line(text: &str) -> bool {
938    let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
939    normalized
940        .split('\n')
941        .skip(1)
942        .take(normalized.split('\n').count().saturating_sub(2))
943        .any(|line| line.trim().is_empty())
944}
945fn next_char(source: &str, pos: usize, end: usize) -> usize {
946    pos + source[pos..end].chars().next().map_or(0, char::len_utf8)
947}
948fn close_tag(source: &str, start: usize, end: usize) -> Option<(String, usize)> {
949    let tail = source.get(start + 2..end)?;
950    if !tail.as_bytes().first()?.is_ascii_uppercase() {
951        return None;
952    }
953    let len = tail.bytes().take_while(u8::is_ascii_alphanumeric).count();
954    let rest = tail[len..].trim_start_matches(char::is_whitespace);
955    if !rest.starts_with('>') {
956        return None;
957    }
958    Some((tail[..len].to_owned(), end - rest.len() + 1))
959}
960fn fence(source: &str, pos: usize, end: usize) -> Option<(String, usize)> {
961    if !at_line_start(source, pos) {
962        return None;
963    }
964    let line_end_pos = line_content_end(source, pos, end);
965    let line = &source[pos..line_end_pos];
966    let trim = line.trim_start_matches(' ');
967    if line.len() - trim.len() > 3 {
968        return None;
969    }
970    let ch = *trim.as_bytes().first()?;
971    if !matches!(ch, b'`' | b'~') {
972        return None;
973    }
974    let count = trim.bytes().take_while(|b| *b == ch).count();
975    if count < 3 || (ch == b'`' && trim[count..].contains('`')) {
976        return None;
977    }
978    let language = trim[count..]
979        .split_whitespace()
980        .next()
981        .unwrap_or("")
982        .to_owned();
983    let mut next = line_end(source, pos, end);
984    while next < end {
985        let stop = line_content_end(source, next, end);
986        let line = &source[next..stop];
987        let trim = line.trim_start_matches(' ');
988        if line.len() - trim.len() <= 3 {
989            let run = trim.bytes().take_while(|b| *b == ch).count();
990            if run >= count && trim[run..].trim().is_empty() {
991                return Some((language, line_end(source, next, end)));
992            }
993        }
994        next = line_end(source, next, end);
995    }
996    Some((language, end))
997}
998fn code_text(source: &str) -> String {
999    let run = source.bytes().take_while(|b| *b == b'`').count();
1000    let text = source[run..source.len() - run]
1001        .replace("\r\n", " ")
1002        .replace(['\r', '\n'], " ");
1003    if text.starts_with(' ') && text.ends_with(' ') && text.chars().any(|c| c != ' ') {
1004        text[1..text.len() - 1].to_owned()
1005    } else {
1006        text
1007    }
1008}
1009fn thematic(source: &str, pos: usize, end: usize) -> Option<usize> {
1010    if !at_line_start(source, pos) {
1011        return None;
1012    }
1013    let stop = line_content_end(source, pos, end);
1014    let line = &source[pos..stop];
1015    let trim = line.trim_start_matches(' ');
1016    if line.len() - trim.len() > 3 {
1017        return None;
1018    }
1019    let mark = *trim.as_bytes().first()?;
1020    if !matches!(mark, b'-' | b'*' | b'_')
1021        || trim.bytes().filter(|b| *b == mark).count() < 3
1022        || !trim.bytes().all(|b| b == mark || matches!(b, b' ' | b'\t'))
1023    {
1024        return None;
1025    }
1026    Some(stop)
1027}
1028#[derive(Default)]
1029struct Contexts {
1030    code: HashMap<usize, usize>,
1031    fences: HashMap<usize, (String, usize, String)>,
1032    links: HashMap<usize, (String, String)>,
1033}
1034fn markdown_contexts(source: &str, start: usize, end: usize) -> Contexts {
1035    use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
1036    let masked = crate::markdown::context_source(&source[start..end]);
1037    let mut contexts = Contexts::default();
1038    let mut current: Option<(usize, String, usize, String)> = None;
1039    for (event, range) in Parser::new_ext(
1040        &masked,
1041        Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TASKLISTS,
1042    )
1043    .into_offset_iter()
1044    {
1045        match event {
1046            Event::Code(_) => {
1047                contexts.code.insert(start + range.start, start + range.end);
1048            }
1049            Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(info))) => {
1050                current = Some((
1051                    start + range.start,
1052                    info.split_whitespace().next().unwrap_or("").to_owned(),
1053                    start + range.end,
1054                    String::new(),
1055                ));
1056            }
1057            Event::Text(text) if current.is_some() => {
1058                if let Some((_, _, _, content)) = &mut current {
1059                    content.push_str(&text);
1060                }
1061            }
1062            Event::End(TagEnd::CodeBlock) => {
1063                if let Some((pos, language, stop, text)) = current.take() {
1064                    contexts.fences.insert(
1065                        pos,
1066                        (
1067                            language,
1068                            stop,
1069                            text.replace('\u{1}', "<")
1070                                .replace('\u{2}', " ")
1071                                .replace('\u{3}', "=")
1072                                .replace('\u{4}', "["),
1073                        ),
1074                    );
1075                }
1076            }
1077            Event::Start(
1078                Tag::Link {
1079                    dest_url, title, ..
1080                }
1081                | Tag::Image {
1082                    dest_url, title, ..
1083                },
1084            ) => {
1085                contexts.links.insert(
1086                    start + range.start,
1087                    (dest_url.into_string(), title.into_string()),
1088                );
1089            }
1090            _ => {}
1091        }
1092    }
1093    contexts
1094}
1095fn fence_content(source: &str, start: usize, end: usize) -> String {
1096    let first = line_end(source, start, end);
1097    let tail = source[first..end].trim_end_matches(['\r', '\n']);
1098    let last = tail.rfind(['\r', '\n']).map_or(first, |i| first + i + 1);
1099    let candidate = source[last..end].trim();
1100    let content_end = if candidate.bytes().all(|b| matches!(b, b'`' | b'~')) {
1101        last
1102    } else {
1103        end
1104    };
1105    source[first..content_end]
1106        .replace("\r\n", "\n")
1107        .replace('\r', "\n")
1108}
1109
1110/// The mode's source limit in bytes. `parse` and `parse_bytes` cut the
1111/// source at it (at a UTF-8 boundary); `StreamParser` keeps three bytes past
1112/// it, so a scalar straddling the limit is complete when `parse_bytes` cuts.
1113pub(crate) fn source_cap(mode: Mode) -> usize {
1114    if mode == Mode::Reply {
1115        REPLY_BYTES
1116    } else {
1117        PROGRAM_BYTES
1118    }
1119}
1120
1121pub fn parse(source: &str, mode: Mode) -> Document {
1122    let mut end = source.len().min(source_cap(mode));
1123    while !source.is_char_boundary(end) {
1124        end -= 1;
1125    }
1126    parse_prefix(source, end, mode, source.len() > source_cap(mode))
1127}
1128
1129/// Parses `source[..end]`. `over` says the original input was longer than
1130/// the mode's limit, which shortens a reply and refuses a program.
1131fn parse_prefix(source: &str, end: usize, mode: Mode, over: bool) -> Document {
1132    let mut engine = Engine::new(source, end, mode);
1133    let mut fallback = mode == Mode::Program && over;
1134    if fallback {
1135        engine.report(C::SourceTooLarge, Span::new(0, end));
1136    }
1137    let start = if source.starts_with('\u{feff}') {
1138        3.min(end)
1139    } else {
1140        0
1141    };
1142    let mut nodes = if fallback {
1143        vec![]
1144    } else {
1145        match engine.body(start, end, 0, false, true, false) {
1146            Ok((nodes, _, _)) => nodes,
1147            Err(code) => {
1148                engine.report(code, Span::new(start, end));
1149                fallback = true;
1150                vec![]
1151            }
1152        }
1153    };
1154    if engine.fatal {
1155        fallback = true;
1156    }
1157    if mode == Mode::Reply && text_scalars(&nodes) > OUTPUT_SCALARS {
1158        engine.report(C::LimitText, Span::new(start, end));
1159        fallback = true;
1160    }
1161    if fallback {
1162        nodes = vec![Node {
1163            span: Span::new(start, end),
1164            kind: Kind::Literal(
1165                source[start..end]
1166                    .chars()
1167                    .take(OUTPUT_SCALARS)
1168                    .map(|c| if c == '\0' { '\u{fffd}' } else { c })
1169                    .collect(),
1170            ),
1171            children: vec![],
1172        }];
1173    }
1174    let mut doc = Document {
1175        mode,
1176        nodes,
1177        diagnostics: engine.diagnostics,
1178        suppressed_diagnostics: engine.suppressed,
1179        parsed_bytes: end,
1180        shortened: over,
1181        fallback,
1182    };
1183    if doc.references().len() > REFERENCES {
1184        add_diagnostic(&mut doc, C::ReferenceLimit, Span::new(start, end));
1185    }
1186    if count_handles(&doc.nodes) > HANDLES {
1187        add_diagnostic(&mut doc, C::ReferenceLimit, Span::new(start, end));
1188    }
1189    doc
1190}
1191fn add_diagnostic(doc: &mut Document, code: DiagnosticCode, span: Span) {
1192    if doc.diagnostics.len() < DIAGNOSTICS {
1193        doc.diagnostics.push(Diagnostic {
1194            code,
1195            span,
1196            subject: None,
1197        });
1198    } else {
1199        doc.suppressed_diagnostics += 1;
1200    }
1201}
1202fn count_handles(nodes: &[Node]) -> usize {
1203    nodes
1204        .iter()
1205        .map(|n| {
1206            usize::from(matches!(n.kind, Kind::Handle { .. }))
1207                + count_handles(&n.children)
1208                + match &n.kind {
1209                    Kind::Tag { props, .. } => props
1210                        .iter()
1211                        .map(|p| count_handles(std::slice::from_ref(&p.value)))
1212                        .sum(),
1213                    _ => 0,
1214                }
1215        })
1216        .sum()
1217}
1218fn text_scalars(nodes: &[Node]) -> usize {
1219    nodes.iter().map(|n| match &n.kind { Kind::Text(t) | Kind::Literal(t) => t.chars().count(), Kind::Markdown(Markdown::Code(t) | Markdown::CodeBlock { text: t, .. }) => t.chars().count(), _ => 0 } + text_scalars(&n.children)).sum()
1220}
1221
1222/// Invalid UTF-8 is replaced before parsing; offsets refer to the repaired
1223/// text. The size limit measures the raw input bytes, before repair. Each
1224/// maximal invalid subsequence (one to three bytes) becomes one three-byte
1225/// U+FFFD, so repaired text is never shorter than the input and may be longer
1226/// than the limit; an input within the limit is never refused for it.
1227pub fn parse_bytes(source: &[u8], mode: Mode) -> Document {
1228    let cut = raw_cut(source, source_cap(mode));
1229    let repaired = String::from_utf8_lossy(&source[..cut]);
1230    let mut doc = parse_prefix(&repaired, repaired.len(), mode, source.len() > cut);
1231    if matches!(repaired, std::borrow::Cow::Owned(_)) {
1232        let end = doc.parsed_bytes;
1233        add_diagnostic(&mut doc, C::Encoding, Span::new(0, end));
1234    }
1235    doc
1236}
1237
1238/// Where a raw input is cut to the limit `cap`. A valid UTF-8 scalar that
1239/// straddles the limit is left out whole, so cutting never manufactures an
1240/// encoding error in valid text.
1241pub(crate) fn raw_cut(source: &[u8], cap: usize) -> usize {
1242    if source.len() <= cap {
1243        return source.len();
1244    }
1245    for lead in (cap.saturating_sub(3)..cap).rev() {
1246        let width = match source[lead] {
1247            0x80..=0xBF => continue,
1248            0xC2..=0xDF => 2,
1249            0xE0..=0xEF => 3,
1250            0xF0..=0xF4 => 4,
1251            _ => break,
1252        };
1253        let straddles = lead + width > cap
1254            && source
1255                .get(lead..lead + width)
1256                .is_some_and(|scalar| std::str::from_utf8(scalar).is_ok());
1257        return if straddles { lead } else { cap };
1258    }
1259    cap
1260}