Skip to content

Commit ba32d8b

Browse files
Rollup merge of #136829 - GuillaumeGomez:move-line-numbers-into-code, r=notriddle
[rustdoc] Move line numbers into the `<code>` directly Fixes #84242. This is the first for adding support for #127334 and also for another feature I'm working on. A side-effect of this change is that it also fixes source code pages display in lynx since they're not directly in the source code. To allow having code wrapping, the grid approach doesn't work as the line numbers are in their own container, so we need to move them into the code. Now with this, it becomes much simpler to do what we want (with CSS mostly). One downside: the highlighting became more complex and slow as we need to generate some extra HTML tags directly into the highlighting process. However that also allows to not have a huge HTML size increase. You can test the result [here](https://rustdoc.crud.net/imperio/move-line-numbers-into-code/scrape_examples/fn.test_many.html) and [here](https://rustdoc.crud.net/imperio/move-line-numbers-into-code/src/scrape_examples/lib.rs.html#10). The appearance should have close to no changes. r? ``@notriddle``
2 parents 1073aea + b594b9f commit ba32d8b

18 files changed

+280
-222
lines changed

Diff for: src/librustdoc/html/highlight.rs

+101-11
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub(crate) fn render_example_with_highlighting(
5454
extra_classes: &[String],
5555
) {
5656
write_header(out, "rust-example-rendered", None, tooltip, extra_classes);
57-
write_code(out, src, None, None);
57+
write_code(out, src, None, None, None);
5858
write_footer(out, playground_button);
5959
}
6060

