Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ tree-sitter-md = { version = "0.1.7", optional = true }
tree-sitter-rust = { version = "0.20.4", optional = true }
tree-sitter-html = { version = "0.20.0", optional = true }
tree-sitter-bash = { version = "0.20.5", optional = true }
itoa = "1.0.10"

[dev-dependencies]
anyhow = { version = "1.0.58" }
Expand Down
55 changes: 41 additions & 14 deletions src/html/comrak_adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,40 @@ pub struct ToCEntry {
pub anchor: String,
}

#[derive(Default)]
pub struct Anchorizer {
map: HashMap<String, i32>,
itoa_buffer: itoa::Buffer,
}

impl Anchorizer {
/// Returns a String that has been converted into an anchor using the GFM algorithm.
/// This replaces comrak's implementation to improve the performance.
/// @see https://docs.rs/comrak/latest/comrak/struct.Anchorizer.html#method.anchorize
pub fn anchorize(&mut self, s: &str) -> String {
let mut s = REJECTED_CHARS
.replace_all(&s.to_lowercase(), "")
.replace(' ', "-");

if let Some(count) = self.map.get_mut(&s) {
let a = self.itoa_buffer.format(*count);
s.push('-');
s.push_str(a);

*count += 1;
} else {
self.map.insert(s.clone(), 1);
}

s
}
}

#[derive(Clone)]
pub struct HeadingToCAdapter {
toc: Arc<Mutex<Vec<ToCEntry>>>,
anchorizer: Arc<Mutex<comrak::html::Anchorizer>>,
offset: Arc<Mutex<u8>>,
anchorizer: Arc<Mutex<Anchorizer>>,
}

impl Default for HeadingToCAdapter {
Expand All @@ -249,18 +278,18 @@ impl Default for HeadingToCAdapter {
}
}

lazy_static! {
static ref REJECTED_CHARS: regex::Regex =
regex::Regex::new(r"[^\p{L}\p{M}\p{N}\p{Pc} -]").unwrap();
}

