markdown 1.0.0-alpha.6

CommonMark compliant markdown parser in Rust with ASTs and extensions
Documentation
//! 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<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.
    events: &'a [Event],
    /// List of bytes.
    bytes: &'a [u8],
    /// Configuration.
    options: &'a CompileOptions,
    // Fields used by handlers to track the things they need to track to
    // compile markdown.
    /// Rank of heading (atx).
    heading_atx_rank: Option<usize>,
    /// Buffer of heading (setext) text.
    heading_setext_buffer: Option<String>,
    /// Whether raw (flow) (code (fenced), math (flow)) or code (indented) contains data.
    raw_flow_seen_data: Option<bool>,
    /// Number of raw (flow) fences.
    raw_flow_fences_count: Option<usize>,
    /// Whether we are in code (text).
    raw_text_inside: bool,
    /// Whether we are in image text.
    image_alt_inside: bool,
    /// Marker of character reference.
    character_reference_marker: Option<u8>,
    /// Whether we are expecting the first list item marker.
    list_expect_first_marker: Option<bool>,
    /// Stack of media (link, image).
    media_stack: Vec<Media>,
    /// Stack of containers.
    tight_stack: Vec<bool>,
    /// List of definitions.
    definitions: Vec<Definition>,
    /// List of definitions.
    gfm_footnote_definitions: Vec<(String, String)>,
    gfm_footnote_definition_calls: Vec<(String, usize)>,
    gfm_footnote_definition_stack: Vec<(usize, usize)>,
    /// Whether we are in a GFM table head.
    gfm_table_in_head: bool,
    /// Current GFM table alignment.
    gfm_table_align: Option<Vec<AlignKind>>,
    /// Current GFM table column.
    gfm_table_column: usize,
    // Fields used to influance the current compilation.
    /// Ignore the next line ending.
    slurp_one_line_ending: bool,
    /// Whether to encode HTML.
    encode_html: bool,
    // Configuration
    /// Line ending to use.
    line_ending_default: LineEnding,
    // Intermediate results.
    /// Stack of buffers.
    buffers: Vec<String>,
    /// Current event index.
    index: usize,
}

impl<'a> CompileContext<'a> {
    /// Create a new compile context.
    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,
        }
    }

    /// Push a buffer.
    fn buffer(&mut self) {
        self.buffers.push(String::new());
    }

    /// Pop a buffer, returning its value.
    fn resume(&mut self) -> String {
        self.buffers.pop().expect("Cannot resume w/o buffer")
    }

    /// Push a str to the last 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);
    }

    /// Add a line ending.
    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).
    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();
        }
    }
}

/// Turn events and bytes into a string of HTML.
pub fn compile(events: &[Event], bytes: &[u8], options: &CompileOptions) -> 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)
        {
            let slice = Slice::from_position(bytes, &Position::from_exit_event(events, index));
            line_ending_inferred = Some(slice.as_str().parse().unwrap());
            break;
        }

        index += 1;
    }

    // Figure out which line ending style we’ll use.
    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;

    // 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.
    //
    // We don’t need to handle GFM footnote definitions like this, because
    // unlike normal definitions, what they produce is not used in calls.
    // It would also get very complex, because footnote definitions can be
    // nested.
    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); // Also handle start.
                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;
        }
    }

    // No section to generate.
    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()
}

/// 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::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),
        _ => {}
    }
}

/// Handle [`Exit`][Kind::Exit].
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),
        _ => {}
    }
}

/// 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.raw_flow_seen_data = Some(false);
    context.line_ending_if_needed();
    context.push("<pre><code>");
}

/// 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("<pre><code");
    context.raw_flow_fences_count = Some(0);

    if context.events[context.index].name == Name::MathFlow {
        context.push(" class=\"language-math math-display\"");
    }
}

/// Handle [`Enter`][Kind::Enter]:{[`CodeText`][Name::CodeText],[`MathText`][Name::MathText]}.
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();
}

/// 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]:[`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("<del>");
    }
}

/// 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("<table>");
}

/// Handle [`Enter`][Kind::Enter]:[`GfmTableBody`][Name::GfmTableBody].
fn on_enter_gfm_table_body(context: &mut CompileContext) {
    context.push("<tbody>");
}

/// 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("<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(">");
    }
}

/// Handle [`Enter`][Kind::Enter]:[`GfmTableHead`][Name::GfmTableHead].
fn on_enter_gfm_table_head(context: &mut CompileContext) {
    context.line_ending_if_needed();
    context.push("<thead>");
    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("<tr>");
}

/// 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.options.allow_dangerous_html {
        context.encode_html = false;
    }
}

/// Handle [`Enter`][Kind::Enter]:[`HtmlText`][Name::HtmlText].
fn on_enter_html_text(context: &mut CompileContext) {
    if context.options.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 loose = list_loose(context.events, context.index, true);
    context.tight_stack.push(!loose);
    context.line_ending_if_needed();

    // Note: no `>`.
    context.push(if context.events[context.index].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(String::new());
}

/// 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(),
        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("<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 = 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("</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;
}

/// 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("</code>");
    }
}

/// 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("</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]:[`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("<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>");
}

/// 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("</del>");
    }
}

/// 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("</table>");
}

/// Handle [`Exit`][Kind::Exit]:[`GfmTableBody`][Name::GfmTableBody].
fn on_exit_gfm_table_body(context: &mut CompileContext) {
    context.line_ending_if_needed();
    context.push("</tbody>");
}

/// 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("</th>");
        } else {
            context.push("</td>");
        }
    } 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("</thead>");
}

/// 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("</tr>");
}

/// 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]:[`HeadingSetextUnderlineSequence`][Name::HeadingSetextUnderlineSequence].
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(">");
}

/// 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) {
    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);
}

/// 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.raw_text_inside {
        context.push(" ");
    } else if context.slurp_one_line_ending
        // Ignore line endings after definitions.
        || (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,
        ));
    }
}

/// 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::BlockQuotePrefix,
            Name::LineEnding,
            Name::SpaceOrTab,
            // Also ignore things that don’t contribute to the document.
            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>");
}

/// 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 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>");
        }
    }
}

/// 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 a footnote section.
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();
}

/// Generate a footnote item from a call.
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());

    // Find definition: we’ll always find it.
    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();

    // Create one or more backreferences.
    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();
    // Move back past EOL.
    while byte_index > 0 && matches!(bytes[byte_index - 1], b'\n' | b'\r') {
        byte_index -= 1;
    }
    // Check if it ends in `</p>`.
    // This is a bit funky if someone wrote a safe paragraph by hand in
    // there.
    // But in all other cases, `<` and `>` would be encoded, so we can be
    // sure that this is generated by our compiler.
    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>");
}

/// Generate an autolink (used by unicode autolinks and GFM autolink literals).
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>");
    }
}