]> git.lizzy.rs Git - rust.git/blob - compiler/rustc_mir/src/util/spanview.rs
use RegionNameHighlight for async fn and closure returns
[rust.git] / compiler / rustc_mir / src / util / spanview.rs
1 use rustc_hir::def_id::DefId;
2 use rustc_middle::hir;
3 use rustc_middle::mir::*;
4 use rustc_middle::ty::TyCtxt;
5 use rustc_session::config::MirSpanview;
6 use rustc_span::{BytePos, Pos, Span, SyntaxContext};
7
8 use std::cmp;
9 use std::io::{self, Write};
10
11 pub const TOOLTIP_INDENT: &str = "    ";
12
13 const CARET: char = '\u{2038}'; // Unicode `CARET`
14 const ANNOTATION_LEFT_BRACKET: char = '\u{298a}'; // Unicode `Z NOTATION RIGHT BINDING BRACKET
15 const ANNOTATION_RIGHT_BRACKET: char = '\u{2989}'; // Unicode `Z NOTATION LEFT BINDING BRACKET`
16 const NEW_LINE_SPAN: &str = "</span>\n<span class=\"line\">";
17 const HEADER: &str = r#"<!DOCTYPE html>
18 <html>
19 <head>
20     <title>coverage_of_if_else - Code Regions</title>
21     <style>
22     .line {
23         counter-increment: line;
24     }
25     .line:before {
26         content: counter(line) ": ";
27         font-family: Menlo, Monaco, monospace;
28         font-style: italic;
29         width: 3.8em;
30         display: inline-block;
31         text-align: right;
32         filter: opacity(50%);
33         -webkit-user-select: none;
34     }
35     .code {
36         color: #dddddd;
37         background-color: #222222;
38         font-family: Menlo, Monaco, monospace;
39         line-height: 1.4em;
40         border-bottom: 2px solid #222222;
41         white-space: pre;
42         display: inline-block;
43     }
44     .odd {
45         background-color: #55bbff;
46         color: #223311;
47     }
48     .even {
49         background-color: #ee7756;
50         color: #551133;
51     }
52     .code {
53         --index: calc(var(--layer) - 1);
54         padding-top: calc(var(--index) * 0.15em);
55         filter:
56             hue-rotate(calc(var(--index) * 25deg))
57             saturate(calc(100% - (var(--index) * 2%)))
58             brightness(calc(100% - (var(--index) * 1.5%)));
59     }
60     .annotation {
61         color: #4444ff;
62         font-family: monospace;
63         font-style: italic;
64         display: none;
65         -webkit-user-select: none;
66     }
67     body:active .annotation {
68         /* requires holding mouse down anywhere on the page */
69         display: inline-block;
70     }
71     span:hover .annotation {
72         /* requires hover over a span ONLY on its first line */
73         display: inline-block;
74     }
75     </style>
76 </head>
77 <body>"#;
78
79 const FOOTER: &str = r#"
80 </body>
81 </html>"#;
82
83 /// Metadata to highlight the span of a MIR BasicBlock, Statement, or Terminator.
84 pub struct SpanViewable {
85     pub span: Span,
86     pub id: String,
87     pub tooltip: String,
88 }
89
90 /// Write a spanview HTML+CSS file to analyze MIR element spans.
91 pub fn write_mir_fn_spanview<'tcx, W>(
92     tcx: TyCtxt<'tcx>,
93     body: &Body<'tcx>,
94     spanview: MirSpanview,
95     w: &mut W,
96 ) -> io::Result<()>
97 where
98     W: Write,
99 {
100     let def_id = body.source.def_id();
101     let body_span = hir_body(tcx, def_id).value.span;
102     let mut span_viewables = Vec::new();
103     for (bb, data) in body.basic_blocks().iter_enumerated() {
104         match spanview {
105             MirSpanview::Statement => {
106                 for (i, statement) in data.statements.iter().enumerate() {
107                     if let Some(span_viewable) =
108                         statement_span_viewable(tcx, body_span, bb, i, statement)
109                     {
110                         span_viewables.push(span_viewable);
111                     }
112                 }
113                 if let Some(span_viewable) = terminator_span_viewable(tcx, body_span, bb, data) {
114                     span_viewables.push(span_viewable);
115                 }
116             }
117             MirSpanview::Terminator => {
118                 if let Some(span_viewable) = terminator_span_viewable(tcx, body_span, bb, data) {
119                     span_viewables.push(span_viewable);
120                 }
121             }
122             MirSpanview::Block => {
123                 if let Some(span_viewable) = block_span_viewable(tcx, body_span, bb, data) {
124                     span_viewables.push(span_viewable);
125                 }
126             }
127         }
128     }
129     write_spanview_document(tcx, def_id, span_viewables, w)?;
130     Ok(())
131 }
132
133 /// Generate a spanview HTML+CSS document for the given local function `def_id`, and a pre-generated
134 /// list `SpanViewable`s.
135 pub fn write_spanview_document<'tcx, W>(
136     tcx: TyCtxt<'tcx>,
137     def_id: DefId,
138     mut span_viewables: Vec<SpanViewable>,
139     w: &mut W,
140 ) -> io::Result<()>
141 where
142     W: Write,
143 {
144     let fn_span = fn_span(tcx, def_id);
145     let mut from_pos = fn_span.lo();
146     let end_pos = fn_span.hi();
147     let source_map = tcx.sess.source_map();
148     let start = source_map.lookup_char_pos(from_pos);
149     let indent_to_initial_start_col = " ".repeat(start.col.to_usize());
150     debug!(
151         "fn_span source is:\n{}{}",
152         indent_to_initial_start_col,
153         source_map.span_to_snippet(fn_span).expect("function should have printable source")
154     );
155     writeln!(w, "{}", HEADER)?;
156     write!(
157         w,
158         r#"<div class="code" style="counter-reset: line {}"><span class="line">{}"#,
159         start.line - 1,
160         indent_to_initial_start_col,
161     )?;
162     span_viewables.sort_unstable_by(|a, b| {
163         let a = a.span;
164         let b = b.span;
165         if a.lo() == b.lo() {
166             // Sort hi() in reverse order so shorter spans are attempted after longer spans.
167             // This should give shorter spans a higher "layer", so they are not covered by
168             // the longer spans.
169             b.hi().partial_cmp(&a.hi())
170         } else {
171             a.lo().partial_cmp(&b.lo())
172         }
173         .unwrap()
174     });
175     let mut ordered_viewables = &span_viewables[..];
176     const LOWEST_VIEWABLE_LAYER: usize = 1;
177     let mut alt = false;
178     while ordered_viewables.len() > 0 {
179         debug!(
180             "calling write_next_viewable with from_pos={}, end_pos={}, and viewables len={}",
181             from_pos.to_usize(),
182             end_pos.to_usize(),
183             ordered_viewables.len()
184         );
185         let (next_from_pos, next_ordered_viewables) = write_next_viewable_with_overlaps(
186             tcx,
187             from_pos,
188             end_pos,
189             ordered_viewables,
190             alt,
191             LOWEST_VIEWABLE_LAYER,
192             w,
193         )?;
194         debug!(
195             "DONE calling write_next_viewable, with new from_pos={}, \
196              and remaining viewables len={}",
197             next_from_pos.to_usize(),
198             next_ordered_viewables.len()
199         );
200         assert!(
201             from_pos != next_from_pos || ordered_viewables.len() != next_ordered_viewables.len(),
202             "write_next_viewable_with_overlaps() must make a state change"
203         );
204         from_pos = next_from_pos;
205         if next_ordered_viewables.len() != ordered_viewables.len() {
206             ordered_viewables = next_ordered_viewables;
207             alt = !alt;
208         }
209     }
210     if from_pos < end_pos {
211         write_coverage_gap(tcx, from_pos, end_pos, w)?;
212     }
213     write!(w, r#"</span></div>"#)?;
214     writeln!(w, "{}", FOOTER)?;
215     Ok(())
216 }
217
218 /// Format a string showing the start line and column, and end line and column within a file.
219 pub fn source_range_no_file<'tcx>(tcx: TyCtxt<'tcx>, span: &Span) -> String {
220     let source_map = tcx.sess.source_map();
221     let start = source_map.lookup_char_pos(span.lo());
222     let end = source_map.lookup_char_pos(span.hi());
223     format!("{}:{}-{}:{}", start.line, start.col.to_usize() + 1, end.line, end.col.to_usize() + 1)
224 }
225
226 pub fn statement_kind_name(statement: &Statement<'_>) -> &'static str {
227     use StatementKind::*;
228     match statement.kind {
229         Assign(..) => "Assign",
230         FakeRead(..) => "FakeRead",
231         SetDiscriminant { .. } => "SetDiscriminant",
232         StorageLive(..) => "StorageLive",
233         StorageDead(..) => "StorageDead",
234         LlvmInlineAsm(..) => "LlvmInlineAsm",
235         Retag(..) => "Retag",
236         AscribeUserType(..) => "AscribeUserType",
237         Coverage(..) => "Coverage",
238         Nop => "Nop",
239     }
240 }
241
242 pub fn terminator_kind_name(term: &Terminator<'_>) -> &'static str {
243     use TerminatorKind::*;
244     match term.kind {
245         Goto { .. } => "Goto",
246         SwitchInt { .. } => "SwitchInt",
247         Resume => "Resume",
248         Abort => "Abort",
249         Return => "Return",
250         Unreachable => "Unreachable",
251         Drop { .. } => "Drop",
252         DropAndReplace { .. } => "DropAndReplace",
253         Call { .. } => "Call",
254         Assert { .. } => "Assert",
255         Yield { .. } => "Yield",
256         GeneratorDrop => "GeneratorDrop",
257         FalseEdge { .. } => "FalseEdge",
258         FalseUnwind { .. } => "FalseUnwind",
259         InlineAsm { .. } => "InlineAsm",
260     }
261 }
262
263 fn statement_span_viewable<'tcx>(
264     tcx: TyCtxt<'tcx>,
265     body_span: Span,
266     bb: BasicBlock,
267     i: usize,
268     statement: &Statement<'tcx>,
269 ) -> Option<SpanViewable> {
270     let span = statement.source_info.span;
271     if !body_span.contains(span) {
272         return None;
273     }
274     let id = format!("{}[{}]", bb.index(), i);
275     let tooltip = tooltip(tcx, &id, span, vec![statement.clone()], &None);
276     Some(SpanViewable { span, id, tooltip })
277 }
278
279 fn terminator_span_viewable<'tcx>(
280     tcx: TyCtxt<'tcx>,
281     body_span: Span,
282     bb: BasicBlock,
283     data: &BasicBlockData<'tcx>,
284 ) -> Option<SpanViewable> {
285     let term = data.terminator();
286     let span = term.source_info.span;
287     if !body_span.contains(span) {
288         return None;
289     }
290     let id = format!("{}:{}", bb.index(), terminator_kind_name(term));
291     let tooltip = tooltip(tcx, &id, span, vec![], &data.terminator);
292     Some(SpanViewable { span, id, tooltip })
293 }
294
295 fn block_span_viewable<'tcx>(
296     tcx: TyCtxt<'tcx>,
297     body_span: Span,
298     bb: BasicBlock,
299     data: &BasicBlockData<'tcx>,
300 ) -> Option<SpanViewable> {
301     let span = compute_block_span(data, body_span);
302     if !body_span.contains(span) {
303         return None;
304     }
305     let id = format!("{}", bb.index());
306     let tooltip = tooltip(tcx, &id, span, data.statements.clone(), &data.terminator);
307     Some(SpanViewable { span, id, tooltip })
308 }
309
310 fn compute_block_span<'tcx>(data: &BasicBlockData<'tcx>, body_span: Span) -> Span {
311     let mut span = data.terminator().source_info.span;
312     for statement_span in data.statements.iter().map(|statement| statement.source_info.span) {
313         // Only combine Spans from the root context, and within the function's body_span.
314         if statement_span.ctxt() == SyntaxContext::root() && body_span.contains(statement_span) {
315             span = span.to(statement_span);
316         }
317     }
318     span
319 }
320
321 /// Recursively process each ordered span. Spans that overlap will have progressively varying
322 /// styles, such as increased padding for each overlap. Non-overlapping adjacent spans will
323 /// have alternating style choices, to help distinguish between them if, visually adjacent.
324 /// The `layer` is incremented for each overlap, and the `alt` bool alternates between true
325 /// and false, for each adjacent non-overlapping span. Source code between the spans (code
326 /// that is not in any coverage region) has neutral styling.
327 fn write_next_viewable_with_overlaps<'tcx, 'b, W>(
328     tcx: TyCtxt<'tcx>,
329     mut from_pos: BytePos,
330     mut to_pos: BytePos,
331     ordered_viewables: &'b [SpanViewable],
332     alt: bool,
333     layer: usize,
334     w: &mut W,
335 ) -> io::Result<(BytePos, &'b [SpanViewable])>
336 where
337     W: Write,
338 {
339     let debug_indent = "  ".repeat(layer);
340     let (viewable, mut remaining_viewables) =
341         ordered_viewables.split_first().expect("ordered_viewables should have some");
342
343     if from_pos < viewable.span.lo() {
344         debug!(
345             "{}advance from_pos to next SpanViewable (from from_pos={} to viewable.span.lo()={} \
346              of {:?}), with to_pos={}",
347             debug_indent,
348             from_pos.to_usize(),
349             viewable.span.lo().to_usize(),
350             viewable.span,
351             to_pos.to_usize()
352         );
353         let hi = cmp::min(viewable.span.lo(), to_pos);
354         write_coverage_gap(tcx, from_pos, hi, w)?;
355         from_pos = hi;
356         if from_pos < viewable.span.lo() {
357             debug!(
358                 "{}EARLY RETURN: stopped before getting to next SpanViewable, at {}",
359                 debug_indent,
360                 from_pos.to_usize()
361             );
362             return Ok((from_pos, ordered_viewables));
363         }
364     }
365
366     if from_pos < viewable.span.hi() {
367         // Set to_pos to the end of this `viewable` to ensure the recursive calls stop writing
368         // with room to print the tail.
369         to_pos = cmp::min(viewable.span.hi(), to_pos);
370         debug!(
371             "{}update to_pos (if not closer) to viewable.span.hi()={}; to_pos is now {}",
372             debug_indent,
373             viewable.span.hi().to_usize(),
374             to_pos.to_usize()
375         );
376     }
377
378     let mut subalt = false;
379     while remaining_viewables.len() > 0 && remaining_viewables[0].span.overlaps(viewable.span) {
380         let overlapping_viewable = &remaining_viewables[0];
381         debug!("{}overlapping_viewable.span={:?}", debug_indent, overlapping_viewable.span);
382
383         let span =
384             trim_span(viewable.span, from_pos, cmp::min(overlapping_viewable.span.lo(), to_pos));
385         let mut some_html_snippet = if from_pos <= viewable.span.hi() || viewable.span.is_empty() {
386             // `viewable` is not yet fully rendered, so start writing the span, up to either the
387             // `to_pos` or the next `overlapping_viewable`, whichever comes first.
388             debug!(
389                 "{}make html_snippet (may not write it if early exit) for partial span {:?} \
390                  of viewable.span {:?}",
391                 debug_indent, span, viewable.span
392             );
393             from_pos = span.hi();
394             make_html_snippet(tcx, span, Some(&viewable))
395         } else {
396             None
397         };
398
399         // Defer writing the HTML snippet (until after early return checks) ONLY for empty spans.
400         // An empty Span with Some(html_snippet) is probably a tail marker. If there is an early
401         // exit, there should be another opportunity to write the tail marker.
402         if !span.is_empty() {
403             if let Some(ref html_snippet) = some_html_snippet {
404                 debug!(
405                     "{}write html_snippet for that partial span of viewable.span {:?}",
406                     debug_indent, viewable.span
407                 );
408                 write_span(html_snippet, &viewable.tooltip, alt, layer, w)?;
409             }
410             some_html_snippet = None;
411         }
412
413         if from_pos < overlapping_viewable.span.lo() {
414             debug!(
415                 "{}EARLY RETURN: from_pos={} has not yet reached the \
416                  overlapping_viewable.span {:?}",
417                 debug_indent,
418                 from_pos.to_usize(),
419                 overlapping_viewable.span
420             );
421             // must have reached `to_pos` before reaching the start of the
422             // `overlapping_viewable.span`
423             return Ok((from_pos, ordered_viewables));
424         }
425
426         if from_pos == to_pos
427             && !(from_pos == overlapping_viewable.span.lo() && overlapping_viewable.span.is_empty())
428         {
429             debug!(
430                 "{}EARLY RETURN: from_pos=to_pos={} and overlapping_viewable.span {:?} is not \
431                  empty, or not from_pos",
432                 debug_indent,
433                 to_pos.to_usize(),
434                 overlapping_viewable.span
435             );
436             // `to_pos` must have occurred before the overlapping viewable. Return
437             // `ordered_viewables` so we can continue rendering the `viewable`, from after the
438             // `to_pos`.
439             return Ok((from_pos, ordered_viewables));
440         }
441
442         if let Some(ref html_snippet) = some_html_snippet {
443             debug!(
444                 "{}write html_snippet for that partial span of viewable.span {:?}",
445                 debug_indent, viewable.span
446             );
447             write_span(html_snippet, &viewable.tooltip, alt, layer, w)?;
448         }
449
450         debug!(
451             "{}recursively calling write_next_viewable with from_pos={}, to_pos={}, \
452              and viewables len={}",
453             debug_indent,
454             from_pos.to_usize(),
455             to_pos.to_usize(),
456             remaining_viewables.len()
457         );
458         // Write the overlaps (and the overlaps' overlaps, if any) up to `to_pos`.
459         let (next_from_pos, next_remaining_viewables) = write_next_viewable_with_overlaps(
460             tcx,
461             from_pos,
462             to_pos,
463             &remaining_viewables,
464             subalt,
465             layer + 1,
466             w,
467         )?;
468         debug!(
469             "{}DONE recursively calling write_next_viewable, with new from_pos={}, and remaining \
470              viewables len={}",
471             debug_indent,
472             next_from_pos.to_usize(),
473             next_remaining_viewables.len()
474         );
475         assert!(
476             from_pos != next_from_pos
477                 || remaining_viewables.len() != next_remaining_viewables.len(),
478             "write_next_viewable_with_overlaps() must make a state change"
479         );
480         from_pos = next_from_pos;
481         if next_remaining_viewables.len() != remaining_viewables.len() {
482             remaining_viewables = next_remaining_viewables;
483             subalt = !subalt;
484         }
485     }
486     if from_pos <= viewable.span.hi() {
487         let span = trim_span(viewable.span, from_pos, to_pos);
488         debug!(
489             "{}After overlaps, writing (end span?) {:?} of viewable.span {:?}",
490             debug_indent, span, viewable.span
491         );
492         if let Some(ref html_snippet) = make_html_snippet(tcx, span, Some(&viewable)) {
493             from_pos = span.hi();
494             write_span(html_snippet, &viewable.tooltip, alt, layer, w)?;
495         }
496     }
497     debug!("{}RETURN: No more overlap", debug_indent);
498     Ok((
499         from_pos,
500         if from_pos < viewable.span.hi() { ordered_viewables } else { remaining_viewables },
501     ))
502 }
503
504 #[inline(always)]
505 fn write_coverage_gap<'tcx, W>(
506     tcx: TyCtxt<'tcx>,
507     lo: BytePos,
508     hi: BytePos,
509     w: &mut W,
510 ) -> io::Result<()>
511 where
512     W: Write,
513 {
514     let span = Span::with_root_ctxt(lo, hi);
515     if let Some(ref html_snippet) = make_html_snippet(tcx, span, None) {
516         write_span(html_snippet, "", false, 0, w)
517     } else {
518         Ok(())
519     }
520 }
521
522 fn write_span<W>(
523     html_snippet: &str,
524     tooltip: &str,
525     alt: bool,
526     layer: usize,
527     w: &mut W,
528 ) -> io::Result<()>
529 where
530     W: Write,
531 {
532     let maybe_alt_class = if layer > 0 {
533         if alt { " odd" } else { " even" }
534     } else {
535         ""
536     };
537     let maybe_title_attr = if !tooltip.is_empty() {
538         format!(" title=\"{}\"", escape_attr(tooltip))
539     } else {
540         "".to_owned()
541     };
542     if layer == 1 {
543         write!(w, "<span>")?;
544     }
545     for (i, line) in html_snippet.lines().enumerate() {
546         if i > 0 {
547             write!(w, "{}", NEW_LINE_SPAN)?;
548         }
549         write!(
550             w,
551             r#"<span class="code{}" style="--layer: {}"{}>{}</span>"#,
552             maybe_alt_class, layer, maybe_title_attr, line
553         )?;
554     }
555     // Check for and translate trailing newlines, because `str::lines()` ignores them
556     if html_snippet.ends_with('\n') {
557         write!(w, "{}", NEW_LINE_SPAN)?;
558     }
559     if layer == 1 {
560         write!(w, "</span>")?;
561     }
562     Ok(())
563 }
564
565 fn make_html_snippet<'tcx>(
566     tcx: TyCtxt<'tcx>,
567     span: Span,
568     some_viewable: Option<&SpanViewable>,
569 ) -> Option<String> {
570     let source_map = tcx.sess.source_map();
571     let snippet = source_map
572         .span_to_snippet(span)
573         .unwrap_or_else(|err| bug!("span_to_snippet error for span {:?}: {:?}", span, err));
574     let html_snippet = if let Some(viewable) = some_viewable {
575         let is_head = span.lo() == viewable.span.lo();
576         let is_tail = span.hi() == viewable.span.hi();
577         let mut labeled_snippet = if is_head {
578             format!(r#"<span class="annotation">{}{}</span>"#, viewable.id, ANNOTATION_LEFT_BRACKET)
579         } else {
580             "".to_owned()
581         };
582         if span.is_empty() {
583             if is_head && is_tail {
584                 labeled_snippet.push(CARET);
585             }
586         } else {
587             labeled_snippet.push_str(&escape_html(&snippet));
588         };
589         if is_tail {
590             labeled_snippet.push_str(&format!(
591                 r#"<span class="annotation">{}{}</span>"#,
592                 ANNOTATION_RIGHT_BRACKET, viewable.id
593             ));
594         }
595         labeled_snippet
596     } else {
597         escape_html(&snippet)
598     };
599     if html_snippet.is_empty() { None } else { Some(html_snippet) }
600 }
601
602 fn tooltip<'tcx>(
603     tcx: TyCtxt<'tcx>,
604     spanview_id: &str,
605     span: Span,
606     statements: Vec<Statement<'tcx>>,
607     terminator: &Option<Terminator<'tcx>>,
608 ) -> String {
609     let source_map = tcx.sess.source_map();
610     let mut text = Vec::new();
611     text.push(format!("{}: {}:", spanview_id, &source_map.span_to_string(span)));
612     for statement in statements {
613         let source_range = source_range_no_file(tcx, &statement.source_info.span);
614         text.push(format!(
615             "\n{}{}: {}: {}",
616             TOOLTIP_INDENT,
617             source_range,
618             statement_kind_name(&statement),
619             format!("{:?}", statement)
620         ));
621     }
622     if let Some(term) = terminator {
623         let source_range = source_range_no_file(tcx, &term.source_info.span);
624         text.push(format!(
625             "\n{}{}: {}: {:?}",
626             TOOLTIP_INDENT,
627             source_range,
628             terminator_kind_name(term),
629             term.kind
630         ));
631     }
632     text.join("")
633 }
634
635 fn trim_span(span: Span, from_pos: BytePos, to_pos: BytePos) -> Span {
636     trim_span_hi(trim_span_lo(span, from_pos), to_pos)
637 }
638
639 fn trim_span_lo(span: Span, from_pos: BytePos) -> Span {
640     if from_pos <= span.lo() { span } else { span.with_lo(cmp::min(span.hi(), from_pos)) }
641 }
642
643 fn trim_span_hi(span: Span, to_pos: BytePos) -> Span {
644     if to_pos >= span.hi() { span } else { span.with_hi(cmp::max(span.lo(), to_pos)) }
645 }
646
647 fn fn_span<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId) -> Span {
648     let hir_id =
649         tcx.hir().local_def_id_to_hir_id(def_id.as_local().expect("expected DefId is local"));
650     let fn_decl_span = tcx.hir().span(hir_id);
651     let body_span = hir_body(tcx, def_id).value.span;
652     debug_assert_eq!(fn_decl_span.ctxt(), body_span.ctxt());
653     fn_decl_span.to(body_span)
654 }
655
656 fn hir_body<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId) -> &'tcx rustc_hir::Body<'tcx> {
657     let hir_node = tcx.hir().get_if_local(def_id).expect("expected DefId is local");
658     let fn_body_id = hir::map::associated_body(hir_node).expect("HIR node is a function with body");
659     tcx.hir().body(fn_body_id)
660 }
661
662 fn escape_html(s: &str) -> String {
663     s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
664 }
665
666 fn escape_attr(s: &str) -> String {
667     s.replace("&", "&amp;")
668         .replace("\"", "&quot;")
669         .replace("'", "&#39;")
670         .replace("<", "&lt;")
671         .replace(">", "&gt;")
672 }