1use std::fmt::{self, Write as _};
12use std::fs::{File, create_dir_all, read_to_string};
13use std::io::prelude::*;
14use std::path::Path;
15
16use rustc_span::edition::Edition;
17
18use crate::config::RenderOptions;
19use crate::html::escape::Escape;
20use crate::html::markdown;
21use crate::html::markdown::{ErrorCodes, HeadingOffset, IdMap, Markdown, MarkdownWithToc};
22
23fn extract_leading_metadata(s: &str) -> (Vec<&str>, &str) {
25 let mut metadata = Vec::new();
26 let mut count = 0;
27
28 for line in s.lines() {
29 if line.starts_with("# ") || line.starts_with('%') {
30 metadata.push(line[1..].trim_start());
32 count += line.len() + 1;
33 } else {
34 return (metadata, &s[count..]);
35 }
36 }
37
38 (metadata, "")
40}
41
42pub(crate) fn render_and_write<P: AsRef<Path>>(
47 input: P,
48 options: RenderOptions,
49 edition: Edition,
50) -> Result<(), String> {
51 if let Err(e) = create_dir_all(&options.output) {
52 return Err(format!("{output}: {e}", output = options.output.display()));
53 }
54
55 let input = input.as_ref();
56 let mut output = options.output;
57 output.push(input.file_name().unwrap());
58 output.set_extension("html");
59
60 let mut css = String::new();
61 for name in &options.markdown_css {
62 write!(css, r#"<link rel="stylesheet" href="{name}">"#)
63 .expect("Writing to a String can't fail");
64 }
65
66 let input_str =
67 read_to_string(input).map_err(|err| format!("{input}: {err}", input = input.display()))?;
68 let playground_url = options.markdown_playground_url.or(options.playground_url);
69 let playground = playground_url.map(|url| markdown::Playground { crate_name: None, url });
70
71 let mut out =
72 File::create(&output).map_err(|e| format!("{output}: {e}", output = output.display()))?;
73
74 let (metadata, text) = extract_leading_metadata(&input_str);
75 if metadata.is_empty() {
76 return Err("invalid markdown file: no initial lines starting with `# ` or `%`".to_owned());
77 }
78 let title = metadata[0];
79
80 let error_codes = ErrorCodes::from(options.unstable_features.is_nightly_build());
81 let text = fmt::from_fn(|f| {
82 if !options.markdown_no_toc {
83 MarkdownWithToc {
84 content: text,
85 links: &[],
86 ids: &mut IdMap::new(),
87 error_codes,
88 edition,
89 playground: &playground,
90 }
91 .write_into(f)
92 } else {
93 Markdown {
94 content: text,
95 links: &[],
96 ids: &mut IdMap::new(),
97 error_codes,
98 edition,
99 playground: &playground,
100 heading_offset: HeadingOffset::H1,
101 }
102 .write_into(f)
103 }
104 });
105
106 let res = write!(
107 &mut out,
108 r#"<!DOCTYPE html>
109<html lang="en">
110<head>
111 <meta charset="utf-8">
112 <meta name="viewport" content="width=device-width, initial-scale=1.0">
113 <meta name="generator" content="rustdoc">
114 <title>{title}</title>
115
116 {css}
117 {in_header}
118</head>
119<body class="rustdoc">
120 <!--[if lte IE 8]>
121 <div class="warning">
122 This old browser is unsupported and will most likely display funky
123 things.
124 </div>
125 <![endif]-->
126
127 {before_content}
128 <h1 class="title">{title}</h1>
129 {text}
130 {after_content}
131</body>
132</html>"#,
133 title = Escape(title),
134 in_header = options.external_html.in_header,
135 before_content = options.external_html.before_content,
136 after_content = options.external_html.after_content,
137 );
138
139 res.map_err(|e| format!("cannot write to `{output}`: {e}", output = output.display()))
140}