text: """
  Module for formatting output data in HTML.
  """
  
  from textwrap import dedent
  from typing import IO, Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union, cast
  
  from pandas._config import get_option
  
  from pandas._libs import lib
  
  from pandas.core.dtypes.generic import ABCMultiIndex
  
  from pandas import option_context
  
  from pandas.io.common import is_url
  from pandas.io.formats.format import (
      DataFrameFormatter,
      TableFormatter,
      buffer_put_lines,
      get_level_lengths,
  )
  from pandas.io.formats.printing import pprint_thing
  
  
  class HTMLFormatter(TableFormatter):
      """
      Internal class for formatting output data in html.
      This class is intended for shared functionality between
      DataFrame.to_html() and DataFrame._repr_html_().
      Any logic in common with other output formatting methods
      should ideally be inherited from classes in format.py
      and this class responsible for only producing html markup.
      """
  
      indent_delta = 2
  
      def __init__(
          self,
          formatter: DataFrameFormatter,
          classes: Optional[Union[str, List[str], Tuple[str, ...]]] = None,
          border: Optional[int] = None,
      ) -> None:
          self.fmt = formatter
          self.classes = classes
  
          self.frame = self.fmt.frame
          self.columns = self.fmt.tr_frame.columns
          self.elements: List[str] = []
          self.bold_rows = self.fmt.bold_rows
          self.escape = self.fmt.escape
          self.show_dimensions = self.fmt.show_dimensions
          if border is None:
              border = cast(int, get_option("display.html.border"))
          self.border = border
          self.table_id = self.fmt.table_id
          self.render_links = self.fmt.render_links
          if isinstance(self.fmt.col_space, int):
              self.fmt.col_space = f"{self.fmt.col_space}px"
  
      @property
      def show_row_idx_names(self) -> bool:
          return self.fmt.show_row_idx_names
  
      @property
      def show_col_idx_names(self) -> bool:
          return self.fmt.show_col_idx_names
  
      @property
      def row_levels(self) -> int:
          if self.fmt.index:
              # showing (row) index
              return self.frame.index.nlevels
          elif self.show_col_idx_names:
              # see gh-22579
              # Column misalignment also occurs for
              # a standard index when the columns index is named.
              # If the row index is not displayed a column of
              # blank cells need to be included before the DataFrame values.
              return 1
          # not showing (row) index
          return 0
  
      def _get_columns_formatted_values(self) -> Iterable:
          return self.columns
  
      # https://github.com/python/mypy/issues/1237
      @property
      def is_truncated(self) -> bool:  # type: ignore
          return self.fmt.is_truncated
  
      @property
      def ncols(self) -> int:
          return len(self.fmt.tr_frame.columns)
  
      def write(self, s: Any, indent: int = 0) -> None:
          rs = pprint_thing(s)
          self.elements.append(" " * indent + rs)
  
      def write_th(
          self, s: Any, header: bool = False, indent: int = 0, tags: Optional[str] = None
      ) -> None:
          """
          Method for writing a formatted <th> cell.
  
          If col_space is set on the formatter then that is used for
          the value of min-width.
  
          Parameters
          ----------
          s : object
              The data to be written inside the cell.
          header : bool, default False
              Set to True if the <th> is for use inside <thead>.  This will
              cause min-width to be set if there is one.
          indent : int, default 0
              The indentation level of the cell.
          tags : str, default None
              Tags to include in the cell.
  
          Returns
          -------
          A written <th> cell.
          """
          if header and self.fmt.col_space is not None:
              tags = tags or ""
              tags += f'style="min-width: {self.fmt.col_space};"'
  
          self._write_cell(s, kind="th", indent=indent, tags=tags)
  
      def write_td(self, s: Any, indent: int = 0, tags: Optional[str] = None) -> None:
          self._write_cell(s, kind="td", indent=indent, tags=tags)
  
      def _write_cell(
          self, s: Any, kind: str = "td", indent: int = 0, tags: Optional[str] = None
      ) -> None:
          if tags is not None:
              start_tag = f"<{kind} {tags}>"
          else:
              start_tag = f"<{kind}>"
  
          if self.escape:
              # escape & first to prevent double escaping of &
              esc = {"&": r"&amp;", "<": r"&lt;", ">": r"&gt;"}
          else:
              esc = {}
  
          rs = pprint_thing(s, escape_chars=esc).strip()
  
          if self.render_links and is_url(rs):
              rs_unescaped = pprint_thing(s, escape_chars={}).strip()
              start_tag += f'<a href="{rs_unescaped}" target="_blank">'
              end_a = "</a>"
          else:
              end_a = ""
  
          self.write(f"{start_tag}{rs}{end_a}</{kind}>", indent)
  
      def write_tr(
          self,
          line: Iterable,
          indent: int = 0,
          indent_delta: int = 0,
          header: bool = False,
          align: Optional[str] = None,
          tags: Optional[Dict[int, str]] = None,
          nindex_levels: int = 0,
      ) -> None:
          if tags is None:
              tags = {}
  
          if align is None:
              self.write("<tr>", indent)
          else:
              self.write(f'<tr style="text-align: {align};">', indent)
          indent += indent_delta
  
          for i, s in enumerate(line):
              val_tag = tags.get(i, None)
              if header or (self.bold_rows and i < nindex_levels):
                  self.write_th(s, indent=indent, header=header, tags=val_tag)
              else:
                  self.write_td(s, indent, tags=val_tag)
  
          indent -= indent_delta
          self.write("</tr>", indent)
  
      def render(self) -> List[str]:
          self._write_table()
  
          if self.should_show_dimensions:
              by = chr(215)  # ×
              self.write(
                  f"<p>{len(self.frame)} rows {by} {len(self.frame.columns)} columns</p>"
              )
  
          return self.elements
  
      def write_result(self, buf: IO[str]) -> None:
          buffer_put_lines(buf, self.render())
  
      def _write_table(self, indent: int = 0) -> None:
          _classes = ["dataframe"]  # Default class.
          use_mathjax = get_option("display.html.use_mathjax")
          if not use_mathjax:
              _classes.append("tex2jax_ignore")
          if self.classes is not None:
              if isinstance(self.classes, str):
                  self.classes = self.classes.split()
              if not isinstance(self.classes, (list, tuple)):
                  raise TypeError(
                      "classes must be a string, list, "
                      f"or tuple, not {type(self.classes)}"
                  )
              _classes.extend(self.classes)
  
          if self.table_id is None:
              id_section = ""
          else:
              id_section = f' id="{self.table_id}"'
  
          self.write(
              f'<table border="{self.border}" class="{" ".join(_classes)}"{id_section}>',
              indent,
          )
  
          if self.fmt.header or self.show_row_idx_names:
              self._write_header(indent + self.indent_delta)
  
          self._write_body(indent + self.indent_delta)
  
          self.write("</table>", indent)
  
      def _write_col_header(self, indent: int) -> None:
          truncate_h = self.fmt.truncate_h
          if isinstance(self.columns, ABCMultiIndex):
              template = 'colspan="{span:d}" halign="left"'
  
              if self.fmt.sparsify:
                  # GH3547
                  sentinel = lib.no_default
              else:
                  sentinel = False
              levels = self.columns.format(sparsify=sentinel, adjoin=False, names=False)
              level_lengths = get_level_lengths(levels, sentinel)
              inner_lvl = len(level_lengths) - 1
              for lnum, (records, values) in enumerate(zip(level_lengths, levels)):
                  if truncate_h:
                      # modify the header lines
                      ins_col = self.fmt.tr_col_num
                      if self.fmt.sparsify:
                          recs_new = {}
                          # Increment tags after ... col.
                          for tag, span in list(records.items()):
                              if tag >= ins_col:
                                  recs_new[tag + 1] = span
                              elif tag + span > ins_col:
                                  recs_new[tag] = span + 1
                                  if lnum == inner_lvl:
                                      values = (
                                          values[:ins_col] + ("...",) + values[ins_col:]
                                      )
                                  else:
                                      # sparse col headers do not receive a ...
                                      values = (
                                          values[:ins_col]
                                          + (values[ins_col - 1],)
                                          + values[ins_col:]
                                      )
                              else:
                                  recs_new[tag] = span
                              # if ins_col lies between tags, all col headers
                              # get ...
                              if tag + span == ins_col:
                                  recs_new[ins_col] = 1
                                  values = values[:ins_col] + ("...",) + values[ins_col:]
                          records = recs_new
                          inner_lvl = len(level_lengths) - 1
                          if lnum == inner_lvl:
                              records[ins_col] = 1
                      else:
                          recs_new = {}
                          for tag, span in list(records.items()):
                              if tag >= ins_col:
                                  recs_new[tag + 1] = span
                              else:
                                  recs_new[tag] = span
                          recs_new[ins_col] = 1
                          records = recs_new
                          values = values[:ins_col] + ["..."] + values[ins_col:]
  
                  # see gh-22579
                  # Column Offset Bug with to_html(index=False) with
                  # MultiIndex Columns and Index.
                  # Initially fill row with blank cells before column names.
                  # TODO: Refactor to remove code duplication with code
                  # block below for standard columns index.
                  row = [""] * (self.row_levels - 1)
                  if self.fmt.index or self.show_col_idx_names:
                      # see gh-22747
                      # If to_html(index_names=False) do not show columns
                      # index names.
                      # TODO: Refactor to use _get_column_name_list from
                      # DataFrameFormatter class and create a
                      # _get_formatted_column_labels function for code
                      # parity with DataFrameFormatter class.
                      if self.fmt.show_index_names:
                          name = self.columns.names[lnum]
                          row.append(pprint_thing(name or ""))
                      else:
                          row.append("")
  
                  tags = {}
                  j = len(row)
                  for i, v in enumerate(values):
                      if i in records:
                          if records[i] > 1:
                              tags[j] = template.format(span=records[i])
                      else:
                          continue
                      j += 1
                      row.append(v)
                  self.write_tr(row, indent, self.indent_delta, tags=tags, header=True)
          else:
              # see gh-22579
              # Column misalignment also occurs for
              # a standard index when the columns index is named.
              # Initially fill row with blank cells before column names.
              # TODO: Refactor to remove code duplication with code block
              # above for columns MultiIndex.
              row = [""] * (self.row_levels - 1)
              if self.fmt.index or self.show_col_idx_names:
                  # see gh-22747
                  # If to_html(index_names=False) do not show columns
                  # index names.
                  # TODO: Refactor to use _get_column_name_list from
                  # DataFrameFormatter class.
                  if self.fmt.show_index_names:
                      row.append(self.columns.name or "")
                  else:
                      row.append("")
              row.extend(self._get_columns_formatted_values())
              align = self.fmt.justify
  
              if truncate_h:
                  ins_col = self.row_levels + self.fmt.tr_col_num
                  row.insert(ins_col, "...")
  
              self.write_tr(row, indent, self.indent_delta, header=True, align=align)
  
      def _write_row_header(self, indent: int) -> None:
          truncate_h = self.fmt.truncate_h
          row = [x if x is not None else "" for x in self.frame.index.names] + [""] * (
              self.ncols + (1 if truncate_h else 0)
          )
          self.write_tr(row, indent, self.indent_delta, header=True)
  
      def _write_header(self, indent: int) -> None:
          self.write("<thead>", indent)
  
          if self.fmt.header:
              self._write_col_header(indent + self.indent_delta)
  
          if self.show_row_idx_names:
              self._write_row_header(indent + self.indent_delta)
  
          self.write("</thead>", indent)
  
      def _get_formatted_values(self) -> Dict[int, List[str]]:
          with option_context("display.max_colwidth", None):
              fmt_values = {i: self.fmt._format_col(i) for i in range(self.ncols)}
          return fmt_values
  
      def _write_body(self, indent: int) -> None:
          self.write("<tbody>", indent)
          fmt_values = self._get_formatted_values()
  
          # write values
          if self.fmt.index and isinstance(self.frame.index, ABCMultiIndex):
              self._write_hierarchical_rows(fmt_values, indent + self.indent_delta)
          else:
              self._write_regular_rows(fmt_values, indent + self.indent_delta)
  
          self.write("</tbody>", indent)
  
      def _write_regular_rows(
          self, fmt_values: Mapping[int, List[str]], indent: int
      ) -> None:
          truncate_h = self.fmt.truncate_h
          truncate_v = self.fmt.truncate_v
  
          nrows = len(self.fmt.tr_frame)
  
          if self.fmt.index:
              fmt = self.fmt._get_formatter("__index__")
              if fmt is not None:
                  index_values = self.fmt.tr_frame.index.map(fmt)
              else:
                  index_values = self.fmt.tr_frame.index.format()
  
          row: List[str] = []
          for i in range(nrows):
  
              if truncate_v and i == (self.fmt.tr_row_num):
                  str_sep_row = ["..."] * len(row)
                  self.write_tr(
                      str_sep_row,
                      indent,
                      self.indent_delta,
                      tags=None,
                      nindex_levels=self.row_levels,
                  )
  
              row = []
              if self.fmt.index:
                  row.append(index_values[i])
              # see gh-22579
              # Column misalignment also occurs for
              # a standard index when the columns index is named.
              # Add blank cell before data cells.
              elif self.show_col_idx_names:
                  row.append("")
              row.extend(fmt_values[j][i] for j in range(self.ncols))
  
              if truncate_h:
                  dot_col_ix = self.fmt.tr_col_num + self.row_levels
                  row.insert(dot_col_ix, "...")
              self.write_tr(
                  row, indent, self.indent_delta, tags=None, nindex_levels=self.row_levels
              )
  
      def _write_hierarchical_rows(
          self, fmt_values: Mapping[int, List[str]], indent: int
      ) -> None:
          template = 'rowspan="{span}" valign="top"'
  
          truncate_h = self.fmt.truncate_h
          truncate_v = self.fmt.truncate_v
          frame = self.fmt.tr_frame
          nrows = len(frame)
  
          idx_values = frame.index.format(sparsify=False, adjoin=False, names=False)
          idx_values = list(zip(*idx_values))
  
          if self.fmt.sparsify:
              # GH3547
              sentinel = lib.no_default
              levels = frame.index.format(sparsify=sentinel, adjoin=False, names=False)
  
              level_lengths = get_level_lengths(levels, sentinel)
              inner_lvl = len(level_lengths) - 1
              if truncate_v:
                  # Insert ... row and adjust idx_values and
                  # level_lengths to take this into account.
                  ins_row = self.fmt.tr_row_num
                  # cast here since if truncate_v is True, self.fmt.tr_row_num is not None
                  ins_row = cast(int, ins_row)
                  inserted = False
                  for lnum, records in enumerate(level_lengths):
                      rec_new = {}
                      for tag, span in list(records.items()):
                          if tag >= ins_row:
                              rec_new[tag + 1] = span
                          elif tag + span > ins_row:
                              rec_new[tag] = span + 1
  
                              # GH 14882 - Make sure insertion done once
                              if not inserted:
                                  dot_row = list(idx_values[ins_row - 1])
                                  dot_row[-1] = "..."
                                  idx_values.insert(ins_row, tuple(dot_row))
                                  inserted = True
                              else:
                                  dot_row = list(idx_values[ins_row])
                                  dot_row[inner_lvl - lnum] = "..."
                                  idx_values[ins_row] = tuple(dot_row)
                          else:
                              rec_new[tag] = span
                          # If ins_row lies between tags, all cols idx cols
                          # receive ...
                          if tag + span == ins_row:
                              rec_new[ins_row] = 1
                              if lnum == 0:
                                  idx_values.insert(
                                      ins_row, tuple(["..."] * len(level_lengths))
                                  )
  
                              # GH 14882 - Place ... in correct level
                              elif inserted:
                                  dot_row = list(idx_values[ins_row])
                                  dot_row[inner_lvl - lnum] = "..."
                                  idx_values[ins_row] = tuple(dot_row)
                      level_lengths[lnum] = rec_new
  
                  level_lengths[inner_lvl][ins_row] = 1
                  for ix_col in range(len(fmt_values)):
                      fmt_values[ix_col].insert(ins_row, "...")
                  nrows += 1
  
              for i in range(nrows):
                  row = []
                  tags = {}
  
                  sparse_offset = 0
                  j = 0
                  for records, v in zip(level_lengths, idx_values[i]):
                      if i in records:
                          if records[i] > 1:
                              tags[j] = template.format(span=records[i])
                      else:
                          sparse_offset += 1
                          continue
  
                      j += 1
                      row.append(v)
  
                  row.extend(fmt_values[j][i] for j in range(self.ncols))
                  if truncate_h:
                      row.insert(
                          self.row_levels - sparse_offset + self.fmt.tr_col_num, "..."
                      )
                  self.write_tr(
                      row,
                      indent,
                      self.indent_delta,
                      tags=tags,
                      nindex_levels=len(levels) - sparse_offset,
                  )
          else:
              row = []
              for i in range(len(frame)):
                  if truncate_v and i == (self.fmt.tr_row_num):
                      str_sep_row = ["..."] * len(row)
                      self.write_tr(
                          str_sep_row,
                          indent,
                          self.indent_delta,
                          tags=None,
                          nindex_levels=self.row_levels,
                      )
  
                  idx_values = list(
                      zip(*frame.index.format(sparsify=False, adjoin=False, names=False))
                  )
                  row = []
                  row.extend(idx_values[i])
                  row.extend(fmt_values[j][i] for j in range(self.ncols))
                  if truncate_h:
                      row.insert(self.row_levels + self.fmt.tr_col_num, "...")
                  self.write_tr(
                      row,
                      indent,
                      self.indent_delta,
                      tags=None,
                      nindex_levels=frame.index.nlevels,
                  )
  
  
  class NotebookFormatter(HTMLFormatter):
      """
      Internal class for formatting output data in html for display in Jupyter
      Notebooks. This class is intended for functionality specific to
      DataFrame._repr_html_() and DataFrame.to_html(notebook=True)
      """
  
      def _get_formatted_values(self) -> Dict[int, List[str]]:
          return {i: self.fmt._format_col(i) for i in range(self.ncols)}
  
      def _get_columns_formatted_values(self) -> List[str]:
          return self.columns.format()
  
      def write_style(self) -> None:
          # We use the "scoped" attribute here so that the desired
          # style properties for the data frame are not then applied
          # throughout the entire notebook.
          template_first = """\
              <style scoped>"""
          template_last = """\
              </style>"""
          template_select = """\
                  .dataframe %s {
                      %s: %s;
                  }"""
          element_props = [
              ("tbody tr th:only-of-type", "vertical-align", "middle"),
              ("tbody tr th", "vertical-align", "top"),
          ]
          if isinstance(self.columns, ABCMultiIndex):
              element_props.append(("thead tr th", "text-align", "left"))
              if self.show_row_idx_names:
                  element_props.append(
                      ("thead tr:last-of-type th", "text-align", "right")
                  )
          else:
              element_props.append(("thead th", "text-align", "right"))
          template_mid = "\n\n".join(map(lambda t: template_select % t, element_props))
          template = dedent("\n".join((template_first, template_mid, template_last)))
          self.write(template)
  
      def render(self) -> List[str]:
          self.write("<div>")
          self.write_style()
          super().render()
          self.write("</div>")
          return self.elements
  