1use crate::stream::{Event, EventKind, StreamError, Tag, TagKind, Text};
6use crate::util::{
7 constant::{SAFE_PROTOCOL_HREF, SAFE_PROTOCOL_SRC},
8 encode::{find_escape, write_escaped},
9 sanitize_uri::{sanitize, write_sanitized},
10};
11use crate::{CompileOptions, LineEnding};
12use alloc::{
13 string::{String, ToString},
14 vec::Vec,
15};
16use core::{
17 convert::TryFrom,
18 fmt::{self, Write},
19};
20
21fn decimal(mut value: u64, buffer: &mut [u8; 20]) -> &str {
23 let mut start = buffer.len();
24 loop {
25 start -= 1;
26 buffer[start] = b'0' + u8::try_from(value % 10).expect("a decimal digit fits in u8");
27 value /= 10;
28 if value == 0 {
29 break;
30 }
31 }
32 core::str::from_utf8(&buffer[start..]).expect("decimal digits are ASCII")
33}
34
35struct Frame {
36 kind: TagKind,
37 flag: bool,
38 level: u8,
39 end: usize,
40}
41struct HeadingCapture {
42 output: String,
43 text: String,
44}
45struct FootnoteCall {
46 key: String,
47 id: String,
48 count: usize,
49 definition: Option<usize>,
50}
51
52struct Capture {
53 key: String,
54 output: String,
55 previous: Option<u8>,
56}
57
58pub struct EventHtmlRenderer<'a, W> {
62 output: &'a mut W,
63 source: &'a str,
64 options: &'a CompileOptions,
65 line_ending: LineEnding,
66 last: Option<u8>,
67 stack: Vec<Frame>,
68 images: usize,
69 image_titles: Vec<Text<'static>>,
70 captures: Vec<Capture>,
71 notes: Vec<(String, String)>,
72 calls: Vec<FootnoteCall>,
73 started: bool,
74 ended: bool,
75 heading: Option<HeadingCapture>,
76 anchors: crate::headings::AnchorGenerator,
77}
78
79impl<'a, W: Write> EventHtmlRenderer<'a, W> {
80 pub fn new(source: &'a str, options: &'a CompileOptions, output: &'a mut W) -> Self {
82 Self {
83 output,
84 source,
85 options,
86 line_ending: options.default_line_ending.clone(),
87 last: None,
88 stack: Vec::new(),
89 images: 0,
90 image_titles: Vec::new(),
91 captures: Vec::new(),
92 notes: Vec::new(),
93 calls: Vec::new(),
94 started: false,
95 ended: false,
96 heading: None,
97 anchors: crate::headings::AnchorGenerator::default(),
98 }
99 }
100
101 #[allow(clippy::inline_always)]
103 #[inline(always)]
104 fn write(&mut self, value: &str) -> Result<(), StreamError> {
105 if let Some(heading) = self.heading.as_mut() {
106 heading.output.push_str(value);
107 } else if let Some(capture) = self.captures.last_mut() {
108 capture.output.push_str(value);
109 } else {
110 self.output.write_str(value)?;
111 }
112 if let Some(&byte) = value.as_bytes().last() {
113 self.last = Some(byte);
114 }
115 Ok(())
116 }
117
118 fn escaped(&mut self, value: &str, encode: bool) -> Result<(), StreamError> {
119 let index = find_escape(value.as_bytes(), encode);
120 if index == value.len() {
122 return self.write(value);
123 }
124 if let Some(heading) = self.heading.as_mut() {
125 write_escaped(&mut heading.output, &mut self.last, value, encode, index)
126 } else if let Some(capture) = self.captures.last_mut() {
127 write_escaped(&mut capture.output, &mut self.last, value, encode, index)
128 } else {
129 write_escaped(self.output, &mut self.last, value, encode, index)
130 }
131 .map_err(StreamError::Write)
132 }
133
134 fn newline(&mut self) -> Result<(), StreamError> {
135 let value = self.line_ending.clone();
136 self.write(value.as_str())
137 }
138 fn number(&mut self, value: u64) -> Result<(), StreamError> {
139 self.write(decimal(value, &mut [0; 20]))
140 }
141 fn terminate(&mut self) -> Result<(), StreamError> {
142 if !matches!(self.last, None | Some(b'\r' | b'\n')) {
143 self.newline()?;
144 }
145 Ok(())
146 }
147
148 fn destination(&mut self, value: &str, image: bool) -> Result<(), StreamError> {
149 let protocols =
150 if self.options.allow_dangerous_protocol || (image && self.options.allow_any_img_src) {
151 None
152 } else if image {
153 Some(SAFE_PROTOCOL_SRC.as_slice())
154 } else {
155 Some(SAFE_PROTOCOL_HREF.as_slice())
156 };
157 write_sanitized(value, protocols, |value| self.write(value))
158 }
159
160 fn start(&mut self, tag: &Tag<'_>, end: usize) -> Result<(), StreamError> {
161 let kind = tag.kind();
162 if kind == TagKind::Document {
163 if self.started || !self.stack.is_empty() {
164 return Err(StreamError::Unbalanced);
165 }
166 self.started = true;
167 } else if self.stack.is_empty() {
168 return Err(StreamError::Unbalanced);
169 }
170 if matches!(kind, TagKind::Heading | TagKind::FootnoteDefinition)
171 && self
172 .stack
173 .iter()
174 .any(|frame| !frame.kind.allows_descendant(kind))
175 {
176 return Err(StreamError::Unbalanced);
177 }
178 let mut frame = Frame {
179 kind,
180 flag: false,
181 level: 0,
182 end,
183 };
184 let tags = self.images == 0;
186 if !tags
187 && matches!(
188 kind,
189 TagKind::Table
190 | TagKind::TableHead
191 | TagKind::TableBody
192 | TagKind::TableRow
193 | TagKind::TableCell
194 )
195 {
196 self.stack.push(frame);
197 return Ok(());
198 }
199 match tag {
200 Tag::Document(line_ending) => {
201 if let Some(value) = line_ending {
202 self.line_ending = value.clone();
203 }
204 }
205 Tag::Paragraph { tight } => {
206 frame.flag = *tight;
207 if tags && !tight {
208 self.terminate()?;
209 self.write("<p>")?;
210 }
211 }
212 Tag::Heading(level) => {
213 frame.level = *level;
214 self.terminate()?;
215 if self.options.heading_ids {
216 self.heading = Some(HeadingCapture {
217 output: String::new(),
218 text: String::new(),
219 });
220 } else {
221 self.write(
222 ["<h1>", "<h2>", "<h3>", "<h4>", "<h5>", "<h6>"][usize::from(*level - 1)],
223 )?;
224 }
225 }
226 Tag::BlockQuote => {
227 if tags {
228 self.terminate()?;
229 self.write("<blockquote>")?;
230 }
231 }
232 Tag::List { start, .. } => {
233 frame.flag = start.is_some();
234 if tags {
235 self.terminate()?;
236 match start {
237 None => self.write("<ul>")?,
238 Some(1) => self.write("<ol>")?,
239 Some(start) => {
240 self.write("<ol start=\"")?;
241 self.number(u64::from(*start))?;
242 self.write("\">")?;
243 }
244 }
245 }
246 }
247 Tag::Item { block_end } => {
248 frame.flag = *block_end;
249 if tags {
250 self.terminate()?;
251 self.write("<li>")?;
252 }
253 }
254 Tag::Emphasis => {
255 if tags {
256 self.write("<em>")?;
257 }
258 }
259 Tag::Strong => {
260 if tags {
261 self.write("<strong>")?;
262 }
263 }
264 Tag::Delete => {
265 if tags {
266 self.write("<del>")?;
267 }
268 }
269 Tag::Link { destination, title } => {
270 if tags {
271 self.write("<a href=\"")?;
272 self.destination(
273 destination
274 .resolve(self.source)
275 .expect("event values are source-valid"),
276 false,
277 )?;
278 if let Some(title) = title {
279 self.write("\" title=\"")?;
280 self.escaped(
281 title
282 .resolve(self.source)
283 .expect("event values are source-valid"),
284 true,
285 )?;
286 }
287 self.write("\">")?;
288 }
289 }
290 Tag::Image { destination, title } => {
291 frame.flag = tags;
292 if tags {
293 self.write("<img src=\"")?;
294 self.destination(
295 destination
296 .resolve(self.source)
297 .expect("event values are source-valid"),
298 true,
299 )?;
300 self.write("\" alt=\"")?;
301 }
302 if tags {
304 if let Some(title) = title {
305 frame.level = 1;
306 self.image_titles.push(title.borrowed().owned());
307 }
308 }
309 self.images += 1;
310 }
311 Tag::Table => {
312 self.terminate()?;
313 self.write("<table>")?;
314 }
315 Tag::TableHead => {
316 self.terminate()?;
317 self.write("<thead>")?;
318 }
319 Tag::TableBody => self.write("<tbody>")?,
320 Tag::TableRow => {
321 self.terminate()?;
322 self.write("<tr>")?;
323 }
324 Tag::TableCell { alignment, header } => {
325 frame.flag = *header;
326 self.terminate()?;
327 self.write(match (header, alignment) {
328 (true, crate::AlignKind::Left) => "<th align=\"left\">",
329 (true, crate::AlignKind::Right) => "<th align=\"right\">",
330 (true, crate::AlignKind::Center) => "<th align=\"center\">",
331 (true, crate::AlignKind::None) => "<th>",
332 (false, crate::AlignKind::Left) => "<td align=\"left\">",
333 (false, crate::AlignKind::Right) => "<td align=\"right\">",
334 (false, crate::AlignKind::Center) => "<td align=\"center\">",
335 (false, crate::AlignKind::None) => "<td>",
336 })?;
337 }
338 Tag::FootnoteDefinition(label) => {
339 let key = label
340 .resolve(self.source)
341 .expect("event values are source-valid")
342 .to_string();
343 self.captures.push(Capture {
344 key,
345 output: String::new(),
346 previous: self.last,
347 });
348 self.last = None;
349 }
350 }
351 self.stack.push(frame);
352 Ok(())
353 }
354
355 fn end(&mut self, kind: TagKind, end: usize) -> Result<(), StreamError> {
356 let frame = self.stack.pop().ok_or(StreamError::Unbalanced)?;
357 if frame.kind != kind {
358 return Err(StreamError::Unbalanced);
359 }
360 if end < frame.end {
361 return Err(StreamError::InvalidRange);
362 }
363 if kind == TagKind::Image {
364 self.images -= 1;
365 if frame.flag {
366 if frame.level != 0 {
367 let title = self.image_titles.pop().unwrap();
368 self.write("\" title=\"")?;
369 self.escaped(
370 title
371 .resolve(self.source)
372 .expect("event values are source-valid"),
373 true,
374 )?;
375 }
376 self.write("\" />")?;
377 }
378 return Ok(());
379 }
380 if self.images != 0 {
381 return Ok(());
382 }
383 match kind {
384 TagKind::Document => {
385 self.footnotes()?;
386 self.ended = true;
387 }
388 TagKind::Paragraph => {
389 if !frame.flag {
390 self.write("</p>")?;
391 }
392 }
393 TagKind::Heading => {
394 if let Some(heading) = self.heading.take() {
395 let text = crate::util::line_ending::trim_terminal_ending(&heading.text);
396 let anchor = self.anchors.anchor_for(text);
397 self.write(
398 [
399 "<h1 id=\"",
400 "<h2 id=\"",
401 "<h3 id=\"",
402 "<h4 id=\"",
403 "<h5 id=\"",
404 "<h6 id=\"",
405 ][usize::from(frame.level - 1)],
406 )?;
407 self.escaped(&anchor, true)?;
408 self.write("\">")?;
409 self.write(&heading.output)?;
410 }
411 self.write(
412 ["</h1>", "</h2>", "</h3>", "</h4>", "</h5>", "</h6>"]
413 [usize::from(frame.level - 1)],
414 )?;
415 }
416 TagKind::BlockQuote => {
417 self.terminate()?;
418 self.write("</blockquote>")?;
419 }
420 TagKind::List => {
421 self.newline()?;
422 self.write(if frame.flag { "</ol>" } else { "</ul>" })?;
423 }
424 TagKind::Item => {
425 if frame.flag {
426 self.terminate()?;
427 }
428 self.write("</li>")?;
429 }
430 TagKind::Emphasis => self.write("</em>")?,
431 TagKind::Strong => self.write("</strong>")?,
432 TagKind::Delete => self.write("</del>")?,
433 TagKind::Link => self.write("</a>")?,
434 TagKind::Table => {
435 self.terminate()?;
436 self.write("</table>")?;
437 }
438 TagKind::TableHead => {
439 self.terminate()?;
440 self.write("</thead>")?;
441 }
442 TagKind::TableBody => {
443 self.terminate()?;
444 self.write("</tbody>")?;
445 }
446 TagKind::TableRow => {
447 self.terminate()?;
448 self.write("</tr>")?;
449 }
450 TagKind::TableCell => self.write(if frame.flag { "</th>" } else { "</td>" })?,
451 TagKind::FootnoteDefinition => {
452 let capture = self.captures.pop().unwrap();
453 self.last = capture.previous;
454 self.notes.push((capture.key, capture.output));
455 }
456 TagKind::Image => {
457 unreachable!("images were handled before suppressing tags in alt text")
458 }
459 }
460 Ok(())
461 }
462
463 pub fn render<'e>(
468 &mut self,
469 events: impl IntoIterator<Item = Result<Event<'e>, StreamError>>,
470 ) -> Result<(), StreamError> {
471 self.render_inner::<true>(events)
472 }
473
474 pub(crate) fn render_markdown(
477 &mut self,
478 options: &crate::ParseOptions,
479 ) -> Result<(), StreamError> {
480 let events = crate::EventParser::new(self.source, options);
481 self.render_inner::<false>(events)
482 }
483
484 fn render_inner<'e, const CHECK: bool>(
485 &mut self,
486 events: impl IntoIterator<Item = Result<Event<'e>, StreamError>>,
487 ) -> Result<(), StreamError> {
488 for event in events {
489 self.event_ref::<CHECK>(&event?)?;
490 }
491 if !self.started || !self.ended || !self.stack.is_empty() {
492 return Err(StreamError::Unbalanced);
493 }
494 Ok(())
495 }
496
497 #[allow(clippy::needless_pass_by_value)]
503 pub fn event(&mut self, event: Event<'_>) -> Result<(), StreamError> {
504 self.event_ref::<true>(&event)
505 }
506
507 #[allow(clippy::inline_always)]
509 #[inline(always)]
510 fn event_ref<const CHECK: bool>(&mut self, event: &Event<'_>) -> Result<(), StreamError> {
511 if self.ended {
512 return Err(StreamError::Unbalanced);
513 }
514 if CHECK {
515 event.validate(Some(self.source))?;
516 }
517 if let Some(heading) = &mut self.heading {
518 crate::headings::append_text::<true>(&event.kind, self.source, &mut heading.text);
519 }
520 match &event.kind {
521 EventKind::Start(tag) => self.start(tag, event.span.end),
522 EventKind::End(kind) => self.end(*kind, event.span.end),
523 kind => {
524 if self.stack.is_empty() {
525 return Err(StreamError::Unbalanced);
526 }
527 match kind {
528 EventKind::Text(value) | EventKind::SoftBreak(value) => self.escaped(
529 value
530 .resolve(self.source)
531 .expect("event values are source-valid"),
532 true,
533 ),
534 EventKind::Separator { value, conditional } => {
535 if *conditional {
536 self.terminate()
537 } else {
538 self.escaped(
539 value
540 .resolve(self.source)
541 .expect("event values are source-valid"),
542 true,
543 )
544 }
545 }
546 EventKind::HardBreak => {
547 if self.images == 0 {
548 self.write("<br />")
549 } else {
550 Ok(())
551 }
552 }
553 EventKind::Rule => {
554 if self.images == 0 {
555 self.terminate()?;
556 self.write("<hr />")
557 } else {
558 Ok(())
559 }
560 }
561 EventKind::Code { value, language } => {
562 if self.images == 0 {
563 if let Some(language) = language {
564 self.write("<code class=\"language-")?;
565 self.escaped(
566 language
567 .resolve(self.source)
568 .expect("event values are source-valid"),
569 true,
570 )?;
571 self.write("\">")?;
572 } else {
573 self.write("<code>")?;
574 }
575 }
576 self.escaped(
577 value
578 .resolve(self.source)
579 .expect("event values are source-valid"),
580 true,
581 )?;
582 if self.images == 0 {
583 self.write("</code>")?;
584 }
585 Ok(())
586 }
587 EventKind::Math(value) => {
588 if self.images == 0 {
589 self.write("<code class=\"language-math math-inline\">")?;
590 }
591 self.escaped(
592 value
593 .resolve(self.source)
594 .expect("event values are source-valid"),
595 true,
596 )?;
597 if self.images == 0 {
598 self.write("</code>")?;
599 }
600 Ok(())
601 }
602 EventKind::CodeBlock {
603 value,
604 language,
605 math,
606 unclosed,
607 ..
608 } => {
609 if self.images != 0 {
610 return self.escaped(
611 value
612 .resolve(self.source)
613 .expect("event values are source-valid"),
614 true,
615 );
616 }
617 self.terminate()?;
618 if *math {
619 self.write("<pre><code class=\"language-math math-display\">")?;
620 } else if let Some(language) = language {
621 self.write("<pre><code class=\"language-")?;
622 self.escaped(
623 language
624 .resolve(self.source)
625 .expect("event values are source-valid"),
626 true,
627 )?;
628 self.write("\">")?;
629 } else {
630 self.write("<pre><code>")?;
631 }
632 let value = value
633 .resolve(self.source)
634 .expect("event values are source-valid");
635 self.escaped(value, true)?;
636 if !value.is_empty() {
637 self.terminate()?;
638 }
639 self.write("</code></pre>")?;
640 if *unclosed {
641 self.terminate()?;
642 }
643 Ok(())
644 }
645 EventKind::Html { value, block } => {
646 if *block {
647 self.terminate()?;
648 }
649 let value = value
650 .resolve(self.source)
651 .expect("event values are source-valid");
652 if self.options.allow_dangerous_html && self.options.tagfilter {
653 crate::util::tagfilter::write_filtered(value, |part| {
654 self.escaped(part, false)
655 })
656 } else {
657 self.escaped(value, !self.options.allow_dangerous_html)
658 }
659 }
660 EventKind::TaskMarker(checked) => {
661 if self.images == 0 {
662 self.write(match (self.options.task_list_item_checkable, checked) {
663 (false, false) => "<input type=\"checkbox\" disabled=\"\" />",
664 (false, true) => {
665 "<input type=\"checkbox\" disabled=\"\" checked=\"\" />"
666 }
667 (true, false) => "<input type=\"checkbox\" />",
668 (true, true) => "<input type=\"checkbox\" checked=\"\" />",
669 })?;
670 }
671 Ok(())
672 }
673 EventKind::Definition { .. } | EventKind::Opaque { .. } => Ok(()),
674 EventKind::FootnoteReference(value) => self.footnote_call(
675 value
676 .resolve(self.source)
677 .expect("event values are source-valid"),
678 ),
679 EventKind::Start(_) | EventKind::End(_) => {
680 unreachable!("containers were handled above")
681 }
682 }
683 }
684 }
685 }
686
687 fn prefix(&mut self) -> Result<(), StreamError> {
688 self.escaped(
689 self.options
690 .footnote_clobber_prefix
691 .as_deref()
692 .unwrap_or("user-content-"),
693 true,
694 )
695 }
696
697 fn footnote_call(&mut self, key: &str) -> Result<(), StreamError> {
698 let index = self
699 .calls
700 .iter()
701 .position(|call| call.key == key)
702 .unwrap_or_else(|| {
703 self.calls.push(FootnoteCall {
704 key: key.into(),
705 id: sanitize(&key.to_lowercase()),
706 count: 0,
707 definition: None,
708 });
709 self.calls.len() - 1
710 });
711 let call = &mut self.calls[index];
712 call.count += 1;
713 let count = call.count;
714 if self.images != 0 {
715 return Ok(());
716 }
717 let safe = core::mem::take(&mut call.id);
719 let result = self.write_footnote_call(&safe, index + 1, count);
720 self.calls[index].id = safe;
721 result
722 }
723
724 fn write_footnote_call(
725 &mut self,
726 safe: &str,
727 number: usize,
728 count: usize,
729 ) -> Result<(), StreamError> {
730 self.write("<sup><a href=\"#")?;
731 self.prefix()?;
732 self.write("fn-")?;
733 self.write(safe)?;
734 self.write("\" id=\"")?;
735 self.prefix()?;
736 self.write("fnref-")?;
737 self.write(safe)?;
738 if count > 1 {
739 self.write("-")?;
740 self.number(count as u64)?;
741 }
742 self.write("\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">")?;
743 self.number(number as u64)?;
744 self.write("</a></sup>")
745 }
746
747 fn backreferences(&mut self, key: &str, count: usize) -> Result<(), StreamError> {
748 for reference in 1..=count {
749 if reference != 1 {
750 self.write(" ")?;
751 }
752 let mut buffer = [0; 20];
753 let number = (reference > 1).then(|| decimal(reference as u64, &mut buffer));
754 self.write("<a href=\"#")?;
755 self.prefix()?;
756 self.write("fnref-")?;
757 self.write(key)?;
758 if let Some(number) = number {
759 self.write("-")?;
760 self.write(number)?;
761 }
762 self.write("\" data-footnote-backref=\"\" aria-label=\"")?;
763 self.escaped(
764 self.options
765 .footnote_back_label
766 .as_deref()
767 .unwrap_or("Back to content"),
768 true,
769 )?;
770 self.write("\" class=\"data-footnote-backref\">↩")?;
771 if let Some(number) = number {
772 self.write("<sup>")?;
773 self.write(number)?;
774 self.write("</sup>")?;
775 }
776 self.write("</a>")?;
777 }
778 Ok(())
779 }
780
781 fn footnotes(&mut self) -> Result<(), StreamError> {
782 if self.calls.is_empty() {
783 return Ok(());
784 }
785 self.terminate()?;
786 self.write("<section data-footnotes=\"\" class=\"footnotes\"><")?;
787 let tag = self
788 .options
789 .footnote_label_tag_name
790 .as_deref()
791 .unwrap_or("h2");
792 self.escaped(tag, true)?;
793 self.write(" id=\"footnote-label\" ")?;
794 self.write(
795 self.options
796 .footnote_label_attributes
797 .as_deref()
798 .unwrap_or("class=\"sr-only\""),
799 )?;
800 self.write(">")?;
801 self.escaped(
802 self.options
803 .footnote_label
804 .as_deref()
805 .unwrap_or("Footnotes"),
806 true,
807 )?;
808 self.write("</")?;
809 self.escaped(tag, true)?;
810 self.write(">")?;
811 self.newline()?;
812 self.write("<ol>")?;
813 let mut order = Vec::new();
815 crate::definitions::build_index(&mut order, &self.calls, |call| call.key.as_str());
816 let mut remaining = self.calls.len();
817 for (index, (key, _)) in self.notes.iter().enumerate() {
818 if let Some(call) =
819 crate::definitions::find_index(&order, &self.calls, key, |call| call.key.as_str())
820 {
821 if self.calls[call].definition.is_none() {
822 self.calls[call].definition = Some(index);
823 remaining -= 1;
824 if remaining == 0 {
825 break;
826 }
827 }
828 }
829 }
830 for index in 0..self.calls.len() {
831 let call = &self.calls[index];
832 let note = call.definition.ok_or(StreamError::Unbalanced)?;
833 let count = call.count;
834 let safe = core::mem::take(&mut self.calls[index].id);
835 let value = core::mem::take(&mut self.notes[note].1);
836 self.newline()?;
837 self.write("<li id=\"")?;
838 self.prefix()?;
839 self.write("fn-")?;
840 self.write(&safe)?;
841 self.write("\">")?;
842 self.newline()?;
843 let trimmed = value.trim_end_matches(['\r', '\n']);
844 if let Some(before) = trimmed.strip_suffix("</p>") {
845 self.write(before)?;
846 self.write(" ")?;
847 self.backreferences(&safe, count)?;
848 self.write(&value[before.len()..])?;
849 } else {
850 self.write(&value)?;
851 self.terminate()?;
852 self.backreferences(&safe, count)?;
853 }
854 self.terminate()?;
855 self.write("</li>")?;
856 }
857 self.newline()?;
858 self.write("</ol>")?;
859 self.newline()?;
860 self.write("</section>")?;
861 self.newline()
862 }
863}
864
865pub fn render_events<'a>(
870 source: &str,
871 events: impl IntoIterator<Item = Result<Event<'a>, StreamError>>,
872 options: &CompileOptions,
873 output: &mut impl fmt::Write,
874) -> Result<(), StreamError> {
875 EventHtmlRenderer::new(source, options, output).render(events)
876}
877
878#[cfg(test)]
879mod tests {
880 use super::EventHtmlRenderer;
881 use crate::stream::{Event, EventKind, Tag, Text};
882 use crate::CompileOptions;
883 use alloc::string::String;
884 use core::fmt::{self, Write};
885
886 #[test]
887 fn decimal_formats_zero_boundaries_and_full_width_values() {
888 let mut buffer = [0; 20];
889 for value in [0, 1, 9, 10, 99, 100, u64::from(u32::MAX), u64::MAX] {
890 assert_eq!(
891 super::decimal(value, &mut buffer),
892 alloc::format!("{value}")
893 );
894 }
895 for exponent in 1..=19 {
896 let power = 10_u64.pow(exponent);
897 for value in [power - 1, power, power + 1] {
898 assert_eq!(
899 super::decimal(value, &mut buffer),
900 alloc::format!("{value}")
901 );
902 }
903 }
904 }
905
906 struct Writer {
907 remaining: usize,
908 output: String,
909 }
910 impl Write for Writer {
911 fn write_str(&mut self, value: &str) -> fmt::Result {
912 if self.remaining == 0 {
913 return Err(fmt::Error);
914 }
915 self.remaining -= 1;
916 self.output.push_str(value);
917 Ok(())
918 }
919 }
920
921 #[test]
922 fn filtered_html_preserves_written_prefix_when_a_writer_fails() {
923 let options = CompileOptions {
924 allow_dangerous_html: true,
925 tagfilter: true,
926 ..CompileOptions::default()
927 };
928 for (remaining, expected) in [
929 "",
930 "prefix",
931 "prefix<",
932 "prefix<script>",
933 "prefix<script>�",
934 ]
935 .iter()
936 .copied()
937 .enumerate()
938 {
939 let mut writer = Writer {
940 remaining,
941 output: String::new(),
942 };
943 let mut renderer = EventHtmlRenderer::new("", &options, &mut writer);
944 renderer
945 .event(Event {
946 kind: EventKind::Start(Tag::Document(None)),
947 span: 0..0,
948 })
949 .unwrap();
950 renderer
951 .event(Event {
952 kind: EventKind::Html {
953 value: Text::Literal("prefix<script>\0tail"),
954 block: false,
955 },
956 span: 0..0,
957 })
958 .unwrap_err();
959 assert_eq!(renderer.last, expected.as_bytes().last().copied());
960 assert_eq!(writer.output, expected);
961 }
962 }
963
964 #[test]
965 fn footnote_backreferences_escape_options_and_propagate_each_write_failure() {
966 let options = CompileOptions {
967 footnote_clobber_prefix: Some("<&é".into()),
968 footnote_back_label: Some("back \"<&".into()),
969 ..CompileOptions::default()
970 };
971 let expected = "<a href=\"#<&éfnref-note\" data-footnote-backref=\"\" aria-label=\"back "<&\" class=\"data-footnote-backref\">↩</a> <a href=\"#<&éfnref-note-2\" data-footnote-backref=\"\" aria-label=\"back "<&\" class=\"data-footnote-backref\">↩<sup>2</sup></a>";
972 let mut complete = false;
973 for remaining in 0..64 {
974 let mut writer = Writer {
975 remaining,
976 output: String::new(),
977 };
978 let mut renderer = EventHtmlRenderer::new("", &options, &mut writer);
979 let result = renderer.backreferences("note", 2);
980 let last = renderer.last;
981 assert_eq!(last, writer.output.as_bytes().last().copied());
982 assert!(expected.starts_with(&writer.output));
983 if result.is_ok() {
984 assert_eq!(writer.output, expected);
985 complete = true;
986 }
987 }
988 assert!(
989 complete,
990 "backreferences should finish within the write budget"
991 );
992 }
993}