/**
 * While micromark is a lexer/tokenizer, the common case of going from markdown
 * to html is currently built in as this module, even though the parts can be
 * used separately to build ASTs, CSTs, or many other output formats.
 *
 * Having an HTML compiler built in is useful because it allows us to check for
 * compliancy to CommonMark, the de facto norm of markdown, specified in roughly
 * 600 input/output cases.
 *
 * This module has an interface that accepts lists of events instead of the
 * whole at once, however, because markdown can’t be truly streaming, we buffer
 * events before processing and outputting the final result.
 */

/**
 * @import {
 *   CompileContext,
 *   CompileData,
 *   CompileOptions,
 *   Compile,
 *   Definition,
 *   Event,
 *   Handle,
 *   HtmlExtension,
 *   LineEnding,
 *   NormalizedHtmlExtension,
 *   Token
 * } from 'micromark-util-types'
 */

/**
 * @typedef Media
 * @property {boolean | undefined} [image]
 * @property {string | undefined} [labelId]
 * @property {string | undefined} [label]
 * @property {string | undefined} [referenceId]
 * @property {string | undefined} [destination]
 * @property {string | undefined} [title]
 */

import { decodeNamedCharacterReference } from 'decode-named-character-reference';
import { push } from 'micromark-util-chunked';
import { combineHtmlExtensions } from 'micromark-util-combine-extensions';
import { decodeNumericCharacterReference } from 'micromark-util-decode-numeric-character-reference';
import { encode as _encode } from 'micromark-util-encode';
import { normalizeIdentifier } from 'micromark-util-normalize-identifier';
import { sanitizeUri } from 'micromark-util-sanitize-uri';
const hasOwnProperty = {}.hasOwnProperty;

/**
 * These two are allowlists of safe protocols for full URLs in respectively the
 * `href` (on `<a>`) and `src` (on `<img>`) attributes.
 * They are based on what is allowed on GitHub,
 * <https://github.com/syntax-tree/hast-util-sanitize/blob/9275b21/lib/github.json#L31>
 */
const protocolHref = /^(https?|ircs?|mailto|xmpp)$/i;
const protocolSource = /^https?$/i;

/**
 * @param {CompileOptions | null | undefined} [options]
 * @returns {Compile}
 */