@@ -150,6 +150,7 @@ struct TokenHandler<'a, 'tcx, F: Write> {
150150
/// used to generate links.
151151
pending_elems: Vec<(&'a str, Option<Class>)>,
152152
href_context: Option<HrefContext<'a, 'tcx>>,
153+
write_line_number: fn(&mut F, u32, &'static str),
153154
}
154155

155156
impl<F: Write> TokenHandler<'_, '_, F> {
@@ -182,7 +183,14 @@ impl<F: Write> TokenHandler<'_, '_, F> {
182183
&& can_merge(current_class, Some(*parent_class), "")
183184
{
184185
for (text, class) in self.pending_elems.iter() {
185-
string(self.out, EscapeBodyText(text), *class, &self.href_context, false);
186+
string(
187+
self.out,
188+
EscapeBodyText(text),
189+
*class,
190+
&self.href_context,
191+
false,
192+
self.write_line_number,
193+
);
186194
}
187195
} else {
188196
// We only want to "open" the tag ourselves if we have more than one pending and if the
@@ -204,6 +212,7 @@ impl<F: Write> TokenHandler<'_, '_, F> {
204212
*class,
205213
&self.href_context,
206214
close_tag.is_none(),
215+
self.write_line_number,
207216
);
208217
}
209218
if let Some(close_tag) = close_tag {
@@ -213,6 +222,11 @@ impl<F: Write> TokenHandler<'_, '_, F> {
213222
self.pending_elems.clear();
214223
true
215224
}
225+
226+
#[inline]
227+
fn write_line_number(&mut self, line: u32, extra: &'static str) {
228+
(self.write_line_number)(&mut self.out, line, extra);
229+
}
216230
}
217231

218232
impl<F: Write> Drop for TokenHandler<'_, '_, F> {
@@ -226,6 +240,43 @@ impl<F: Write> Drop for TokenHandler<'_, '_, F> {
226240
}
227241
}
228242

243+
fn write_scraped_line_number(out: &mut impl Write, line: u32, extra: &'static str) {
244+
// https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag#data-nosnippet-attr
245+
// Do not show "1 2 3 4 5 ..." in web search results.
246+
write!(out, "{extra}<span data-nosnippet>{line}</span>",).unwrap();
247+
}
248+
249+
fn write_line_number(out: &mut impl Write, line: u32, extra: &'static str) {
250+
// https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag#data-nosnippet-attr
251+
// Do not show "1 2 3 4 5 ..." in web search results.
252+
write!(out, "{extra}<a href=#{line} id={line} data-nosnippet>{line}</a>",).unwrap();
253+
}
254+
255+
fn empty_line_number(out: &mut impl Write, _: u32, extra: &'static str) {
256+
out.write_str(extra).unwrap();
257+
}
258+
259+
#[derive(Clone, Copy)]
260+
pub(super) struct LineInfo {
261+
pub(super) start_line: u32,
262+
max_lines: u32,
263+
pub(super) is_scraped_example: bool,
264+
}
265+
266+
impl LineInfo {
267+
pub(super) fn new(max_lines: u32) -> Self {
268+
Self { start_line: 1, max_lines: max_lines + 1, is_scraped_example: false }
269+
}
270+
271+
pub(super) fn new_scraped(max_lines: u32, start_line: u32) -> Self {
272+
Self {
273+
start_line: start_line + 1,
274+
max_lines: max_lines + start_line + 1,
275+
is_scraped_example: true,
276+
}
277+
}
278+
}
279+
229280
/// Convert the given `src` source code into HTML by adding classes for highlighting.
230281
///
231282
/// This code is used to render code blocks (in the documentation) as well as the source code pages.
@@ -242,6 +293,7 @@ pub(super) fn write_code(
242293
src: &str,
243294
href_context: Option<HrefContext<'_, '_>>,
244295
decoration_info: Option<&DecorationInfo>,
296+
line_info: Option<LineInfo>,
245297
) {
246298
// This replace allows to fix how the code source with DOS backline characters is displayed.
247299
let src = src.replace("\r\n", "\n");
@@ -252,6 +304,23 @@ pub(super) fn write_code(
252304
current_class: None,
253305
pending_elems: Vec::new(),
254306
href_context,
307+
write_line_number: match line_info {
308+
Some(line_info) => {
309+
if line_info.is_scraped_example {
310+
write_scraped_line_number
311+
} else {
312+
write_line_number
313+
}
314+
}
315+
None => empty_line_number,
316+
},
317+
};
318+
319+
let (mut line, max_lines) = if let Some(line_info) = line_info {
320+
token_handler.write_line_number(line_info.start_line, "");
321+
(line_info.start_line, line_info.max_lines)
322+
} else {
323+
(0, u32::MAX)
255324
};
256325

257326
Classifier::new(
@@ -282,7 +351,14 @@ pub(super) fn write_code(
282351
if need_current_class_update {
283352
token_handler.current_class = class.map(Class::dummy);
284353
}
285-
token_handler.pending_elems.push((text, class));
354+
if text == "\n" {
355+
line += 1;
356+
if line < max_lines {
357+
token_handler.pending_elems.push((text, Some(Class::Backline(line))));
358+
}
359+
} else {
360+
token_handler.pending_elems.push((text, class));
361+
}
286362
}
287363
Highlight::EnterSpan { class } => {
288364
let mut should_add = true;
@@ -348,6 +424,7 @@ enum Class {
348424
PreludeVal(Span),
349425
QuestionMark,
350426
Decoration(&'static str),
427+
Backline(u32),
351428
}
352429

353430
impl Class {
@@ -396,6 +473,7 @@ impl Class {
396473
Class::PreludeVal(_) => "prelude-val",
397474
Class::QuestionMark => "question-mark",
398475
Class::Decoration(kind) => kind,
476+
Class::Backline(_) => "",
399477
}
400478
}
401479

@@ -419,7 +497,8 @@ impl Class {
419497
| Self::Bool
420498
| Self::Lifetime
421499
| Self::QuestionMark
422-
| Self::Decoration(_) => None,
500+
| Self::Decoration(_)
501+
| Self::Backline(_) => None,
423502
}
424503
}
425504
}
@@ -694,8 +773,13 @@ impl<'src> Classifier<'src> {
694773
) {
695774
let lookahead = self.peek();
696775
let no_highlight = |sink: &mut dyn FnMut(_)| sink(Highlight::Token { text, class: None });
776+
let whitespace = |sink: &mut dyn FnMut(_)| {
777+
for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
778+
sink(Highlight::Token { text: part, class: None });
779+
}
780+
};
697781
let class = match token {
698-
TokenKind::Whitespace => return no_highlight(sink),
782+
TokenKind::Whitespace => return whitespace(sink),
699783
TokenKind::LineComment { doc_style } | TokenKind::BlockComment { doc_style, .. } => {
700784
if doc_style.is_some() {
701785
Class::DocComment
@@ -716,7 +800,7 @@ impl<'src> Classifier<'src> {
716800
// or a reference or pointer type. Unless, of course, it looks like
717801
// a logical and or a multiplication operator: `&&` or `* `.
718802
TokenKind::Star => match self.tokens.peek() {
719-
Some((TokenKind::Whitespace, _)) => return no_highlight(sink),
803+
Some((TokenKind::Whitespace, _)) => return whitespace(sink),
720804
Some((TokenKind::Ident, "mut")) => {
721805
self.next();
722806
sink(Highlight::Token { text: "*mut", class: Some(Class::RefKeyWord) });
@@ -740,7 +824,7 @@ impl<'src> Classifier<'src> {
740824
sink(Highlight::Token { text: "&=", class: None });
741825
return;
742826
}
743-
Some((TokenKind::Whitespace, _)) => return no_highlight(sink),
827+
Some((TokenKind::Whitespace, _)) => return whitespace(sink),
744828
Some((TokenKind::Ident, "mut")) => {
745829
self.next();
746830
sink(Highlight::Token { text: "&mut", class: Some(Class::RefKeyWord) });
@@ -887,7 +971,9 @@ impl<'src> Classifier<'src> {
887971
};
888972
// Anything that didn't return above is the simple case where we the
889973
// class just spans a single token, so we can use the `string` method.
890-
sink(Highlight::Token { text, class: Some(class) });
974+
for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
975+
sink(Highlight::Token { text: part, class: Some(class) });
976+
}
891977
}
892978

893979
fn peek(&mut self) -> Option<TokenKind> {
@@ -939,14 +1025,18 @@ fn exit_span(out: &mut impl Write, closing_tag: &str) {
9391025
/// Note that if `context` is not `None` and that the given `klass` contains a `Span`, the function
9401026
/// will then try to find this `span` in the `span_correspondence_map`. If found, it'll then
9411027
/// generate a link for this element (which corresponds to where its definition is located).
942-
fn string<T: Display>(
943-
out: &mut impl Write,
1028+
fn string<T: Display, W: Write>(
1029+
out: &mut W,
9441030
text: T,
9451031
klass: Option<Class>,
9461032
href_context: &Option<HrefContext<'_, '_>>,
9471033
open_tag: bool,
1034+
write_line_number_callback: fn(&mut W, u32, &'static str),
9481035
) {
949-
if let Some(closing_tag) = string_without_closing_tag(out, text, klass, href_context, open_tag)
1036+
if let Some(Class::Backline(line)) = klass {
1037+
write_line_number_callback(out, line, "\n");
1038+
} else if let Some(closing_tag) =
1039+
string_without_closing_tag(out, text, klass, href_context, open_tag)
9501040
{
9511041
out.write_str(closing_tag).unwrap();
9521042
}

Diff for: src/librustdoc/html/highlight/tests.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ fn test_html_highlighting() {
2323
let src = include_str!("fixtures/sample.rs");
2424
let html = {
2525
let mut out = Buffer::new();
26-
write_code(&mut out, src, None, None);
26+
write_code(&mut out, src, None, None, None);
2727
format!("{STYLE}<pre><code>{}</code></pre>\n", out.into_inner())
2828
};
2929
expect_file!["fixtures/sample.html"].assert_eq(&html);
@@ -37,7 +37,7 @@ fn test_dos_backline() {
3737
println!(\"foo\");\r\n\
3838
}\r\n";
3939
let mut html = Buffer::new();
40-
write_code(&mut html, src, None, None);
40+
write_code(&mut html, src, None, None, None);
4141
expect_file!["fixtures/dos_line.html"].assert_eq(&html.into_inner());
4242
});
4343
}
@@ -51,7 +51,7 @@ let x = super::b::foo;
5151
let y = Self::whatever;";
5252

5353
let mut html = Buffer::new();
54-
write_code(&mut html, src, None, None);
54+
write_code(&mut html, src, None, None, None);
5555
expect_file!["fixtures/highlight.html"].assert_eq(&html.into_inner());
5656
});
5757
}
@@ -61,7 +61,7 @@ fn test_union_highlighting() {
6161
create_default_session_globals_then(|| {
6262
let src = include_str!("fixtures/union.rs");
6363
let mut html = Buffer::new();
64-
write_code(&mut html, src, None, None);
64+
write_code(&mut html, src, None, None, None);
6565
expect_file!["fixtures/union.html"].assert_eq(&html.into_inner());
6666
});
6767
}
@@ -78,7 +78,7 @@ let a = 4;";
7878
decorations.insert("example2", vec![(22, 32)]);
7979

8080
let mut html = Buffer::new();
81-
write_code(&mut html, src, None, Some(&DecorationInfo(decorations)));
81+
write_code(&mut html, src, None, Some(&DecorationInfo(decorations)), None);
8282
expect_file!["fixtures/decorations.html"].assert_eq(&html.into_inner());
8383
});
8484
}

Diff for: src/librustdoc/html/sources.rs

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use std::cell::RefCell;
22
use std::ffi::OsStr;
3-
use std::ops::RangeInclusive;
43
use std::path::{Component, Path, PathBuf};
54
use std::{fmt, fs};
65

@@ -303,16 +302,16 @@ pub(crate) struct ScrapedInfo<'a> {
303302
#[template(path = "scraped_source.html")]
304303
struct ScrapedSource<'a, Code: std::fmt::Display> {
305304
info: ScrapedInfo<'a>,
306-
lines: RangeInclusive<usize>,
307305
code_html: Code,
306+
max_nb_digits: u32,
308307
}
309308

310309
#[derive(Template)]
311310
#[template(path = "source.html")]
312311
struct Source<Code: std::fmt::Display> {
313-
lines: RangeInclusive<usize>,
314312
code_html: Code,
315313
file_path: Option<(String, String)>,
314+
max_nb_digits: u32,
316315
}
317316

318317
pub(crate) enum SourceContext<'a> {
@@ -331,6 +330,15 @@ pub(crate) fn print_src(
331330
decoration_info: &highlight::DecorationInfo,
332331
source_context: SourceContext<'_>,
333332
) {
333+
let mut lines = s.lines().count();
334+
let line_info = if let SourceContext::Embedded(ref info) = source_context {
335+
highlight::LineInfo::new_scraped(lines as u32, info.offset as u32)
336+
} else {
337+
highlight::LineInfo::new(lines as u32)
338+
};
339+
if line_info.is_scraped_example {
340+
lines += line_info.start_line as usize;
341+
}
334342
let code = fmt::from_fn(move |fmt| {
335343
let current_href = context
336344
.href_from_span(clean::Span::new(file_span), false)
@@ -340,13 +348,13 @@ pub(crate) fn print_src(
340348
s,
341349
Some(highlight::HrefContext { context, file_span, root_path, current_href }),
342350
Some(decoration_info),
351+
Some(line_info),
343352
);
344353
Ok(())
345354
});
346-
let lines = s.lines().count();
355+
let max_nb_digits = if lines > 0 { lines.ilog(10) + 1 } else { 1 };
347356
match source_context {
348357
SourceContext::Standalone { file_path } => Source {
349-
lines: (1..=lines),
350358
code_html: code,
351359
file_path: if let Some(file_name) = file_path.file_name()
352360
&& let Some(file_path) = file_path.parent()
@@ -355,12 +363,14 @@ pub(crate) fn print_src(
355363
} else {
356364
None
357365
},
366+
max_nb_digits,
358367
}
359368
.render_into(&mut writer)
360369
.unwrap(),
361370
SourceContext::Embedded(info) => {
362-
let lines = (1 + info.offset)..=(lines + info.offset);
363-
ScrapedSource { info, lines, code_html: code }.render_into(&mut writer).unwrap();
371+
ScrapedSource { info, code_html: code, max_nb_digits }
372+
.render_into(&mut writer)
373+
.unwrap();
364374
}
365375
};
366376
}

0 commit comments

Comments
 (0)