use crate::event::{Event, Kind, Name};
use crate::mdast::AlignKind;
use crate::util::{
character_reference::decode as decode_character_reference,
constant::{SAFE_PROTOCOL_HREF, SAFE_PROTOCOL_SRC},
encode::encode,
gfm_tagfilter::gfm_tagfilter,
infer::{gfm_table_align, list_loose},
normalize_identifier::normalize_identifier,
sanitize_uri::{sanitize, sanitize_with_protocols},
skip,
slice::{Position, Slice},
};
use crate::{CompileOptions, LineEnding};
use alloc::{
format,
string::{String, ToString},
vec,
vec::Vec,
};
use core::str;
#[derive(Debug)]
struct Media {
image: bool,
label_id: Option<(usize, usize)>,
label: Option<String>,
reference_id: Option<(usize, usize)>,
destination: Option<String>,
title: Option<String>,
}
#[derive(Debug)]
struct Definition {
id: String,
destination: Option<String>,
title: Option<String>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug)]
struct CompileContext<'a> {
events: &'a [Event],
bytes: &'a [u8],
options: &'a CompileOptions,
heading_atx_rank: Option<usize>,
heading_setext_buffer: Option<String>,
raw_flow_seen_data: Option<bool>,
raw_flow_fences_count: Option<usize>,
raw_text_inside: bool,
image_alt_inside: bool,
character_reference_marker: Option<u8>,
list_expect_first_marker: Option<bool>,
media_stack: Vec<Media>,
tight_stack: Vec<bool>,
definitions: Vec<Definition>,
gfm_footnote_definitions: Vec<(String, String)>,
gfm_footnote_definition_calls: Vec<(String, usize)>,
gfm_footnote_definition_stack: Vec<(usize, usize)>,
gfm_table_in_head: bool,
gfm_table_align: Option<Vec<AlignKind>>,
gfm_table_column: usize,
slurp_one_line_ending: bool,
encode_html: bool,
line_ending_default: LineEnding,
buffers: Vec<String>,
index: usize,
}
impl<'a> CompileContext<'a> {
fn new(
events: &'a [Event],
bytes: &'a [u8],
options: &'a CompileOptions,
line_ending: LineEnding,
) -> CompileContext<'a> {
CompileContext {
events,
bytes,
heading_atx_rank: None,
heading_setext_buffer: None,
raw_flow_seen_data: None,
raw_flow_fences_count: None,
raw_text_inside: false,
character_reference_marker: None,
list_expect_first_marker: None,
media_stack: vec![],
definitions: vec![],
gfm_footnote_definitions: vec![],
gfm_footnote_definition_calls: vec![],
gfm_footnote_definition_stack: vec![],
gfm_table_in_head: false,
gfm_table_align: None,
gfm_table_column: 0,
tight_stack: vec![],
slurp_one_line_ending: false,
image_alt_inside: false,
encode_html: true,
line_ending_default: line_ending,
buffers: vec![String::new()],
index: 0,
options,
}
}
fn buffer(&mut self) {
self.buffers.push(String::new());
}
fn resume(&mut self) -> String {
self.buffers.pop().expect("Cannot resume w/o buffer")
}
fn push(&mut self, value: &str) {
let last_buf_opt = self.buffers.last_mut();
let last_buf = last_buf_opt.expect("at least one buffer should exist");
last_buf.push_str(value);
}
fn line_ending(&mut self) {
let eol = self.line_ending_default.as_str().to_string();
self.push(&eol);
}
fn line_ending_if_needed(&mut self) {
let last_buf_opt = self.buffers.last();
let last_buf = last_buf_opt.expect("at least one buffer should exist");
let last_byte = last_buf.as_bytes().last();
if !matches!(last_byte, None | Some(b'\n' | b'\r')) {
self.line_ending();
}
}
}
pub fn compile(events: &[Event], bytes: &[u8], options: &CompileOptions) -> String {
let mut index = 0;
let mut line_ending_inferred = None;
while index < events.len() {
let event = &events[index];
if event.kind == Kind::Exit
&& (event.name == Name::BlankLineEnding || event.name == Name::LineEnding)
{
let slice = Slice::from_position(bytes, &Position::from_exit_event(events, index));
line_ending_inferred = Some(slice.as_str().parse().unwrap());
break;
}
index += 1;
}
let line_ending_default =
line_ending_inferred.unwrap_or_else(|| options.default_line_ending.clone());
let mut context = CompileContext::new(events, bytes, options, line_ending_default);
let mut definition_indices = vec![];
let mut index = 0;
let mut definition_inside = false;
while index < events.len() {
let event = &events[index];
if definition_inside {
handle(&mut context, index);
}
if event.kind == Kind::Enter {
if event.name == Name::Definition {
handle(&mut context, index); definition_inside = true;
definition_indices.push((index, index));
}
} else if event.name == Name::Definition {
definition_inside = false;
definition_indices.last_mut().unwrap().1 = index;
}
index += 1;
}
let mut index = 0;
let jump_default = (events.len(), events.len());
let mut definition_index = 0;
let mut jump = definition_indices
.get(definition_index)
.unwrap_or(&jump_default);
while index < events.len() {
if index == jump.0 {
index = jump.1 + 1;
definition_index += 1;
jump = definition_indices
.get(definition_index)
.unwrap_or(&jump_default);
} else {
handle(&mut context, index);
index += 1;
}
}
if !context.gfm_footnote_definition_calls.is_empty() {
generate_footnote_section(&mut context);
}
debug_assert_eq!(context.buffers.len(), 1, "expected 1 final buffer");
context
.buffers
.get(0)
.expect("expected 1 final buffer")
.into()
}
fn handle(context: &mut CompileContext, index: usize) {
context.index = index;
if context.events[index].kind == Kind::Enter {
enter(context);
} else {
exit(context);
}
}
fn enter(context: &mut CompileContext) {
match context.events[context.index].name {
Name::CodeFencedFenceInfo
| Name::CodeFencedFenceMeta
| Name::MathFlowFenceMeta
| Name::DefinitionLabelString
| Name::DefinitionTitleString
| Name::GfmFootnoteDefinitionPrefix
| Name::HeadingAtxText
| Name::HeadingSetextText
| Name::Label
| Name::MdxEsm
| Name::MdxFlowExpression
| Name::MdxTextExpression
| Name::MdxJsxFlowTag
| Name::MdxJsxTextTag
| Name::ReferenceString
| Name::ResourceTitleString => on_enter_buffer(context),
Name::BlockQuote => on_enter_block_quote(context),
Name::CodeIndented => on_enter_code_indented(context),
Name::CodeFenced | Name::MathFlow => on_enter_raw_flow(context),
Name::CodeText | Name::MathText => on_enter_raw_text(context),
Name::Definition => on_enter_definition(context),
Name::DefinitionDestinationString => on_enter_definition_destination_string(context),
Name::Emphasis => on_enter_emphasis(context),
Name::Frontmatter => on_enter_frontmatter(context),
Name::GfmFootnoteDefinition => on_enter_gfm_footnote_definition(context),
Name::GfmFootnoteCall => on_enter_gfm_footnote_call(context),
Name::GfmStrikethrough => on_enter_gfm_strikethrough(context),
Name::GfmTable => on_enter_gfm_table(context),
Name::GfmTableBody => on_enter_gfm_table_body(context),
Name::GfmTableCell => on_enter_gfm_table_cell(context),
Name::GfmTableHead => on_enter_gfm_table_head(context),
Name::GfmTableRow => on_enter_gfm_table_row(context),
Name::GfmTaskListItemCheck => on_enter_gfm_task_list_item_check(context),
Name::HtmlFlow => on_enter_html_flow(context),
Name::HtmlText => on_enter_html_text(context),
Name::Image => on_enter_image(context),
Name::Link => on_enter_link(context),
Name::ListItemMarker => on_enter_list_item_marker(context),
Name::ListOrdered | Name::ListUnordered => on_enter_list(context),
Name::Paragraph => on_enter_paragraph(context),
Name::Resource => on_enter_resource(context),
Name::ResourceDestinationString => on_enter_resource_destination_string(context),
Name::Strong => on_enter_strong(context),
_ => {}
}
}
fn exit(context: &mut CompileContext) {
match context.events[context.index].name {
Name::CodeFencedFenceMeta
| Name::MathFlowFenceMeta
| Name::MdxJsxTextTag
| Name::MdxTextExpression
| Name::Resource => {
on_exit_drop(context);
}
Name::MdxEsm | Name::MdxFlowExpression | Name::MdxJsxFlowTag => on_exit_drop_slurp(context),
Name::CharacterEscapeValue | Name::CodeTextData | Name::Data | Name::MathTextData => {
on_exit_data(context);
}
Name::AutolinkEmail => on_exit_autolink_email(context),
Name::AutolinkProtocol => on_exit_autolink_protocol(context),
Name::BlankLineEnding => on_exit_blank_line_ending(context),
Name::BlockQuote => on_exit_block_quote(context),
Name::CharacterReferenceMarker => on_exit_character_reference_marker(context),
Name::CharacterReferenceMarkerNumeric => {
on_exit_character_reference_marker_numeric(context);
}
Name::CharacterReferenceMarkerHexadecimal => {
on_exit_character_reference_marker_hexadecimal(context);
}
Name::CharacterReferenceValue => on_exit_character_reference_value(context),
Name::CodeFenced | Name::CodeIndented | Name::MathFlow => on_exit_raw_flow(context),
Name::CodeFencedFence | Name::MathFlowFence => on_exit_raw_flow_fence(context),
Name::CodeFencedFenceInfo => on_exit_raw_flow_fence_info(context),
Name::CodeFlowChunk | Name::MathFlowChunk => on_exit_raw_flow_chunk(context),
Name::CodeText | Name::MathText => on_exit_raw_text(context),
Name::Definition => on_exit_definition(context),
Name::DefinitionDestinationString => on_exit_definition_destination_string(context),
Name::DefinitionLabelString => on_exit_definition_label_string(context),
Name::DefinitionTitleString => on_exit_definition_title_string(context),
Name::Emphasis => on_exit_emphasis(context),
Name::Frontmatter => on_exit_frontmatter(context),
Name::GfmAutolinkLiteralEmail => on_exit_gfm_autolink_literal_email(context),
Name::GfmAutolinkLiteralMailto => on_exit_gfm_autolink_literal_mailto(context),
Name::GfmAutolinkLiteralProtocol => on_exit_gfm_autolink_literal_protocol(context),
Name::GfmAutolinkLiteralWww => on_exit_gfm_autolink_literal_www(context),
Name::GfmAutolinkLiteralXmpp => on_exit_gfm_autolink_literal_xmpp(context),
Name::GfmFootnoteCall => on_exit_gfm_footnote_call(context),
Name::GfmFootnoteDefinitionLabelString => {
on_exit_gfm_footnote_definition_label_string(context);
}
Name::GfmFootnoteDefinitionPrefix => on_exit_gfm_footnote_definition_prefix(context),
Name::GfmFootnoteDefinition => on_exit_gfm_footnote_definition(context),
Name::GfmStrikethrough => on_exit_gfm_strikethrough(context),
Name::GfmTable => on_exit_gfm_table(context),
Name::GfmTableBody => on_exit_gfm_table_body(context),
Name::GfmTableCell => on_exit_gfm_table_cell(context),
Name::GfmTableHead => on_exit_gfm_table_head(context),
Name::GfmTableRow => on_exit_gfm_table_row(context),
Name::GfmTaskListItemCheck => on_exit_gfm_task_list_item_check(context),
Name::GfmTaskListItemValueChecked => on_exit_gfm_task_list_item_value_checked(context),
Name::HardBreakEscape | Name::HardBreakTrailing => on_exit_break(context),
Name::HeadingAtx => on_exit_heading_atx(context),
Name::HeadingAtxSequence => on_exit_heading_atx_sequence(context),
Name::HeadingAtxText => on_exit_heading_atx_text(context),
Name::HeadingSetextText => on_exit_heading_setext_text(context),
Name::HeadingSetextUnderlineSequence => on_exit_heading_setext_underline_sequence(context),
Name::HtmlFlow | Name::HtmlText => on_exit_html(context),
Name::HtmlFlowData | Name::HtmlTextData => on_exit_html_data(context),
Name::Image | Name::Link => on_exit_media(context),
Name::Label => on_exit_label(context),
Name::LabelText => on_exit_label_text(context),
Name::LineEnding => on_exit_line_ending(context),
Name::ListOrdered | Name::ListUnordered => on_exit_list(context),
Name::ListItem => on_exit_list_item(context),
Name::ListItemValue => on_exit_list_item_value(context),
Name::Paragraph => on_exit_paragraph(context),
Name::ReferenceString => on_exit_reference_string(context),
Name::ResourceDestinationString => on_exit_resource_destination_string(context),
Name::ResourceTitleString => on_exit_resource_title_string(context),
Name::Strong => on_exit_strong(context),
Name::ThematicBreak => on_exit_thematic_break(context),
_ => {}
}
}
fn on_enter_buffer(context: &mut CompileContext) {
context.buffer();
}
fn on_enter_block_quote(context: &mut CompileContext) {
context.tight_stack.push(false);
context.line_ending_if_needed();
context.push("<blockquote>");
}
fn on_enter_code_indented(context: &mut CompileContext) {
context.raw_flow_seen_data = Some(false);
context.line_ending_if_needed();
context.push("<pre><code>");
}
fn on_enter_raw_flow(context: &mut CompileContext) {
context.raw_flow_seen_data = Some(false);
context.line_ending_if_needed();
context.push("<pre><code");
context.raw_flow_fences_count = Some(0);
if context.events[context.index].name == Name::MathFlow {
context.push(" class=\"language-math math-display\"");
}
}
fn on_enter_raw_text(context: &mut CompileContext) {
context.raw_text_inside = true;
if !context.image_alt_inside {
context.push("<code");
if context.events[context.index].name == Name::MathText {
context.push(" class=\"language-math math-inline\"");
}
context.push(">");
}
context.buffer();
}
fn on_enter_definition(context: &mut CompileContext) {
context.buffer();
context.media_stack.push(Media {
image: false,
label: None,
label_id: None,
reference_id: None,
destination: None,
title: None,
});
}
fn on_enter_definition_destination_string(context: &mut CompileContext) {
context.buffer();
context.encode_html = false;
}
fn on_enter_emphasis(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("<em>");
}
}
fn on_enter_frontmatter(context: &mut CompileContext) {
context.buffer();
}
fn on_enter_gfm_footnote_definition(context: &mut CompileContext) {
context.tight_stack.push(false);
}
fn on_enter_gfm_footnote_call(context: &mut CompileContext) {
context.media_stack.push(Media {
image: false,
label_id: None,
label: None,
reference_id: None,
destination: None,
title: None,
});
}
fn on_enter_gfm_strikethrough(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("<del>");
}
}
fn on_enter_gfm_table(context: &mut CompileContext) {
let align = gfm_table_align(context.events, context.index);
context.gfm_table_align = Some(align);
context.line_ending_if_needed();
context.push("<table>");
}
fn on_enter_gfm_table_body(context: &mut CompileContext) {
context.push("<tbody>");
}
fn on_enter_gfm_table_cell(context: &mut CompileContext) {
let column = context.gfm_table_column;
let align = context.gfm_table_align.as_ref().unwrap();
if column >= align.len() {
context.buffer();
} else {
let value = align[column];
context.line_ending_if_needed();
if context.gfm_table_in_head {
context.push("<th");
} else {
context.push("<td");
}
match value {
AlignKind::Left => context.push(" align=\"left\""),
AlignKind::Right => context.push(" align=\"right\""),
AlignKind::Center => context.push(" align=\"center\""),
AlignKind::None => {}
}
context.push(">");
}
}
fn on_enter_gfm_table_head(context: &mut CompileContext) {
context.line_ending_if_needed();
context.push("<thead>");
context.gfm_table_in_head = true;
}
fn on_enter_gfm_table_row(context: &mut CompileContext) {
context.line_ending_if_needed();
context.push("<tr>");
}
fn on_enter_gfm_task_list_item_check(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("<input type=\"checkbox\" disabled=\"\" ");
}
}
fn on_enter_html_flow(context: &mut CompileContext) {
context.line_ending_if_needed();
if context.options.allow_dangerous_html {
context.encode_html = false;
}
}
fn on_enter_html_text(context: &mut CompileContext) {
if context.options.allow_dangerous_html {
context.encode_html = false;
}
}
fn on_enter_image(context: &mut CompileContext) {
context.media_stack.push(Media {
image: true,
label_id: None,
label: None,
reference_id: None,
destination: None,
title: None,
});
context.image_alt_inside = true; }
fn on_enter_link(context: &mut CompileContext) {
context.media_stack.push(Media {
image: false,
label_id: None,
label: None,
reference_id: None,
destination: None,
title: None,
});
}
fn on_enter_list(context: &mut CompileContext) {
let loose = list_loose(context.events, context.index, true);
context.tight_stack.push(!loose);
context.line_ending_if_needed();
context.push(if context.events[context.index].name == Name::ListOrdered {
"<ol"
} else {
"<ul"
});
context.list_expect_first_marker = Some(true);
}
fn on_enter_list_item_marker(context: &mut CompileContext) {
if context.list_expect_first_marker.take().unwrap() {
context.push(">");
}
context.line_ending_if_needed();
context.push("<li>");
context.list_expect_first_marker = Some(false);
}
fn on_enter_paragraph(context: &mut CompileContext) {
let tight = context.tight_stack.last().unwrap_or(&false);
if !tight {
context.line_ending_if_needed();
context.push("<p>");
}
}
fn on_enter_resource(context: &mut CompileContext) {
context.buffer(); context.media_stack.last_mut().unwrap().destination = Some(String::new());
}
fn on_enter_resource_destination_string(context: &mut CompileContext) {
context.buffer();
context.encode_html = false;
}
fn on_enter_strong(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("<strong>");
}
}
fn on_exit_autolink_email(context: &mut CompileContext) {
generate_autolink(
context,
Some("mailto:"),
Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
.as_str(),
false,
);
}
fn on_exit_autolink_protocol(context: &mut CompileContext) {
generate_autolink(
context,
None,
Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
.as_str(),
false,
);
}
fn on_exit_break(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("<br />");
}
}
fn on_exit_blank_line_ending(context: &mut CompileContext) {
if context.index == context.events.len() - 1 {
context.line_ending_if_needed();
}
}
fn on_exit_block_quote(context: &mut CompileContext) {
context.tight_stack.pop();
context.line_ending_if_needed();
context.slurp_one_line_ending = false;
context.push("</blockquote>");
}
fn on_exit_character_reference_marker(context: &mut CompileContext) {
context.character_reference_marker = Some(b'&');
}
fn on_exit_character_reference_marker_hexadecimal(context: &mut CompileContext) {
context.character_reference_marker = Some(b'x');
}
fn on_exit_character_reference_marker_numeric(context: &mut CompileContext) {
context.character_reference_marker = Some(b'#');
}
fn on_exit_character_reference_value(context: &mut CompileContext) {
let marker = context
.character_reference_marker
.take()
.expect("expected `character_reference_kind` to be set");
let slice = Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
);
let value = decode_character_reference(slice.as_str(), marker, true)
.expect("expected to parse only valid named references");
context.push(&encode(&value, context.encode_html));
}
fn on_exit_raw_flow_chunk(context: &mut CompileContext) {
context.raw_flow_seen_data = Some(true);
context.push(&encode(
&Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
.serialize(),
context.encode_html,
));
}
fn on_exit_raw_flow_fence(context: &mut CompileContext) {
let count = context
.raw_flow_fences_count
.expect("expected `raw_flow_fences_count`");
if count == 0 {
context.push(">");
context.slurp_one_line_ending = true;
}
context.raw_flow_fences_count = Some(count + 1);
}
fn on_exit_raw_flow_fence_info(context: &mut CompileContext) {
let value = context.resume();
context.push(" class=\"language-");
context.push(&value);
context.push("\"");
}
fn on_exit_raw_flow(context: &mut CompileContext) {
if let Some(count) = context.raw_flow_fences_count {
if count == 1
&& !context.tight_stack.is_empty()
&& !matches!(context.events[context.index - 1].name, Name::CodeFencedFence | Name::MathFlowFence)
{
context.line_ending();
}
}
if context
.raw_flow_seen_data
.take()
.expect("`raw_flow_seen_data` must be defined")
{
context.line_ending_if_needed();
}
context.push("</code></pre>");
if let Some(count) = context.raw_flow_fences_count.take() {
if count < 2 {
context.line_ending_if_needed();
}
}
context.slurp_one_line_ending = false;
}
fn on_exit_raw_text(context: &mut CompileContext) {
let result = context.resume();
let mut bytes = result.as_bytes().to_vec();
if context.gfm_table_align.is_some() {
let mut index = 0;
let mut len = bytes.len();
while index < len {
if index + 1 < len && bytes[index] == b'\\' && bytes[index + 1] == b'|' {
bytes.remove(index);
len -= 1;
}
index += 1;
}
}
let mut trim = false;
let mut index = 0;
let mut end = bytes.len();
if end > 2 && bytes[index] == b' ' && bytes[end - 1] == b' ' {
index += 1;
end -= 1;
while index < end && !trim {
if bytes[index] != b' ' {
trim = true;
break;
}
index += 1;
}
}
if trim {
bytes.remove(0);
bytes.pop();
}
context.raw_text_inside = false;
context.push(str::from_utf8(&bytes).unwrap());
if !context.image_alt_inside {
context.push("</code>");
}
}
fn on_exit_drop(context: &mut CompileContext) {
context.resume();
}
fn on_exit_drop_slurp(context: &mut CompileContext) {
context.resume();
context.slurp_one_line_ending = true;
}
fn on_exit_data(context: &mut CompileContext) {
context.push(&encode(
Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
.as_str(),
context.encode_html,
));
}
fn on_exit_definition(context: &mut CompileContext) {
context.resume();
let media = context.media_stack.pop().unwrap();
let indices = media.reference_id.unwrap();
let id =
normalize_identifier(Slice::from_indices(context.bytes, indices.0, indices.1).as_str());
context.definitions.push(Definition {
id,
destination: media.destination,
title: media.title,
});
}
fn on_exit_definition_destination_string(context: &mut CompileContext) {
let buf = context.resume();
context.media_stack.last_mut().unwrap().destination = Some(buf);
context.encode_html = true;
}
fn on_exit_definition_label_string(context: &mut CompileContext) {
context.resume();
context.media_stack.last_mut().unwrap().reference_id =
Some(Position::from_exit_event(context.events, context.index).to_indices());
}
fn on_exit_definition_title_string(context: &mut CompileContext) {
let buf = context.resume();
context.media_stack.last_mut().unwrap().title = Some(buf);
}
fn on_exit_emphasis(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("</em>");
}
}
fn on_exit_frontmatter(context: &mut CompileContext) {
context.resume();
context.slurp_one_line_ending = true;
}
fn on_exit_gfm_autolink_literal_email(context: &mut CompileContext) {
generate_autolink(
context,
Some("mailto:"),
Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
.as_str(),
true,
);
}
fn on_exit_gfm_autolink_literal_mailto(context: &mut CompileContext) {
generate_autolink(
context,
None,
Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
.as_str(),
true,
);
}
fn on_exit_gfm_autolink_literal_protocol(context: &mut CompileContext) {
generate_autolink(
context,
None,
Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
.as_str(),
true,
);
}
fn on_exit_gfm_autolink_literal_www(context: &mut CompileContext) {
generate_autolink(
context,
Some("http://"),
Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
.as_str(),
true,
);
}
fn on_exit_gfm_autolink_literal_xmpp(context: &mut CompileContext) {
generate_autolink(
context,
None,
Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
.as_str(),
true,
);
}
fn on_exit_gfm_footnote_call(context: &mut CompileContext) {
let indices = context.media_stack.pop().unwrap().label_id.unwrap();
let id =
normalize_identifier(Slice::from_indices(context.bytes, indices.0, indices.1).as_str());
let safe_id = sanitize(&id.to_lowercase());
let mut call_index = 0;
while call_index < context.gfm_footnote_definition_calls.len() {
if context.gfm_footnote_definition_calls[call_index].0 == id {
break;
}
call_index += 1;
}
if call_index == context.gfm_footnote_definition_calls.len() {
context.gfm_footnote_definition_calls.push((id, 0));
}
context.gfm_footnote_definition_calls[call_index].1 += 1;
if context.image_alt_inside {
return;
}
context.push("<sup><a href=\"#");
if let Some(ref value) = context.options.gfm_footnote_clobber_prefix {
context.push(&encode(value, context.encode_html));
} else {
context.push("user-content-");
}
context.push("fn-");
context.push(&safe_id);
context.push("\" id=\"");
if let Some(ref value) = context.options.gfm_footnote_clobber_prefix {
context.push(&encode(value, context.encode_html));
} else {
context.push("user-content-");
}
context.push("fnref-");
context.push(&safe_id);
if context.gfm_footnote_definition_calls[call_index].1 > 1 {
context.push("-");
context.push(
&context.gfm_footnote_definition_calls[call_index]
.1
.to_string(),
);
}
context.push("\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">");
context.push(&(call_index + 1).to_string());
context.push("</a></sup>");
}
fn on_exit_gfm_footnote_definition_label_string(context: &mut CompileContext) {
context
.gfm_footnote_definition_stack
.push(Position::from_exit_event(context.events, context.index).to_indices());
}
fn on_exit_gfm_footnote_definition_prefix(context: &mut CompileContext) {
context.resume();
context.buffer();
}
fn on_exit_gfm_footnote_definition(context: &mut CompileContext) {
let value = context.resume();
let indices = context.gfm_footnote_definition_stack.pop().unwrap();
context.tight_stack.pop();
context.gfm_footnote_definitions.push((
normalize_identifier(Slice::from_indices(context.bytes, indices.0, indices.1).as_str()),
value,
));
}
fn on_exit_gfm_strikethrough(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("</del>");
}
}
fn on_exit_gfm_table(context: &mut CompileContext) {
context.gfm_table_align = None;
context.line_ending_if_needed();
context.push("</table>");
}
fn on_exit_gfm_table_body(context: &mut CompileContext) {
context.line_ending_if_needed();
context.push("</tbody>");
}
fn on_exit_gfm_table_cell(context: &mut CompileContext) {
let align = context.gfm_table_align.as_ref().unwrap();
if context.gfm_table_column < align.len() {
if context.gfm_table_in_head {
context.push("</th>");
} else {
context.push("</td>");
}
} else {
context.resume();
}
context.gfm_table_column += 1;
}
fn on_exit_gfm_table_head(context: &mut CompileContext) {
context.gfm_table_in_head = false;
context.line_ending_if_needed();
context.push("</thead>");
}
fn on_exit_gfm_table_row(context: &mut CompileContext) {
let mut column = context.gfm_table_column;
let len = context.gfm_table_align.as_ref().unwrap().len();
while column < len {
on_enter_gfm_table_cell(context);
on_exit_gfm_table_cell(context);
column += 1;
}
context.gfm_table_column = 0;
context.line_ending_if_needed();
context.push("</tr>");
}
fn on_exit_gfm_task_list_item_check(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("/>");
}
}
fn on_exit_gfm_task_list_item_value_checked(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("checked=\"\" ");
}
}
fn on_exit_heading_atx(context: &mut CompileContext) {
let rank = context
.heading_atx_rank
.take()
.expect("`heading_atx_rank` must be set in headings");
context.push("</h");
context.push(&rank.to_string());
context.push(">");
}
fn on_exit_heading_atx_sequence(context: &mut CompileContext) {
if context.heading_atx_rank.is_none() {
let rank = Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
.len();
context.line_ending_if_needed();
context.heading_atx_rank = Some(rank);
context.push("<h");
context.push(&rank.to_string());
context.push(">");
}
}
fn on_exit_heading_atx_text(context: &mut CompileContext) {
let value = context.resume();
context.push(&value);
}
fn on_exit_heading_setext_text(context: &mut CompileContext) {
let buf = context.resume();
context.heading_setext_buffer = Some(buf);
context.slurp_one_line_ending = true;
}
fn on_exit_heading_setext_underline_sequence(context: &mut CompileContext) {
let text = context
.heading_setext_buffer
.take()
.expect("`heading_atx_rank` must be set in headings");
let position = Position::from_exit_event(context.events, context.index);
let head = context.bytes[position.start.index];
let rank = if head == b'-' { "2" } else { "1" };
context.line_ending_if_needed();
context.push("<h");
context.push(rank);
context.push(">");
context.push(&text);
context.push("</h");
context.push(rank);
context.push(">");
}
fn on_exit_html(context: &mut CompileContext) {
context.encode_html = true;
}
fn on_exit_html_data(context: &mut CompileContext) {
let slice = Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
);
let value = slice.as_str();
let encoded = if context.options.gfm_tagfilter && context.options.allow_dangerous_html {
encode(&gfm_tagfilter(value), context.encode_html)
} else {
encode(value, context.encode_html)
};
context.push(&encoded);
}
fn on_exit_label(context: &mut CompileContext) {
let buf = context.resume();
context.media_stack.last_mut().unwrap().label = Some(buf);
}
fn on_exit_label_text(context: &mut CompileContext) {
context.media_stack.last_mut().unwrap().label_id =
Some(Position::from_exit_event(context.events, context.index).to_indices());
}
fn on_exit_line_ending(context: &mut CompileContext) {
if context.raw_text_inside {
context.push(" ");
} else if context.slurp_one_line_ending
|| (context.index > 1
&& (context.events[context.index - 2].name == Name::Definition
|| context.events[context.index - 2].name == Name::GfmFootnoteDefinition))
{
context.slurp_one_line_ending = false;
} else {
context.push(&encode(
Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
.as_str(),
context.encode_html,
));
}
}
fn on_exit_list(context: &mut CompileContext) {
context.tight_stack.pop();
context.line_ending();
context.push(if context.events[context.index].name == Name::ListOrdered {
"</ol>"
} else {
"</ul>"
});
}
fn on_exit_list_item(context: &mut CompileContext) {
let tight = context.tight_stack.last().unwrap_or(&false);
let before_item = skip::opt_back(
context.events,
context.index - 1,
&[
Name::BlankLineEnding,
Name::BlockQuotePrefix,
Name::LineEnding,
Name::SpaceOrTab,
Name::Definition,
Name::GfmFootnoteDefinition,
],
);
let previous = &context.events[before_item];
let tight_paragraph = *tight && previous.name == Name::Paragraph;
let empty_item = previous.name == Name::ListItemPrefix;
context.slurp_one_line_ending = false;
if !tight_paragraph && !empty_item {
context.line_ending_if_needed();
}
context.push("</li>");
}
fn on_exit_list_item_value(context: &mut CompileContext) {
if context.list_expect_first_marker.unwrap() {
let slice = Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
);
let value = slice.as_str().parse::<u32>().ok().unwrap();
if value != 1 {
context.push(" start=\"");
context.push(&value.to_string());
context.push("\"");
}
}
}
fn on_exit_media(context: &mut CompileContext) {
let mut is_in_image = false;
let mut index = 0;
let end = context.media_stack.len() - 1;
while index < end {
if context.media_stack[index].image {
is_in_image = true;
break;
}
index += 1;
}
context.image_alt_inside = is_in_image;
let media = context.media_stack.pop().unwrap();
let label = media.label.unwrap();
let id = media.reference_id.or(media.label_id).map(|indices| {
normalize_identifier(Slice::from_indices(context.bytes, indices.0, indices.1).as_str())
});
let definition_index = if media.destination.is_none() {
id.map(|id| {
let mut index = 0;
while index < context.definitions.len() && context.definitions[index].id != id {
index += 1;
}
debug_assert!(
index < context.definitions.len(),
"expected defined definition"
);
index
})
} else {
None
};
if !is_in_image {
if media.image {
context.push("<img src=\"");
} else {
context.push("<a href=\"");
};
let destination = if let Some(index) = definition_index {
context.definitions[index].destination.as_ref()
} else {
media.destination.as_ref()
};
if let Some(destination) = destination {
let url = if context.options.allow_dangerous_protocol {
sanitize(destination)
} else {
sanitize_with_protocols(
destination,
if media.image {
&SAFE_PROTOCOL_SRC
} else {
&SAFE_PROTOCOL_HREF
},
)
};
context.push(&url);
}
if media.image {
context.push("\" alt=\"");
};
}
if media.image {
context.push(&label);
}
if !is_in_image {
context.push("\"");
let title = if let Some(index) = definition_index {
context.definitions[index].title.clone()
} else {
media.title
};
if let Some(title) = title {
context.push(" title=\"");
context.push(&title);
context.push("\"");
};
if media.image {
context.push(" /");
}
context.push(">");
}
if !media.image {
context.push(&label);
if !is_in_image {
context.push("</a>");
}
}
}
fn on_exit_paragraph(context: &mut CompileContext) {
let tight = context.tight_stack.last().unwrap_or(&false);
if *tight {
context.slurp_one_line_ending = true;
} else {
context.push("</p>");
}
}
fn on_exit_reference_string(context: &mut CompileContext) {
context.resume();
context.media_stack.last_mut().unwrap().reference_id =
Some(Position::from_exit_event(context.events, context.index).to_indices());
}
fn on_exit_resource_destination_string(context: &mut CompileContext) {
let buf = context.resume();
context.media_stack.last_mut().unwrap().destination = Some(buf);
context.encode_html = true;
}
fn on_exit_resource_title_string(context: &mut CompileContext) {
let buf = context.resume();
context.media_stack.last_mut().unwrap().title = Some(buf);
}
fn on_exit_strong(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("</strong>");
}
}
fn on_exit_thematic_break(context: &mut CompileContext) {
context.line_ending_if_needed();
context.push("<hr />");
}
fn generate_footnote_section(context: &mut CompileContext) {
context.line_ending_if_needed();
context.push("<section data-footnotes=\"\" class=\"footnotes\"><");
if let Some(ref value) = context.options.gfm_footnote_label_tag_name {
context.push(&encode(value, context.encode_html));
} else {
context.push("h2");
}
context.push(" id=\"footnote-label\" ");
if let Some(ref value) = context.options.gfm_footnote_label_attributes {
context.push(value);
} else {
context.push("class=\"sr-only\"");
}
context.push(">");
if let Some(ref value) = context.options.gfm_footnote_label {
context.push(&encode(value, context.encode_html));
} else {
context.push("Footnotes");
}
context.push("</");
if let Some(ref value) = context.options.gfm_footnote_label_tag_name {
context.push(&encode(value, context.encode_html));
} else {
context.push("h2");
}
context.push(">");
context.line_ending();
context.push("<ol>");
let mut index = 0;
while index < context.gfm_footnote_definition_calls.len() {
generate_footnote_item(context, index);
index += 1;
}
context.line_ending();
context.push("</ol>");
context.line_ending();
context.push("</section>");
context.line_ending();
}
fn generate_footnote_item(context: &mut CompileContext, index: usize) {
let id = &context.gfm_footnote_definition_calls[index].0;
let safe_id = sanitize(&id.to_lowercase());
let mut definition_index = 0;
while definition_index < context.gfm_footnote_definitions.len() {
if &context.gfm_footnote_definitions[definition_index].0 == id {
break;
}
definition_index += 1;
}
debug_assert_ne!(
definition_index,
context.gfm_footnote_definitions.len(),
"expected definition"
);
context.line_ending();
context.push("<li id=\"");
if let Some(ref value) = context.options.gfm_footnote_clobber_prefix {
context.push(&encode(value, context.encode_html));
} else {
context.push("user-content-");
}
context.push("fn-");
context.push(&safe_id);
context.push("\">");
context.line_ending();
let mut reference_index = 0;
let mut backreferences = String::new();
while reference_index < context.gfm_footnote_definition_calls[index].1 {
if reference_index != 0 {
backreferences.push(' ');
}
backreferences.push_str("<a href=\"#");
if let Some(ref value) = context.options.gfm_footnote_clobber_prefix {
backreferences.push_str(&encode(value, context.encode_html));
} else {
backreferences.push_str("user-content-");
}
backreferences.push_str("fnref-");
backreferences.push_str(&safe_id);
if reference_index != 0 {
backreferences.push('-');
backreferences.push_str(&(reference_index + 1).to_string());
}
backreferences.push_str("\" data-footnote-backref=\"\" aria-label=\"");
if let Some(ref value) = context.options.gfm_footnote_back_label {
backreferences.push_str(&encode(value, context.encode_html));
} else {
backreferences.push_str("Back to content");
}
backreferences.push_str("\" class=\"data-footnote-backref\">↩");
if reference_index != 0 {
backreferences.push_str("<sup>");
backreferences.push_str(&(reference_index + 1).to_string());
backreferences.push_str("</sup>");
}
backreferences.push_str("</a>");
reference_index += 1;
}
let value = context.gfm_footnote_definitions[definition_index].1.clone();
let bytes = value.as_bytes();
let mut byte_index = bytes.len();
while byte_index > 0 && matches!(bytes[byte_index - 1], b'\n' | b'\r') {
byte_index -= 1;
}
if byte_index > 3
&& bytes[byte_index - 4] == b'<'
&& bytes[byte_index - 3] == b'/'
&& bytes[byte_index - 2] == b'p'
&& bytes[byte_index - 1] == b'>'
{
let (before, after) = bytes.split_at(byte_index - 4);
let mut result = String::new();
result.push_str(str::from_utf8(before).unwrap());
result.push(' ');
result.push_str(&backreferences);
result.push_str(str::from_utf8(after).unwrap());
context.push(&result);
} else {
context.push(&value);
context.line_ending_if_needed();
context.push(&backreferences);
}
context.line_ending_if_needed();
context.push("</li>");
}
fn generate_autolink(
context: &mut CompileContext,
protocol: Option<&str>,
value: &str,
is_gfm_literal: bool,
) {
let mut is_in_link = false;
let mut index = 0;
while index < context.media_stack.len() {
if !context.media_stack[index].image {
is_in_link = true;
break;
}
index += 1;
}
if !context.image_alt_inside && (!is_in_link || !is_gfm_literal) {
context.push("<a href=\"");
let url = if let Some(protocol) = protocol {
format!("{}{}", protocol, value)
} else {
value.into()
};
let url = if context.options.allow_dangerous_protocol {
sanitize(&url)
} else {
sanitize_with_protocols(&url, &SAFE_PROTOCOL_HREF)
};
context.push(&url);
context.push("\">");
}
context.push(&encode(value, context.encode_html));
if !context.image_alt_inside && (!is_in_link || !is_gfm_literal) {
context.push("</a>");
}
}