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 failure: Option<(DiagnosticCode, Option<Span>, Option<String>)>,
25}
26
27impl<'s> Engine<'s> {
28 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 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 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 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
1110pub(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
1129fn 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
1222pub 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
1238pub(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}