// Copyright 2013-2014 The Rust Project Developers. See the COPYRIGHT // file at the top-level directory of this distribution and at // http://rust-lang.org/COPYRIGHT. // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! Markdown formatting for rustdoc //! //! This module implements markdown formatting through the hoedown C-library //! (bundled into the rust runtime). This module self-contains the C bindings //! and necessary legwork to render markdown, and exposes all of the //! functionality through a unit-struct, `Markdown`, which has an implementation //! of `fmt::Display`. Example usage: //! //! ```rust,ignore //! use rustdoc::html::markdown::Markdown; //! //! let s = "My *markdown* _text_"; //! let html = format!("{}", Markdown(s)); //! // ... something using html //! ``` #![allow(non_camel_case_types)] use libc; use rustc::session::config::get_unstable_features_setting; use std::ascii::AsciiExt; use std::cell::RefCell; use std::default::Default; use std::ffi::CString; use std::fmt; use std::slice; use std::str; use syntax::feature_gate::UnstableFeatures; use html::render::derive_id; use html::toc::TocBuilder; use html::highlight; use html::escape::Escape; use test; /// A unit struct which has the `fmt::Display` trait implemented. When /// formatted, this struct will emit the HTML corresponding to the rendered /// version of the contained markdown string. pub struct Markdown<'a>(pub &'a str); /// A unit struct like `Markdown`, that renders the markdown with a /// table of contents. pub struct MarkdownWithToc<'a>(pub &'a str); const DEF_OUNIT: libc::size_t = 64; const HOEDOWN_EXT_NO_INTRA_EMPHASIS: libc::c_uint = 1 << 11; const HOEDOWN_EXT_TABLES: libc::c_uint = 1 << 0; const HOEDOWN_EXT_FENCED_CODE: libc::c_uint = 1 << 1; const HOEDOWN_EXT_AUTOLINK: libc::c_uint = 1 << 3; const HOEDOWN_EXT_STRIKETHROUGH: libc::c_uint = 1 << 4; const HOEDOWN_EXT_SUPERSCRIPT: libc::c_uint = 1 << 8; const HOEDOWN_EXT_FOOTNOTES: libc::c_uint = 1 << 2; const HOEDOWN_EXTENSIONS: libc::c_uint = HOEDOWN_EXT_NO_INTRA_EMPHASIS | HOEDOWN_EXT_TABLES | HOEDOWN_EXT_FENCED_CODE | HOEDOWN_EXT_AUTOLINK | HOEDOWN_EXT_STRIKETHROUGH | HOEDOWN_EXT_SUPERSCRIPT | HOEDOWN_EXT_FOOTNOTES; enum hoedown_document {} type blockcodefn = extern "C" fn(*mut hoedown_buffer, *const hoedown_buffer, *const hoedown_buffer, *const hoedown_renderer_data); type blockquotefn = extern "C" fn(*mut hoedown_buffer, *const hoedown_buffer, *const hoedown_renderer_data); type headerfn = extern "C" fn(*mut hoedown_buffer, *const hoedown_buffer, libc::c_int, *const hoedown_renderer_data); type blockhtmlfn = extern "C" fn(*mut hoedown_buffer, *const hoedown_buffer, *const hoedown_renderer_data); type codespanfn = extern "C" fn(*mut hoedown_buffer, *const hoedown_buffer, *const hoedown_renderer_data) -> libc::c_int; type linkfn = extern "C" fn (*mut hoedown_buffer, *const hoedown_buffer, *const hoedown_buffer, *const hoedown_buffer, *const hoedown_renderer_data) -> libc::c_int; type entityfn = extern "C" fn (*mut hoedown_buffer, *const hoedown_buffer, *const hoedown_renderer_data); type normaltextfn = extern "C" fn(*mut hoedown_buffer, *const hoedown_buffer, *const hoedown_renderer_data); #[repr(C)] struct hoedown_renderer_data { opaque: *mut libc::c_void, } #[repr(C)] struct hoedown_renderer { opaque: *mut libc::c_void, blockcode: Option, blockquote: Option, header: Option, other_block_level_callbacks: [libc::size_t; 11], blockhtml: Option, /* span level callbacks - NULL or return 0 prints the span verbatim */ autolink: libc::size_t, // unused codespan: Option, other_span_level_callbacks_1: [libc::size_t; 7], link: Option, other_span_level_callbacks_2: [libc::size_t; 6], /* low level callbacks - NULL copies input directly into the output */ entity: Option, normal_text: Option, /* header and footer */ other_callbacks: [libc::size_t; 2], } #[repr(C)] struct hoedown_html_renderer_state { opaque: *mut libc::c_void, toc_data: html_toc_data, flags: libc::c_uint, link_attributes: Option, } #[repr(C)] struct html_toc_data { header_count: libc::c_int, current_level: libc::c_int, level_offset: libc::c_int, nesting_level: libc::c_int, } struct MyOpaque { dfltblk: extern "C" fn(*mut hoedown_buffer, *const hoedown_buffer, *const hoedown_buffer, *const hoedown_renderer_data), toc_builder: Option, } #[repr(C)] struct hoedown_buffer { data: *const u8, size: libc::size_t, asize: libc::size_t, unit: libc::size_t, } // hoedown FFI #[link(name = "hoedown", kind = "static")] #[cfg(not(cargobuild))] extern {} extern { fn hoedown_html_renderer_new(render_flags: libc::c_uint, nesting_level: libc::c_int) -> *mut hoedown_renderer; fn hoedown_html_renderer_free(renderer: *mut hoedown_renderer); fn hoedown_document_new(rndr: *const hoedown_renderer, extensions: libc::c_uint, max_nesting: libc::size_t) -> *mut hoedown_document; fn hoedown_document_render(doc: *mut hoedown_document, ob: *mut hoedown_buffer, document: *const u8, doc_size: libc::size_t); fn hoedown_document_free(md: *mut hoedown_document); fn hoedown_buffer_new(unit: libc::size_t) -> *mut hoedown_buffer; fn hoedown_buffer_put(b: *mut hoedown_buffer, c: *const libc::c_char, n: libc::size_t); fn hoedown_buffer_puts(b: *mut hoedown_buffer, c: *const libc::c_char); fn hoedown_buffer_free(b: *mut hoedown_buffer); } // hoedown_buffer helpers impl hoedown_buffer { fn as_bytes(&self) -> &[u8] { unsafe { slice::from_raw_parts(self.data, self.size as usize) } } } /// Returns Some(code) if `s` is a line that should be stripped from /// documentation but used in example code. `code` is the portion of /// `s` that should be used in tests. (None for lines that should be /// left as-is.) fn stripped_filtered_line<'a>(s: &'a str) -> Option<&'a str> { let trimmed = s.trim(); if trimmed == "#" { Some("") } else if trimmed.starts_with("# ") { Some(&trimmed[2..]) } else { None } } /// Returns a new string with all consecutive whitespace collapsed into /// single spaces. /// /// Any leading or trailing whitespace will be trimmed. fn collapse_whitespace(s: &str) -> String { s.split_whitespace().collect::>().join(" ") } thread_local!(pub static PLAYGROUND_KRATE: RefCell>> = { RefCell::new(None) }); pub fn render(w: &mut fmt::Formatter, s: &str, print_toc: bool) -> fmt::Result { extern fn block(ob: *mut hoedown_buffer, orig_text: *const hoedown_buffer, lang: *const hoedown_buffer, data: *const hoedown_renderer_data) { unsafe { if orig_text.is_null() { return } let opaque = (*data).opaque as *mut hoedown_html_renderer_state; let my_opaque: &MyOpaque = &*((*opaque).opaque as *const MyOpaque); let text = (*orig_text).as_bytes(); let origtext = str::from_utf8(text).unwrap(); debug!("docblock: ==============\n{:?}\n=======", text); let rendered = if lang.is_null() { false } else { let rlang = (*lang).as_bytes(); let rlang = str::from_utf8(rlang).unwrap(); if !LangString::parse(rlang).rust { (my_opaque.dfltblk)(ob, orig_text, lang, opaque as *const hoedown_renderer_data); true } else { false } }; let lines = origtext.lines().filter(|l| { stripped_filtered_line(*l).is_none() }); let text = lines.collect::>().join("\n"); if rendered { return } PLAYGROUND_KRATE.with(|krate| { // insert newline to clearly separate it from the // previous block so we can shorten the html output let mut s = String::from("\n"); krate.borrow().as_ref().map(|krate| { let test = origtext.lines().map(|l| { stripped_filtered_line(l).unwrap_or(l) }).collect::>().join("\n"); let krate = krate.as_ref().map(|s| &**s); let test = test::maketest(&test, krate, false, &Default::default()); s.push_str(&format!("{}", Escape(&test))); }); s.push_str(&highlight::render_with_highlighting(&text, Some("rust-example-rendered"), None)); let output = CString::new(s).unwrap(); hoedown_buffer_puts(ob, output.as_ptr()); }) } } extern fn header(ob: *mut hoedown_buffer, text: *const hoedown_buffer, level: libc::c_int, data: *const hoedown_renderer_data) { // hoedown does this, we may as well too unsafe { hoedown_buffer_puts(ob, "\n\0".as_ptr() as *const _); } // Extract the text provided let s = if text.is_null() { "".to_owned() } else { let s = unsafe { (*text).as_bytes() }; str::from_utf8(&s).unwrap().to_owned() }; // Discard '', '' tags and some escaped characters, // transform the contents of the header into a hyphenated string // without non-alphanumeric characters other than '-' and '_'. // // This is a terrible hack working around how hoedown gives us rendered // html for text rather than the raw text. let mut id = s.clone(); let repl_sub = vec!["", "", "", "", "", "", "<", ">", "&", "'", """]; for sub in repl_sub { id = id.replace(sub, ""); } let id = id.chars().filter_map(|c| { if c.is_alphanumeric() || c == '-' || c == '_' { if c.is_ascii() { Some(c.to_ascii_lowercase()) } else { Some(c) } } else if c.is_whitespace() && c.is_ascii() { Some('-') } else { None } }).collect::(); let opaque = unsafe { (*data).opaque as *mut hoedown_html_renderer_state }; let opaque = unsafe { &mut *((*opaque).opaque as *mut MyOpaque) }; let id = derive_id(id); let sec = opaque.toc_builder.as_mut().map_or("".to_owned(), |builder| { format!("{} ", builder.push(level as u32, s.clone(), id.clone())) }); // Render the HTML let text = format!("\ {sec}{}", s, lvl = level, id = id, sec = sec); let text = CString::new(text).unwrap(); unsafe { hoedown_buffer_puts(ob, text.as_ptr()) } } extern fn codespan( ob: *mut hoedown_buffer, text: *const hoedown_buffer, _: *const hoedown_renderer_data, ) -> libc::c_int { let content = if text.is_null() { "".to_owned() } else { let bytes = unsafe { (*text).as_bytes() }; let s = str::from_utf8(bytes).unwrap(); collapse_whitespace(s) }; let content = format!("{}", Escape(&content)); let element = CString::new(content).unwrap(); unsafe { hoedown_buffer_puts(ob, element.as_ptr()); } // Return anything except 0, which would mean "also print the code span verbatim". 1 } unsafe { let ob = hoedown_buffer_new(DEF_OUNIT); let renderer = hoedown_html_renderer_new(0, 0); let mut opaque = MyOpaque { dfltblk: (*renderer).blockcode.unwrap(), toc_builder: if print_toc {Some(TocBuilder::new())} else {None} }; (*((*renderer).opaque as *mut hoedown_html_renderer_state)).opaque = &mut opaque as *mut _ as *mut libc::c_void; (*renderer).blockcode = Some(block); (*renderer).header = Some(header); (*renderer).codespan = Some(codespan); let document = hoedown_document_new(renderer, HOEDOWN_EXTENSIONS, 16); hoedown_document_render(document, ob, s.as_ptr(), s.len() as libc::size_t); hoedown_document_free(document); hoedown_html_renderer_free(renderer); let mut ret = opaque.toc_builder.map_or(Ok(()), |builder| { write!(w, "", builder.into_toc()) }); if ret.is_ok() { let buf = (*ob).as_bytes(); ret = w.write_str(str::from_utf8(buf).unwrap()); } hoedown_buffer_free(ob); ret } } pub fn find_testable_code(doc: &str, tests: &mut ::test::Collector) { extern fn block(_ob: *mut hoedown_buffer, text: *const hoedown_buffer, lang: *const hoedown_buffer, data: *const hoedown_renderer_data) { unsafe { if text.is_null() { return } let block_info = if lang.is_null() { LangString::all_false() } else { let lang = (*lang).as_bytes(); let s = str::from_utf8(lang).unwrap(); LangString::parse(s) }; if !block_info.rust { return } let text = (*text).as_bytes(); let opaque = (*data).opaque as *mut hoedown_html_renderer_state; let tests = &mut *((*opaque).opaque as *mut ::test::Collector); let text = str::from_utf8(text).unwrap(); let lines = text.lines().map(|l| { stripped_filtered_line(l).unwrap_or(l) }); let text = lines.collect::>().join("\n"); tests.add_test(text.to_owned(), block_info.should_panic, block_info.no_run, block_info.ignore, block_info.test_harness, block_info.compile_fail); } } extern fn header(_ob: *mut hoedown_buffer, text: *const hoedown_buffer, level: libc::c_int, data: *const hoedown_renderer_data) { unsafe { let opaque = (*data).opaque as *mut hoedown_html_renderer_state; let tests = &mut *((*opaque).opaque as *mut ::test::Collector); if text.is_null() { tests.register_header("", level as u32); } else { let text = (*text).as_bytes(); let text = str::from_utf8(text).unwrap(); tests.register_header(text, level as u32); } } } unsafe { let ob = hoedown_buffer_new(DEF_OUNIT); let renderer = hoedown_html_renderer_new(0, 0); (*renderer).blockcode = Some(block); (*renderer).header = Some(header); (*((*renderer).opaque as *mut hoedown_html_renderer_state)).opaque = tests as *mut _ as *mut libc::c_void; let document = hoedown_document_new(renderer, HOEDOWN_EXTENSIONS, 16); hoedown_document_render(document, ob, doc.as_ptr(), doc.len() as libc::size_t); hoedown_document_free(document); hoedown_html_renderer_free(renderer); hoedown_buffer_free(ob); } } #[derive(Eq, PartialEq, Clone, Debug)] struct LangString { should_panic: bool, no_run: bool, ignore: bool, rust: bool, test_harness: bool, compile_fail: bool, } impl LangString { fn all_false() -> LangString { LangString { should_panic: false, no_run: false, ignore: false, rust: true, // NB This used to be `notrust = false` test_harness: false, compile_fail: false, } } fn parse(string: &str) -> LangString { let mut seen_rust_tags = false; let mut seen_other_tags = false; let mut data = LangString::all_false(); let allow_compile_fail = match get_unstable_features_setting() { UnstableFeatures::Allow | UnstableFeatures::Cheat=> true, _ => false, }; let tokens = string.split(|c: char| !(c == '_' || c == '-' || c.is_alphanumeric()) ); for token in tokens { match token { "" => {}, "should_panic" => { data.should_panic = true; seen_rust_tags = true; }, "no_run" => { data.no_run = true; seen_rust_tags = true; }, "ignore" => { data.ignore = true; seen_rust_tags = true; }, "rust" => { data.rust = true; seen_rust_tags = true; }, "test_harness" => { data.test_harness = true; seen_rust_tags = true; }, "compile_fail" if allow_compile_fail => { data.compile_fail = true; seen_rust_tags = true; data.no_run = true; }, _ => { seen_other_tags = true } } } data.rust &= !seen_other_tags || seen_rust_tags; data } } impl<'a> fmt::Display for Markdown<'a> { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { let Markdown(md) = *self; // This is actually common enough to special-case if md.is_empty() { return Ok(()) } render(fmt, md, false) } } impl<'a> fmt::Display for MarkdownWithToc<'a> { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { let MarkdownWithToc(md) = *self; render(fmt, md, true) } } pub fn plain_summary_line(md: &str) -> String { extern fn link(_ob: *mut hoedown_buffer, _link: *const hoedown_buffer, _title: *const hoedown_buffer, content: *const hoedown_buffer, data: *const hoedown_renderer_data) -> libc::c_int { unsafe { if !content.is_null() && (*content).size > 0 { let ob = (*data).opaque as *mut hoedown_buffer; hoedown_buffer_put(ob, (*content).data as *const libc::c_char, (*content).size); } } 1 } extern fn normal_text(_ob: *mut hoedown_buffer, text: *const hoedown_buffer, data: *const hoedown_renderer_data) { unsafe { let ob = (*data).opaque as *mut hoedown_buffer; hoedown_buffer_put(ob, (*text).data as *const libc::c_char, (*text).size); } } unsafe { let ob = hoedown_buffer_new(DEF_OUNIT); let mut plain_renderer: hoedown_renderer = ::std::mem::zeroed(); let renderer: *mut hoedown_renderer = &mut plain_renderer; (*renderer).opaque = ob as *mut libc::c_void; (*renderer).link = Some(link); (*renderer).normal_text = Some(normal_text); let document = hoedown_document_new(renderer, HOEDOWN_EXTENSIONS, 16); hoedown_document_render(document, ob, md.as_ptr(), md.len() as libc::size_t); hoedown_document_free(document); let plain_slice = (*ob).as_bytes(); let plain = str::from_utf8(plain_slice).unwrap_or("").to_owned(); hoedown_buffer_free(ob); plain } } #[cfg(test)] mod tests { use super::{LangString, Markdown}; use super::plain_summary_line; use html::render::reset_ids; #[test] fn test_lang_string_parse() { fn t(s: &str, should_panic: bool, no_run: bool, ignore: bool, rust: bool, test_harness: bool, compile_fail: bool) { assert_eq!(LangString::parse(s), LangString { should_panic: should_panic, no_run: no_run, ignore: ignore, rust: rust, test_harness: test_harness, compile_fail: compile_fail, }) } // marker | should_panic| no_run| ignore| rust | test_harness| compile_fail t("", false, false, false, true, false, false); t("rust", false, false, false, true, false, false); t("sh", false, false, false, false, false, false); t("ignore", false, false, true, true, false, false); t("should_panic", true, false, false, true, false, false); t("no_run", false, true, false, true, false, false); t("test_harness", false, false, false, true, true, false); t("compile_fail", false, true, false, true, false, true); t("{.no_run .example}", false, true, false, true, false, false); t("{.sh .should_panic}", true, false, false, true, false, false); t("{.example .rust}", false, false, false, true, false, false); t("{.test_harness .rust}", false, false, false, true, true, false); } #[test] fn issue_17736() { let markdown = "# title"; format!("{}", Markdown(markdown)); reset_ids(true); } #[test] fn test_header() { fn t(input: &str, expect: &str) { let output = format!("{}", Markdown(input)); assert_eq!(output, expect); reset_ids(true); } t("# Foo bar", "\n

