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