2018-01-21 22:35:11 +08:00
|
|
|
|
#![allow(missing_docs)] // FIXME: Document this
|
|
|
|
|
|
2016-03-18 05:41:00 +08:00
|
|
|
|
pub mod fs;
|
2018-01-06 05:03:30 +08:00
|
|
|
|
mod string;
|
2020-05-21 05:32:00 +08:00
|
|
|
|
pub(crate) mod toml_ext;
|
2019-05-26 02:50:41 +08:00
|
|
|
|
use crate::errors::Error;
|
2022-06-24 19:20:02 +08:00
|
|
|
|
use log::error;
|
2022-09-23 06:05:39 +08:00
|
|
|
|
use once_cell::sync::Lazy;
|
2020-05-21 05:32:00 +08:00
|
|
|
|
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
|
2022-06-24 19:20:02 +08:00
|
|
|
|
use regex::Regex;
|
2018-03-07 21:02:06 +08:00
|
|
|
|
|
2017-06-01 13:28:08 +08:00
|
|
|
|
use std::borrow::Cow;
|
2022-02-18 23:27:24 +08:00
|
|
|
|
use std::collections::HashMap;
|
2019-07-01 23:52:25 +08:00
|
|
|
|
use std::fmt::Write;
|
|
|
|
|
use std::path::Path;
|
2015-09-17 05:35:16 +08:00
|
|
|
|
|
2019-10-06 06:27:03 +08:00
|
|
|
|
pub use self::string::{
|
|
|
|
|
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
|
|
|
|
|
take_rustdoc_include_lines,
|
|
|
|
|
};
|
2015-09-17 05:35:16 +08:00
|
|
|
|
|
2018-03-07 21:02:06 +08:00
|
|
|
|
/// Replaces multiple consecutive whitespace characters with a single space character.
|
2019-05-07 02:20:58 +08:00
|
|
|
|
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
|
2022-09-23 06:05:39 +08:00
|
|
|
|
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s\s+").unwrap());
|
2018-03-07 21:02:06 +08:00
|
|
|
|
RE.replace_all(text, " ")
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-09 11:52:32 +08:00
|
|
|
|
/// Convert the given string to a valid HTML element ID.
|
|
|
|
|
/// The only restriction is that the ID must not contain any ASCII whitespace.
|
2018-03-07 21:02:06 +08:00
|
|
|
|
pub fn normalize_id(content: &str) -> String {
|
2018-09-09 11:52:32 +08:00
|
|
|
|
content
|
2018-03-07 21:02:06 +08:00
|
|
|
|
.chars()
|
|
|
|
|
.filter_map(|ch| {
|
|
|
|
|
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
|
|
|
|
|
Some(ch.to_ascii_lowercase())
|
|
|
|
|
} else if ch.is_whitespace() {
|
|
|
|
|
Some('-')
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
2019-05-05 22:57:43 +08:00
|
|
|
|
})
|
|
|
|
|
.collect::<String>()
|
2018-03-07 21:02:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate an ID for use with anchors which is derived from a "normalised"
|
|
|
|
|
/// string.
|
2022-02-18 23:27:24 +08:00
|
|
|
|
// This function should be made private when the deprecation expires.
|
|
|
|
|
#[deprecated(since = "0.4.16", note = "use unique_id_from_content instead")]
|
2018-03-07 21:02:06 +08:00
|
|
|
|
pub fn id_from_content(content: &str) -> String {
|
|
|
|
|
let mut content = content.to_string();
|
|
|
|
|
|
|
|
|
|
// Skip any tags or html-encoded stuff
|
2022-09-23 06:05:39 +08:00
|
|
|
|
static HTML: Lazy<Regex> = Lazy::new(|| Regex::new(r"(<.*?>)").unwrap());
|
2021-11-10 05:41:44 +08:00
|
|
|
|
content = HTML.replace_all(&content, "").into();
|
|
|
|
|
const REPL_SUB: &[&str] = &["<", ">", "&", "'", """];
|
2018-03-07 21:02:06 +08:00
|
|
|
|
for sub in REPL_SUB {
|
|
|
|
|
content = content.replace(sub, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove spaces and hashes indicating a header
|
2019-03-23 20:47:10 +08:00
|
|
|
|
let trimmed = content.trim().trim_start_matches('#').trim();
|
2018-03-07 21:02:06 +08:00
|
|
|
|
normalize_id(trimmed)
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-18 23:27:24 +08:00
|
|
|
|
/// Generate an ID for use with anchors which is derived from a "normalised"
|
|
|
|
|
/// string.
|
|
|
|
|
///
|
|
|
|
|
/// Each ID returned will be unique, if the same `id_counter` is provided on
|
|
|
|
|
/// each call.
|
|
|
|
|
pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, usize>) -> String {
|
|
|
|
|
let id = {
|
|
|
|
|
#[allow(deprecated)]
|
|
|
|
|
id_from_content(content)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// If we have headers with the same normalized id, append an incrementing counter
|
|
|
|
|
let id_count = id_counter.entry(id.clone()).or_insert(0);
|
|
|
|
|
let unique_id = match *id_count {
|
|
|
|
|
0 => id,
|
|
|
|
|
id_count => format!("{}-{}", id, id_count),
|
|
|
|
|
};
|
|
|
|
|
*id_count += 1;
|
|
|
|
|
unique_id
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-01 23:52:25 +08:00
|
|
|
|
/// Fix links to the correct location.
|
|
|
|
|
///
|
|
|
|
|
/// This adjusts links, such as turning `.md` extensions to `.html`.
|
|
|
|
|
///
|
|
|
|
|
/// `path` is the path to the page being rendered relative to the root of the
|
|
|
|
|
/// book. This is used for the `print.html` page so that links on the print
|
|
|
|
|
/// page go to the original location. Normal page rendering sets `path` to
|
|
|
|
|
/// None. Ideally, print page links would link to anchors on the print page,
|
|
|
|
|
/// but that is very difficult.
|
|
|
|
|
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
2022-09-23 06:05:39 +08:00
|
|
|
|
static SCHEME_LINK: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap());
|
|
|
|
|
static MD_LINK: Lazy<Regex> =
|
|
|
|
|
Lazy::new(|| Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap());
|
2018-07-11 21:33:44 +08:00
|
|
|
|
|
2019-07-01 23:52:25 +08:00
|
|
|
|
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
|
|
|
|
|
if dest.starts_with('#') {
|
|
|
|
|
// Fragment-only link.
|
|
|
|
|
if let Some(path) = path {
|
|
|
|
|
let mut base = path.display().to_string();
|
|
|
|
|
if base.ends_with(".md") {
|
|
|
|
|
base.replace_range(base.len() - 3.., ".html");
|
|
|
|
|
}
|
|
|
|
|
return format!("{}{}", base, dest).into();
|
|
|
|
|
} else {
|
|
|
|
|
return dest;
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-05-09 05:50:59 +08:00
|
|
|
|
// Don't modify links with schemes like `https`.
|
|
|
|
|
if !SCHEME_LINK.is_match(&dest) {
|
|
|
|
|
// This is a relative link, adjust it as necessary.
|
|
|
|
|
let mut fixed_link = String::new();
|
2019-07-01 23:52:25 +08:00
|
|
|
|
if let Some(path) = path {
|
|
|
|
|
let base = path
|
|
|
|
|
.parent()
|
|
|
|
|
.expect("path can't be empty")
|
|
|
|
|
.to_str()
|
|
|
|
|
.expect("utf-8 paths only");
|
|
|
|
|
if !base.is_empty() {
|
|
|
|
|
write!(fixed_link, "{}/", base).unwrap();
|
|
|
|
|
}
|
2019-05-09 05:50:59 +08:00
|
|
|
|
}
|
2018-07-11 21:33:44 +08:00
|
|
|
|
|
2019-05-09 05:50:59 +08:00
|
|
|
|
if let Some(caps) = MD_LINK.captures(&dest) {
|
|
|
|
|
fixed_link.push_str(&caps["link"]);
|
|
|
|
|
fixed_link.push_str(".html");
|
|
|
|
|
if let Some(anchor) = caps.name("anchor") {
|
|
|
|
|
fixed_link.push_str(anchor.as_str());
|
2018-07-11 21:33:44 +08:00
|
|
|
|
}
|
2019-05-09 05:50:59 +08:00
|
|
|
|
} else {
|
|
|
|
|
fixed_link.push_str(&dest);
|
|
|
|
|
};
|
2019-06-12 00:26:24 +08:00
|
|
|
|
return CowStr::from(fixed_link);
|
2019-05-09 05:50:59 +08:00
|
|
|
|
}
|
|
|
|
|
dest
|
|
|
|
|
}
|
2018-07-11 21:33:44 +08:00
|
|
|
|
|
2019-07-01 23:52:25 +08:00
|
|
|
|
fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
|
|
|
|
|
// This is a terrible hack, but should be reasonably reliable. Nobody
|
|
|
|
|
// should ever parse a tag with a regex. However, there isn't anything
|
|
|
|
|
// in Rust that I know of that is suitable for handling partial html
|
|
|
|
|
// fragments like those generated by pulldown_cmark.
|
|
|
|
|
//
|
|
|
|
|
// There are dozens of HTML tags/attributes that contain paths, so
|
|
|
|
|
// feel free to add more tags if desired; these are the only ones I
|
|
|
|
|
// care about right now.
|
2022-09-23 06:05:39 +08:00
|
|
|
|
static HTML_LINK: Lazy<Regex> =
|
|
|
|
|
Lazy::new(|| Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap());
|
2019-07-01 23:52:25 +08:00
|
|
|
|
|
|
|
|
|
HTML_LINK
|
|
|
|
|
.replace_all(&html, |caps: ®ex::Captures<'_>| {
|
|
|
|
|
let fixed = fix(caps[2].into(), path);
|
|
|
|
|
format!("{}{}\"", &caps[1], fixed)
|
|
|
|
|
})
|
|
|
|
|
.into_owned()
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-09 05:50:59 +08:00
|
|
|
|
match event {
|
2019-06-12 00:26:24 +08:00
|
|
|
|
Event::Start(Tag::Link(link_type, dest, title)) => {
|
2019-07-01 23:52:25 +08:00
|
|
|
|
Event::Start(Tag::Link(link_type, fix(dest, path), title))
|
2019-05-09 05:50:59 +08:00
|
|
|
|
}
|
2019-06-12 00:26:24 +08:00
|
|
|
|
Event::Start(Tag::Image(link_type, dest, title)) => {
|
2019-07-01 23:52:25 +08:00
|
|
|
|
Event::Start(Tag::Image(link_type, fix(dest, path), title))
|
2018-07-24 01:45:01 +08:00
|
|
|
|
}
|
2019-07-01 23:52:25 +08:00
|
|
|
|
Event::Html(html) => Event::Html(fix_html(html, path)),
|
2018-07-24 01:45:01 +08:00
|
|
|
|
_ => event,
|
2018-07-11 21:33:44 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
|
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
|
2017-06-01 13:28:08 +08:00
|
|
|
|
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
2019-07-01 23:52:25 +08:00
|
|
|
|
render_markdown_with_path(text, curly_quotes, None)
|
2019-01-16 02:19:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-25 20:46:27 +08:00
|
|
|
|
pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> {
|
2016-01-03 19:02:39 +08:00
|
|
|
|
let mut opts = Options::empty();
|
2019-06-12 00:26:24 +08:00
|
|
|
|
opts.insert(Options::ENABLE_TABLES);
|
|
|
|
|
opts.insert(Options::ENABLE_FOOTNOTES);
|
2019-06-12 23:02:03 +08:00
|
|
|
|
opts.insert(Options::ENABLE_STRIKETHROUGH);
|
|
|
|
|
opts.insert(Options::ENABLE_TASKLISTS);
|
2023-02-09 14:58:54 +08:00
|
|
|
|
opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
|
2021-11-20 09:26:30 +08:00
|
|
|
|
if curly_quotes {
|
|
|
|
|
opts.insert(Options::ENABLE_SMART_PUNCTUATION);
|
|
|
|
|
}
|
2019-06-12 23:02:03 +08:00
|
|
|
|
Parser::new_ext(text, opts)
|
|
|
|
|
}
|
2016-01-03 19:02:39 +08:00
|
|
|
|
|
2019-07-01 23:52:25 +08:00
|
|
|
|
pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&Path>) -> String {
|
2019-06-12 23:02:03 +08:00
|
|
|
|
let mut s = String::with_capacity(text.len() * 3 / 2);
|
2021-11-20 09:26:30 +08:00
|
|
|
|
let p = new_cmark_parser(text, curly_quotes);
|
2018-08-03 09:22:49 +08:00
|
|
|
|
let events = p
|
|
|
|
|
.map(clean_codeblock_headers)
|
2021-07-30 16:11:36 +08:00
|
|
|
|
.map(|event| adjust_links(event, path))
|
|
|
|
|
.flat_map(|event| {
|
|
|
|
|
let (a, b) = wrap_tables(event);
|
|
|
|
|
a.into_iter().chain(b)
|
|
|
|
|
});
|
2017-08-11 17:29:14 +08:00
|
|
|
|
|
2017-06-01 13:28:08 +08:00
|
|
|
|
html::push_html(&mut s, events);
|
2015-12-30 07:46:55 +08:00
|
|
|
|
s
|
|
|
|
|
}
|
2017-06-01 13:28:08 +08:00
|
|
|
|
|
2021-07-30 16:11:36 +08:00
|
|
|
|
/// Wraps tables in a `.table-wrapper` class to apply overflow-x rules to.
|
|
|
|
|
fn wrap_tables(event: Event<'_>) -> (Option<Event<'_>>, Option<Event<'_>>) {
|
|
|
|
|
match event {
|
|
|
|
|
Event::Start(Tag::Table(_)) => (
|
|
|
|
|
Some(Event::Html(r#"<div class="table-wrapper">"#.into())),
|
|
|
|
|
Some(event),
|
|
|
|
|
),
|
|
|
|
|
Event::End(Tag::Table(_)) => (Some(event), Some(Event::Html(r#"</div>"#.into()))),
|
|
|
|
|
_ => (Some(event), None),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-07 04:50:34 +08:00
|
|
|
|
fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
|
2017-08-11 17:29:14 +08:00
|
|
|
|
match event {
|
2020-05-21 05:32:00 +08:00
|
|
|
|
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info))) => {
|
2021-02-21 14:15:50 +08:00
|
|
|
|
let info: String = info
|
|
|
|
|
.chars()
|
|
|
|
|
.map(|x| match x {
|
2021-07-11 00:33:34 +08:00
|
|
|
|
' ' | '\t' => ',',
|
2021-02-21 14:15:50 +08:00
|
|
|
|
_ => x,
|
|
|
|
|
})
|
|
|
|
|
.filter(|ch| !ch.is_whitespace())
|
|
|
|
|
.collect();
|
2017-08-11 17:29:14 +08:00
|
|
|
|
|
2020-05-21 05:32:00 +08:00
|
|
|
|
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::from(info))))
|
2017-10-03 19:40:23 +08:00
|
|
|
|
}
|
2017-08-11 17:29:14 +08:00
|
|
|
|
_ => event,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
|
/// Prints a "backtrace" of some `Error`.
|
|
|
|
|
pub fn log_backtrace(e: &Error) {
|
|
|
|
|
error!("Error: {}", e);
|
|
|
|
|
|
2020-05-21 05:32:00 +08:00
|
|
|
|
for cause in e.chain().skip(1) {
|
2018-01-07 22:10:48 +08:00
|
|
|
|
error!("\tCaused By: {}", cause);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-15 11:35:39 +08:00
|
|
|
|
pub(crate) fn bracket_escape(mut s: &str) -> String {
|
|
|
|
|
let mut escaped = String::with_capacity(s.len());
|
|
|
|
|
let needs_escape: &[char] = &['<', '>'];
|
|
|
|
|
while let Some(next) = s.find(needs_escape) {
|
|
|
|
|
escaped.push_str(&s[..next]);
|
|
|
|
|
match s.as_bytes()[next] {
|
|
|
|
|
b'<' => escaped.push_str("<"),
|
|
|
|
|
b'>' => escaped.push_str(">"),
|
|
|
|
|
_ => unreachable!(),
|
|
|
|
|
}
|
|
|
|
|
s = &s[next + 1..];
|
|
|
|
|
}
|
|
|
|
|
escaped.push_str(s);
|
|
|
|
|
escaped
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-01 13:28:08 +08:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2022-04-15 11:35:39 +08:00
|
|
|
|
use super::bracket_escape;
|
|
|
|
|
|
2017-06-01 13:28:08 +08:00
|
|
|
|
mod render_markdown {
|
|
|
|
|
use super::super::render_markdown;
|
|
|
|
|
|
2018-07-11 21:33:44 +08:00
|
|
|
|
#[test]
|
|
|
|
|
fn preserves_external_links() {
|
2018-07-24 01:45:01 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
render_markdown("[example](https://www.rust-lang.org/)", false),
|
|
|
|
|
"<p><a href=\"https://www.rust-lang.org/\">example</a></p>\n"
|
|
|
|
|
);
|
2018-07-11 21:33:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_can_adjust_markdown_links() {
|
2018-07-24 01:45:01 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
render_markdown("[example](example.md)", false),
|
|
|
|
|
"<p><a href=\"example.html\">example</a></p>\n"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
render_markdown("[example_anchor](example.md#anchor)", false),
|
|
|
|
|
"<p><a href=\"example.html#anchor\">example_anchor</a></p>\n"
|
|
|
|
|
);
|
2019-01-17 04:44:51 +08:00
|
|
|
|
|
|
|
|
|
// this anchor contains 'md' inside of it
|
|
|
|
|
assert_eq!(
|
|
|
|
|
render_markdown("[phantom data](foo.html#phantomdata)", false),
|
|
|
|
|
"<p><a href=\"foo.html#phantomdata\">phantom data</a></p>\n"
|
|
|
|
|
);
|
2018-07-11 21:33:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-07-30 16:17:02 +08:00
|
|
|
|
#[test]
|
|
|
|
|
fn it_can_wrap_tables() {
|
|
|
|
|
let src = r#"
|
|
|
|
|
| Original | Punycode | Punycode + Encoding |
|
|
|
|
|
|-----------------|-----------------|---------------------|
|
|
|
|
|
| føø | f-5gaa | f_5gaa |
|
|
|
|
|
"#;
|
|
|
|
|
let out = r#"
|
|
|
|
|
<div class="table-wrapper"><table><thead><tr><th>Original</th><th>Punycode</th><th>Punycode + Encoding</th></tr></thead><tbody>
|
|
|
|
|
<tr><td>føø</td><td>f-5gaa</td><td>f_5gaa</td></tr>
|
|
|
|
|
</tbody></table>
|
|
|
|
|
</div>
|
|
|
|
|
"#.trim();
|
|
|
|
|
assert_eq!(render_markdown(src, false), out);
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-01 13:28:08 +08:00
|
|
|
|
#[test]
|
|
|
|
|
fn it_can_keep_quotes_straight() {
|
|
|
|
|
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_can_make_quotes_curly_except_when_they_are_in_code() {
|
|
|
|
|
let input = r#"
|
|
|
|
|
'one'
|
|
|
|
|
```
|
|
|
|
|
'two'
|
|
|
|
|
```
|
|
|
|
|
`'three'` 'four'"#;
|
|
|
|
|
let expected = r#"<p>‘one’</p>
|
|
|
|
|
<pre><code>'two'
|
|
|
|
|
</code></pre>
|
|
|
|
|
<p><code>'three'</code> ‘four’</p>
|
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(render_markdown(input, true), expected);
|
|
|
|
|
}
|
2017-08-11 17:29:14 +08:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn whitespace_outside_of_codeblock_header_is_preserved() {
|
|
|
|
|
let input = r#"
|
|
|
|
|
some text with spaces
|
|
|
|
|
```rust
|
|
|
|
|
fn main() {
|
|
|
|
|
// code inside is unchanged
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
more text with spaces
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
let expected = r#"<p>some text with spaces</p>
|
|
|
|
|
<pre><code class="language-rust">fn main() {
|
|
|
|
|
// code inside is unchanged
|
|
|
|
|
}
|
|
|
|
|
</code></pre>
|
|
|
|
|
<p>more text with spaces</p>
|
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(render_markdown(input, false), expected);
|
|
|
|
|
assert_eq!(render_markdown(input, true), expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn rust_code_block_properties_are_passed_as_space_delimited_class() {
|
|
|
|
|
let input = r#"
|
|
|
|
|
```rust,no_run,should_panic,property_3
|
|
|
|
|
```
|
|
|
|
|
"#;
|
|
|
|
|
|
2020-01-01 08:23:25 +08:00
|
|
|
|
let expected = r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre>
|
2017-08-11 17:29:14 +08:00
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(render_markdown(input, false), expected);
|
|
|
|
|
assert_eq!(render_markdown(input, true), expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn rust_code_block_properties_with_whitespace_are_passed_as_space_delimited_class() {
|
|
|
|
|
let input = r#"
|
|
|
|
|
```rust, no_run,,,should_panic , ,property_3
|
|
|
|
|
```
|
|
|
|
|
"#;
|
|
|
|
|
|
2021-02-21 14:28:16 +08:00
|
|
|
|
let expected = r#"<pre><code class="language-rust,,,,,no_run,,,should_panic,,,,property_3"></code></pre>
|
2017-08-11 17:29:14 +08:00
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(render_markdown(input, false), expected);
|
|
|
|
|
assert_eq!(render_markdown(input, true), expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn rust_code_block_without_properties_has_proper_html_class() {
|
|
|
|
|
let input = r#"
|
2017-10-03 19:40:23 +08:00
|
|
|
|
```rust
|
2017-08-11 17:29:14 +08:00
|
|
|
|
```
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
let expected = r#"<pre><code class="language-rust"></code></pre>
|
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(render_markdown(input, false), expected);
|
|
|
|
|
assert_eq!(render_markdown(input, true), expected);
|
|
|
|
|
|
|
|
|
|
let input = r#"
|
|
|
|
|
```rust
|
|
|
|
|
```
|
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(render_markdown(input, false), expected);
|
|
|
|
|
assert_eq!(render_markdown(input, true), expected);
|
|
|
|
|
}
|
2017-06-01 13:28:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-18 23:27:24 +08:00
|
|
|
|
#[allow(deprecated)]
|
|
|
|
|
mod id_from_content {
|
|
|
|
|
use super::super::id_from_content;
|
2018-03-07 21:02:06 +08:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_generates_anchors() {
|
2018-09-09 11:47:44 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
id_from_content("## Method-call expressions"),
|
|
|
|
|
"method-call-expressions"
|
|
|
|
|
);
|
2018-12-04 07:10:09 +08:00
|
|
|
|
assert_eq!(id_from_content("## **Bold** title"), "bold-title");
|
|
|
|
|
assert_eq!(id_from_content("## `Code` title"), "code-title");
|
2021-11-10 05:41:44 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
id_from_content("## title <span dir=rtl>foo</span>"),
|
|
|
|
|
"title-foo"
|
|
|
|
|
);
|
2018-09-09 11:47:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_generates_anchors_from_non_ascii_initial() {
|
2018-07-24 01:45:01 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
id_from_content("## `--passes`: add more rustdoc passes"),
|
2018-09-09 11:47:44 +08:00
|
|
|
|
"--passes-add-more-rustdoc-passes"
|
2018-07-24 01:45:01 +08:00
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
2018-09-09 11:47:44 +08:00
|
|
|
|
id_from_content("## 中文標題 CJK title"),
|
|
|
|
|
"中文標題-cjk-title"
|
|
|
|
|
);
|
2018-12-04 07:10:09 +08:00
|
|
|
|
assert_eq!(id_from_content("## Über"), "Über");
|
2018-03-07 21:02:06 +08:00
|
|
|
|
}
|
2022-02-18 23:27:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mod html_munging {
|
|
|
|
|
use super::super::{normalize_id, unique_id_from_content};
|
2018-03-07 21:02:06 +08:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_normalizes_ids() {
|
2018-07-24 01:45:01 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
normalize_id("`--passes`: add more rustdoc passes"),
|
2018-09-09 11:47:44 +08:00
|
|
|
|
"--passes-add-more-rustdoc-passes"
|
2018-07-24 01:45:01 +08:00
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
normalize_id("Method-call 🐙 expressions \u{1f47c}"),
|
|
|
|
|
"method-call--expressions-"
|
|
|
|
|
);
|
2018-09-09 11:47:44 +08:00
|
|
|
|
assert_eq!(normalize_id("_-_12345"), "_-_12345");
|
|
|
|
|
assert_eq!(normalize_id("12345"), "12345");
|
|
|
|
|
assert_eq!(normalize_id("中文"), "中文");
|
|
|
|
|
assert_eq!(normalize_id("にほんご"), "にほんご");
|
|
|
|
|
assert_eq!(normalize_id("한국어"), "한국어");
|
2018-03-07 21:02:06 +08:00
|
|
|
|
assert_eq!(normalize_id(""), "");
|
|
|
|
|
}
|
2022-02-18 23:27:24 +08:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_generates_unique_ids_from_content() {
|
|
|
|
|
// Same id if not given shared state
|
|
|
|
|
assert_eq!(
|
|
|
|
|
unique_id_from_content("## 中文標題 CJK title", &mut Default::default()),
|
|
|
|
|
"中文標題-cjk-title"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
unique_id_from_content("## 中文標題 CJK title", &mut Default::default()),
|
|
|
|
|
"中文標題-cjk-title"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Different id if given shared state
|
|
|
|
|
let mut id_counter = Default::default();
|
|
|
|
|
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
unique_id_from_content("## 中文標題 CJK title", &mut id_counter),
|
|
|
|
|
"中文標題-cjk-title"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-1");
|
|
|
|
|
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-2");
|
|
|
|
|
}
|
2018-03-07 21:02:06 +08:00
|
|
|
|
}
|
2022-04-15 11:35:39 +08:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn escaped_brackets() {
|
|
|
|
|
assert_eq!(bracket_escape(""), "");
|
|
|
|
|
assert_eq!(bracket_escape("<"), "<");
|
|
|
|
|
assert_eq!(bracket_escape(">"), ">");
|
|
|
|
|
assert_eq!(bracket_escape("<>"), "<>");
|
|
|
|
|
assert_eq!(bracket_escape("<test>"), "<test>");
|
|
|
|
|
assert_eq!(bracket_escape("a<test>b"), "a<test>b");
|
|
|
|
|
}
|
2017-06-01 13:28:08 +08:00
|
|
|
|
}
|