export function compile(options) {
  const settings = options || {};

  /**
   * Tags is needed because according to markdown, links and emphasis and
   * whatnot can exist in images, however, as HTML doesn’t allow content in
   * images, the tags are ignored in the `alt` attribute, but the content
   * remains.
   *
   * @type {boolean | undefined}
   */
  let tags = true;

  /**
   * An object to track identifiers to media (URLs and titles) defined with
   * definitions.
   *
   * @type {Record<string, Definition>}
   */
  const definitions = {};

  /**
   * A lot of the handlers need to capture some of the output data, modify it
   * somehow, and then deal with it.
   * We do that by tracking a stack of buffers, that can be opened (with
   * `buffer`) and closed (with `resume`) to access them.
   *
   * @type {Array<Array<string>>}
   */
  const buffers = [[]];

  /**
   * As we can have links in images and the other way around, where the deepest
   * ones are closed first, we need to track which one we’re in.
   *
   * @type {Array<Media>}
   */
  const mediaStack = [];

  /**
   * Same as `mediaStack` for tightness, which is specific to lists.
   * We need to track if we’re currently in a tight or loose container.
   *
   * @type {Array<boolean>}
   */
  const tightStack = [];

  /** @type {HtmlExtension} */
  const defaultHandlers = {
    enter: {
      blockQuote: onenterblockquote,
      codeFenced: onentercodefenced,
      codeFencedFenceInfo: buffer,
      codeFencedFenceMeta: buffer,
      codeIndented: onentercodeindented,
      codeText: onentercodetext,
      content: onentercontent,
      definition: onenterdefinition,
      definitionDestinationString: onenterdefinitiondestinationstring,
      definitionLabelString: buffer,
      definitionTitleString: buffer,
      emphasis: onenteremphasis,
      htmlFlow: onenterhtmlflow,
      htmlText: onenterhtml,
      image: onenterimage,
      label: buffer,
      link: onenterlink,
      listItemMarker: onenterlistitemmarker,
      listItemValue: onenterlistitemvalue,
      listOrdered: onenterlistordered,
      listUnordered: onenterlistunordered,
      paragraph: onenterparagraph,
      reference: buffer,
      resource: onenterresource,
      resourceDestinationString: onenterresourcedestinationstring,
      resourceTitleString: buffer,
      setextHeading: onentersetextheading,
      strong: onenterstrong
    },
    exit: {
      atxHeading: onexitatxheading,
      atxHeadingSequence: onexitatxheadingsequence,
      autolinkEmail: onexitautolinkemail,
      autolinkProtocol: onexitautolinkprotocol,
      blockQuote: onexitblockquote,
      characterEscapeValue: onexitdata,
      characterReferenceMarkerHexadecimal: onexitcharacterreferencemarker,
      characterReferenceMarkerNumeric: onexitcharacterreferencemarker,
      characterReferenceValue: onexitcharacterreferencevalue,
      codeFenced: onexitflowcode,
      codeFencedFence: onexitcodefencedfence,
      codeFencedFenceInfo: onexitcodefencedfenceinfo,
      codeFencedFenceMeta: onresumedrop,
      codeFlowValue: onexitcodeflowvalue,
      codeIndented: onexitflowcode,
      codeText: onexitcodetext,
      codeTextData: onexitdata,
      data: onexitdata,
      definition: onexitdefinition,
      definitionDestinationString: onexitdefinitiondestinationstring,
      definitionLabelString: onexitdefinitionlabelstring,
      definitionTitleString: onexitdefinitiontitlestring,
      emphasis: onexitemphasis,
      hardBreakEscape: onexithardbreak,
      hardBreakTrailing: onexithardbreak,
      htmlFlow: onexithtml,
      htmlFlowData: onexitdata,
      htmlText: onexithtml,
      htmlTextData: onexitdata,
      image: onexitmedia,
      label: onexitlabel,
      labelText: onexitlabeltext,
      lineEnding: onexitlineending,
      link: onexitmedia,
      listOrdered: onexitlistordered,
      listUnordered: onexitlistunordered,
      paragraph: onexitparagraph,
      reference: onresumedrop,
      referenceString: onexitreferencestring,
      resource: onresumedrop,
      resourceDestinationString: onexitresourcedestinationstring,
      resourceTitleString: onexitresourcetitlestring,
      setextHeading: onexitsetextheading,
      setextHeadingLineSequence: onexitsetextheadinglinesequence,
      setextHeadingText: onexitsetextheadingtext,
      strong: onexitstrong,
      thematicBreak: onexitthematicbreak
    }
  };

  /**
   * Combine the HTML extensions with the default handlers.
   * An HTML extension is an object whose fields are either `enter` or `exit`
   * (reflecting whether a token is entered or exited).
   * The values at such objects are names of tokens mapping to handlers.
   * Handlers are called, respectively when a token is opener or closed, with
   * that token, and a context as `this`.
   */
  const handlers = /** @type {NormalizedHtmlExtension} */
  combineHtmlExtensions([defaultHandlers, ...(settings.htmlExtensions || [])]);

  /**
   * Handlers do often need to keep track of some state.
   * That state is provided here as a key-value store (an object).
   *
   * @type {CompileData}
   */
  const data = {
    definitions,
    tightStack
  };

  /**
   * The context for handlers references a couple of useful functions.
   * In handlers from extensions, those can be accessed at `this`.
   * For the handlers here, they can be accessed directly.
   *
   * @type {Omit<CompileContext, 'sliceSerialize'>}
   */
  const context = {
    buffer,
    encode,
    getData,
    lineEndingIfNeeded,
    options: settings,
    raw,
    resume,
    setData,
    tag
  };

  /**
   * Generally, micromark copies line endings (`'\r'`, `'\n'`, `'\r\n'`) in the
   * markdown document over to the compiled HTML.
   * In some cases, such as `> a`, CommonMark requires that extra line endings
   * are added: `<blockquote>\n<p>a</p>\n</blockquote>`.
   * This variable hold the default line ending when given (or `undefined`),
   * and in the latter case will be updated to the first found line ending if
   * there is one.
   */
  let lineEndingStyle = settings.defaultLineEnding;

  // Return the function that handles a slice of events.
  return compile;

  /**
   * Deal w/ a slice of events.
   * Return either the empty string if there’s nothing of note to return, or the
   * result when done.
   *
   * @param {ReadonlyArray<Event>} events
   * @returns {string}
   */
  function compile(events) {
    let index = -1;
    let start = 0;
    /** @type {Array<number>} */
    const listStack = [];
    // As definitions can come after references, we need to figure out the media
    // (urls and titles) defined by them before handling the references.
    // So, we do sort of what HTML does: put metadata at the start (in head), and
    // then put content after (`body`).
    /** @type {Array<Event>} */
    let head = [];
    /** @type {Array<Event>} */
    let body = [];
    while (++index < events.length) {
      // Figure out the line ending style used in the document.
      if (!lineEndingStyle && (events[index][1].type === "lineEnding" || events[index][1].type === "lineEndingBlank")) {
        lineEndingStyle = /** @type {LineEnding} */
        events[index][2].sliceSerialize(events[index][1]);
      }

      // Preprocess lists to infer whether the list is loose or not.
      if (events[index][1].type === "listOrdered" || events[index][1].type === "listUnordered") {
        if (events[index][0] === 'enter') {
          listStack.push(index);
        } else {
          prepareList(events.slice(listStack.pop(), index));
        }
      }

      // Move definitions to the front.
      if (events[index][1].type === "definition") {
        if (events[index][0] === 'enter') {
          body = push(body, events.slice(start, index));
          start = index;
        } else {
          head = push(head, events.slice(start, index + 1));
          start = index + 1;
        }
      }
    }
    head = push(head, body);
    head = push(head, events.slice(start));
    index = -1;
    const result = head;

    // Handle the start of the document, if defined.
    if (handlers.enter.null) {
      handlers.enter.null.call(context);
    }

    // Handle all events.
    while (++index < events.length) {
      const handles = handlers[result[index][0]];
      const kind = result[index][1].type;
      const handle = handles[kind];
      if (hasOwnProperty.call(handles, kind) && handle) {
        handle.call({
          sliceSerialize: result[index][2].sliceSerialize,
          ...context
        }, result[index][1]);
      }
    }

    // Handle the end of the document, if defined.
    if (handlers.exit.null) {
      handlers.exit.null.call(context);
    }
    return buffers[0].join('');
  }

  /**
   * Figure out whether lists are loose or not.
   *
   * @param {ReadonlyArray<Event>} slice
   * @returns {undefined}
   */
  function prepareList(slice) {
    const length = slice.length;
    let index = 0; // Skip open.
    let containerBalance = 0;
    let loose = false;
    /** @type {boolean | undefined} */
    let atMarker;
    while (++index < length) {
      const event = slice[index];
      if (event[1]._container) {
        atMarker = undefined;
        if (event[0] === 'enter') {
          containerBalance++;
        } else {
          containerBalance--;
        }
      } else switch (event[1].type) {
        case "listItemPrefix":
          {
            if (event[0] === 'exit') {
              atMarker = true;
            }
            break;
          }
        case "linePrefix":
          {
            // Ignore

            break;
          }
        case "lineEndingBlank":
          {
            if (event[0] === 'enter' && !containerBalance) {
              if (atMarker) {
                atMarker = undefined;
              } else {
                loose = true;
              }
            }
            break;
          }
        default:
          {
            atMarker = undefined;
          }
      }
    }
    slice[0][1]._loose = loose;
  }

  /**
   * @type {CompileContext['setData']}
   */
  function setData(key, value) {
    // @ts-expect-error: assume `value` is omitted (`undefined` is passed) only
    // if allowed.
    data[key] = value;
  }

  /**
   * @type {CompileContext['getData']}
   */
  function getData(key) {
    return data[key];
  }

  /** @type {CompileContext['buffer']} */
  function buffer() {
    buffers.push([]);
  }

  /** @type {CompileContext['resume']} */
  function resume() {
    const buf = buffers.pop();
    return buf.join('');
  }

  /** @type {CompileContext['tag']} */
  function tag(value) {
    if (!tags) return;
    setData('lastWasTag', true);
    buffers[buffers.length - 1].push(value);
  }

  /** @type {CompileContext['raw']} */
  function raw(value) {
    setData('lastWasTag');
    buffers[buffers.length - 1].push(value);
  }

  /**
   * Output an extra line ending.
   *
   * @returns {undefined}
   */
  function lineEnding() {
    raw(lineEndingStyle || '\n');
  }

  /** @type {CompileContext['lineEndingIfNeeded']} */
  function lineEndingIfNeeded() {
    const buffer = buffers[buffers.length - 1];
    const slice = buffer[buffer.length - 1];
    const previous = slice ? slice.charCodeAt(slice.length - 1) : null;
    if (previous === 10 || previous === 13 || previous === null) {
      return;
    }
    lineEnding();
  }

  /** @type {CompileContext['encode']} */
  function encode(value) {
    return getData('ignoreEncode') ? value : _encode(value);
  }

  //
  // Handlers.
  //

  /**
   * @returns {undefined}
   */
  function onresumedrop() {
    resume();
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onenterlistordered(token) {
    tightStack.push(!token._loose);
    lineEndingIfNeeded();
    tag('<ol');
    setData('expectFirstItem', true);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onenterlistunordered(token) {
    tightStack.push(!token._loose);
    lineEndingIfNeeded();
    tag('<ul');
    setData('expectFirstItem', true);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onenterlistitemvalue(token) {
    if (getData('expectFirstItem')) {
      const value = Number.parseInt(this.sliceSerialize(token), 10);
      if (value !== 1) {
        tag(' start="' + encode(String(value)) + '"');
      }
    }
  }

  /**
   * @returns {undefined}
   */
  function onenterlistitemmarker() {
    if (getData('expectFirstItem')) {
      tag('>');
    } else {
      onexitlistitem();
    }
    lineEndingIfNeeded();
    tag('<li>');
    setData('expectFirstItem');
    // “Hack” to prevent a line ending from showing up if the item is empty.
    setData('lastWasTag');
  }

  /**
   * @returns {undefined}
   */
  function onexitlistordered() {
    onexitlistitem();
    tightStack.pop();
    lineEnding();
    tag('</ol>');
  }

  /**
   * @returns {undefined}
   */
  function onexitlistunordered() {
    onexitlistitem();
    tightStack.pop();
    lineEnding();
    tag('</ul>');
  }

  /**
   * @returns {undefined}
   */
  function onexitlistitem() {
    if (getData('lastWasTag') && !getData('slurpAllLineEndings')) {
      lineEndingIfNeeded();
    }
    tag('</li>');
    setData('slurpAllLineEndings');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onenterblockquote() {
    tightStack.push(false);
    lineEndingIfNeeded();
    tag('<blockquote>');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitblockquote() {
    tightStack.pop();
    lineEndingIfNeeded();
    tag('</blockquote>');
    setData('slurpAllLineEndings');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onenterparagraph() {
    if (!tightStack[tightStack.length - 1]) {
      lineEndingIfNeeded();
      tag('<p>');
    }
    setData('slurpAllLineEndings');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitparagraph() {
    if (tightStack[tightStack.length - 1]) {
      setData('slurpAllLineEndings', true);
    } else {
      tag('</p>');
    }
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onentercodefenced() {
    lineEndingIfNeeded();
    tag('<pre><code');
    setData('fencesCount', 0);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitcodefencedfenceinfo() {
    const value = resume();
    tag(' class="language-' + value + '"');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitcodefencedfence() {
    const count = getData('fencesCount') || 0;
    if (!count) {
      tag('>');
      setData('slurpOneLineEnding', true);
    }
    setData('fencesCount', count + 1);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onentercodeindented() {
    lineEndingIfNeeded();
    tag('<pre><code>');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitflowcode() {
    const count = getData('fencesCount');

    // 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 (count !== undefined && count < 2 && data.tightStack.length > 0 && !getData('lastWasTag')) {
      lineEnding();
    }

    // But in most cases, it’s simpler: when we’ve seen some data, emit an extra
    // line ending when needed.
    if (getData('flowCodeSeenData')) {
      lineEndingIfNeeded();
    }
    tag('</code></pre>');
    if (count !== undefined && count < 2) lineEndingIfNeeded();
    setData('flowCodeSeenData');
    setData('fencesCount');
    setData('slurpOneLineEnding');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onenterimage() {
    mediaStack.push({
      image: true
    });
    tags = undefined; // Disallow tags.
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onenterlink() {
    mediaStack.push({});
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitlabeltext(token) {
    mediaStack[mediaStack.length - 1].labelId = this.sliceSerialize(token);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitlabel() {
    mediaStack[mediaStack.length - 1].label = resume();
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitreferencestring(token) {
    mediaStack[mediaStack.length - 1].referenceId = this.sliceSerialize(token);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onenterresource() {
    buffer(); // We can have line endings in the resource, ignore them.
    mediaStack[mediaStack.length - 1].destination = '';
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onenterresourcedestinationstring() {
    buffer();
    // Ignore encoding the result, as we’ll first percent encode the url and
    // encode manually after.
    setData('ignoreEncode', true);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitresourcedestinationstring() {
    mediaStack[mediaStack.length - 1].destination = resume();
    setData('ignoreEncode');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitresourcetitlestring() {
    mediaStack[mediaStack.length - 1].title = resume();
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitmedia() {
    let index = mediaStack.length - 1; // Skip current.
    const media = mediaStack[index];
    const id = media.referenceId || media.labelId;
    const context = media.destination === undefined ? definitions[normalizeIdentifier(id)] : media;
    tags = true;
    while (index--) {
      if (mediaStack[index].image) {
        tags = undefined;
        break;
      }
    }
    if (media.image) {
      tag('<img src="' + sanitizeUri(context.destination, settings.allowDangerousProtocol ? undefined : protocolSource) + '" alt="');
      raw(media.label);
      tag('"');
    } else {
      tag('<a href="' + sanitizeUri(context.destination, settings.allowDangerousProtocol ? undefined : protocolHref) + '"');
    }
    tag(context.title ? ' title="' + context.title + '"' : '');
    if (media.image) {
      tag(' />');
    } else {
      tag('>');
      raw(media.label);
      tag('</a>');
    }
    mediaStack.pop();
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onenterdefinition() {
    buffer();
    mediaStack.push({});
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitdefinitionlabelstring(token) {
    // Discard label, use the source content instead.
    resume();
    mediaStack[mediaStack.length - 1].labelId = this.sliceSerialize(token);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onenterdefinitiondestinationstring() {
    buffer();
    setData('ignoreEncode', true);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitdefinitiondestinationstring() {
    mediaStack[mediaStack.length - 1].destination = resume();
    setData('ignoreEncode');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitdefinitiontitlestring() {
    mediaStack[mediaStack.length - 1].title = resume();
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitdefinition() {
    const media = mediaStack[mediaStack.length - 1];
    const id = normalizeIdentifier(media.labelId);
    resume();
    if (!hasOwnProperty.call(definitions, id)) {
      definitions[id] = mediaStack[mediaStack.length - 1];
    }
    mediaStack.pop();
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onentercontent() {
    setData('slurpAllLineEndings', true);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitatxheadingsequence(token) {
    // Exit for further sequences.
    if (getData('headingRank')) return;
    setData('headingRank', this.sliceSerialize(token).length);
    lineEndingIfNeeded();
    tag('<h' + getData('headingRank') + '>');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onentersetextheading() {
    buffer();
    setData('slurpAllLineEndings');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitsetextheadingtext() {
    setData('slurpAllLineEndings', true);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitatxheading() {
    tag('</h' + getData('headingRank') + '>');
    setData('headingRank');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitsetextheadinglinesequence(token) {
    setData('headingRank', this.sliceSerialize(token).charCodeAt(0) === 61 ? 1 : 2);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitsetextheading() {
    const value = resume();
    lineEndingIfNeeded();
    tag('<h' + getData('headingRank') + '>');
    raw(value);
    tag('</h' + getData('headingRank') + '>');
    setData('slurpAllLineEndings');
    setData('headingRank');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitdata(token) {
    raw(encode(this.sliceSerialize(token)));
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitlineending(token) {
    if (getData('slurpAllLineEndings')) {
      return;
    }
    if (getData('slurpOneLineEnding')) {
      setData('slurpOneLineEnding');
      return;
    }
    if (getData('inCodeText')) {
      raw(' ');
      return;
    }
    raw(encode(this.sliceSerialize(token)));
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitcodeflowvalue(token) {
    raw(encode(this.sliceSerialize(token)));
    setData('flowCodeSeenData', true);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexithardbreak() {
    tag('<br />');
  }

  /**
   * @returns {undefined}
   */
  function onenterhtmlflow() {
    lineEndingIfNeeded();
    onenterhtml();
  }

  /**
   * @returns {undefined}
   */
  function onexithtml() {
    setData('ignoreEncode');
  }

  /**
   * @returns {undefined}
   */
  function onenterhtml() {
    if (settings.allowDangerousHtml) {
      setData('ignoreEncode', true);
    }
  }

  /**
   * @returns {undefined}
   */
  function onenteremphasis() {
    tag('<em>');
  }

  /**
   * @returns {undefined}
   */
  function onenterstrong() {
    tag('<strong>');
  }

  /**
   * @returns {undefined}
   */
  function onentercodetext() {
    setData('inCodeText', true);
    tag('<code>');
  }

  /**
   * @returns {undefined}
   */
  function onexitcodetext() {
    setData('inCodeText');
    tag('</code>');
  }

  /**
   * @returns {undefined}
   */
  function onexitemphasis() {
    tag('</em>');
  }

  /**
   * @returns {undefined}
   */
  function onexitstrong() {
    tag('</strong>');
  }

  /**
   * @returns {undefined}
   */
  function onexitthematicbreak() {
    lineEndingIfNeeded();
    tag('<hr />');
  }

  /**
   * @this {CompileContext}
   * @param {Token} token
   * @returns {undefined}
   */
  function onexitcharacterreferencemarker(token) {
    setData('characterReferenceType', token.type);
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitcharacterreferencevalue(token) {
    const value = this.sliceSerialize(token);
    const decoded = getData('characterReferenceType') ? decodeNumericCharacterReference(value, getData('characterReferenceType') === "characterReferenceMarkerNumeric" ? 10 : 16) : decodeNamedCharacterReference(value);

    // `decodeNamedCharacterReference` can return `false` for invalid named
    // character references,
    // but everything we’ve tokenized is valid.
    raw(encode(/** @type {string} */decoded));
    setData('characterReferenceType');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitautolinkprotocol(token) {
    const uri = this.sliceSerialize(token);
    tag('<a href="' + sanitizeUri(uri, settings.allowDangerousProtocol ? undefined : protocolHref) + '">');
    raw(encode(uri));
    tag('</a>');
  }

  /**
   * @this {CompileContext}
   * @type {Handle}
   */
  function onexitautolinkemail(token) {
    const uri = this.sliceSerialize(token);
    tag('<a href="' + sanitizeUri('mailto:' + uri) + '">');
    raw(encode(uri));
    tag('</a>');
  }
}