1use std::collections::{BTreeSet, HashMap};
18use std::io::Read;
19use std::sync::OnceLock;
20
21use crate::pycompat::{is_re_digit, py_casefold, py_is_space, py_repr_str, py_strip};
22
23pub const DEFAULT_MAX_FILE_BYTES: u128 = 1 << 20; pub const MAX_FIELD_CHARS: usize = 256 << 10; pub const MAX_CAPTURED_LINES: usize = 50_000; const ND_RUN_STARTS: &[u32] = &[
35 48, 1632, 1776, 1984, 2406, 2534, 2662, 2790, 2918, 3046, 3174, 3302, 3430, 3558, 3664, 3792,
36 3872, 4160, 4240, 6112, 6160, 6470, 6608, 6784, 6800, 6992, 7088, 7232, 7248, 42528, 43216,
37 43264, 43472, 43504, 43600, 44016, 65296, 66720, 68912, 69734, 69872, 69942, 70096, 70384,
38 70736, 70864, 71248, 71360, 71472, 71904, 72016, 72784, 73040, 73120, 92768, 92864, 93008,
39 120782, 123200, 123632, 125264, 130032,
40];
41
42fn nd_digit_value(c: char) -> Option<u32> {
43 if !is_re_digit(c) {
44 return None;
45 }
46 let cp = c as u32;
47 let idx = match ND_RUN_STARTS.binary_search(&cp) {
48 Ok(i) => i,
49 Err(0) => return None,
50 Err(i) => i - 1,
51 };
52 Some((cp - ND_RUN_STARTS[idx]) % 10)
53}
54
55pub fn py_parse_int(raw: &str) -> Option<i128> {
65 let s = raw.trim();
66 let mut chars = s.chars().peekable();
67 let mut neg = false;
68 match chars.peek() {
69 Some('+') => {
70 chars.next();
71 }
72 Some('-') => {
73 neg = true;
74 chars.next();
75 }
76 _ => {}
77 }
78 let mut value: i128 = 0;
79 let mut last_was_digit = false;
80 let mut any_digit = false;
81 for c in chars {
82 if c == '_' {
83 if !last_was_digit {
84 return None;
85 }
86 last_was_digit = false;
87 continue;
88 }
89 let d = nd_digit_value(c)?;
90 value = value
91 .saturating_mul(10)
92 .saturating_add(d as i128);
93 last_was_digit = true;
94 any_digit = true;
95 }
96 if !any_digit || !last_was_digit {
97 return None;
98 }
99 Some(if neg { -value } else { value })
100}
101
102pub fn max_file_bytes_from(raw: Option<&str>) -> u128 {
104 if let Some(raw) = raw {
105 if let Some(v) = py_parse_int(raw) {
106 if v > 0 {
107 return v as u128;
108 }
109 }
110 }
111 DEFAULT_MAX_FILE_BYTES
112}
113
114pub fn max_file_bytes() -> u128 {
116 match std::env::var("DECIDED_MAX_FILE_BYTES") {
117 Ok(v) => max_file_bytes_from(Some(&v)),
118 Err(_) => DEFAULT_MAX_FILE_BYTES,
119 }
120}
121
122fn exceeds_byte_cap(text: &str, cap: u128) -> bool {
125 text.len() as u128 > cap
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct Issue {
134 pub severity: &'static str,
135 pub code: &'static str,
136 pub message: String,
137 pub line: Option<i64>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct Requirement {
142 pub id: String,
143 pub text: String,
144 pub line: i64,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct MalformedRequirement {
149 pub raw: String,
150 pub line: i64,
151 pub bad_id: Option<String>,
152 pub empty_text: bool,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Default)]
156pub struct SearchSection {
157 pub heading: String,
158 pub lines: Vec<String>,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct FrontmatterSplit {
164 pub raw: Option<String>,
165 pub body: String,
166 pub line_offset: usize,
167 pub unterminated: bool,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Default)]
171pub struct Product {
172 pub title: Option<String>,
173 pub extra_title_lines: Vec<i64>,
174 pub problem: Option<String>,
175 pub requirements: Vec<Requirement>,
176 pub malformed_requirements: Vec<MalformedRequirement>,
177 pub success_metrics: Vec<String>,
178 pub risks: Vec<String>,
179 pub sections: Vec<(String, String)>,
181 pub search_sections: Vec<SearchSection>,
182 pub has_problem_section: bool,
183 pub has_requirements_section: bool,
184 pub has_metrics_section: bool,
185 pub has_risks_section: bool,
186 pub source_path: String,
187 pub frontmatter_raw: Option<String>,
190 pub metadata_issues: Vec<Issue>,
193 pub parse_issues: Vec<Issue>,
194}
195
196pub fn split_frontmatter(text: &str) -> FrontmatterSplit {
198 let lines: Vec<&str> = text.split('\n').collect();
199 if lines.is_empty() || py_strip(lines[0]) != "---" {
200 return FrontmatterSplit {
201 raw: None,
202 body: text.to_string(),
203 line_offset: 0,
204 unterminated: false,
205 };
206 }
207 for i in 1..lines.len() {
208 let s = py_strip(lines[i]);
209 if s == "---" || s == "..." {
210 return FrontmatterSplit {
211 raw: Some(lines[1..i].join("\n")),
212 body: lines[i + 1..].join("\n"),
213 line_offset: i + 1,
214 unterminated: false,
215 };
216 }
217 }
218 FrontmatterSplit {
219 raw: None,
220 body: text.to_string(),
221 line_offset: 0,
222 unterminated: true,
223 }
224}
225
226#[derive(Debug, Clone)]
231pub struct Token {
232 pub typ: &'static str,
233 pub tag: &'static str,
234 pub map: Option<(usize, usize)>,
235 pub content: String,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239enum Parent {
240 Root,
241 Paragraph,
242 Blockquote,
243 List,
244 Reference,
245}
246
247const MAX_NESTING: i32 = 20; #[inline]
250fn is_str_space(c: char) -> bool {
251 c == ' ' || c == '\t'
252}
253
254struct State<'s> {
255 src: &'s [char],
256 b_marks: Vec<usize>,
257 e_marks: Vec<usize>,
258 t_shift: Vec<usize>,
259 s_count: Vec<i32>,
260 bs_count: Vec<i32>,
261 blk_indent: i32,
262 line: usize,
263 line_max: usize,
264 tight: bool,
265 list_indent: i32,
266 parent_type: Parent,
267 level: i32,
268 tokens: Vec<Token>,
269}
270
271impl<'s> State<'s> {
272 fn new(src: &'s [char]) -> Self {
273 let mut st = State {
274 src,
275 b_marks: Vec::new(),
276 e_marks: Vec::new(),
277 t_shift: Vec::new(),
278 s_count: Vec::new(),
279 bs_count: Vec::new(),
280 blk_indent: 0,
281 line: 0,
282 line_max: 0,
283 tight: false,
284 list_indent: -1,
285 parent_type: Parent::Root,
286 level: 0,
287 tokens: Vec::new(),
288 };
289 let length = src.len();
290 let mut indent_found = false;
291 let mut start = 0usize;
292 let mut indent = 0usize;
293 let mut offset = 0i32;
294 for (pos, &ch) in src.iter().enumerate() {
295 if !indent_found {
296 if is_str_space(ch) {
297 indent += 1;
298 if ch == '\t' {
299 offset += 4 - offset % 4;
300 } else {
301 offset += 1;
302 }
303 continue;
304 } else {
305 indent_found = true;
306 }
307 }
308 if ch == '\n' || pos == length - 1 {
309 let p = if ch != '\n' { pos + 1 } else { pos };
310 st.b_marks.push(start);
311 st.e_marks.push(p);
312 st.t_shift.push(indent);
313 st.s_count.push(offset);
314 st.bs_count.push(0);
315 indent_found = false;
316 indent = 0;
317 offset = 0;
318 start = p + 1;
319 }
320 }
321 st.b_marks.push(length);
323 st.e_marks.push(length);
324 st.t_shift.push(0);
325 st.s_count.push(0);
326 st.bs_count.push(0);
327 st.line_max = st.b_marks.len() - 1;
328 st
329 }
330
331 fn push(&mut self, typ: &'static str, tag: &'static str, nesting: i32) -> usize {
332 if nesting < 0 {
333 self.level -= 1;
334 }
335 if nesting > 0 {
336 self.level += 1;
337 }
338 self.tokens.push(Token {
339 typ,
340 tag,
341 map: None,
342 content: String::new(),
343 });
344 self.tokens.len() - 1
345 }
346
347 fn is_empty(&self, line: usize) -> bool {
348 self.b_marks[line] + self.t_shift[line] >= self.e_marks[line]
349 }
350
351 fn skip_empty_lines(&self, mut from: usize) -> usize {
352 while from < self.line_max {
353 if self.b_marks[from] + self.t_shift[from] < self.e_marks[from] {
354 break;
355 }
356 from += 1;
357 }
358 from
359 }
360
361 fn skip_spaces(&self, mut pos: usize) -> usize {
362 while let Some(&c) = self.src.get(pos) {
363 if !is_str_space(c) {
364 break;
365 }
366 pos += 1;
367 }
368 pos
369 }
370
371 fn skip_spaces_back(&self, mut pos: usize, minimum: usize) -> usize {
372 if pos <= minimum {
373 return pos;
374 }
375 while pos > minimum {
376 pos -= 1;
377 if !is_str_space(self.src[pos]) {
378 return pos + 1;
379 }
380 }
381 pos
382 }
383
384 fn skip_chars_str(&self, mut pos: usize, ch: char) -> usize {
385 while let Some(&c) = self.src.get(pos) {
386 if c != ch {
387 break;
388 }
389 pos += 1;
390 }
391 pos
392 }
393
394 fn skip_chars_str_back(&self, mut pos: usize, ch: char, minimum: usize) -> usize {
395 if pos <= minimum {
396 return pos;
397 }
398 while pos > minimum {
399 pos -= 1;
400 if ch != self.src[pos] {
401 return pos + 1;
402 }
403 }
404 pos
405 }
406
407 fn get_lines(&self, begin: usize, end: usize, indent: i32, keep_last_lf: bool) -> String {
408 if begin >= end {
409 return String::new();
410 }
411 let mut out = String::new();
412 for line in begin..end {
413 let mut line_indent: i32 = 0;
414 let line_start = self.b_marks[line];
415 let mut first = line_start;
416 let last = if line + 1 < end || keep_last_lf {
417 self.e_marks[line] + 1
418 } else {
419 self.e_marks[line]
420 };
421 while first < last && line_indent < indent {
422 match self.src.get(first) {
423 None => break, Some(&ch) => {
425 if is_str_space(ch) {
426 if ch == '\t' {
427 line_indent += 4 - (line_indent + self.bs_count[line]) % 4;
428 } else {
429 line_indent += 1;
430 }
431 } else if first - line_start < self.t_shift[line] {
432 line_indent += 1;
433 } else {
434 break;
435 }
436 }
437 }
438 first += 1;
439 }
440 if line_indent > indent {
441 for _ in 0..(line_indent - indent) {
442 out.push(' ');
443 }
444 }
445 let last_c = last.min(self.src.len());
446 if first < last_c {
447 out.extend(self.src[first..last_c].iter());
448 }
449 }
450 out
451 }
452
453 fn is_code_block(&self, line: usize) -> bool {
454 self.s_count[line] - self.blk_indent >= 4
455 }
456}
457
458type Rule = fn(&mut State, usize, usize, bool) -> bool;
459
460const TERM_PARAGRAPH: &[Rule] = &[
464 rule_fence,
465 rule_blockquote,
466 rule_hr,
467 rule_list,
468 rule_html_block,
469 rule_heading,
470];
471const TERM_LIST: &[Rule] = &[rule_fence, rule_blockquote, rule_hr];
472const RULES_ROOT: &[Rule] = &[
473 rule_code,
474 rule_fence,
475 rule_blockquote,
476 rule_hr,
477 rule_list,
478 rule_reference,
479 rule_html_block,
480 rule_heading,
481 rule_lheading,
482 rule_paragraph,
483];
484
485fn tokenize(state: &mut State, start_line: usize, end_line: usize) {
486 let mut has_empty_lines = false;
487 let mut line = start_line;
488 while line < end_line {
489 line = state.skip_empty_lines(line);
490 state.line = line;
491 if line >= end_line {
492 break;
493 }
494 if state.s_count[line] < state.blk_indent {
495 break;
496 }
497 if state.level >= MAX_NESTING {
498 state.line = end_line;
499 break;
500 }
501 for rule in RULES_ROOT {
502 if rule(state, line, end_line, false) {
503 break;
504 }
505 }
506 state.tight = !has_empty_lines;
507 line = state.line;
508 if line >= 1 && (line - 1) < end_line && state.is_empty(line - 1) {
509 has_empty_lines = true;
510 }
511 if line < end_line && state.is_empty(line) {
512 has_empty_lines = true;
513 line += 1;
514 state.line = line;
515 }
516 }
517}
518
519fn rule_code(state: &mut State, start_line: usize, end_line: usize, _silent: bool) -> bool {
524 if !state.is_code_block(start_line) {
525 return false;
526 }
527 let mut last = start_line + 1;
528 let mut next_line = start_line + 1;
529 while next_line < end_line {
530 if state.is_empty(next_line) {
531 next_line += 1;
532 continue;
533 }
534 if state.is_code_block(next_line) {
535 next_line += 1;
536 last = next_line;
537 continue;
538 }
539 break;
540 }
541 state.line = last;
542 let mut content = state.get_lines(start_line, last, 4 + state.blk_indent, false);
543 content.push('\n');
544 let i = state.push("code_block", "code", 0);
545 state.tokens[i].content = content;
546 state.tokens[i].map = Some((start_line, state.line));
547 true
548}
549
550fn rule_fence(state: &mut State, start_line: usize, end_line: usize, silent: bool) -> bool {
551 let mut have_end_marker = false;
552 let mut pos = state.b_marks[start_line] + state.t_shift[start_line];
553 let mut maximum = state.e_marks[start_line];
554 if state.is_code_block(start_line) {
555 return false;
556 }
557 if pos + 3 > maximum {
558 return false;
559 }
560 let marker = state.src[pos];
561 if marker != '~' && marker != '`' {
562 return false;
563 }
564 let mem = pos;
565 pos = state.skip_chars_str(pos, marker);
566 let length = pos - mem;
567 if length < 3 {
568 return false;
569 }
570 let params: String = state.src[pos..maximum].iter().collect();
571 if marker == '`' && params.contains('`') {
572 return false;
573 }
574 if silent {
575 return true;
576 }
577 let mut next_line = start_line;
578 loop {
579 next_line += 1;
580 if next_line >= end_line {
581 break;
582 }
583 pos = state.b_marks[next_line] + state.t_shift[next_line];
584 let mem2 = pos;
585 maximum = state.e_marks[next_line];
586 if pos < maximum && state.s_count[next_line] < state.blk_indent {
587 break;
588 }
589 match state.src.get(pos) {
590 Some(&c) if c == marker => {}
591 Some(_) => continue,
592 None => break,
593 }
594 if state.is_code_block(next_line) {
595 continue;
596 }
597 pos = state.skip_chars_str(pos, marker);
598 if pos - mem2 < length {
599 continue;
600 }
601 pos = state.skip_spaces(pos);
602 if pos < maximum {
603 continue;
604 }
605 have_end_marker = true;
606 break;
607 }
608 let indent = state.s_count[start_line];
609 state.line = next_line + if have_end_marker { 1 } else { 0 };
610 let i = state.push("fence", "code", 0);
611 state.tokens[i].content = state.get_lines(start_line + 1, next_line, indent, true);
612 state.tokens[i].map = Some((start_line, state.line));
613 true
614}
615
616fn rule_hr(state: &mut State, start_line: usize, _end_line: usize, silent: bool) -> bool {
617 let mut pos = state.b_marks[start_line] + state.t_shift[start_line];
618 let maximum = state.e_marks[start_line];
619 if state.is_code_block(start_line) {
620 return false;
621 }
622 let marker = match state.src.get(pos) {
623 Some(&c) => c,
624 None => return false,
625 };
626 pos += 1;
627 if marker != '*' && marker != '-' && marker != '_' {
628 return false;
629 }
630 let mut cnt = 1;
631 while pos < maximum {
632 let ch = state.src[pos];
633 pos += 1;
634 if ch != marker && !is_str_space(ch) {
635 return false;
636 }
637 if ch == marker {
638 cnt += 1;
639 }
640 }
641 if cnt < 3 {
642 return false;
643 }
644 if silent {
645 return true;
646 }
647 state.line = start_line + 1;
648 let i = state.push("hr", "hr", 0);
649 state.tokens[i].map = Some((start_line, state.line));
650 true
651}
652
653const H_TAGS: [&str; 6] = ["h1", "h2", "h3", "h4", "h5", "h6"];
654
655fn rule_heading(state: &mut State, start_line: usize, _end_line: usize, silent: bool) -> bool {
656 let mut pos = state.b_marks[start_line] + state.t_shift[start_line];
657 let mut maximum = state.e_marks[start_line];
658 if state.is_code_block(start_line) {
659 return false;
660 }
661 let mut ch = state.src.get(pos).copied();
662 if ch != Some('#') || pos >= maximum {
663 return false;
664 }
665 let mut level = 1usize;
666 pos += 1;
667 ch = state.src.get(pos).copied();
668 while ch == Some('#') && pos < maximum && level <= 6 {
669 level += 1;
670 pos += 1;
671 ch = state.src.get(pos).copied();
672 }
673 if level > 6 || (pos < maximum && !ch.is_some_and(is_str_space)) {
674 return false;
675 }
676 if silent {
677 return true;
678 }
679 maximum = state.skip_spaces_back(maximum, pos);
680 let tmp = state.skip_chars_str_back(maximum, '#', pos);
681 if tmp > pos && is_str_space(state.src[tmp - 1]) {
682 maximum = tmp;
683 }
684 state.line = start_line + 1;
685 let tag = H_TAGS[level - 1];
686 let i = state.push("heading_open", tag, 1);
687 state.tokens[i].map = Some((start_line, state.line));
688 let content: String = if maximum > pos {
689 state.src[pos..maximum].iter().collect()
690 } else {
691 String::new()
692 };
693 let i2 = state.push("inline", "", 0);
694 state.tokens[i2].content = py_strip(&content).to_string();
695 state.tokens[i2].map = Some((start_line, state.line));
696 state.push("heading_close", tag, -1);
697 true
698}
699
700fn rule_lheading(state: &mut State, start_line: usize, end_line: usize, _silent: bool) -> bool {
701 let mut level: Option<usize> = None;
702 let mut next_line = start_line + 1;
703 if state.is_code_block(start_line) {
704 return false;
705 }
706 let old_parent = state.parent_type;
707 state.parent_type = Parent::Paragraph;
708 while next_line < end_line && !state.is_empty(next_line) {
709 if state.s_count[next_line] - state.blk_indent > 3 {
710 next_line += 1;
711 continue;
712 }
713 if state.s_count[next_line] >= state.blk_indent {
714 let mut pos = state.b_marks[next_line] + state.t_shift[next_line];
715 let maximum = state.e_marks[next_line];
716 if pos < maximum {
717 let m = state.src[pos];
718 if m == '-' || m == '=' {
719 pos = state.skip_chars_str(pos, m);
720 pos = state.skip_spaces(pos);
721 if pos >= maximum {
722 level = Some(if m == '=' { 1 } else { 2 });
723 break;
724 }
725 }
726 }
727 }
728 if state.s_count[next_line] < 0 {
729 next_line += 1;
730 continue;
731 }
732 let mut terminate = false;
733 for rule in TERM_PARAGRAPH {
734 if rule(state, next_line, end_line, true) {
735 terminate = true;
736 break;
737 }
738 }
739 if terminate {
740 break;
741 }
742 next_line += 1;
743 }
744 let lv = match level {
745 Some(lv) => lv,
746 None => return false,
749 };
750 let content =
751 py_strip(&state.get_lines(start_line, next_line, state.blk_indent, false)).to_string();
752 state.line = next_line + 1;
753 let tag = H_TAGS[lv - 1];
754 let i = state.push("heading_open", tag, 1);
755 state.tokens[i].map = Some((start_line, state.line));
756 let i2 = state.push("inline", "", 0);
757 state.tokens[i2].content = content;
758 state.tokens[i2].map = Some((start_line, state.line - 1));
759 state.push("heading_close", tag, -1);
760 state.parent_type = old_parent;
761 true
762}
763
764fn rule_paragraph(state: &mut State, start_line: usize, _end_line: usize, _silent: bool) -> bool {
765 let mut next_line = start_line + 1;
766 let end_line = state.line_max; let old_parent = state.parent_type;
768 state.parent_type = Parent::Paragraph;
769 while next_line < end_line {
770 if state.is_empty(next_line) {
771 break;
772 }
773 if state.s_count[next_line] - state.blk_indent > 3 {
774 next_line += 1;
775 continue;
776 }
777 if state.s_count[next_line] < 0 {
778 next_line += 1;
779 continue;
780 }
781 let mut terminate = false;
782 for rule in TERM_PARAGRAPH {
783 if rule(state, next_line, end_line, true) {
784 terminate = true;
785 break;
786 }
787 }
788 if terminate {
789 break;
790 }
791 next_line += 1;
792 }
793 let content =
794 py_strip(&state.get_lines(start_line, next_line, state.blk_indent, false)).to_string();
795 state.line = next_line;
796 let i = state.push("paragraph_open", "p", 1);
797 state.tokens[i].map = Some((start_line, state.line));
798 let i2 = state.push("inline", "", 0);
799 state.tokens[i2].content = content;
800 state.tokens[i2].map = Some((start_line, state.line));
801 state.push("paragraph_close", "p", -1);
802 state.parent_type = old_parent;
803 true
804}
805
806fn rule_blockquote(state: &mut State, start_line: usize, end_line: usize, silent: bool) -> bool {
807 let old_line_max = state.line_max;
808 let mut pos = state.b_marks[start_line] + state.t_shift[start_line];
809 let mut max = state.e_marks[start_line];
810 if state.is_code_block(start_line) {
811 return false;
812 }
813 if state.src.get(pos) != Some(&'>') {
814 return false;
815 }
816 pos += 1;
817 if silent {
818 return true;
819 }
820 let mut initial: i32 = state.s_count[start_line] + 1;
821 let mut offset: i32 = initial;
822 let second = state.src.get(pos).copied();
823 let mut adjust_tab = false;
824 let space_after_marker;
825 if second == Some(' ') {
826 pos += 1;
827 initial += 1;
828 offset += 1;
829 adjust_tab = false;
830 space_after_marker = true;
831 } else if second == Some('\t') {
832 space_after_marker = true;
833 if (state.bs_count[start_line] + offset) % 4 == 3 {
834 pos += 1;
835 initial += 1;
836 offset += 1;
837 adjust_tab = false;
838 } else {
839 adjust_tab = true;
840 }
841 } else {
842 space_after_marker = false;
843 }
844 let mut old_b_marks = vec![state.b_marks[start_line]];
845 state.b_marks[start_line] = pos;
846 while pos < max {
847 let ch = state.src[pos];
848 if is_str_space(ch) {
849 if ch == '\t' {
850 offset +=
851 4 - (offset + state.bs_count[start_line] + if adjust_tab { 1 } else { 0 }) % 4;
852 } else {
853 offset += 1;
854 }
855 } else {
856 break;
857 }
858 pos += 1;
859 }
860 let mut old_bs_count = vec![state.bs_count[start_line]];
861 state.bs_count[start_line] =
862 state.s_count[start_line] + 1 + if space_after_marker { 1 } else { 0 };
863 let mut last_line_empty = pos >= max;
864 let mut old_s_count = vec![state.s_count[start_line]];
865 state.s_count[start_line] = offset - initial;
866 let mut old_t_shift = vec![state.t_shift[start_line]];
867 state.t_shift[start_line] = pos - state.b_marks[start_line];
868 let old_parent = state.parent_type;
869 state.parent_type = Parent::Blockquote;
870
871 let mut next_line = start_line + 1;
872 while next_line < end_line {
873 let is_outdented = state.s_count[next_line] < state.blk_indent;
874 pos = state.b_marks[next_line] + state.t_shift[next_line];
875 max = state.e_marks[next_line];
876 if pos >= max {
877 break;
878 }
879 let evaluates_true = state.src[pos] == '>' && !is_outdented;
880 pos += 1;
881 if evaluates_true {
882 let mut initial2: i32 = state.s_count[next_line] + 1;
883 let mut offset2: i32 = initial2;
884 let next_char = state.src.get(pos).copied();
885 let mut adjust_tab2 = false;
886 let space_after_marker2;
887 if next_char == Some(' ') {
888 pos += 1;
889 initial2 += 1;
890 offset2 += 1;
891 adjust_tab2 = false;
892 space_after_marker2 = true;
893 } else if next_char == Some('\t') {
894 space_after_marker2 = true;
895 if (state.bs_count[next_line] + offset2) % 4 == 3 {
896 pos += 1;
897 initial2 += 1;
898 offset2 += 1;
899 adjust_tab2 = false;
900 } else {
901 adjust_tab2 = true;
902 }
903 } else {
904 space_after_marker2 = false;
905 }
906 old_b_marks.push(state.b_marks[next_line]);
907 state.b_marks[next_line] = pos;
908 while pos < max {
909 let ch = state.src[pos];
910 if is_str_space(ch) {
911 if ch == '\t' {
912 offset2 += 4
913 - (offset2
914 + state.bs_count[next_line]
915 + if adjust_tab2 { 1 } else { 0 })
916 % 4;
917 } else {
918 offset2 += 1;
919 }
920 } else {
921 break;
922 }
923 pos += 1;
924 }
925 last_line_empty = pos >= max;
926 old_bs_count.push(state.bs_count[next_line]);
927 state.bs_count[next_line] =
928 state.s_count[next_line] + 1 + if space_after_marker2 { 1 } else { 0 };
929 old_s_count.push(state.s_count[next_line]);
930 state.s_count[next_line] = offset2 - initial2;
931 old_t_shift.push(state.t_shift[next_line]);
932 state.t_shift[next_line] = pos - state.b_marks[next_line];
933 next_line += 1;
934 continue;
935 }
936 if last_line_empty {
937 break;
938 }
939 let mut terminate = false;
940 for rule in TERM_PARAGRAPH {
941 if rule(state, next_line, end_line, true) {
942 terminate = true;
943 break;
944 }
945 }
946 if terminate {
947 state.line_max = next_line;
949 if state.blk_indent != 0 {
950 old_b_marks.push(state.b_marks[next_line]);
951 old_bs_count.push(state.bs_count[next_line]);
952 old_t_shift.push(state.t_shift[next_line]);
953 old_s_count.push(state.s_count[next_line]);
954 state.s_count[next_line] -= state.blk_indent;
955 }
956 break;
957 }
958 old_b_marks.push(state.b_marks[next_line]);
959 old_bs_count.push(state.bs_count[next_line]);
960 old_t_shift.push(state.t_shift[next_line]);
961 old_s_count.push(state.s_count[next_line]);
962 state.s_count[next_line] = -1;
964 next_line += 1;
965 }
966 let old_indent = state.blk_indent;
967 state.blk_indent = 0;
968
969 let open_idx = state.push("blockquote_open", "blockquote", 1);
970 state.tokens[open_idx].map = Some((start_line, 0));
971 tokenize(state, start_line, next_line);
972 state.push("blockquote_close", "blockquote", -1);
973
974 state.line_max = old_line_max;
975 state.parent_type = old_parent;
976 let end = state.line;
977 state.tokens[open_idx].map = Some((start_line, end));
978
979 for (i, &ts) in old_t_shift.iter().enumerate() {
980 state.b_marks[i + start_line] = old_b_marks[i];
981 state.t_shift[i + start_line] = ts;
982 state.s_count[i + start_line] = old_s_count[i];
983 state.bs_count[i + start_line] = old_bs_count[i];
984 }
985 state.blk_indent = old_indent;
986 true
987}
988
989fn skip_bullet_list_marker(state: &State, start_line: usize) -> Option<usize> {
990 let mut pos = state.b_marks[start_line] + state.t_shift[start_line];
991 let maximum = state.e_marks[start_line];
992 let marker = *state.src.get(pos)?;
993 pos += 1;
994 if marker != '*' && marker != '-' && marker != '+' {
995 return None;
996 }
997 if pos < maximum {
998 let ch = state.src[pos];
999 if !is_str_space(ch) {
1000 return None;
1001 }
1002 }
1003 Some(pos)
1004}
1005
1006fn skip_ordered_list_marker(state: &State, start_line: usize) -> Option<usize> {
1007 let start = state.b_marks[start_line] + state.t_shift[start_line];
1008 let mut pos = start;
1009 let maximum = state.e_marks[start_line];
1010 if pos + 1 >= maximum {
1011 return None;
1012 }
1013 let ch = state.src[pos];
1014 pos += 1;
1015 if !ch.is_ascii_digit() {
1016 return None;
1017 }
1018 loop {
1019 if pos >= maximum {
1020 return None;
1021 }
1022 let ch = state.src[pos];
1023 pos += 1;
1024 if ch.is_ascii_digit() {
1025 if pos - start >= 10 {
1026 return None;
1027 }
1028 continue;
1029 }
1030 if ch == ')' || ch == '.' {
1031 break;
1032 }
1033 return None;
1034 }
1035 if pos < maximum {
1036 let ch = state.src[pos];
1037 if !is_str_space(ch) {
1038 return None;
1039 }
1040 }
1041 Some(pos)
1042}
1043
1044fn rule_list(state: &mut State, start_line: usize, end_line: usize, silent: bool) -> bool {
1045 let mut is_terminating_paragraph = false;
1046 if state.is_code_block(start_line) {
1047 return false;
1048 }
1049 if state.list_indent >= 0
1050 && state.s_count[start_line] - state.list_indent >= 4
1051 && state.s_count[start_line] < state.blk_indent
1052 {
1053 return false;
1054 }
1055 if silent
1056 && state.parent_type == Parent::Paragraph
1057 && state.s_count[start_line] >= state.blk_indent
1058 {
1059 is_terminating_paragraph = true;
1060 }
1061 let is_ordered;
1062 let mut pos_after_marker;
1063 if let Some(p) = skip_ordered_list_marker(state, start_line) {
1064 is_ordered = true;
1065 pos_after_marker = p;
1066 let start = state.b_marks[start_line] + state.t_shift[start_line];
1067 let digits: String = state.src[start..pos_after_marker - 1].iter().collect();
1068 let marker_value: i64 = digits.parse().unwrap_or(0);
1069 if is_terminating_paragraph && marker_value != 1 {
1070 return false;
1071 }
1072 } else if let Some(p) = skip_bullet_list_marker(state, start_line) {
1073 is_ordered = false;
1074 pos_after_marker = p;
1075 } else {
1076 return false;
1077 }
1078 if is_terminating_paragraph && state.skip_spaces(pos_after_marker) >= state.e_marks[start_line]
1079 {
1080 return false;
1081 }
1082 let marker_char = state.src[pos_after_marker - 1];
1083 if silent {
1084 return true;
1085 }
1086
1087 let open_idx = state.push(
1088 if is_ordered {
1089 "ordered_list_open"
1090 } else {
1091 "bullet_list_open"
1092 },
1093 if is_ordered { "ol" } else { "ul" },
1094 1,
1095 );
1096 let list_start_line = start_line;
1097 state.tokens[open_idx].map = Some((list_start_line, 0));
1098
1099 let mut start_line = start_line;
1100 let mut next_line = start_line;
1101 let old_parent = state.parent_type;
1102 state.parent_type = Parent::List;
1103
1104 while next_line < end_line {
1105 let mut pos = pos_after_marker;
1106 let maximum = state.e_marks[next_line];
1107 let initial: i32 = state.s_count[next_line] + pos_after_marker as i32
1108 - (state.b_marks[start_line] as i32 + state.t_shift[start_line] as i32);
1109 let mut offset = initial;
1110 while pos < maximum {
1111 let ch = state.src[pos];
1112 if ch == '\t' {
1113 offset += 4 - (offset + state.bs_count[next_line]) % 4;
1114 } else if ch == ' ' {
1115 offset += 1;
1116 } else {
1117 break;
1118 }
1119 pos += 1;
1120 }
1121 let content_start = pos;
1122 let mut indent_after_marker: i32 = if content_start >= maximum {
1123 1
1124 } else {
1125 offset - initial
1126 };
1127 if indent_after_marker > 4 {
1128 indent_after_marker = 1;
1129 }
1130 let indent = initial + indent_after_marker;
1131
1132 let item_idx = state.push("list_item_open", "li", 1);
1133 state.tokens[item_idx].map = Some((start_line, 0));
1134
1135 let old_tight = state.tight;
1136 let old_t_shift = state.t_shift[start_line];
1137 let old_s_count = state.s_count[start_line];
1138 let old_list_indent = state.list_indent;
1139 state.list_indent = state.blk_indent;
1140 state.blk_indent = indent;
1141 state.tight = true;
1142 state.t_shift[start_line] = content_start - state.b_marks[start_line];
1143 state.s_count[start_line] = offset;
1144
1145 if content_start >= maximum && state.is_empty(start_line + 1) {
1146 state.line = (state.line + 2).min(end_line);
1148 } else {
1149 tokenize(state, start_line, end_line);
1150 }
1151
1152 state.blk_indent = state.list_indent;
1156 state.list_indent = old_list_indent;
1157 state.t_shift[start_line] = old_t_shift;
1158 state.s_count[start_line] = old_s_count;
1159 state.tight = old_tight;
1160
1161 state.push("list_item_close", "li", -1);
1162
1163 next_line = state.line;
1164 start_line = state.line;
1165 if let Some(m) = state.tokens[item_idx].map {
1166 state.tokens[item_idx].map = Some((m.0, next_line));
1167 }
1168 if next_line >= end_line {
1169 break;
1170 }
1171 if state.s_count[next_line] < state.blk_indent {
1172 break;
1173 }
1174 if state.is_code_block(start_line) {
1175 break;
1176 }
1177 let mut terminate = false;
1178 for rule in TERM_LIST {
1179 if rule(state, next_line, end_line, true) {
1180 terminate = true;
1181 break;
1182 }
1183 }
1184 if terminate {
1185 break;
1186 }
1187 if is_ordered {
1188 match skip_ordered_list_marker(state, next_line) {
1189 Some(p) => pos_after_marker = p,
1190 None => break,
1191 }
1192 } else {
1193 match skip_bullet_list_marker(state, next_line) {
1194 Some(p) => pos_after_marker = p,
1195 None => break,
1196 }
1197 }
1198 if marker_char != state.src[pos_after_marker - 1] {
1199 break;
1200 }
1201 }
1202
1203 state.push(
1204 if is_ordered {
1205 "ordered_list_close"
1206 } else {
1207 "bullet_list_close"
1208 },
1209 if is_ordered { "ol" } else { "ul" },
1210 -1,
1211 );
1212 state.tokens[open_idx].map = Some((list_start_line, next_line));
1213 state.line = next_line;
1214 state.parent_type = old_parent;
1215 true
1216}
1217
1218struct DestResult {
1221 ok: bool,
1222 pos: usize,
1223 str_: String,
1224}
1225
1226fn parse_link_destination(s: &[char], mut pos: usize, maximum: usize) -> DestResult {
1227 let start = pos;
1228 let mut result = DestResult {
1229 ok: false,
1230 pos: 0,
1231 str_: String::new(),
1232 };
1233 if s.get(pos) == Some(&'<') {
1234 pos += 1;
1235 while pos < maximum {
1236 let code = s[pos];
1237 if code == '\n' || code == '<' {
1238 return result;
1239 }
1240 if code == '>' {
1241 result.pos = pos + 1;
1242 let inner: String = s[start + 1..pos].iter().collect();
1243 result.str_ = unescape_all(&inner);
1244 result.ok = true;
1245 return result;
1246 }
1247 if code == '\\' && pos + 1 < maximum {
1248 pos += 2;
1249 continue;
1250 }
1251 pos += 1;
1252 }
1253 return result;
1254 }
1255 let mut level: i32 = 0;
1256 while pos < maximum {
1257 let code = s[pos];
1258 if code == ' ' {
1259 break;
1260 }
1261 if (code as u32) < 0x20 || code == '\u{7f}' {
1262 break;
1263 }
1264 if code == '\\' && pos + 1 < maximum {
1265 if s[pos + 1] == ' ' {
1266 break;
1267 }
1268 pos += 2;
1269 continue;
1270 }
1271 if code == '(' {
1272 level += 1;
1273 if level > 32 {
1274 return result;
1275 }
1276 }
1277 if code == ')' {
1278 if level == 0 {
1279 break;
1280 }
1281 level -= 1;
1282 }
1283 pos += 1;
1284 }
1285 if start == pos {
1286 return result;
1287 }
1288 if level != 0 {
1289 return result;
1290 }
1291 let inner: String = s[start..pos].iter().collect();
1292 result.str_ = unescape_all(&inner);
1293 result.pos = pos;
1294 result.ok = true;
1295 result
1296}
1297
1298struct TitleResult {
1299 ok: bool,
1300 can_continue: bool,
1301 pos: usize,
1302 str_: String,
1303 marker: char,
1304}
1305
1306fn parse_link_title(
1307 s: &[char],
1308 start: usize,
1309 maximum: usize,
1310 prev_state: Option<TitleResult>,
1311) -> TitleResult {
1312 let mut pos = start;
1313 let mut start = start;
1314 let mut state = TitleResult {
1315 ok: false,
1316 can_continue: false,
1317 pos: 0,
1318 str_: String::new(),
1319 marker: '\0',
1320 };
1321 if let Some(prev) = prev_state {
1322 state.str_ = prev.str_;
1323 state.marker = prev.marker;
1324 } else {
1325 if pos >= maximum {
1326 return state;
1327 }
1328 let marker = s[pos];
1329 if marker != '"' && marker != '\'' && marker != '(' {
1330 return state;
1331 }
1332 start += 1;
1333 pos += 1;
1334 state.marker = if marker == '(' { ')' } else { marker };
1335 }
1336 while pos < maximum {
1337 let code = s[pos];
1338 if code == state.marker {
1339 state.pos = pos + 1;
1340 let inner: String = s[start..pos].iter().collect();
1341 state.str_.push_str(&unescape_all(&inner));
1342 state.ok = true;
1343 return state;
1344 } else if code == '(' && state.marker == ')' {
1345 return state;
1346 } else if code == '\\' && pos + 1 < maximum {
1347 pos += 1;
1348 }
1349 pos += 1;
1350 }
1351 state.can_continue = true;
1352 let inner: String = s[start..pos.min(s.len())].iter().collect();
1353 state.str_.push_str(&unescape_all(&inner));
1354 state
1355}
1356
1357const ENTITIES_JSON: &str = include_str!("../assets/spec/markdown-entities.json");
1358
1359fn entities() -> &'static HashMap<String, String> {
1360 static MAP: OnceLock<HashMap<String, String>> = OnceLock::new();
1361 MAP.get_or_init(|| serde_json::from_str(ENTITIES_JSON).expect("entities json parses"))
1362}
1363
1364fn is_valid_entity_code(c: u32) -> bool {
1365 if (0xD800..=0xDFFF).contains(&c) {
1366 return false;
1367 }
1368 if (0xFDD0..=0xFDEF).contains(&c) {
1369 return false;
1370 }
1371 if (c & 0xFFFF) == 0xFFFF || (c & 0xFFFF) == 0xFFFE {
1372 return false;
1373 }
1374 if c <= 0x08 {
1375 return false;
1376 }
1377 if c == 0x0B {
1378 return false;
1379 }
1380 if (0x0E..=0x1F).contains(&c) {
1381 return false;
1382 }
1383 if (0x7F..=0x9F).contains(&c) {
1384 return false;
1385 }
1386 c <= 0x10FFFF
1387}
1388
1389const MD_ESCAPABLE: &str = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
1390
1391fn unescape_all(s: &str) -> String {
1396 if !s.contains('\\') && !s.contains('&') {
1397 return s.to_string();
1398 }
1399 let chars: Vec<char> = s.chars().collect();
1400 let mut out = String::with_capacity(s.len());
1401 let mut i = 0usize;
1402 while i < chars.len() {
1403 let c = chars[i];
1404 if c == '\\' && i + 1 < chars.len() {
1405 let n = chars[i + 1];
1406 if n.is_ascii() && MD_ESCAPABLE.contains(n) {
1407 out.push(n);
1408 i += 2;
1409 continue;
1410 }
1411 out.push(c);
1412 i += 1;
1413 continue;
1414 }
1415 if c == '&' {
1416 if let Some(&head) = chars.get(i + 1) {
1419 if head.is_ascii_alphabetic() || head == '#' {
1420 let mut j = i + 2;
1421 while j < chars.len() && chars[j].is_ascii_alphanumeric() {
1422 j += 1;
1423 }
1424 let run = j - (i + 2);
1425 if (1..=31).contains(&run) && chars.get(j) == Some(&';') {
1426 let name: String = chars[i + 1..j].iter().collect();
1427 if let Some(rep) = resolve_entity(&name) {
1428 out.push_str(&rep);
1429 i = j + 1;
1430 continue;
1431 }
1432 out.extend(chars[i..=j].iter());
1434 i = j + 1;
1435 continue;
1436 }
1437 }
1438 }
1439 out.push(c);
1440 i += 1;
1441 continue;
1442 }
1443 out.push(c);
1444 i += 1;
1445 }
1446 out
1447}
1448
1449fn resolve_entity(name: &str) -> Option<String> {
1450 if let Some(v) = entities().get(name) {
1451 return Some(v.clone());
1452 }
1453 let rest = name.strip_prefix('#')?;
1454 let code: Option<u32> = if let Some(hex) = rest.strip_prefix(['x', 'X']) {
1455 if (1..=8).contains(&hex.len()) && hex.chars().all(|c| c.is_ascii_hexdigit()) {
1456 u32::from_str_radix(hex, 16).ok()
1457 } else {
1458 None
1459 }
1460 } else if (1..=8).contains(&rest.len()) && rest.chars().all(|c| c.is_ascii_digit()) {
1461 rest.parse::<u32>().ok()
1462 } else {
1463 None
1464 };
1465 let code = code?;
1466 if is_valid_entity_code(code) {
1467 char::from_u32(code).map(|c| c.to_string())
1468 } else {
1469 None
1470 }
1471}
1472
1473fn validate_link(url: &str) -> bool {
1475 let stripped = py_strip(url);
1476 let lower: String = stripped.chars().flat_map(|c| c.to_lowercase()).collect();
1477 if lower.starts_with("javascript:")
1478 || lower.starts_with("vbscript:")
1479 || lower.starts_with("file:")
1480 {
1481 return false;
1482 }
1483 if lower.starts_with("data:") {
1484 return [
1485 "data:image/gif;",
1486 "data:image/png;",
1487 "data:image/jpeg;",
1488 "data:image/webp;",
1489 ]
1490 .iter()
1491 .any(|p| lower.starts_with(p));
1492 }
1493 true
1494}
1495
1496fn get_next_line(state: &mut State, next_line: usize) -> Option<Vec<char>> {
1497 let end_line = state.line_max;
1498 if next_line >= end_line || state.is_empty(next_line) {
1499 return None;
1500 }
1501 let mut is_continuation = false;
1502 if state.is_code_block(next_line) {
1503 is_continuation = true;
1504 }
1505 if state.s_count[next_line] < 0 {
1506 is_continuation = true;
1507 }
1508 if !is_continuation {
1509 let old_parent = state.parent_type;
1510 state.parent_type = Parent::Reference;
1511 let mut terminate = false;
1512 for rule in TERM_PARAGRAPH {
1513 if rule(state, next_line, end_line, true) {
1514 terminate = true;
1515 break;
1516 }
1517 }
1518 state.parent_type = old_parent;
1519 if terminate {
1520 return None;
1521 }
1522 }
1523 let pos = state.b_marks[next_line] + state.t_shift[next_line];
1524 let maximum = state.e_marks[next_line];
1525 Some(state.src[pos..(maximum + 1).min(state.src.len())].to_vec())
1526}
1527
1528fn rule_reference(state: &mut State, start_line: usize, _end_line: usize, silent: bool) -> bool {
1529 let pos0 = state.b_marks[start_line] + state.t_shift[start_line];
1530 let maximum0 = state.e_marks[start_line];
1531 let mut next_line = start_line + 1;
1532 if state.is_code_block(start_line) {
1533 return false;
1534 }
1535 if state.src.get(pos0) != Some(&'[') {
1536 return false;
1537 }
1538 let mut string: Vec<char> = state.src[pos0..(maximum0 + 1).min(state.src.len())].to_vec();
1539 let mut maximum = string.len();
1540
1541 let mut label_end: Option<usize> = None;
1542 let mut pos = 1usize;
1543 while pos < maximum {
1544 let ch = string[pos];
1545 if ch == '[' {
1546 return false;
1547 } else if ch == ']' {
1548 label_end = Some(pos);
1549 break;
1550 } else if ch == '\n' {
1551 if let Some(cont) = get_next_line(state, next_line) {
1552 string.extend(cont);
1553 maximum = string.len();
1554 next_line += 1;
1555 }
1556 } else if ch == '\\' {
1557 pos += 1;
1558 if pos < maximum && string[pos] == '\n' {
1559 if let Some(cont) = get_next_line(state, next_line) {
1560 string.extend(cont);
1561 maximum = string.len();
1562 next_line += 1;
1563 }
1564 }
1565 }
1566 pos += 1;
1567 }
1568 let label_end = match label_end {
1569 Some(le) => le,
1570 None => return false,
1571 };
1572 if string.get(label_end + 1) != Some(&':') {
1573 return false;
1574 }
1575
1576 pos = label_end + 2;
1578 while pos < maximum {
1579 let ch = string[pos];
1580 if ch == '\n' {
1581 if let Some(cont) = get_next_line(state, next_line) {
1582 string.extend(cont);
1583 maximum = string.len();
1584 next_line += 1;
1585 }
1586 } else if is_str_space(ch) {
1587 } else {
1588 break;
1589 }
1590 pos += 1;
1591 }
1592
1593 let dest_res = parse_link_destination(&string, pos, maximum);
1594 if !dest_res.ok {
1595 return false;
1596 }
1597 if !validate_link(&dest_res.str_) {
1600 return false;
1601 }
1602 pos = dest_res.pos;
1603
1604 let dest_end_pos = pos;
1605 let dest_end_line_no = next_line;
1606
1607 let start_pos = pos;
1608 while pos < maximum {
1609 let ch = string[pos];
1610 if ch == '\n' {
1611 if let Some(cont) = get_next_line(state, next_line) {
1612 string.extend(cont);
1613 maximum = string.len();
1614 next_line += 1;
1615 }
1616 } else if is_str_space(ch) {
1617 } else {
1618 break;
1619 }
1620 pos += 1;
1621 }
1622
1623 let mut title_res = parse_link_title(&string, pos, maximum, None);
1624 while title_res.can_continue {
1625 match get_next_line(state, next_line) {
1626 None => break,
1627 Some(cont) => {
1628 string.extend(cont);
1629 pos = maximum;
1630 maximum = string.len();
1631 next_line += 1;
1632 title_res = parse_link_title(&string, pos, maximum, Some(title_res));
1633 }
1634 }
1635 }
1636
1637 let mut title;
1638 if pos < maximum && start_pos != pos && title_res.ok {
1639 title = title_res.str_;
1640 pos = title_res.pos;
1641 } else {
1642 title = String::new();
1643 pos = dest_end_pos;
1644 next_line = dest_end_line_no;
1645 }
1646
1647 while pos < maximum {
1648 if !is_str_space(string[pos]) {
1649 break;
1650 }
1651 pos += 1;
1652 }
1653
1654 if pos < maximum && string[pos] != '\n' && !title.is_empty() {
1655 title = String::new();
1656 pos = dest_end_pos;
1657 next_line = dest_end_line_no;
1658 while pos < maximum {
1659 if !is_str_space(string[pos]) {
1660 break;
1661 }
1662 pos += 1;
1663 }
1664 }
1665 let _ = title;
1666
1667 if pos < maximum && string[pos] != '\n' {
1668 return false;
1669 }
1670
1671 let label: String = string[1..label_end].iter().collect();
1672 if py_strip(&label).is_empty() {
1674 return false;
1675 }
1676
1677 if silent {
1678 return true;
1679 }
1680
1681 state.line = next_line;
1682 true
1683}
1684
1685const HTML_BLOCK_NAMES: &[&str] = &[
1688 "address", "article", "aside", "base", "basefont", "blockquote", "body", "caption", "center",
1689 "col", "colgroup", "dd", "details", "dialog", "dir", "div", "dl", "dt", "fieldset",
1690 "figcaption", "figure", "footer", "form", "frame", "frameset", "h1", "h2", "h3", "h4", "h5",
1691 "h6", "head", "header", "hr", "html", "iframe", "legend", "li", "link", "main", "menu",
1692 "menuitem", "nav", "noframes", "ol", "optgroup", "option", "p", "param", "search", "section",
1693 "summary", "table", "tbody", "td", "tfoot", "th", "thead", "title", "tr", "track", "ul",
1694];
1695
1696#[inline]
1700fn ci_eq(c: char, lower: char) -> bool {
1701 c.eq_ignore_ascii_case(&lower)
1702 || (lower == 's' && c == '\u{17f}')
1703 || (lower == 'k' && c == '\u{212a}')
1704}
1705
1706fn starts_with_ci(hay: &[char], pos: usize, needle: &str) -> bool {
1707 needle
1708 .chars()
1709 .enumerate()
1710 .all(|(i, l)| hay.get(pos + i).is_some_and(|&c| ci_eq(c, l)))
1711}
1712
1713fn contains_ci(hay: &[char], needle: &str) -> bool {
1714 if needle.is_empty() {
1715 return true;
1716 }
1717 (0..hay.len()).any(|i| starts_with_ci(hay, i, needle))
1718}
1719
1720fn contains_exact(hay: &[char], needle: &str) -> bool {
1721 let n: Vec<char> = needle.chars().collect();
1722 if n.is_empty() {
1723 return true;
1724 }
1725 if hay.len() < n.len() {
1726 return false;
1727 }
1728 (0..=hay.len() - n.len()).any(|i| hay[i..i + n.len()] == n[..])
1729}
1730
1731#[inline]
1732fn unquoted_value_char(c: char) -> bool {
1733 (c as u32) > 0x20 && !matches!(c, '"' | '\'' | '=' | '<' | '>' | '`')
1734}
1735
1736fn match_open_close_tag_line(line: &[char]) -> bool {
1738 let n = line.len();
1739 if line.first() != Some(&'<') {
1740 return false;
1741 }
1742 let mut p = 1usize;
1743 let closing = line.get(1) == Some(&'/');
1744 if closing {
1745 p = 2;
1746 }
1747 if !line.get(p).is_some_and(|c| c.is_ascii_alphabetic()) {
1748 return false;
1749 }
1750 p += 1;
1751 while line
1752 .get(p)
1753 .is_some_and(|c| c.is_ascii_alphanumeric() || *c == '-')
1754 {
1755 p += 1;
1756 }
1757 if closing {
1758 while line.get(p).is_some_and(|&c| py_is_space(c)) {
1759 p += 1;
1760 }
1761 if line.get(p) != Some(&'>') {
1762 return false;
1763 }
1764 p += 1;
1765 } else {
1766 loop {
1768 let save = p;
1769 let mut q = p;
1770 while line.get(q).is_some_and(|&c| py_is_space(c)) {
1771 q += 1;
1772 }
1773 if q == p {
1774 break;
1775 }
1776 if !line
1777 .get(q)
1778 .is_some_and(|c| c.is_ascii_alphabetic() || *c == '_' || *c == ':')
1779 {
1780 p = save;
1781 break;
1782 }
1783 q += 1;
1784 while line
1785 .get(q)
1786 .is_some_and(|c| c.is_ascii_alphanumeric() || matches!(*c, ':' | '.' | '_' | '-'))
1787 {
1788 q += 1;
1789 }
1790 let save2 = q;
1792 let mut r = q;
1793 while line.get(r).is_some_and(|&c| py_is_space(c)) {
1794 r += 1;
1795 }
1796 if line.get(r) == Some(&'=') {
1797 r += 1;
1798 while line.get(r).is_some_and(|&c| py_is_space(c)) {
1799 r += 1;
1800 }
1801 match line.get(r) {
1802 Some(&'\'') => {
1803 r += 1;
1804 while line.get(r).is_some_and(|&c| c != '\'') {
1805 r += 1;
1806 }
1807 if line.get(r) == Some(&'\'') {
1808 q = r + 1;
1809 } else {
1810 q = save2;
1811 }
1812 }
1813 Some(&'"') => {
1814 r += 1;
1815 while line.get(r).is_some_and(|&c| c != '"') {
1816 r += 1;
1817 }
1818 if line.get(r) == Some(&'"') {
1819 q = r + 1;
1820 } else {
1821 q = save2;
1822 }
1823 }
1824 Some(&c) if unquoted_value_char(c) => {
1825 r += 1;
1826 while line.get(r).is_some_and(|&cc| unquoted_value_char(cc)) {
1827 r += 1;
1828 }
1829 q = r;
1830 }
1831 _ => {
1832 q = save2;
1833 }
1834 }
1835 } else {
1836 q = save2;
1837 }
1838 p = q;
1839 }
1840 while line.get(p).is_some_and(|&c| py_is_space(c)) {
1841 p += 1;
1842 }
1843 if line.get(p) == Some(&'/') {
1844 p += 1;
1845 }
1846 if line.get(p) != Some(&'>') {
1847 return false;
1848 }
1849 p += 1;
1850 }
1851 while p < n {
1852 if !py_is_space(line[p]) {
1853 return false;
1854 }
1855 p += 1;
1856 }
1857 true
1858}
1859
1860#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1861enum HtmlSeq {
1862 Raw, Comment, Pi, Decl, Cdata, Block, AnyTag, }
1870
1871fn starts_with_exact(hay: &[char], pos: usize, needle: &str) -> bool {
1872 needle
1873 .chars()
1874 .enumerate()
1875 .all(|(i, l)| hay.get(pos + i) == Some(&l))
1876}
1877
1878fn html_seq_open(line: &[char]) -> Option<(HtmlSeq, bool)> {
1879 if line.first() != Some(&'<') {
1880 return None;
1881 }
1882 for name in ["script", "pre", "style", "textarea"] {
1884 if starts_with_ci(line, 1, name) {
1885 let after = 1 + name.len();
1886 match line.get(after) {
1887 None => return Some((HtmlSeq::Raw, true)),
1888 Some(&c) if py_is_space(c) || c == '>' => return Some((HtmlSeq::Raw, true)),
1889 _ => {}
1890 }
1891 }
1892 }
1893 if line.len() >= 4 && line[1] == '!' && line[2] == '-' && line[3] == '-' {
1895 return Some((HtmlSeq::Comment, true));
1896 }
1897 if line.get(1) == Some(&'?') {
1899 return Some((HtmlSeq::Pi, true));
1900 }
1901 if line.get(1) == Some(&'!') && line.get(2).is_some_and(|c| c.is_ascii_uppercase()) {
1905 return Some((HtmlSeq::Decl, true));
1906 }
1907 if starts_with_exact(line, 0, "<![CDATA[") {
1909 return Some((HtmlSeq::Cdata, true));
1910 }
1911 {
1913 let mut p = 1usize;
1914 if line.get(1) == Some(&'/') {
1915 p = 2;
1916 }
1917 for name in HTML_BLOCK_NAMES {
1918 if starts_with_ci(line, p, name) {
1919 let after = p + name.len();
1920 let ok = match line.get(after) {
1921 None => true,
1922 Some(&c) if py_is_space(c) || c == '>' => true,
1923 Some(&'/') => line.get(after + 1) == Some(&'>'),
1924 _ => false,
1925 };
1926 if ok {
1927 return Some((HtmlSeq::Block, true));
1928 }
1929 }
1930 }
1931 }
1932 if match_open_close_tag_line(line) {
1934 return Some((HtmlSeq::AnyTag, false));
1935 }
1936 None
1937}
1938
1939fn html_seq_close(seq: HtmlSeq, line: &[char]) -> bool {
1940 match seq {
1941 HtmlSeq::Raw => ["</script>", "</pre>", "</style>", "</textarea>"]
1942 .iter()
1943 .any(|c| contains_ci(line, c)),
1944 HtmlSeq::Comment => contains_exact(line, "-->"),
1945 HtmlSeq::Pi => contains_exact(line, "?>"),
1946 HtmlSeq::Decl => contains_exact(line, ">"),
1947 HtmlSeq::Cdata => contains_exact(line, "]]>"),
1948 HtmlSeq::Block | HtmlSeq::AnyTag => line.is_empty(), }
1950}
1951
1952fn rule_html_block(state: &mut State, start_line: usize, end_line: usize, silent: bool) -> bool {
1953 let mut pos = state.b_marks[start_line] + state.t_shift[start_line];
1954 let mut maximum = state.e_marks[start_line];
1955 if state.is_code_block(start_line) {
1956 return false;
1957 }
1958 if state.src.get(pos) != Some(&'<') {
1959 return false;
1960 }
1961 let mut line_text: Vec<char> = state.src[pos..maximum].to_vec();
1962 let (seq, terminator) = match html_seq_open(&line_text) {
1963 Some(x) => x,
1964 None => return false,
1965 };
1966 if silent {
1967 return terminator;
1968 }
1969 let mut next_line = start_line + 1;
1970 if !html_seq_close(seq, &line_text) {
1971 while next_line < end_line {
1972 if state.s_count[next_line] < state.blk_indent {
1973 break;
1974 }
1975 pos = state.b_marks[next_line] + state.t_shift[next_line];
1976 maximum = state.e_marks[next_line];
1977 line_text = state.src[pos.min(maximum)..maximum].to_vec();
1978 if html_seq_close(seq, &line_text) {
1979 if !line_text.is_empty() {
1980 next_line += 1;
1981 }
1982 break;
1983 }
1984 next_line += 1;
1985 }
1986 }
1987 state.line = next_line;
1988 let i = state.push("html_block", "", 0);
1989 state.tokens[i].map = Some((start_line, next_line));
1990 state.tokens[i].content = state.get_lines(start_line, next_line, state.blk_indent, true);
1991 true
1992}
1993
1994fn normalize_src(s: &str) -> String {
2000 let mut out = String::with_capacity(s.len());
2001 let mut it = s.chars().peekable();
2002 while let Some(c) = it.next() {
2003 match c {
2004 '\r' => {
2005 if it.peek() == Some(&'\n') {
2006 it.next();
2007 }
2008 out.push('\n');
2009 }
2010 '\0' => out.push('\u{fffd}'),
2011 c => out.push(c),
2012 }
2013 }
2014 out
2015}
2016
2017pub fn tokenize_blocks(body: &str) -> Vec<Token> {
2020 let normalized = normalize_src(body);
2021 if normalized.is_empty() {
2022 return Vec::new();
2023 }
2024 let src: Vec<char> = normalized.chars().collect();
2025 let mut state = State::new(&src);
2026 let line_max = state.line_max;
2027 tokenize(&mut state, 0, line_max);
2028 state.tokens
2029}
2030
2031#[derive(Debug, Clone, PartialEq, Eq)]
2034pub struct BlockEvent {
2035 pub heading: bool,
2036 pub tag: &'static str,
2037 pub line: i64, pub content: String,
2039}
2040
2041pub fn consumed_events(body: &str) -> Vec<BlockEvent> {
2043 let tokens = tokenize_blocks(body);
2044 let mut events = Vec::new();
2045 for i in 0..tokens.len() {
2046 let t = &tokens[i];
2047 if t.typ == "heading_open" {
2048 let content = if i + 1 < tokens.len() {
2049 tokens[i + 1].content.clone()
2050 } else {
2051 String::new()
2052 };
2053 events.push(BlockEvent {
2054 heading: true,
2055 tag: t.tag,
2056 line: t.map.map_or(-1, |m| m.0 as i64),
2057 content,
2058 });
2059 } else if t.typ == "inline" && !(i > 0 && tokens[i - 1].typ == "heading_open") {
2060 events.push(BlockEvent {
2061 heading: false,
2062 tag: "",
2063 line: t.map.map_or(-1, |m| m.0 as i64),
2064 content: t.content.clone(),
2065 });
2066 }
2067 }
2068 events
2069}
2070
2071#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2076enum Sect {
2077 None,
2078 Other,
2079 Problem,
2080 Requirements,
2081 SuccessMetrics,
2082 Risks,
2083}
2084
2085struct Walk {
2086 offset: i64,
2087 title: Option<String>,
2088 extra_title_lines: Vec<i64>,
2089 section: Sect,
2090 current_h2: Option<String>,
2091 search_sections: Vec<SearchSection>,
2092 current_search: Option<usize>,
2093 problem_lines: Vec<String>,
2094 requirement_lines: Vec<(String, i64)>,
2095 metric_lines: Vec<String>,
2096 risk_lines: Vec<String>,
2097 section_order: Vec<String>,
2098 section_bodies: HashMap<String, Vec<String>>,
2099 section_chars: HashMap<String, usize>,
2100 captured_lines: usize,
2101 truncated_fields: BTreeSet<String>,
2102 body_truncated: bool,
2103 has_problem: bool,
2104 has_requirements: bool,
2105 has_metrics: bool,
2106 has_risks: bool,
2107}
2108
2109impl Walk {
2110 fn new(offset: i64) -> Self {
2111 Walk {
2112 offset,
2113 title: None,
2114 extra_title_lines: Vec::new(),
2115 section: Sect::None,
2116 current_h2: None,
2117 search_sections: Vec::new(),
2118 current_search: None,
2119 problem_lines: Vec::new(),
2120 requirement_lines: Vec::new(),
2121 metric_lines: Vec::new(),
2122 risk_lines: Vec::new(),
2123 section_order: Vec::new(),
2124 section_bodies: HashMap::new(),
2125 section_chars: HashMap::new(),
2126 captured_lines: 0,
2127 truncated_fields: BTreeSet::new(),
2128 body_truncated: false,
2129 has_problem: false,
2130 has_requirements: false,
2131 has_metrics: false,
2132 has_risks: false,
2133 }
2134 }
2135
2136 fn open_heading(&mut self, tag: &str, map: Option<(usize, usize)>, heading_text: &str) {
2137 if tag == "h1" {
2138 if self.title.is_none() {
2139 self.title = Some(py_strip(heading_text).to_string());
2140 } else {
2141 self.extra_title_lines
2142 .push(map.map_or(0, |m| m.0 as i64 + 1 + self.offset));
2143 }
2144 self.section = Sect::None;
2145 self.current_h2 = None;
2146 self.current_search = None;
2147 } else if tag == "h2" {
2148 let normalized = py_casefold(py_strip(heading_text));
2149 self.current_h2 = Some(normalized.clone());
2150 if !self.section_bodies.contains_key(&normalized) {
2151 self.section_bodies.insert(normalized.clone(), Vec::new());
2152 self.section_order.push(normalized.clone());
2153 }
2154 self.search_sections.push(SearchSection {
2155 heading: py_strip(heading_text).to_string(),
2156 lines: Vec::new(),
2157 });
2158 self.current_search = Some(self.search_sections.len() - 1);
2159 self.section = match normalized.as_str() {
2160 "problem" => {
2161 self.has_problem = true;
2162 Sect::Problem
2163 }
2164 "requirements" => {
2165 self.has_requirements = true;
2166 Sect::Requirements
2167 }
2168 "success metrics" => {
2169 self.has_metrics = true;
2170 Sect::SuccessMetrics
2171 }
2172 "risks" => {
2173 self.has_risks = true;
2174 Sect::Risks
2175 }
2176 _ => Sect::None,
2177 };
2178 } else {
2179 self.section = Sect::Other;
2180 }
2181 }
2182
2183 fn capture_inline(&mut self, content: &str, map: Option<(usize, usize)>) {
2184 if self.body_truncated {
2185 return;
2186 }
2187 if let Some(h2) = self.current_h2.clone() {
2188 self.capture_generic_body(content, &h2);
2189 }
2190 if self.body_truncated || matches!(self.section, Sect::None | Sect::Other) {
2191 return;
2192 }
2193 self.capture_field(content, map);
2194 }
2195
2196 fn capture_generic_body(&mut self, content: &str, heading: &str) {
2197 for raw in content.split('\n') {
2198 let stripped = py_strip(raw);
2199 if stripped.is_empty() {
2200 continue;
2201 }
2202 if self.captured_lines >= MAX_CAPTURED_LINES {
2203 self.body_truncated = true;
2204 break;
2205 }
2206 let n_chars = stripped.chars().count();
2207 let used = self.section_chars.get(heading).copied().unwrap_or(0);
2208 if used + n_chars > MAX_FIELD_CHARS {
2209 self.truncated_fields.insert(heading.to_string());
2210 continue;
2211 }
2212 if !self.section_bodies.contains_key(heading) {
2213 self.section_bodies.insert(heading.to_string(), Vec::new());
2214 self.section_order.push(heading.to_string());
2215 }
2216 self.section_bodies
2217 .get_mut(heading)
2218 .expect("just ensured")
2219 .push(stripped.to_string());
2220 self.section_chars
2221 .insert(heading.to_string(), used + n_chars + 1);
2222 self.captured_lines += 1;
2223 if let Some(cs) = self.current_search {
2224 self.search_sections[cs].lines.push(stripped.to_string());
2225 }
2226 }
2227 }
2228
2229 fn capture_field(&mut self, content: &str, map: Option<(usize, usize)>) {
2230 let start_line = map.map_or(0, |m| m.0 as i64 + self.offset);
2231 let mut lines: Vec<(String, i64)> = Vec::new();
2232 for (offset, raw) in content.split('\n').enumerate() {
2233 let stripped = py_strip(raw);
2234 if !stripped.is_empty() {
2235 lines.push((stripped.to_string(), start_line + offset as i64 + 1));
2236 }
2237 }
2238 match self.section {
2239 Sect::Problem => self
2240 .problem_lines
2241 .extend(lines.into_iter().map(|(t, _)| t)),
2242 Sect::Requirements => self.requirement_lines.extend(lines),
2243 Sect::SuccessMetrics => self.metric_lines.extend(lines.into_iter().map(|(t, _)| t)),
2244 Sect::Risks => self.risk_lines.extend(lines.into_iter().map(|(t, _)| t)),
2245 _ => {}
2246 }
2247 }
2248}
2249
2250fn classify_requirement_line(text: &str, line: i64) -> Result<Requirement, MalformedRequirement> {
2251 let malformed_no_id = || MalformedRequirement {
2253 raw: text.to_string(),
2254 line,
2255 bad_id: None,
2256 empty_text: false,
2257 };
2258 if !text.starts_with('[') {
2259 return Err(malformed_no_id());
2260 }
2261 let close = match text.find(']') {
2262 Some(i) => i,
2263 None => return Err(malformed_no_id()),
2264 };
2265 let id_group = &text[1..close];
2266 let mut rest = &text[close + 1..];
2267 rest = rest.trim_start_matches(py_is_space);
2269 let req_id = py_strip(id_group);
2270 let desc = py_strip(rest);
2271 let canonical = req_id
2272 .strip_prefix("REQ-")
2273 .is_some_and(|d| !d.is_empty() && d.chars().all(is_re_digit));
2274 if !canonical {
2275 return Err(MalformedRequirement {
2276 raw: text.to_string(),
2277 line,
2278 bad_id: Some(req_id.to_string()),
2279 empty_text: false,
2280 });
2281 }
2282 if desc.is_empty() {
2283 return Err(MalformedRequirement {
2284 raw: text.to_string(),
2285 line,
2286 bad_id: Some(req_id.to_string()),
2287 empty_text: true,
2288 });
2289 }
2290 Ok(Requirement {
2291 id: req_id.to_string(),
2292 text: desc.to_string(),
2293 line,
2294 })
2295}
2296
2297fn budget_issues(walk: &Walk) -> Vec<Issue> {
2298 let mut issues = Vec::new();
2299 for heading in &walk.truncated_fields {
2300 issues.push(Issue {
2301 severity: "warning",
2302 code: "field-truncated",
2303 message: format!(
2304 "section {} exceeds the {}-char field cap and was truncated",
2305 py_repr_str(heading),
2306 MAX_FIELD_CHARS
2307 ),
2308 line: None,
2309 });
2310 }
2311 if walk.body_truncated {
2312 issues.push(Issue {
2313 severity: "warning",
2314 code: "body-truncated",
2315 message: format!(
2316 "document body exceeds the {}-line capture cap and was truncated",
2317 MAX_CAPTURED_LINES
2318 ),
2319 line: None,
2320 });
2321 }
2322 issues
2323}
2324
2325fn degraded_product(source_path: &str, issues: Vec<Issue>) -> Product {
2326 Product {
2327 source_path: source_path.to_string(),
2328 parse_issues: issues,
2329 ..Default::default()
2330 }
2331}
2332
2333fn oversize_issue(cap: u128, kind: &str) -> Issue {
2334 Issue {
2335 severity: "error",
2336 code: "artifact-oversize",
2337 message: format!(
2338 "artifact exceeds the {cap}-byte {kind} cap (set DECIDED_MAX_FILE_BYTES to raise it)"
2339 ),
2340 line: Some(1),
2341 }
2342}
2343
2344pub fn parse(text: &str, source_path: &str) -> Product {
2351 parse_with_cap(text, source_path, max_file_bytes())
2352}
2353
2354pub fn parse_with_cap(text: &str, source_path: &str, cap: u128) -> Product {
2356 if exceeds_byte_cap(text, cap) {
2357 return degraded_product(source_path, vec![oversize_issue(cap, "parse")]);
2358 }
2359 let split = split_frontmatter(text);
2360 let mut metadata_issues = Vec::new();
2361 if split.raw.is_none() && split.unterminated {
2362 metadata_issues.push(Issue {
2363 severity: "error",
2364 code: "malformed-frontmatter",
2365 message: "frontmatter block opened with --- on line 1 but never closed".to_string(),
2366 line: Some(1),
2367 });
2368 }
2369
2370 let tokens = tokenize_blocks(&split.body);
2371 let mut walk = Walk::new(split.line_offset as i64);
2372 for i in 0..tokens.len() {
2373 let t = &tokens[i];
2374 if t.typ == "heading_open" {
2375 let heading_text = if i + 1 < tokens.len() {
2376 tokens[i + 1].content.clone()
2377 } else {
2378 String::new()
2379 };
2380 walk.open_heading(t.tag, t.map, &heading_text);
2381 } else if t.typ == "inline" && !(i > 0 && tokens[i - 1].typ == "heading_open") {
2382 walk.capture_inline(&t.content, t.map);
2383 }
2384 }
2385
2386 let mut requirements = Vec::new();
2387 let mut malformed = Vec::new();
2388 for (line_text, line_no) in &walk.requirement_lines {
2389 match classify_requirement_line(line_text, *line_no) {
2390 Ok(r) => requirements.push(r),
2391 Err(m) => malformed.push(m),
2392 }
2393 }
2394 let problem = if walk.has_problem {
2395 Some(py_strip(&walk.problem_lines.join("\n")).to_string())
2396 } else {
2397 None
2398 };
2399 let sections: Vec<(String, String)> = walk
2400 .section_order
2401 .iter()
2402 .map(|h| (h.clone(), walk.section_bodies[h].join("\n")))
2403 .collect();
2404 let parse_issues = budget_issues(&walk);
2405
2406 Product {
2407 title: walk.title,
2408 extra_title_lines: walk.extra_title_lines,
2409 problem,
2410 requirements,
2411 malformed_requirements: malformed,
2412 success_metrics: walk.metric_lines,
2413 risks: walk.risk_lines,
2414 sections,
2415 search_sections: walk.search_sections,
2416 has_problem_section: walk.has_problem,
2417 has_requirements_section: walk.has_requirements,
2418 has_metrics_section: walk.has_metrics,
2419 has_risks_section: walk.has_risks,
2420 source_path: source_path.to_string(),
2421 frontmatter_raw: split.raw,
2422 metadata_issues,
2423 parse_issues,
2424 }
2425}
2426
2427fn strerror(errno: i32) -> String {
2430 let s = std::io::Error::from_raw_os_error(errno).to_string();
2431 match s.rfind(" (os error ") {
2432 Some(i) => s[..i].to_string(),
2433 None => s,
2434 }
2435}
2436
2437fn os_error_message(err: &std::io::Error, path: &str) -> String {
2439 match err.raw_os_error() {
2440 Some(n) => format!("[Errno {}] {}: {}", n, strerror(n), py_repr_str(path)),
2441 None => err.to_string(),
2442 }
2443}
2444
2445fn unreadable_issue(err: &std::io::Error, path: &str) -> Issue {
2446 Issue {
2447 severity: "error",
2448 code: "unreadable-artifact",
2449 message: format!("cannot read artifact: {}", os_error_message(err, path)),
2450 line: Some(1),
2451 }
2452}
2453
2454pub fn parse_file(path: &str) -> Product {
2456 parse_file_with_cap(path, max_file_bytes())
2457}
2458
2459pub fn parse_file_with_cap(path: &str, cap: u128) -> Product {
2461 let size = match std::fs::metadata(path) {
2462 Ok(m) => m.len(),
2463 Err(e) => return degraded_product(path, vec![unreadable_issue(&e, path)]),
2464 };
2465 if size as u128 > cap {
2466 return degraded_product(path, vec![oversize_issue(cap, "file")]);
2467 }
2468 let file = match std::fs::File::open(path) {
2469 Ok(f) => f,
2470 Err(e) => return degraded_product(path, vec![unreadable_issue(&e, path)]),
2471 };
2472 let mut data = Vec::new();
2473 let take_n: u64 = cap.saturating_add(1).min(u64::MAX as u128) as u64;
2474 if let Err(e) = file.take(take_n).read_to_end(&mut data) {
2475 return degraded_product(path, vec![unreadable_issue(&e, path)]);
2476 }
2477 if data.len() as u128 > cap {
2478 return degraded_product(path, vec![oversize_issue(cap, "file")]);
2479 }
2480 match String::from_utf8(data) {
2481 Ok(text) => parse_with_cap(&text, path, cap),
2482 Err(e) => {
2483 let text = String::from_utf8_lossy(e.as_bytes()).into_owned();
2484 let mut product = parse_with_cap(&text, path, cap);
2485 product.parse_issues.push(Issue {
2486 severity: "warning",
2487 code: "non-utf8-content",
2488 message: "artifact is not valid UTF-8; decoded lossily".to_string(),
2489 line: Some(1),
2490 });
2491 product
2492 }
2493 }
2494}