//! Turn events into a string of HTML.
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;
/// Link, image, or footnote call.
/// 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 ");
}
}
/// 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("".into());
}
/// 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("");
}
}
/// 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(),
false,
);
}
/// 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(),
false,
);
}
/// Handle [`Exit`][Kind::Exit]:{[`HardBreakEscape`][Name::HardBreakEscape],[`HardBreakTrailing`][Name::HardBreakTrailing]}.
fn on_exit_break(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("");
}
/// Handle [`Enter`][Kind::Enter]:[`CodeIndented`][Name::CodeIndented].
fn on_enter_code_indented(context: &mut CompileContext) {
context.raw_flow_seen_data = Some(false);
context.line_ending_if_needed();
context.push("
");
}
/// Handle [`Enter`][Kind::Enter]:{[`CodeFenced`][Name::CodeFenced],[`MathFlow`][Name::MathFlow]}.
fn on_enter_raw_flow(context: &mut CompileContext) {
context.raw_flow_seen_data = Some(false);
context.line_ending_if_needed();
// Note that no `>` is used, which is added later (due to info)
context.push("
");
}
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("");
}
}
/// Handle [`Enter`][Kind::Enter]:[`Frontmatter`][Name::Frontmatter].
fn on_enter_frontmatter(context: &mut CompileContext) {
context.buffer();
}
/// Handle [`Enter`][Kind::Enter]:[`GfmFootnoteDefinition`][Name::GfmFootnoteDefinition].
fn on_enter_gfm_footnote_definition(context: &mut CompileContext) {
context.tight_stack.push(false);
}
/// Handle [`Enter`][Kind::Enter]:[`GfmFootnoteCall`][Name::GfmFootnoteCall].
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,
});
}
/// Handle [`Enter`][Kind::Enter]:[`GfmStrikethrough`][Name::GfmStrikethrough].
fn on_enter_gfm_strikethrough(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("
");
}
}
/// Handle [`Enter`][Kind::Enter]:[`GfmTable`][Name::GfmTable].
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("");
}
/// Handle [`Enter`][Kind::Enter]:[`GfmTableBody`][Name::GfmTableBody].
fn on_enter_gfm_table_body(context: &mut CompileContext) {
context.push("");
}
/// Handle [`Enter`][Kind::Enter]:[`GfmTableCell`][Name::GfmTableCell].
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() {
// Capture cell to ignore it.
context.buffer();
} else {
let value = align[column];
context.line_ending_if_needed();
if context.gfm_table_in_head {
context.push("
");
}
/// Handle [`Exit`][Kind::Exit]:[`GfmTableBody`][Name::GfmTableBody].
fn on_exit_gfm_table_body(context: &mut CompileContext) {
context.line_ending_if_needed();
context.push("");
}
/// Handle [`Exit`][Kind::Exit]:[`GfmTableCell`][Name::GfmTableCell].
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("");
} else {
context.push("");
}
} else {
// Stop capturing.
context.resume();
}
context.gfm_table_column += 1;
}
/// Handle [`Exit`][Kind::Exit]:[`GfmTableHead`][Name::GfmTableHead].
fn on_exit_gfm_table_head(context: &mut CompileContext) {
context.gfm_table_in_head = false;
context.line_ending_if_needed();
context.push("");
}
/// Handle [`Exit`][Kind::Exit]:[`GfmTableRow`][Name::GfmTableRow].
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();
// Add “phantom” cells, for body rows that are shorter than the delimiter
// row (which is equal to the head row).
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("");
}
/// 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("");
}
/// 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(" context.push(" align=\"left\""),
AlignKind::Right => context.push(" align=\"right\""),
AlignKind::Center => context.push(" align=\"center\""),
AlignKind::None => {}
}
context.push(">");
}
}
/// Handle [`Enter`][Kind::Enter]:[`GfmTableHead`][Name::GfmTableHead].
fn on_enter_gfm_table_head(context: &mut CompileContext) {
context.line_ending_if_needed();
context.push("");
context.gfm_table_in_head = true;
}
/// Handle [`Enter`][Kind::Enter]:[`GfmTableRow`][Name::GfmTableRow].
fn on_enter_gfm_table_row(context: &mut CompileContext) {
context.line_ending_if_needed();
context.push(" ");
}
/// 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("`.
context.push(if context.events[context.index].name == Name::ListOrdered {
" ");
}
context.line_ending_if_needed();
context.push("
");
}
}
/// 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("");
}
/// 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 = decode_character_reference(slice.as_str(), marker, true)
.expect("expected to parse only valid named references");
context.push(&encode(&value, context.encode_html));
}
/// Handle [`Exit`][Kind::Exit]:{[`CodeFlowChunk`][Name::CodeFlowChunk],[`MathFlowChunk`][Name::MathFlowChunk]}.
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),
)
// Must serialize to get virtual spaces.
.serialize(),
context.encode_html,
));
}
/// Handle [`Exit`][Kind::Exit]:{[`CodeFencedFence`][Name::CodeFencedFence],[`MathFlowFence`][Name::MathFlowFence]}.
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);
}
/// Handle [`Exit`][Kind::Exit]:[`CodeFencedFenceInfo`][Name::CodeFencedFenceInfo].
///
/// Note: math (flow) does not support `info`.
fn on_exit_raw_flow_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],[`MathFlow`][Name::MathFlow]}.
fn on_exit_raw_flow(context: &mut CompileContext) {
// One special case is if we are inside a container, and the raw (flow) 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 `markdown-rs`, but CM wants to treat that
// ending as part of the code.
if let Some(count) = context.raw_flow_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)
&& !matches!(context.events[context.index - 1].name, Name::CodeFencedFence | Name::MathFlowFence)
{
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
.raw_flow_seen_data
.take()
.expect("`raw_flow_seen_data` must be defined")
{
context.line_ending_if_needed();
}
context.push("");
if let Some(count) = context.raw_flow_fences_count.take() {
if count < 2 {
context.line_ending_if_needed();
}
}
context.slurp_one_line_ending = false;
}
/// Handle [`Exit`][Kind::Exit]:{[`CodeText`][Name::CodeText],[`MathText`][Name::MathText]}.
fn on_exit_raw_text(context: &mut CompileContext) {
let result = context.resume();
// To do: share with `to_mdast`.
let mut bytes = result.as_bytes().to_vec();
// If we are in a GFM table, we need to decode escaped pipes.
// This is a rather weird GFM feature.
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("");
}
}
/// Handle [`Exit`][Kind::Exit]:*.
///
/// Resumes, and ignores what was resumed.
fn on_exit_drop(context: &mut CompileContext) {
context.resume();
}
/// Handle [`Exit`][Kind::Exit]:*.
///
/// Resumes, ignores what was resumed, and slurps the following line ending.
fn on_exit_drop_slurp(context: &mut CompileContext) {
context.resume();
context.slurp_one_line_ending = true;
}
/// 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("");
}
}
/// 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]:[`GfmAutolinkLiteralEmail`][Name::GfmAutolinkLiteralEmail].
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,
);
}
/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralMailto`][Name::GfmAutolinkLiteralMailto].
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,
);
}
/// 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(),
true,
);
}
/// 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(),
true,
);
}
/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralXmpp`][Name::GfmAutolinkLiteralXmpp].
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,
);
}
/// Handle [`Exit`][Kind::Exit]:[`GfmFootnoteCall`][Name::GfmFootnoteCall].
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;
// See if this has been called before.
while call_index < context.gfm_footnote_definition_calls.len() {
if context.gfm_footnote_definition_calls[call_index].0 == id {
break;
}
call_index += 1;
}
// New.
if call_index == context.gfm_footnote_definition_calls.len() {
context.gfm_footnote_definition_calls.push((id, 0));
}
// Increment.
context.gfm_footnote_definition_calls[call_index].1 += 1;
// No call is output in an image alt, though the definition and
// backreferences are generated as if it was the case.
if context.image_alt_inside {
return;
}
context.push(" 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("");
}
/// Handle [`Exit`][Kind::Exit]:[`GfmFootnoteDefinitionLabelString`][Name::GfmFootnoteDefinitionLabelString].
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());
}
/// Handle [`Exit`][Kind::Exit]:[`GfmFootnoteDefinitionPrefix`][Name::GfmFootnoteDefinitionPrefix].
fn on_exit_gfm_footnote_definition_prefix(context: &mut CompileContext) {
// Drop the prefix.
context.resume();
// Capture everything until end of definition.
context.buffer();
}
/// Handle [`Exit`][Kind::Exit]:[`GfmFootnoteDefinition`][Name::GfmFootnoteDefinition].
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,
));
}
/// Handle [`Exit`][Kind::Exit]:[`GfmStrikethrough`][Name::GfmStrikethrough].
fn on_exit_gfm_strikethrough(context: &mut CompileContext) {
if !context.image_alt_inside {
context.push("");
}
}
/// Handle [`Exit`][Kind::Exit]:[`GfmTable`][Name::GfmTable].
fn on_exit_gfm_table(context: &mut CompileContext) {
context.gfm_table_align = None;
context.line_ending_if_needed();
context.push("