]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/highlight.rs
Added docs to internal_macro const
[rust.git] / src / librustdoc / html / highlight.rs
1 //! Basic syntax highlighting functionality.
2 //!
3 //! This module uses librustc_ast's lexer to provide token-based highlighting for
4 //! the HTML documentation generated by rustdoc.
5 //!
6 //! Use the `render_with_highlighting` to highlight some rust code.
7
8 use crate::clean::PrimitiveType;
9 use crate::html::escape::Escape;
10 use crate::html::render::Context;
11
12 use std::collections::VecDeque;
13 use std::fmt::{Display, Write};
14
15 use rustc_lexer::{LiteralKind, TokenKind};
16 use rustc_span::edition::Edition;
17 use rustc_span::symbol::Symbol;
18 use rustc_span::{BytePos, Span, DUMMY_SP};
19
20 use super::format::{self, Buffer};
21 use super::render::LinkFromSrc;
22
23 /// This type is needed in case we want to render links on items to allow to go to their definition.
24 crate struct ContextInfo<'a, 'b, 'c> {
25     crate context: &'a Context<'b>,
26     /// This span contains the current file we're going through.
27     crate file_span: Span,
28     /// This field is used to know "how far" from the top of the directory we are to link to either
29     /// documentation pages or other source pages.
30     crate root_path: &'c str,
31 }
32
33 /// Highlights `src`, returning the HTML output.
34 crate fn render_with_highlighting(
35     src: &str,
36     out: &mut Buffer,
37     class: Option<&str>,
38     playground_button: Option<&str>,
39     tooltip: Option<(Option<Edition>, &str)>,
40     edition: Edition,
41     extra_content: Option<Buffer>,
42     context_info: Option<ContextInfo<'_, '_, '_>>,
43 ) {
44     debug!("highlighting: ================\n{}\n==============", src);
45     if let Some((edition_info, class)) = tooltip {
46         write!(
47             out,
48             "<div class='information'><div class='tooltip {}'{}>ⓘ</div></div>",
49             class,
50             if let Some(edition_info) = edition_info {
51                 format!(" data-edition=\"{}\"", edition_info)
52             } else {
53                 String::new()
54             },
55         );
56     }
57
58     write_header(out, class, extra_content);
59     write_code(out, &src, edition, context_info);
60     write_footer(out, playground_button);
61 }
62
63 fn write_header(out: &mut Buffer, class: Option<&str>, extra_content: Option<Buffer>) {
64     write!(out, "<div class=\"example-wrap\">");
65     if let Some(extra) = extra_content {
66         out.push_buffer(extra);
67     }
68     if let Some(class) = class {
69         write!(out, "<pre class=\"rust {}\">", class);
70     } else {
71         write!(out, "<pre class=\"rust\">");
72     }
73     write!(out, "<code>");
74 }
75
76 /// Convert the given `src` source code into HTML by adding classes for highlighting.
77 ///
78 /// This code is used to render code blocks (in the documentation) as well as the source code pages.
79 ///
80 /// Some explanations on the last arguments:
81 ///
82 /// In case we are rendering a code block and not a source code file, `context_info` will be `None`.
83 /// To put it more simply: if `context_info` is `None`, the code won't try to generate links to an
84 /// item definition.
85 ///
86 /// More explanations about spans and how we use them here are provided in the
87 fn write_code(
88     out: &mut Buffer,
89     src: &str,
90     edition: Edition,
91     context_info: Option<ContextInfo<'_, '_, '_>>,
92 ) {
93     // This replace allows to fix how the code source with DOS backline characters is displayed.
94     let src = src.replace("\r\n", "\n");
95     Classifier::new(&src, edition, context_info.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP))
96         .highlight(&mut |highlight| {
97             match highlight {
98                 Highlight::Token { text, class } => string(out, Escape(text), class, &context_info),
99                 Highlight::EnterSpan { class } => enter_span(out, class),
100                 Highlight::ExitSpan => exit_span(out),
101             };
102         });
103 }
104
105 fn write_footer(out: &mut Buffer, playground_button: Option<&str>) {
106     writeln!(out, "</code></pre>{}</div>", playground_button.unwrap_or_default());
107 }
108
109 /// How a span of text is classified. Mostly corresponds to token kinds.
110 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
111 enum Class {
112     Comment,
113     DocComment,
114     Attribute,
115     KeyWord,
116     // Keywords that do pointer/reference stuff.
117     RefKeyWord,
118     Self_(Span),
119     Op,
120     Macro,
121     MacroNonTerminal,
122     String,
123     Number,
124     Bool,
125     Ident(Span),
126     Lifetime,
127     PreludeTy,
128     PreludeVal,
129     QuestionMark,
130 }
131
132 impl Class {
133     /// Returns the css class expected by rustdoc for each `Class`.
134     fn as_html(self) -> &'static str {
135         match self {
136             Class::Comment => "comment",
137             Class::DocComment => "doccomment",
138             Class::Attribute => "attribute",
139             Class::KeyWord => "kw",
140             Class::RefKeyWord => "kw-2",
141             Class::Self_(_) => "self",
142             Class::Op => "op",
143             Class::Macro => "macro",
144             Class::MacroNonTerminal => "macro-nonterminal",
145             Class::String => "string",
146             Class::Number => "number",
147             Class::Bool => "bool-val",
148             Class::Ident(_) => "ident",
149             Class::Lifetime => "lifetime",
150             Class::PreludeTy => "prelude-ty",
151             Class::PreludeVal => "prelude-val",
152             Class::QuestionMark => "question-mark",
153         }
154     }
155
156     /// In case this is an item which can be converted into a link to a definition, it'll contain
157     /// a "span" (a tuple representing `(lo, hi)` equivalent of `Span`).
158     fn get_span(self) -> Option<Span> {
159         match self {
160             Self::Ident(sp) | Self::Self_(sp) => Some(sp),
161             _ => None,
162         }
163     }
164 }
165
166 enum Highlight<'a> {
167     Token { text: &'a str, class: Option<Class> },
168     EnterSpan { class: Class },
169     ExitSpan,
170 }
171
172 struct TokenIter<'a> {
173     src: &'a str,
174 }
175
176 impl Iterator for TokenIter<'a> {
177     type Item = (TokenKind, &'a str);
178     fn next(&mut self) -> Option<(TokenKind, &'a str)> {
179         if self.src.is_empty() {
180             return None;
181         }
182         let token = rustc_lexer::first_token(self.src);
183         let (text, rest) = self.src.split_at(token.len);
184         self.src = rest;
185         Some((token.kind, text))
186     }
187 }
188
189 /// Classifies into identifier class; returns `None` if this is a non-keyword identifier.
190 fn get_real_ident_class(text: &str, edition: Edition, allow_path_keywords: bool) -> Option<Class> {
191     let ignore: &[&str] =
192         if allow_path_keywords { &["self", "Self", "super", "crate"] } else { &["self", "Self"] };
193     if ignore.iter().any(|k| *k == text) {
194         return None;
195     }
196     Some(match text {
197         "ref" | "mut" => Class::RefKeyWord,
198         "false" | "true" => Class::Bool,
199         _ if Symbol::intern(text).is_reserved(|| edition) => Class::KeyWord,
200         _ => return None,
201     })
202 }
203
204 /// This iterator comes from the same idea than "Peekable" except that it allows to "peek" more than
205 /// just the next item by using `peek_next`. The `peek` method always returns the next item after
206 /// the current one whereas `peek_next` will return the next item after the last one peeked.
207 ///
208 /// You can use both `peek` and `peek_next` at the same time without problem.
209 struct PeekIter<'a> {
210     stored: VecDeque<(TokenKind, &'a str)>,
211     /// This position is reinitialized when using `next`. It is used in `peek_next`.
212     peek_pos: usize,
213     iter: TokenIter<'a>,
214 }
215
216 impl PeekIter<'a> {
217     fn new(iter: TokenIter<'a>) -> Self {
218         Self { stored: VecDeque::new(), peek_pos: 0, iter }
219     }
220     /// Returns the next item after the current one. It doesn't interfer with `peek_next` output.
221     fn peek(&mut self) -> Option<&(TokenKind, &'a str)> {
222         if self.stored.is_empty() {
223             if let Some(next) = self.iter.next() {
224                 self.stored.push_back(next);
225             }
226         }
227         self.stored.front()
228     }
229     /// Returns the next item after the last one peeked. It doesn't interfer with `peek` output.
230     fn peek_next(&mut self) -> Option<&(TokenKind, &'a str)> {
231         self.peek_pos += 1;
232         if self.peek_pos - 1 < self.stored.len() {
233             self.stored.get(self.peek_pos - 1)
234         } else if let Some(next) = self.iter.next() {
235             self.stored.push_back(next);
236             self.stored.back()
237         } else {
238             None
239         }
240     }
241 }
242
243 impl Iterator for PeekIter<'a> {
244     type Item = (TokenKind, &'a str);
245     fn next(&mut self) -> Option<Self::Item> {
246         self.peek_pos = 0;
247         if let Some(first) = self.stored.pop_front() { Some(first) } else { self.iter.next() }
248     }
249 }
250
251 /// Processes program tokens, classifying strings of text by highlighting
252 /// category (`Class`).
253 struct Classifier<'a> {
254     tokens: PeekIter<'a>,
255     in_attribute: bool,
256     in_macro: bool,
257     in_macro_nonterminal: bool,
258     edition: Edition,
259     byte_pos: u32,
260     file_span: Span,
261     src: &'a str,
262 }
263
264 impl<'a> Classifier<'a> {
265     /// Takes as argument the source code to HTML-ify, the rust edition to use and the source code
266     /// file span which will be used later on by the `span_correspondance_map`.
267     fn new(src: &str, edition: Edition, file_span: Span) -> Classifier<'_> {
268         let tokens = PeekIter::new(TokenIter { src });
269         Classifier {
270             tokens,
271             in_attribute: false,
272             in_macro: false,
273             in_macro_nonterminal: false,
274             edition,
275             byte_pos: 0,
276             file_span,
277             src,
278         }
279     }
280
281     /// Convenient wrapper to create a [`Span`] from a position in the file.
282     fn new_span(&self, lo: u32, text: &str) -> Span {
283         let hi = lo + text.len() as u32;
284         let file_lo = self.file_span.lo();
285         self.file_span.with_lo(file_lo + BytePos(lo)).with_hi(file_lo + BytePos(hi))
286     }
287
288     /// Concatenate colons and idents as one when possible.
289     fn get_full_ident_path(&mut self) -> Vec<(TokenKind, usize, usize)> {
290         let start = self.byte_pos as usize;
291         let mut pos = start;
292         let mut has_ident = false;
293         let edition = self.edition;
294
295         loop {
296             let mut nb = 0;
297             while let Some((TokenKind::Colon, _)) = self.tokens.peek() {
298                 self.tokens.next();
299                 nb += 1;
300             }
301             // Ident path can start with "::" but if we already have content in the ident path,
302             // the "::" is mandatory.
303             if has_ident && nb == 0 {
304                 return vec![(TokenKind::Ident, start, pos)];
305             } else if nb != 0 && nb != 2 {
306                 if has_ident {
307                     return vec![(TokenKind::Ident, start, pos), (TokenKind::Colon, pos, pos + nb)];
308                 } else {
309                     return vec![(TokenKind::Colon, start, pos + nb)];
310                 }
311             }
312
313             if let Some((None, text)) = self.tokens.peek().map(|(token, text)| {
314                 if *token == TokenKind::Ident {
315                     let class = get_real_ident_class(text, edition, true);
316                     (class, text)
317                 } else {
318                     // Doesn't matter which Class we put in here...
319                     (Some(Class::Comment), text)
320                 }
321             }) {
322                 // We only "add" the colon if there is an ident behind.
323                 pos += text.len() + nb;
324                 has_ident = true;
325                 self.tokens.next();
326             } else if nb > 0 && has_ident {
327                 return vec![(TokenKind::Ident, start, pos), (TokenKind::Colon, pos, pos + nb)];
328             } else if nb > 0 {
329                 return vec![(TokenKind::Colon, start, start + nb)];
330             } else if has_ident {
331                 return vec![(TokenKind::Ident, start, pos)];
332             } else {
333                 return Vec::new();
334             }
335         }
336     }
337
338     /// Wraps the tokens iteration to ensure that the `byte_pos` is always correct.
339     ///
340     /// It returns the token's kind, the token as a string and its byte position in the source
341     /// string.
342     fn next(&mut self) -> Option<(TokenKind, &'a str, u32)> {
343         if let Some((kind, text)) = self.tokens.next() {
344             let before = self.byte_pos;
345             self.byte_pos += text.len() as u32;
346             Some((kind, text, before))
347         } else {
348             None
349         }
350     }
351
352     /// Exhausts the `Classifier` writing the output into `sink`.
353     ///
354     /// The general structure for this method is to iterate over each token,
355     /// possibly giving it an HTML span with a class specifying what flavor of
356     /// token is used.
357     fn highlight(mut self, sink: &mut dyn FnMut(Highlight<'a>)) {
358         loop {
359             if self
360                 .tokens
361                 .peek()
362                 .map(|t| matches!(t.0, TokenKind::Colon | TokenKind::Ident))
363                 .unwrap_or(false)
364             {
365                 let tokens = self.get_full_ident_path();
366                 for (token, start, end) in &tokens {
367                     let text = &self.src[*start..*end];
368                     self.advance(*token, text, sink, *start as u32);
369                     self.byte_pos += text.len() as u32;
370                 }
371                 if !tokens.is_empty() {
372                     continue;
373                 }
374             }
375             if let Some((token, text, before)) = self.next() {
376                 self.advance(token, text, sink, before);
377             } else {
378                 break;
379             }
380         }
381     }
382
383     /// Single step of highlighting. This will classify `token`, but maybe also a couple of
384     /// following ones as well.
385     ///
386     /// `before` is the position of the given token in the `source` string and is used as "lo" byte
387     /// in case we want to try to generate a link for this token using the
388     /// `span_correspondance_map`.
389     fn advance(
390         &mut self,
391         token: TokenKind,
392         text: &'a str,
393         sink: &mut dyn FnMut(Highlight<'a>),
394         before: u32,
395     ) {
396         let lookahead = self.peek();
397         let no_highlight = |sink: &mut dyn FnMut(_)| sink(Highlight::Token { text, class: None });
398         let class = match token {
399             TokenKind::Whitespace => return no_highlight(sink),
400             TokenKind::LineComment { doc_style } | TokenKind::BlockComment { doc_style, .. } => {
401                 if doc_style.is_some() {
402                     Class::DocComment
403                 } else {
404                     Class::Comment
405                 }
406             }
407             // Consider this as part of a macro invocation if there was a
408             // leading identifier.
409             TokenKind::Bang if self.in_macro => {
410                 self.in_macro = false;
411                 sink(Highlight::Token { text, class: None });
412                 sink(Highlight::ExitSpan);
413                 return;
414             }
415
416             // Assume that '&' or '*' is the reference or dereference operator
417             // or a reference or pointer type. Unless, of course, it looks like
418             // a logical and or a multiplication operator: `&&` or `* `.
419             TokenKind::Star => match self.peek() {
420                 Some(TokenKind::Whitespace) => Class::Op,
421                 _ => Class::RefKeyWord,
422             },
423             TokenKind::And => match lookahead {
424                 Some(TokenKind::And) => {
425                     self.next();
426                     sink(Highlight::Token { text: "&&", class: Some(Class::Op) });
427                     return;
428                 }
429                 Some(TokenKind::Eq) => {
430                     self.next();
431                     sink(Highlight::Token { text: "&=", class: Some(Class::Op) });
432                     return;
433                 }
434                 Some(TokenKind::Whitespace) => Class::Op,
435                 _ => Class::RefKeyWord,
436             },
437
438             // These can either be operators, or arrows.
439             TokenKind::Eq => match lookahead {
440                 Some(TokenKind::Eq) => {
441                     self.next();
442                     sink(Highlight::Token { text: "==", class: Some(Class::Op) });
443                     return;
444                 }
445                 Some(TokenKind::Gt) => {
446                     self.next();
447                     sink(Highlight::Token { text: "=>", class: None });
448                     return;
449                 }
450                 _ => Class::Op,
451             },
452             TokenKind::Minus if lookahead == Some(TokenKind::Gt) => {
453                 self.next();
454                 sink(Highlight::Token { text: "->", class: None });
455                 return;
456             }
457
458             // Other operators.
459             TokenKind::Minus
460             | TokenKind::Plus
461             | TokenKind::Or
462             | TokenKind::Slash
463             | TokenKind::Caret
464             | TokenKind::Percent
465             | TokenKind::Bang
466             | TokenKind::Lt
467             | TokenKind::Gt => Class::Op,
468
469             // Miscellaneous, no highlighting.
470             TokenKind::Dot
471             | TokenKind::Semi
472             | TokenKind::Comma
473             | TokenKind::OpenParen
474             | TokenKind::CloseParen
475             | TokenKind::OpenBrace
476             | TokenKind::CloseBrace
477             | TokenKind::OpenBracket
478             | TokenKind::At
479             | TokenKind::Tilde
480             | TokenKind::Colon
481             | TokenKind::Unknown => return no_highlight(sink),
482
483             TokenKind::Question => Class::QuestionMark,
484
485             TokenKind::Dollar => match lookahead {
486                 Some(TokenKind::Ident) => {
487                     self.in_macro_nonterminal = true;
488                     Class::MacroNonTerminal
489                 }
490                 _ => return no_highlight(sink),
491             },
492
493             // This might be the start of an attribute. We're going to want to
494             // continue highlighting it as an attribute until the ending ']' is
495             // seen, so skip out early. Down below we terminate the attribute
496             // span when we see the ']'.
497             TokenKind::Pound => {
498                 match lookahead {
499                     // Case 1: #![inner_attribute]
500                     Some(TokenKind::Bang) => {
501                         self.next();
502                         if let Some(TokenKind::OpenBracket) = self.peek() {
503                             self.in_attribute = true;
504                             sink(Highlight::EnterSpan { class: Class::Attribute });
505                         }
506                         sink(Highlight::Token { text: "#", class: None });
507                         sink(Highlight::Token { text: "!", class: None });
508                         return;
509                     }
510                     // Case 2: #[outer_attribute]
511                     Some(TokenKind::OpenBracket) => {
512                         self.in_attribute = true;
513                         sink(Highlight::EnterSpan { class: Class::Attribute });
514                     }
515                     _ => (),
516                 }
517                 return no_highlight(sink);
518             }
519             TokenKind::CloseBracket => {
520                 if self.in_attribute {
521                     self.in_attribute = false;
522                     sink(Highlight::Token { text: "]", class: None });
523                     sink(Highlight::ExitSpan);
524                     return;
525                 }
526                 return no_highlight(sink);
527             }
528             TokenKind::Literal { kind, .. } => match kind {
529                 // Text literals.
530                 LiteralKind::Byte { .. }
531                 | LiteralKind::Char { .. }
532                 | LiteralKind::Str { .. }
533                 | LiteralKind::ByteStr { .. }
534                 | LiteralKind::RawStr { .. }
535                 | LiteralKind::RawByteStr { .. } => Class::String,
536                 // Number literals.
537                 LiteralKind::Float { .. } | LiteralKind::Int { .. } => Class::Number,
538             },
539             TokenKind::Ident | TokenKind::RawIdent if lookahead == Some(TokenKind::Bang) => {
540                 self.in_macro = true;
541                 sink(Highlight::EnterSpan { class: Class::Macro });
542                 sink(Highlight::Token { text, class: None });
543                 return;
544             }
545             TokenKind::Ident => match get_real_ident_class(text, self.edition, false) {
546                 None => match text {
547                     "Option" | "Result" => Class::PreludeTy,
548                     "Some" | "None" | "Ok" | "Err" => Class::PreludeVal,
549                     // "union" is a weak keyword and is only considered as a keyword when declaring
550                     // a union type.
551                     "union" if self.check_if_is_union_keyword() => Class::KeyWord,
552                     _ if self.in_macro_nonterminal => {
553                         self.in_macro_nonterminal = false;
554                         Class::MacroNonTerminal
555                     }
556                     "self" | "Self" => Class::Self_(self.new_span(before, text)),
557                     _ => Class::Ident(self.new_span(before, text)),
558                 },
559                 Some(c) => c,
560             },
561             TokenKind::RawIdent | TokenKind::UnknownPrefix => {
562                 Class::Ident(self.new_span(before, text))
563             }
564             TokenKind::Lifetime { .. } => Class::Lifetime,
565         };
566         // Anything that didn't return above is the simple case where we the
567         // class just spans a single token, so we can use the `string` method.
568         sink(Highlight::Token { text, class: Some(class) });
569     }
570
571     fn peek(&mut self) -> Option<TokenKind> {
572         self.tokens.peek().map(|(token_kind, _text)| *token_kind)
573     }
574
575     fn check_if_is_union_keyword(&mut self) -> bool {
576         while let Some(kind) = self.tokens.peek_next().map(|(token_kind, _text)| token_kind) {
577             if *kind == TokenKind::Whitespace {
578                 continue;
579             }
580             return *kind == TokenKind::Ident;
581         }
582         false
583     }
584 }
585
586 /// Called when we start processing a span of text that should be highlighted.
587 /// The `Class` argument specifies how it should be highlighted.
588 fn enter_span(out: &mut Buffer, klass: Class) {
589     write!(out, "<span class=\"{}\">", klass.as_html());
590 }
591
592 /// Called at the end of a span of highlighted text.
593 fn exit_span(out: &mut Buffer) {
594     out.write_str("</span>");
595 }
596
597 /// Called for a span of text. If the text should be highlighted differently
598 /// from the surrounding text, then the `Class` argument will be a value other
599 /// than `None`.
600 ///
601 /// The following sequences of callbacks are equivalent:
602 /// ```plain
603 ///     enter_span(Foo), string("text", None), exit_span()
604 ///     string("text", Foo)
605 /// ```
606 ///
607 /// The latter can be thought of as a shorthand for the former, which is more
608 /// flexible.
609 ///
610 /// Note that if `context` is not `None` and that the given `klass` contains a `Span`, the function
611 /// will then try to find this `span` in the `span_correspondance_map`. If found, it'll then
612 /// generate a link for this element (which corresponds to where its definition is located).
613 fn string<T: Display>(
614     out: &mut Buffer,
615     text: T,
616     klass: Option<Class>,
617     context_info: &Option<ContextInfo<'_, '_, '_>>,
618 ) {
619     let klass = match klass {
620         None => return write!(out, "{}", text),
621         Some(klass) => klass,
622     };
623     let def_span = match klass.get_span() {
624         Some(d) => d,
625         None => {
626             write!(out, "<span class=\"{}\">{}</span>", klass.as_html(), text);
627             return;
628         }
629     };
630     let mut text_s = text.to_string();
631     if text_s.contains("::") {
632         text_s = text_s.split("::").intersperse("::").fold(String::new(), |mut path, t| {
633             match t {
634                 "self" | "Self" => write!(
635                     &mut path,
636                     "<span class=\"{}\">{}</span>",
637                     Class::Self_(DUMMY_SP).as_html(),
638                     t
639                 ),
640                 "crate" | "super" => {
641                     write!(&mut path, "<span class=\"{}\">{}</span>", Class::KeyWord.as_html(), t)
642                 }
643                 t => write!(&mut path, "{}", t),
644             }
645             .expect("Failed to build source HTML path");
646             path
647         });
648     }
649     if let Some(context_info) = context_info {
650         if let Some(href) =
651             context_info.context.shared.span_correspondance_map.get(&def_span).and_then(|href| {
652                 let context = context_info.context;
653                 // FIXME: later on, it'd be nice to provide two links (if possible) for all items:
654                 // one to the documentation page and one to the source definition.
655                 // FIXME: currently, external items only generate a link to their documentation,
656                 // a link to their definition can be generated using this:
657                 // https://github.com/rust-lang/rust/blob/60f1a2fc4b535ead9c85ce085fdce49b1b097531/src/librustdoc/html/render/context.rs#L315-L338
658                 match href {
659                     LinkFromSrc::Local(span) => context
660                         .href_from_span(*span)
661                         .map(|s| format!("{}{}", context_info.root_path, s)),
662                     LinkFromSrc::External(def_id) => {
663                         format::href_with_root_path(*def_id, context, Some(context_info.root_path))
664                             .ok()
665                             .map(|(url, _, _)| url)
666                     }
667                     LinkFromSrc::Primitive(prim) => format::href_with_root_path(
668                         PrimitiveType::primitive_locations(context.tcx())[&prim],
669                         context,
670                         Some(context_info.root_path),
671                     )
672                     .ok()
673                     .map(|(url, _, _)| url),
674                 }
675             })
676         {
677             write!(out, "<a class=\"{}\" href=\"{}\">{}</a>", klass.as_html(), href, text_s);
678             return;
679         }
680     }
681     write!(out, "<span class=\"{}\">{}</span>", klass.as_html(), text_s);
682 }
683
684 #[cfg(test)]
685 mod tests;