//! Turn events into a string of HTML.
use crate::event::{Event, Kind, Name};
use crate::util::{
constant::{SAFE_PROTOCOL_HREF, SAFE_PROTOCOL_SRC},
decode_character_reference::{decode_named, decode_numeric},
encode::encode,
normalize_identifier::normalize_identifier,
sanitize_uri::sanitize_uri,
skip,
slice::{Position, Slice},
};
use crate::{LineEnding, Options};
use alloc::{
format,
string::{String, ToString},
vec,
vec::Vec,
};
use core::str;
/// Link or image, resource or reference.
/// Reused for temporary definitions as well, in the first pass.
#[derive(Debug)]
struct Media {
/// Whether this represents an image (`true`) or a link or definition
/// (`false`).
image: bool,
/// The text between the brackets (`x` in `![x]()` and `[x]()`).
///
/// Not interpreted.
label_id: Option<(usize, usize)>,
/// The result of interpreting the text between the brackets
/// (`x` in `![x]()` and `[x]()`).
///
/// When this is a link, it contains further text content and thus HTML
/// tags.
/// Otherwise, when an image, text content is also allowed, but resulting
/// tags are ignored.
label: Option<String>,
/// The string between the explicit brackets of the reference (`y` in
/// `[x][y]`), as content.
///
/// Not interpreted.
reference_id: Option<(usize, usize)>,
/// The destination (url).
///
/// Interpreted string content.
destination: Option<String>,
/// The destination (url).
///
/// Interpreted string content.
title: Option<String>,
}
/// Representation of a definition.
#[derive(Debug)]
struct Definition {
/// Identifier.
id: String,
/// The destination (url).
///
/// Interpreted string content.
destination: Option<String>,
/// The title.
///
/// Interpreted string content.
title: Option<String>,
}
/// Context used to compile markdown.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug)]
struct CompileContext<'a> {
// Static info.
/// List of events.
pub events: &'a [Event],
/// List of bytes.
pub bytes: &'a [u8],
// Fields used by handlers to track the things they need to track to
// compile markdown.
/// Rank of heading (atx).
pub heading_atx_rank: Option<usize>,
/// Buffer of heading (setext) text.
pub heading_setext_buffer: Option<String>,
/// Whether code (flow) contains data.
pub code_flow_seen_data: Option<bool>,
/// Number of code (fenced) fenced.
pub code_fenced_fences_count: Option<usize>,
/// Whether we are in code (text).
pub code_text_inside: bool,
/// Whether we are in image text.
pub image_alt_inside: bool,
/// Marker of character reference.
pub character_reference_marker: Option<u8>,
/// Whether we are expecting the first list item marker.
pub list_expect_first_marker: Option<bool>,
/// Stack of media (link, image).
pub media_stack: Vec<Media>,
/// Stack of containers.
pub tight_stack: Vec<bool>,
/// List of definitions.
pub definitions: Vec<Definition>,
// Fields used to influance the current compilation.
/// Ignore the next line ending.
pub slurp_one_line_ending: bool,
/// Whether to encode HTML.
pub encode_html: bool,
// Configuration
/// Whether to sanitize `href`s, and in which case, which protocols to
/// allow.
pub protocol_href: Option<Vec<&'static str>>,
/// Whether to sanitize `src`s, and in which case, which protocols to
/// allow.
pub protocol_src: Option<Vec<&'static str>>,
/// Line ending to use.
pub line_ending_default: LineEnding,
/// Whether to allow HTML.
pub allow_dangerous_html: bool,
// Intermediate results.
/// Stack of buffers.
pub buffers: Vec<String>,
/// Current event index.
pub index: usize,
}
impl<'a> CompileContext<'a> {
/// Create a new compile context.
pub fn new(
events: &'a [Event],
bytes: &'a [u8],
options: &Options,
line_ending: LineEnding,
) -> CompileContext<'a> {
CompileContext {
events,
bytes,
heading_atx_rank: None,
heading_setext_buffer: None,
code_flow_seen_data: None,
code_fenced_fences_count: None,
code_text_inside: false,
character_reference_marker: None,
list_expect_first_marker: None,
media_stack: vec![],
definitions: vec![],
tight_stack: vec![],
slurp_one_line_ending: false,
image_alt_inside: false,
encode_html: true,
protocol_href: if options.allow_dangerous_protocol {
None
} else {
Some(SAFE_PROTOCOL_HREF.to_vec())
},
protocol_src: if options.allow_dangerous_protocol {
None
} else {
Some(SAFE_PROTOCOL_SRC.to_vec())
},
line_ending_default: line_ending,
allow_dangerous_html: options.allow_dangerous_html,
buffers: vec![String::new()],
index: 0,
}
}
/// Push a buffer.
pub fn buffer(&mut self) {
self.buffers.push(String::new());
}
/// Pop a buffer, returning its value.
pub fn resume(&mut self) -> String {
self.buffers.pop().expect("Cannot resume w/o buffer")
}
/// Push a str to the last buffer.
pub fn push(&mut self, value: &str) {
self.buffers
.last_mut()
.expect("Cannot push w/o buffer")
.push_str(value);
}
/// Add a line ending.
pub fn line_ending(&mut self) {
let eol = self.line_ending_default.as_str().to_string();
self.push(&eol);
}
/// Add a line ending if needed (as in, there’s no eol/eof already).
pub fn line_ending_if_needed(&mut self) {
let tail = self
.buffers
.last()
.expect("at least one buffer should exist")
.as_bytes()
.last();
if !matches!(tail, None | Some(b'\n' | b'\r')) {
self.line_ending();
}
}
}
/// Turn events and codes into a string of HTML.
pub fn compile(events: &[Event], bytes: &[u8], options: &Options) -> String {
let mut index = 0;
let mut line_ending_inferred = None;
// First, we figure out what the used line ending style is.
// Stop when we find a line ending.
while index < events.len() {
let event = &events[index];
if event.kind == Kind::Exit
&& (event.name == Name::BlankLineEnding || event.name == Name::LineEnding)
{
line_ending_inferred = Some(LineEnding::from_str(
Slice::from_position(bytes, &Position::from_exit_event(events, index)).as_str(),
));
break;
}
index += 1;
}
// Figure out which line ending style we’ll use.
let line_ending_default = if let Some(value) = line_ending_inferred {
value
} 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;
// Handle all definitions first.
// We must do two passes because we need to compile the events in
// definitions which come after references already.
//
// To speed things up, we collect the places we can jump over for the
// second pass.
while index < events.len() {
let event = &events[index];
if definition_inside {
handle(&mut context, index);
}
if event.name == Name::Definition {
if event.kind == Kind::Enter {
handle(&mut context, index); // Also handle start.
definition_inside = true;
definition_indices.push((index, index));
} else {
definition_inside = false;
definition_indices.last_mut().unwrap().1 = index;
}
}
index += 1;
}
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);
// Ignore line endings after definitions.
context.slurp_one_line_ending = true;
} else {
handle(&mut context, index);
index += 1;
}
}
assert_eq!(context.buffers.len(), 1, "expected 1 final buffer");
context
.buffers
.get(0)
.expect("expected 1 final buffer")
.to_string()
}
/// Handle the event at `index`.
fn handle(context: &mut CompileContext, index: usize) {
context.index = index;
if context.events[index].kind == Kind::Enter {
enter(context);
} else {
exit(context);
}
}
/// Handle [`Enter`][Kind::Enter].
fn enter(context: &mut CompileContext) {
match context.events[context.index].name {
Name::CodeFencedFenceInfo
| Name::CodeFencedFenceMeta
| Name::DefinitionLabelString
| Name::DefinitionTitleString
| Name::HeadingAtxText
| Name::HeadingSetextText
| Name::Label
| Name::ReferenceString
| Name::ResourceTitleString => on_enter_buffer(context),
Name::BlockQuote => on_enter_block_quote(context),
Name::CodeIndented => on_enter_code_indented(context),
Name::CodeFenced => on_enter_code_fenced(context),
Name::CodeText => on_enter_code_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::GfmStrikethrough => on_enter_gfm_strikethrough(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),
_ => {}
}
}
/// Handle [`Exit`][Kind::Exit].
fn exit(context: &mut CompileContext) {
match context.events[context.index].name {
Name::CodeFencedFenceMeta | Name::Resource => on_exit_drop(context),
Name::CharacterEscapeValue | Name::CodeTextData | Name::Data => 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 => on_exit_code_flow(context),
Name::CodeFencedFence => on_exit_code_fenced_fence(context),
Name::CodeFencedFenceInfo => on_exit_code_fenced_fence_info(context),
Name::CodeFlowChunk => on_exit_code_flow_chunk(context),
Name::CodeText => on_exit_code_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::GfmAutolinkLiteralProtocol => on_exit_gfm_autolink_literal_protocol(context),
Name::GfmAutolinkLiteralWww => on_exit_gfm_autolink_literal_www(context),
Name::GfmAutolinkLiteralEmail => on_exit_gfm_autolink_literal_email(context),
Name::GfmStrikethrough => on_exit_gfm_strikethrough(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::HeadingSetextUnderline => on_exit_heading_setext_underline(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),
_ => {}
}
}
/// Handle [`Enter`][Kind::Enter]:`*`.
///
/// Buffers data.
fn on_enter_buffer(context: &mut CompileContext) {
context.buffer();
}
/// Handle [`Enter`][Kind::Enter]:[`BlockQuote`][Name::BlockQuote].
fn on_enter_block_quote(context: &mut CompileContext) {
context.tight_stack.push(false);
context.line_ending_if_needed();
context.push("<blockquote>");
}
/// Handle [`Enter`][Kind::Enter]:[`CodeIndented`][Name::CodeIndented].
fn on_enter_code_indented(context: &mut CompileContext) {
context.code_flow_seen_data = Some(false);
context.line_ending_if_needed();
context.push("<pre><code>");
}
/// Handle [`Enter`][Kind::Enter]:[`CodeFenced`][Name::CodeFenced].
fn on_enter_code_fenced(context: &mut CompileContext) {
context.code_flow_seen_data = Some(false);
context.line_ending_if_needed();
// Note that no `>` is used, which is added later.
context.push("<pre><code");
context.code_fenced_fences_count = Some(0);
}
/// Handle [`Enter`][Kind::Enter]:[`CodeText`][Name::CodeText].
fn on_enter_code_text(context: &mut CompileContext) {
context.code_text_inside = true;
if !context.image_alt_inside {
context.push("<code>");
}
context.buffer();
}
/// Handle [`Enter`][Kind::Enter]:[`Definition`][Name::Definition].
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,
});
}
/// Handle [`Enter`][Kind::Enter]:[`DefinitionDestinationString`][Name::DefinitionDestinationString].
fn on_enter_definition_destination_string(context: &mut CompileContext) {
context.buffer();
context.encode_html = false;
}
/// Handle [`Enter`][Kind::Enter]:[`Emphasis`][Name::Emphasis].
fn on_enter_emphasis(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("<em>");
}
}
/// Handle [`Enter`][Kind::Enter]:[`Frontmatter`][Name::Frontmatter].
fn on_enter_frontmatter(context: &mut CompileContext) {
context.buffer();
}
/// Handle [`Enter`][Kind::Enter]:[`GfmStrikethrough`][Name::GfmStrikethrough].
fn on_enter_gfm_strikethrough(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("<del>");
}
}
/// Handle [`Enter`][Kind::Enter]:[`GfmTaskListItemCheck`][Name::GfmTaskListItemCheck].
fn on_enter_gfm_task_list_item_check(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("<input type=\"checkbox\" disabled=\"\" ");
}
}
/// Handle [`Enter`][Kind::Enter]:[`HtmlFlow`][Name::HtmlFlow].
fn on_enter_html_flow(context: &mut CompileContext) {
context.line_ending_if_needed();
if context.allow_dangerous_html {
context.encode_html = false;
}
}
/// Handle [`Enter`][Kind::Enter]:[`HtmlText`][Name::HtmlText].
fn on_enter_html_text(context: &mut CompileContext) {
if context.allow_dangerous_html {
context.encode_html = false;
}
}
/// Handle [`Enter`][Kind::Enter]:[`Image`][Name::Image].
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; // Disallow tags.
}
/// Handle [`Enter`][Kind::Enter]:[`Link`][Name::Link].
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,
});
}
/// Handle [`Enter`][Kind::Enter]:{[`ListOrdered`][Name::ListOrdered],[`ListUnordered`][Name::ListUnordered]}.
fn on_enter_list(context: &mut CompileContext) {
let events = &context.events;
let mut index = context.index;
let mut balance = 0;
let mut loose = false;
let name = &events[index].name;
while index < events.len() {
let event = &events[index];
if event.kind == Kind::Enter {
balance += 1;
} else {
balance -= 1;
if balance < 3 && event.name == Name::BlankLineEnding {
// Blank line directly after a prefix:
//
// ```markdown
// > | -␊
// ^
// | a
// ```
let mut at_prefix = false;
// Blank line directly after item, which is just a prefix.
//
// ```markdown
// > | -␊
// ^
// | - a
// ```
let mut at_empty_list_item = false;
// Blank line at block quote prefix:
//
// ```markdown
// > | * >␊
// ^
// | * a
// ```
let mut at_empty_block_quote = false;
if balance == 1 {
let mut before = index - 2;
if events[before].name == Name::ListItem {
before -= 1;
if events[before].name == Name::SpaceOrTab {
before -= 2;
}
if events[before].name == Name::BlockQuote
&& events[before - 1].name == Name::BlockQuotePrefix
{
at_empty_block_quote = true;
} else if events[before].name == Name::ListItemPrefix {
at_empty_list_item = true;
}
}
} else {
let mut before = index - 2;
if events[before].name == Name::SpaceOrTab {
before -= 2;
}
if events[before].name == Name::ListItemPrefix {
at_prefix = true;
}
}
if !at_prefix && !at_empty_list_item && !at_empty_block_quote {
loose = true;
break;
}
}
// Done.
if balance == 0 && event.name == *name {
break;
}
}
index += 1;
}
context.tight_stack.push(!loose);
context.line_ending_if_needed();
// Note: no `>`.
context.push(if *name == Name::ListOrdered {
"<ol"
} else {
"<ul"
});
context.list_expect_first_marker = Some(true);
}
/// Handle [`Enter`][Kind::Enter]:[`ListItemMarker`][Name::ListItemMarker].
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);
}
/// Handle [`Enter`][Kind::Enter]:[`Paragraph`][Name::Paragraph].
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>");
}
}
/// Handle [`Enter`][Kind::Enter]:[`Resource`][Name::Resource].
fn on_enter_resource(context: &mut CompileContext) {
context.buffer(); // We can have line endings in the resource, ignore them.
context.media_stack.last_mut().unwrap().destination = Some("".to_string());
}
/// Handle [`Enter`][Kind::Enter]:[`ResourceDestinationString`][Name::ResourceDestinationString].
fn on_enter_resource_destination_string(context: &mut CompileContext) {
context.buffer();
// Ignore encoding the result, as we’ll first percent encode the url and
// encode manually after.
context.encode_html = false;
}
/// Handle [`Enter`][Kind::Enter]:[`Strong`][Name::Strong].
fn on_enter_strong(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("<strong>");
}
}
/// Handle [`Exit`][Kind::Exit]:[`AutolinkEmail`][Name::AutolinkEmail].
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(),
);
}
/// Handle [`Exit`][Kind::Exit]:[`AutolinkProtocol`][Name::AutolinkProtocol].
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(),
);
}
/// Handle [`Exit`][Kind::Exit]:{[`HardBreakEscape`][Name::HardBreakEscape],[`HardBreakTrailing`][Name::HardBreakTrailing]}.
fn on_exit_break(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("<br />");
}
}
/// Handle [`Exit`][Kind::Exit]:[`BlankLineEnding`][Name::BlankLineEnding].
fn on_exit_blank_line_ending(context: &mut CompileContext) {
if context.index == context.events.len() - 1 {
context.line_ending_if_needed();
}
}
/// Handle [`Exit`][Kind::Exit]:[`BlockQuote`][Name::BlockQuote].
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>");
}
/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarker`][Name::CharacterReferenceMarker].
fn on_exit_character_reference_marker(context: &mut CompileContext) {
context.character_reference_marker = Some(b'&');
}
/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarkerHexadecimal`][Name::CharacterReferenceMarkerHexadecimal].
fn on_exit_character_reference_marker_hexadecimal(context: &mut CompileContext) {
context.character_reference_marker = Some(b'x');
}
/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarkerNumeric`][Name::CharacterReferenceMarkerNumeric].
fn on_exit_character_reference_marker_numeric(context: &mut CompileContext) {
context.character_reference_marker = Some(b'#');
}
/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceValue`][Name::CharacterReferenceValue].
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 = slice.as_str();
let value = match marker {
b'#' => decode_numeric(value, 10),
b'x' => decode_numeric(value, 16),
b'&' => decode_named(value),
_ => panic!("impossible"),
};
context.push(&encode(&value, context.encode_html));
}
/// Handle [`Exit`][Kind::Exit]:[`CodeFlowChunk`][Name::CodeFlowChunk].
fn on_exit_code_flow_chunk(context: &mut CompileContext) {
context.code_flow_seen_data = Some(true);
context.push(&encode(
&Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
// Must serialize to get virtual spaces.
.serialize(),
context.encode_html,
));
}
/// Handle [`Exit`][Kind::Exit]:[`CodeFencedFence`][Name::CodeFencedFence].
fn on_exit_code_fenced_fence(context: &mut CompileContext) {
let count = if let Some(count) = context.code_fenced_fences_count {
count
} else {
0
};
if count == 0 {
context.push(">");
context.slurp_one_line_ending = true;
}
context.code_fenced_fences_count = Some(count + 1);
}
/// Handle [`Exit`][Kind::Exit]:[`CodeFencedFenceInfo`][Name::CodeFencedFenceInfo].
fn on_exit_code_fenced_fence_info(context: &mut CompileContext) {
let value = context.resume();
context.push(" class=\"language-");
context.push(&value);
context.push("\"");
}
/// Handle [`Exit`][Kind::Exit]:{[`CodeFenced`][Name::CodeFenced],[`CodeIndented`][Name::CodeIndented]}.
fn on_exit_code_flow(context: &mut CompileContext) {
// One special case is if we are inside a container, and the fenced code was
// not closed (meaning it runs to the end).
// In that case, the following line ending, is considered *outside* the
// fenced code and block quote by micromark, but CM wants to treat that
// ending as part of the code.
if let Some(count) = context.code_fenced_fences_count {
// No closing fence.
if count == 1
// In a container.
&& !context.tight_stack.is_empty()
// Empty (as the closing is right at the opening fence)
&& context.events[context.index - 1].name != Name::CodeFencedFence
{
context.line_ending();
}
}
// But in most cases, it’s simpler: when we’ve seen some data, emit an extra
// line ending when needed.
if context
.code_flow_seen_data
.take()
.expect("`code_flow_seen_data` must be defined")
{
context.line_ending_if_needed();
}
context.push("</code></pre>");
if let Some(count) = context.code_fenced_fences_count.take() {
if count < 2 {
context.line_ending_if_needed();
}
}
context.slurp_one_line_ending = false;
}
/// Handle [`Exit`][Kind::Exit]:[`CodeText`][Name::CodeText].
fn on_exit_code_text(context: &mut CompileContext) {
let result = context.resume();
let mut bytes = result.as_bytes();
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 = &bytes[1..end];
}
context.code_text_inside = false;
context.push(str::from_utf8(bytes).unwrap());
if !context.image_alt_inside {
context.push("</code>");
}
}
/// Handle [`Exit`][Kind::Exit]:*.
///
/// Resumes, and ignores what was resumed.
fn on_exit_drop(context: &mut CompileContext) {
context.resume();
}
/// Handle [`Exit`][Kind::Exit]:{[`CodeTextData`][Name::CodeTextData],[`Data`][Name::Data],[`CharacterEscapeValue`][Name::CharacterEscapeValue]}.
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,
));
}
/// Handle [`Exit`][Kind::Exit]:[`Definition`][Name::Definition].
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,
});
}
/// Handle [`Exit`][Kind::Exit]:[`DefinitionDestinationString`][Name::DefinitionDestinationString].
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;
}
/// Handle [`Exit`][Kind::Exit]:[`DefinitionLabelString`][Name::DefinitionLabelString].
fn on_exit_definition_label_string(context: &mut CompileContext) {
// Discard label, use the source content instead.
context.resume();
context.media_stack.last_mut().unwrap().reference_id =
Some(Position::from_exit_event(context.events, context.index).to_indices());
}
/// Handle [`Exit`][Kind::Exit]:[`DefinitionTitleString`][Name::DefinitionTitleString].
fn on_exit_definition_title_string(context: &mut CompileContext) {
let buf = context.resume();
context.media_stack.last_mut().unwrap().title = Some(buf);
}
/// Handle [`Exit`][Kind::Exit]:[`Emphasis`][Name::Emphasis].
fn on_exit_emphasis(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("</em>");
}
}
/// Handle [`Exit`][Kind::Exit]:[`Frontmatter`][Name::Frontmatter].
fn on_exit_frontmatter(context: &mut CompileContext) {
context.resume();
context.slurp_one_line_ending = true;
}
/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralProtocol`][Name::GfmAutolinkLiteralProtocol].
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(),
);
}
/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralWww`][Name::GfmAutolinkLiteralWww].
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(),
);
}
/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralEmail`][Name::GfmAutolinkLiteralEmail].
fn on_exit_gfm_autolink_literal_email(context: &mut CompileContext) {
on_exit_autolink_email(context);
}
/// Handle [`Exit`][Kind::Exit]:[`GfmStrikethrough`][Name::GfmStrikethrough].
fn on_exit_gfm_strikethrough(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("</del>");
}
}
/// Handle [`Exit`][Kind::Exit]:[`GfmTaskListItemCheck`][Name::GfmTaskListItemCheck].
fn on_exit_gfm_task_list_item_check(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("/>");
}
}
/// Handle [`Exit`][Kind::Exit]:[`GfmTaskListItemValueChecked`][Name::GfmTaskListItemValueChecked].
fn on_exit_gfm_task_list_item_value_checked(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("checked=\"\" ");
}
}
/// Handle [`Exit`][Kind::Exit]:[`HeadingAtx`][Name::HeadingAtx].
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(">");
}
/// Handle [`Exit`][Kind::Exit]:[`HeadingAtxSequence`][Name::HeadingAtxSequence].
fn on_exit_heading_atx_sequence(context: &mut CompileContext) {
// First fence we see.
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(">");
}
}
/// Handle [`Exit`][Kind::Exit]:[`HeadingAtxText`][Name::HeadingAtxText].
fn on_exit_heading_atx_text(context: &mut CompileContext) {
let value = context.resume();
context.push(&value);
}
/// Handle [`Exit`][Kind::Exit]:[`HeadingSetextText`][Name::HeadingSetextText].
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;
}
/// Handle [`Exit`][Kind::Exit]:[`HeadingSetextUnderline`][Name::HeadingSetextUnderline].
fn on_exit_heading_setext_underline(context: &mut CompileContext) {
let text = context
.heading_setext_buffer
.take()
.expect("`heading_atx_rank` must be set in headings");
let head = Slice::from_position(
context.bytes,
&Position::from_exit_event(context.events, context.index),
)
.head();
let rank = if head == Some(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(">");
}
/// Handle [`Exit`][Kind::Exit]:{[`HtmlFlow`][Name::HtmlFlow],[`HtmlText`][Name::HtmlText]}.
fn on_exit_html(context: &mut CompileContext) {
context.encode_html = true;
}
/// Handle [`Exit`][Kind::Exit]:{[`HtmlFlowData`][Name::HtmlFlowData],[`HtmlTextData`][Name::HtmlTextData]}.
fn on_exit_html_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,
));
}
/// Handle [`Exit`][Kind::Exit]:[`Label`][Name::Label].
fn on_exit_label(context: &mut CompileContext) {
let buf = context.resume();
context.media_stack.last_mut().unwrap().label = Some(buf);
}
/// Handle [`Exit`][Kind::Exit]:[`LabelText`][Name::LabelText].
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());
}
/// Handle [`Exit`][Kind::Exit]:[`LineEnding`][Name::LineEnding].
fn on_exit_line_ending(context: &mut CompileContext) {
if context.code_text_inside {
context.push(" ");
} else if context.slurp_one_line_ending {
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,
));
}
}
/// Handle [`Exit`][Kind::Exit]:{[`ListOrdered`][Name::ListOrdered],[`ListUnordered`][Name::ListUnordered]}.
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>"
});
}
/// Handle [`Exit`][Kind::Exit]:[`ListItem`][Name::ListItem].
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::LineEnding,
Name::SpaceOrTab,
Name::BlockQuotePrefix,
],
);
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>");
}
/// Handle [`Exit`][Kind::Exit]:[`ListItemValue`][Name::ListItemValue].
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("\"");
}
}
}
/// Handle [`Exit`][Kind::Exit]:{[`Image`][Name::Image],[`Link`][Name::Link]}.
fn on_exit_media(context: &mut CompileContext) {
let mut is_in_image = false;
let mut index = 0;
// Skip current.
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 image_alt_inside = context.image_alt_inside;
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.and_then(|id| {
let mut index = 0;
while index < context.definitions.len() {
if context.definitions[index].id == id {
return Some(index);
}
index += 1;
}
None
})
} else {
None
};
if !image_alt_inside {
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 {
context.push(&sanitize_uri(
destination,
if media.image {
&context.protocol_src
} else {
&context.protocol_href
},
));
}
if media.image {
context.push("\" alt=\"");
};
}
if media.image {
context.push(&label);
}
if !image_alt_inside {
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 !image_alt_inside {
context.push("</a>");
}
}
}
/// Handle [`Exit`][Kind::Exit]:[`Paragraph`][Name::Paragraph].
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>");
}
}
/// Handle [`Exit`][Kind::Exit]:[`ReferenceString`][Name::ReferenceString].
fn on_exit_reference_string(context: &mut CompileContext) {
// Drop stuff.
context.resume();
context.media_stack.last_mut().unwrap().reference_id =
Some(Position::from_exit_event(context.events, context.index).to_indices());
}
/// Handle [`Exit`][Kind::Exit]:[`ResourceDestinationString`][Name::ResourceDestinationString].
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;
}
/// Handle [`Exit`][Kind::Exit]:[`ResourceTitleString`][Name::ResourceTitleString].
fn on_exit_resource_title_string(context: &mut CompileContext) {
let buf = context.resume();
context.media_stack.last_mut().unwrap().title = Some(buf);
}
/// Handle [`Exit`][Kind::Exit]:[`Strong`][Name::Strong].
fn on_exit_strong(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("</strong>");
}
}
/// Handle [`Exit`][Kind::Exit]:[`ThematicBreak`][Name::ThematicBreak].
fn on_exit_thematic_break(context: &mut CompileContext) {
context.line_ending_if_needed();
context.push("<hr />");
}
/// Generate an autolink (used by unicode autolinks and GFM autolink literals).
fn generate_autolink(context: &mut CompileContext, protocol: Option<&str>, value: &str) {
if !context.image_alt_inside {
context.push("<a href=\"");
if let Some(protocol) = protocol {
context.push(&sanitize_uri(
&format!("{}{}", protocol, value),
&context.protocol_href,
));
} else {
context.push(&sanitize_uri(value, &context.protocol_href));
};
context.push("\">");
}
context.push(&encode(value, context.encode_html));
if !context.image_alt_inside {
context.push("</a>");
}
}