\ Foo bar

"); t("## Foo-bar_baz qux", "\n

Foo-bar_baz qux

"); t("### **Foo** *bar* baz!?!& -_qux_-%", "\n

\ Foo \ bar baz!?!& -_qux_-%

"); t("####**Foo?** & \\*bar?!* _`baz`_ ❤ #qux", "\n

\ Foo? & *bar?!* \ baz ❤ #qux

"); } #[test] fn test_header_ids_multiple_blocks() { fn t(input: &str, expect: &str) { let output = format!("{}", Markdown(input)); assert_eq!(output, expect); } let test = || { t("# Example", "\n

\ Example

"); t("# Panics", "\n

\ Panics

"); t("# Example", "\n

\ Example

"); t("# Main", "\n

\ Main

"); t("# Example", "\n

\ Example

"); t("# Panics", "\n

\ Panics

"); }; test(); reset_ids(true); test(); } #[test] fn test_plain_summary_line() { fn t(input: &str, expect: &str) { let output = plain_summary_line(input); assert_eq!(output, expect); } t("hello [Rust](https://www.rust-lang.org) :)", "hello Rust :)"); t("code `let x = i32;` ...", "code `let x = i32;` ..."); t("type `Type<'static>` ...", "type `Type<'static>` ..."); t("# top header", "top header"); t("## header", "header"); } }