| //! Standalone markdown rendering. | |
| //! | |
| //! For the (much more common) case of rendering markdown in doc-comments, see | |
| //! [crate::html::markdown]. | |
| //! | |
| //! This is used when [rendering a markdown file to an html file][docs], without processing | |
| //! rust source code. | |
| //! | |
| //! [docs]: https://doc.rust-lang.org/stable/rustdoc/#using-standalone-markdown-files | |
| use std::fmt::{self, Write as _}; | |
| use std::fs::{File, create_dir_all}; | |
| use std::io::prelude::*; | |
| use std::path::PathBuf; | |
| use std::sync::Arc; | |
| use rustc_span::SourceFile; | |
| use rustc_span::edition::Edition; | |
| use crate::config::RenderOptions; | |
| use crate::html::escape::Escape; | |
| use crate::html::markdown; | |
| use crate::html::markdown::{ErrorCodes, HeadingOffset, IdMap, Markdown, MarkdownWithToc}; | |
| /// Separate any lines at the start of the file that begin with `# ` or `%`. | |
| fn extract_leading_metadata(s: &str) -> (Vec<&str>, &str) { | |
| let mut metadata = Vec::new(); | |
| let mut count = 0; | |
| for line in s.lines() { | |
| if line.starts_with("# ") || line.starts_with('%') { | |
| // trim the whitespace after the symbol | |
| metadata.push(line[1..].trim_start()); | |
| count += line.len() + 1; | |
| } else { | |
| return (metadata, &s[count..]); | |
| } | |
| } | |
| // if we're here, then all lines were metadata `# ` or `%` lines. | |
| (metadata, "") | |
| } | |
| /// Render `input` (e.g., "foo.md") into an HTML file in `output` | |
| /// (e.g., output = "bar" => "bar/foo.html"). | |
| /// | |
| /// Requires session globals to be available, for symbol interning. | |
| pub(crate) fn render_and_write( | |
| input: Arc<SourceFile>, | |
| options: RenderOptions, | |
| edition: Edition, | |
| ) -> Result<(), String> { | |
| if let Err(e) = create_dir_all(&options.output) { | |
| return Err(format!("{output}: {e}", output = options.output.display())); | |
| } | |
| let input_path = input.name.clone().into_local_path().unwrap_or(PathBuf::new()); | |
| let mut output = options.output; | |
| output.push(input_path.file_name().unwrap()); | |
| output.set_extension("html"); | |
| let mut css = String::new(); | |
| for name in &options.markdown_css { | |
| write!(css, r#"<link rel="stylesheet" href="{name}">"#) | |
| .expect("Writing to a String can't fail"); | |
| } | |
| let input_str = input.src.as_ref().map(|src| &src[..]).unwrap_or(""); | |
| let playground_url = options.markdown_playground_url.or(options.playground_url); | |
| let playground = playground_url.map(|url| markdown::Playground { crate_name: None, url }); | |
| let (metadata, text) = extract_leading_metadata(&input_str); | |
| if metadata.is_empty() { | |
| return Err("invalid markdown file: no initial lines starting with `# ` or `%`".to_owned()); | |
| } | |
| let mut out = | |
| File::create(&output).map_err(|e| format!("{output}: {e}", output = output.display()))?; | |
| let title = metadata[0]; | |
| let error_codes = ErrorCodes::from(options.unstable_features.is_nightly_build()); | |
| let text = fmt::from_fn(|f| { | |
| if !options.markdown_no_toc { | |
| MarkdownWithToc { | |
| content: text, | |
| links: &[], | |
| ids: &mut IdMap::new(), | |
| error_codes, | |
| edition, | |
| playground: &playground, | |
| } | |
| .write_into(f) | |
| } else { | |
| Markdown { | |
| content: text, | |
| links: &[], | |
| ids: &mut IdMap::new(), | |
| error_codes, | |
| edition, | |
| playground: &playground, | |
| heading_offset: HeadingOffset::H1, | |
| } | |
| .write_into(f) | |
| } | |
| }); | |
| let res = write!( | |
| &mut out, | |
| r#"<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta name="generator" content="rustdoc"> | |
| <title>{title}</title> | |
| {css} | |
| {in_header} | |
| </head> | |
| <body class="rustdoc"> | |
| <!--[if lte IE 8]> | |
| <div class="warning"> | |
| This old browser is unsupported and will most likely display funky | |
| things. | |
| </div> | |
| <![endif]--> | |
| {before_content} | |
| <h1 class="title">{title}</h1> | |
| {text} | |
| {after_content} | |
| </body> | |
| </html>"#, | |
| title = Escape(title), | |
| in_header = options.external_html.in_header, | |
| before_content = options.external_html.before_content, | |
| after_content = options.external_html.after_content, | |
| ); | |
| res.map_err(|e| format!("cannot write to `{output}`: {e}", output = output.display())) | |
| } |