//! Turn events into a string of HTML.
use crate::constant::{SAFE_PROTOCOL_HREF, SAFE_PROTOCOL_SRC};
use crate::construct::character_reference::Kind as CharacterReferenceKind;
use crate::token::Token;
use crate::tokenizer::{Code, Event, EventType};
use crate::util::normalize_identifier::normalize_identifier;
use crate::util::{
decode_character_reference::{decode_named, decode_numeric},
encode::encode,
sanitize_uri::sanitize_uri,
skip,
span::{codes as codes_from_span, from_exit_event, serialize},
};
use std::collections::HashMap;
/// Type of line endings in markdown.
#[derive(Debug, Clone, PartialEq)]
pub enum LineEnding {
/// Both a carriage return (`\r`) and a line feed (`\n`).
///
/// ## Example
///
/// ```markdown
/// a␍␊
/// b
/// ```
CarriageReturnLineFeed,
/// Sole carriage return (`\r`).
///
/// ## Example
///
/// ```markdown
/// a␍
/// b
/// ```
CarriageReturn,
/// Sole line feed (`\n`).
///
/// ## Example
///
/// ```markdown
/// a␊
/// b
/// ```
LineFeed,
}
impl LineEnding {
/// Turn the line ending into a [str].
fn as_str(&self) -> &str {
match self {
LineEnding::CarriageReturnLineFeed => "\r\n",
LineEnding::CarriageReturn => "\r",
LineEnding::LineFeed => "\n",
}
}
/// Turn a [Code] into a line ending.
///
/// ## Panics
///
/// Panics if `code` is not `\r\n`, `\r`, or `\n`.
fn from_code(code: Code) -> LineEnding {
match code {
Code::CarriageReturnLineFeed => LineEnding::CarriageReturnLineFeed,
Code::Char('\r') => LineEnding::CarriageReturn,
Code::Char('\n') => LineEnding::LineFeed,
_ => unreachable!("invalid code"),
}
}
}
/// Representation of a 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]()`), as an
/// identifier, meaning that the original source characters are used
/// instead of interpreting them.
/// Not interpreted.
label_id: Option Hi, <i>venus</i>! Hi, venus! a a a ".to_string());
}
}
/// Handle [`Enter`][EventType::Enter]:[`Resource`][Token::Resource].
fn on_enter_resource(context: &mut CompileContext) {
context.buffer(); // We can have line endings in the resource, ignore them.
let media = context.media_stack.last_mut().unwrap();
media.destination = Some("".to_string());
}
/// Handle [`Enter`][EventType::Enter]:[`ResourceDestinationString`][Token::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.ignore_encode = true;
}
/// Handle [`Enter`][EventType::Enter]:[`Strong`][Token::Strong].
fn on_enter_strong(context: &mut CompileContext) {
context.tag("".to_string());
}
/// Handle [`Exit`][EventType::Exit]:[`AutolinkEmail`][Token::AutolinkEmail].
fn on_exit_autolink_email(context: &mut CompileContext) {
let slice = serialize(
context.codes,
&from_exit_event(context.events, context.index),
false,
);
context.tag(format!(
"",
sanitize_uri(
format!("mailto:{}", slice.as_str()).as_str(),
&context.protocol_href
)
));
context.push(context.encode_opt(&slice));
context.tag("".to_string());
}
/// Handle [`Exit`][EventType::Exit]:[`AutolinkProtocol`][Token::AutolinkProtocol].
fn on_exit_autolink_protocol(context: &mut CompileContext) {
let slice = serialize(
context.codes,
&from_exit_event(context.events, context.index),
false,
);
context.tag(format!(
"",
sanitize_uri(slice.as_str(), &context.protocol_href)
));
context.push(context.encode_opt(&slice));
context.tag("".to_string());
}
/// Handle [`Exit`][EventType::Exit]:{[`HardBreakEscape`][Token::HardBreakEscape],[`HardBreakTrailing`][Token::HardBreakTrailing]}.
fn on_exit_break(context: &mut CompileContext) {
context.tag("\n
`.
///
/// To create that line ending, the document is checked for the first line
/// ending that is used.
/// If there is no line ending, `default_line_ending` is used.
/// If that isn’t configured, `\n` is used.
///
/// ## Examples
///
/// ```rust
/// use micromark::{micromark, micromark_with_options, Options, LineEnding};
///
/// // micromark is safe by default:
/// assert_eq!(
/// micromark("> a"),
/// "\n
"
/// );
///
/// // Define `default_line_ending` to configure the default:
/// assert_eq!(
/// micromark_with_options(
/// "> a",
/// &Options {
/// allow_dangerous_html: false,
/// allow_dangerous_protocol: false,
/// default_line_ending: Some(LineEnding::CarriageReturnLineFeed),
///
/// }
/// ),
/// "\r\n
"
/// );
/// ```
pub default_line_ending: Option".to_string());
}
/// Handle [`Enter`][EventType::Enter]:[`CodeIndented`][Token::CodeIndented].
fn on_enter_code_indented(context: &mut CompileContext) {
context.code_flow_seen_data = Some(false);
context.line_ending_if_needed();
context.tag("
".to_string());
}
/// Handle [`Exit`][EventType::Exit]:[`CharacterReferenceMarker`][Token::CharacterReferenceMarker].
fn on_exit_character_reference_marker(context: &mut CompileContext) {
context.character_reference_kind = Some(CharacterReferenceKind::Named);
}
/// Handle [`Exit`][EventType::Exit]:[`CharacterReferenceMarkerHexadecimal`][Token::CharacterReferenceMarkerHexadecimal].
fn on_exit_character_reference_marker_hexadecimal(context: &mut CompileContext) {
context.character_reference_kind = Some(CharacterReferenceKind::Hexadecimal);
}
/// Handle [`Exit`][EventType::Exit]:[`CharacterReferenceMarkerNumeric`][Token::CharacterReferenceMarkerNumeric].
fn on_exit_character_reference_marker_numeric(context: &mut CompileContext) {
context.character_reference_kind = Some(CharacterReferenceKind::Decimal);
}
/// Handle [`Exit`][EventType::Exit]:[`CharacterReferenceValue`][Token::CharacterReferenceValue].
fn on_exit_character_reference_value(context: &mut CompileContext) {
let kind = context
.character_reference_kind
.take()
.expect("expected `character_reference_kind` to be set");
let reference = serialize(
context.codes,
&from_exit_event(context.events, context.index),
false,
);
let ref_string = reference.as_str();
let value = match kind {
CharacterReferenceKind::Decimal => decode_numeric(ref_string, 10).to_string(),
CharacterReferenceKind::Hexadecimal => decode_numeric(ref_string, 16).to_string(),
CharacterReferenceKind::Named => decode_named(ref_string),
};
context.push(context.encode_opt(&value));
}
/// Handle [`Exit`][EventType::Exit]:[`CodeFlowChunk`][Token::CodeFlowChunk].
fn on_exit_code_flow_chunk(context: &mut CompileContext) {
context.code_flow_seen_data = Some(true);
context.push(context.encode_opt(&serialize(
context.codes,
&from_exit_event(context.events, context.index),
false,
)));
}
/// Handle [`Exit`][EventType::Exit]:[`CodeFencedFence`][Token::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.tag(">".to_string());
context.slurp_one_line_ending = true;
}
context.code_fenced_fences_count = Some(count + 1);
}
/// Handle [`Exit`][EventType::Exit]:[`CodeFencedFenceInfo`][Token::CodeFencedFenceInfo].
fn on_exit_code_fenced_fence_info(context: &mut CompileContext) {
let value = context.resume();
context.tag(format!(" class=\"language-{}\"", value));
}
/// Handle [`Exit`][EventType::Exit]:{[`CodeFenced`][Token::CodeFenced],[`CodeIndented`][Token::CodeIndented]}.
fn on_exit_code_flow(context: &mut CompileContext) {
let seen_data = context
.code_flow_seen_data
.take()
.expect("`code_flow_seen_data` must be defined");
// 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 {
if count == 1 && !context.tight_stack.is_empty() && !context.last_was_tag {
context.line_ending();
}
}
// But in most cases, it’s simpler: when we’ve seen some data, emit an extra
// line ending when needed.
if seen_data {
context.line_ending_if_needed();
}
context.tag("".to_string());
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`][EventType::Exit]:[`CodeText`][Token::CodeText].
fn on_exit_code_text(context: &mut CompileContext) {
let result = context.resume();
let mut chars = result.chars();
let mut trim = false;
if Some(' ') == chars.next() && Some(' ') == chars.next_back() {
let mut next = chars.next();
while next != None && !trim {
if Some(' ') != next {
trim = true;
}
next = chars.next();
}
}
context.push(if trim {
result[1..(result.len() - 1)].to_string()
} else {
result
});
context.tag("".to_string());
}
/// Handle [`Exit`][EventType::Exit]:[`CodeTextLineEnding`][Token::CodeTextLineEnding].
fn on_exit_code_text_line_ending(context: &mut CompileContext) {
context.push(" ".to_string());
}
/// Handle [`Exit`][EventType::Exit]:*.
///
/// Resumes, and ignores what was resumed.
fn on_exit_drop(context: &mut CompileContext) {
context.resume();
}
/// Handle [`Exit`][EventType::Exit]:{[`CodeTextData`][Token::CodeTextData],[`Data`][Token::Data],[`CharacterEscapeValue`][Token::CharacterEscapeValue]}.
fn on_exit_data(context: &mut CompileContext) {
// Just output it.
context.push(context.encode_opt(&serialize(
context.codes,
&from_exit_event(context.events, context.index),
false,
)));
}
/// Handle [`Exit`][EventType::Exit]:[`Definition`][Token::Definition].
fn on_exit_definition(context: &mut CompileContext) {
let definition = context.media_stack.pop().unwrap();
let reference_id = normalize_identifier(&definition.reference_id.unwrap());
let destination = definition.destination;
let title = definition.title;
context.resume();
context
.definitions
.entry(reference_id)
.or_insert(Definition { destination, title });
}
/// Handle [`Exit`][EventType::Exit]:[`DefinitionDestinationString`][Token::DefinitionDestinationString].
fn on_exit_definition_destination_string(context: &mut CompileContext) {
let buf = context.resume();
let definition = context.media_stack.last_mut().unwrap();
definition.destination = Some(buf);
context.ignore_encode = false;
}
/// Handle [`Exit`][EventType::Exit]:[`DefinitionLabelString`][Token::DefinitionLabelString].
fn on_exit_definition_label_string(context: &mut CompileContext) {
// Discard label, use the source content instead.
context.resume();
let definition = context.media_stack.last_mut().unwrap();
definition.reference_id = Some(serialize(
context.codes,
&from_exit_event(context.events, context.index),
false,
));
}
/// Handle [`Exit`][EventType::Exit]:[`DefinitionTitleString`][Token::DefinitionTitleString].
fn on_exit_definition_title_string(context: &mut CompileContext) {
let buf = context.resume();
let definition = context.media_stack.last_mut().unwrap();
definition.title = Some(buf);
}
/// Handle [`Exit`][EventType::Exit]:[`Strong`][Token::Emphasis].
fn on_exit_emphasis(context: &mut CompileContext) {
context.tag("".to_string());
}
/// Handle [`Exit`][EventType::Exit]:[`HeadingAtx`][Token::HeadingAtx].
fn on_exit_heading_atx(context: &mut CompileContext) {
let rank = context
.atx_opening_sequence_size
.take()
.expect("`atx_opening_sequence_size` must be set in headings");
context.tag(format!("", rank));
}
/// Handle [`Exit`][EventType::Exit]:[`HeadingAtxSequence`][Token::HeadingAtxSequence].
fn on_exit_heading_atx_sequence(context: &mut CompileContext) {
// First fence we see.
if context.atx_opening_sequence_size.is_none() {
let rank = serialize(
context.codes,
&from_exit_event(context.events, context.index),
false,
)
.len();
context.line_ending_if_needed();
context.atx_opening_sequence_size = Some(rank);
context.tag(format!("".to_string());
}
/// Handle [`Enter`][EventType::Enter]:[`CodeFenced`][Token::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.tag("
".to_string());
context.buffer();
}
/// Handle [`Enter`][EventType::Enter]:[`Definition`][Token::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`][EventType::Enter]:[`DefinitionDestinationString`][Token::DefinitionDestinationString].
fn on_enter_definition_destination_string(context: &mut CompileContext) {
context.buffer();
context.ignore_encode = true;
}
/// Handle [`Enter`][EventType::Enter]:[`Emphasis`][Token::Emphasis].
fn on_enter_emphasis(context: &mut CompileContext) {
context.tag("".to_string());
}
/// Handle [`Enter`][EventType::Enter]:[`HtmlFlow`][Token::HtmlFlow].
fn on_enter_html_flow(context: &mut CompileContext) {
context.line_ending_if_needed();
if context.allow_dangerous_html {
context.ignore_encode = true;
}
}
/// Handle [`Enter`][EventType::Enter]:[`HtmlText`][Token::HtmlText].
fn on_enter_html_text(context: &mut CompileContext) {
if context.allow_dangerous_html {
context.ignore_encode = true;
}
}
/// Handle [`Enter`][EventType::Enter]:[`Image`][Token::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.tags = false; // Disallow tags.
}
/// Handle [`Enter`][EventType::Enter]:[`Link`][Token::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`][EventType::Enter]:{[`ListOrdered`][Token::ListOrdered],[`ListUnordered`][Token::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 token_type = &events[index].token_type;
while index < events.len() {
let event = &events[index];
if event.event_type == EventType::Enter {
balance += 1;
} else {
balance -= 1;
// Blank line directly in list or directly in list item,
// but not a blank line after an empty list item.
if balance < 3 && event.token_type == Token::BlankLineEnding {
let at_marker = balance == 2
&& events[skip::opt_back(
events,
index - 2,
&[Token::BlankLineEnding, Token::SpaceOrTab],
)]
.token_type
== Token::ListItemPrefix;
let at_list_item = balance == 1 && events[index - 2].token_type == Token::ListItem;
let at_empty_list_item = if at_list_item {
let before_item = skip::opt_back(events, index - 2, &[Token::ListItem]);
let before_prefix = skip::opt_back(
events,
index - 3,
&[Token::ListItemPrefix, Token::SpaceOrTab],
);
before_item + 1 == before_prefix
} else {
false
};
if !at_marker && !at_list_item && !at_empty_list_item {
loose = true;
break;
}
}
// Done.
if balance == 0 && event.token_type == *token_type {
break;
}
}
index += 1;
}
context.tight_stack.push(!loose);
context.line_ending_if_needed();
// Note: no `>`.
context.tag(format!(
"<{}",
if *token_type == Token::ListOrdered {
"ol"
} else {
"ul"
}
));
context.expect_first_item = Some(true);
}
/// Handle [`Enter`][EventType::Enter]:[`ListItemMarker`][Token::ListItemMarker].
fn on_enter_list_item_marker(context: &mut CompileContext) {
let expect_first_item = context.expect_first_item.take().unwrap();
if expect_first_item {
context.tag(">".to_string());
}
context.line_ending_if_needed();
context.tag("
".to_string());
}
/// Handle [`Exit`][EventType::Exit]:[`BlankLineEnding`][Token::BlankLineEnding].
fn on_exit_blank_line_ending(context: &mut CompileContext) {
if context.index == context.events.len() - 1 {
context.line_ending_if_needed();
}
}
/// Handle [`Exit`][EventType::Exit]:[`BlockQuote`][Token::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.tag("