impl HeadingToCAdapter {
pub fn anchorize(&self, content: String) -> String {
pub fn anchorize(&self, content: &str) -> String {
let mut anchorizer = self.anchorizer.lock().unwrap();
anchorizer.anchorize(content.clone())
anchorizer.anchorize(content)
}

pub fn add_entry(
&self,
level: u8,
content: String,
anchor: String,
) -> String {
pub fn add_entry(&self, level: u8, content: &str, anchor: &str) {
let mut toc = self.toc.lock().unwrap();
let mut offset = self.offset.lock().unwrap();

Expand All @@ -269,12 +298,10 @@ impl HeadingToCAdapter {
if toc.last().map_or(true, |toc| toc.content != content) {
toc.push(ToCEntry {
level,
content,
anchor: anchor.clone(),
content: content.to_owned(),
anchor: anchor.to_owned(),
});
}

anchor
}

pub fn render(self) -> Option<String> {
Expand Down Expand Up @@ -322,7 +349,7 @@ impl HeadingAdapter for HeadingToCAdapter {
let mut anchorizer = self.anchorizer.lock().unwrap();
let offset = self.offset.lock().unwrap();

let anchor = anchorizer.anchorize(heading.content.clone());
let anchor = anchorizer.anchorize(&heading.content);
writeln!(output, r#"<h{} id="{anchor}">"#, heading.level)?;

let mut toc = self.toc.lock().unwrap();
Expand Down
203 changes: 114 additions & 89 deletions src/html/jsdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,74 @@ lazy_static! {
regex::Regex::new(r"(^\.{0,2}\/)|(^[A-Za-z]+:\S)").unwrap();
static ref MODULE_LINK_RE: regex::Regex =
regex::Regex::new(r"^\[(\S+)\](?:\.(\S+)|\s|)$").unwrap();

#[cfg(feature = "ammonia")]
static ref AMMONIA: ammonia::Builder<'static> = {
let mut ammonia_builder = ammonia::Builder::default();

ammonia_builder
.add_tags(["video", "button", "svg", "path", "rect"])
.add_generic_attributes(["id", "align"])
.add_tag_attributes("button", ["data-copy"])
.add_tag_attributes(
"svg",
[
"width",
"height",
"viewBox",
"fill",
"xmlns",
"stroke",
"stroke-width",
"stroke-linecap",
"stroke-linejoin",
],
)
.add_tag_attributes(
"path",
[
"d",
"fill",
"fill-rule",
"clip-rule",
"stroke",
"stroke-width",
"stroke-linecap",
"stroke-linejoin",
],
)
.add_tag_attributes("rect", ["x", "y", "width", "height", "fill"])
.add_tag_attributes("video", ["src", "controls"])
.add_allowed_classes("pre", ["highlight"])
.add_allowed_classes("button", ["context_button"])
.add_allowed_classes(
"div",
[
"alert",
"alert-note",
"alert-tip",
"alert-important",
"alert-warning",
"alert-caution",
],
)
.link_rel(Some("nofollow"))
.url_relative(
ammonia::UrlRelative::Custom(Box::new(AmmoniaRelativeUrlEvaluator())));

#[cfg(feature = "syntect")]
ammonia_builder.add_tag_attributes("span", ["style"]);

#[cfg(feature = "tree-sitter")]
ammonia_builder.add_allowed_classes("span", super::tree_sitter::CLASSES);

ammonia_builder
};
}

thread_local! {
static CURRENT_FILE: RefCell<Option<Option<ShortPath>>> = RefCell::new(None);
static URL_REWRITER: RefCell<Option<Option<URLRewriter>>> = RefCell::new(None);
}

fn parse_links<'a>(md: &'a str, ctx: &RenderContext) -> Cow<'a, str> {
Expand Down Expand Up @@ -147,15 +215,23 @@ fn split_markdown_title(
}

#[cfg(feature = "ammonia")]
struct AmmoniaRelativeUrlEvaluator {
current_file: Option<ShortPath>,
url_rewriter: URLRewriter,
}
struct AmmoniaRelativeUrlEvaluator();

#[cfg(feature = "ammonia")]
impl ammonia::UrlRelativeEvaluate for AmmoniaRelativeUrlEvaluator {
fn evaluate<'a>(&self, url: &'a str) -> Option<Cow<'a, str>> {
Some((self.url_rewriter)(self.current_file.as_ref(), url).into())
URL_REWRITER.with(|url_rewriter| {
if let Some(url_rewriter) = url_rewriter.borrow().as_ref().unwrap() {
CURRENT_FILE.with(|current_file| {
Some(
url_rewriter(current_file.borrow().as_ref().unwrap().as_ref(), url)
.into(),
)
})
} else {
Some(Cow::Borrowed(url))
}
})
}
}

Expand Down Expand Up @@ -374,10 +450,13 @@ pub fn markdown_to_html(
}

let mut plugins = comrak::Plugins::default();
plugins.render.codefence_syntax_highlighter =
Some(&render_ctx.ctx.highlight_adapter);
if !render_options.no_toc {
plugins.render.heading_adapter = Some(&render_ctx.toc);

if !render_options.summary {
plugins.render.codefence_syntax_highlighter =
Some(&render_ctx.ctx.highlight_adapter);
if !render_options.no_toc {
plugins.render.heading_adapter = Some(&render_ctx.toc);
}
}

let md = parse_links(md, render_ctx);
Expand All @@ -400,7 +479,7 @@ pub fn markdown_to_html(
"markdown"
};

let mut html = {
let html = {
let arena = Arena::new();
let root = comrak::parse_document(&arena, md, &options);

Expand All @@ -411,75 +490,24 @@ pub fn markdown_to_html(

#[cfg(feature = "ammonia")]
{
let mut ammonia_builder = ammonia::Builder::default();

ammonia_builder
.add_tags(["video", "button", "svg", "path", "rect"])
.add_generic_attributes(["id", "align"])
.add_tag_attributes("button", ["data-copy"])
.add_tag_attributes(
"svg",
[
"width",
"height",
"viewBox",
"fill",
"xmlns",
"stroke",
"stroke-width",
"stroke-linecap",
"stroke-linejoin",
],
)
.add_tag_attributes(
"path",
[
"d",
"fill",
"fill-rule",
"clip-rule",
"stroke",
"stroke-width",
"stroke-linecap",
"stroke-linejoin",
],
)
.add_tag_attributes("rect", ["x", "y", "width", "height", "fill"])
.add_tag_attributes("video", ["src", "controls"])
.add_allowed_classes("pre", ["highlight"])
.add_allowed_classes("button", ["context_button"])
.add_allowed_classes(
"div",
[
"alert",
"alert-note",
"alert-tip",
"alert-important",
"alert-warning",
"alert-caution",
],
)
.link_rel(Some("nofollow"))
.url_relative(render_ctx.ctx.url_rewriter.as_ref().map_or(
ammonia::UrlRelative::PassThrough,
|url_rewriter| {
ammonia::UrlRelative::Custom(Box::new(AmmoniaRelativeUrlEvaluator {
current_file: render_ctx.get_current_resolve().get_file().cloned(),
url_rewriter: url_rewriter.clone(),
}))
},
));
CURRENT_FILE
.set(Some(render_ctx.get_current_resolve().get_file().cloned()));
URL_REWRITER.set(Some(render_ctx.ctx.url_rewriter.clone()));

#[cfg(feature = "syntect")]
ammonia_builder.add_tag_attributes("span", ["style"]);
let html = Some(format!(
r#"<div class="{class_name}">{}</div>"#,
AMMONIA.clean(&html)
));

#[cfg(feature = "tree-sitter")]
ammonia_builder.add_allowed_classes("span", super::tree_sitter::CLASSES);
CURRENT_FILE.set(None);
URL_REWRITER.set(None);

html = ammonia_builder.clean(&html).to_string();
html
}
#[cfg(not(feature = "ammonia"))]
{
Some(format!(r#"<div class="{class_name}">{html}</div>"#))
}

Some(format!(r#"<div class="{class_name}">{html}</div>"#))
}

pub(crate) fn render_markdown_summary(
Expand Down Expand Up @@ -649,20 +677,17 @@ impl ModuleDocCtx {

sections.extend(super::namespace::render_namespace(
render_ctx,
partitions_by_kind
.into_iter()
.map(|(title, nodes)| {
(
SectionHeaderCtx {
title: title.clone(),
anchor: AnchorCtx { id: title },
href: None,
doc: None,
},
nodes,
)
})
.collect(),
partitions_by_kind.into_iter().map(|(title, nodes)| {
(
SectionHeaderCtx {
title: title.clone(),
anchor: AnchorCtx { id: title },
href: None,
doc: None,
},
nodes,
)
}),
));
}

Expand Down
Loading
Loading