def sub_missing(
    self: GTSelf,
    columns: SelectExpr = None,
    rows: int | list[int] | None = None,
    missing_text: str | Text | None = None,
) -> GTSelf:
    """
    Substitute missing values in the table body.

    Wherever there is missing data (i.e., `None` values) customizable content may present better
    than the standard representation of missing values that would otherwise appear. The
    `sub_missing()` method allows for this replacement through its `missing_text=` argument.
    And by not supplying anything to `missing_text=`, an em dash will serve as a default indicator
    of missingness.

    Parameters
    ----------
    columns
        The columns to target. Can either be a single column name or a series of column names
        provided in a list.
    rows
        In conjunction with `columns=`, we can specify which of their rows should be scanned for
        missing values. The default is all rows, resulting in all rows in all targeted columns being
        considered for this substitution. Alternatively, we can supply a list of row indices.
    missing_text
        The text to be used in place of missing values in the rendered table. We can optionally use
        the [`md()`](`great_tables.md`) or [`html()`](`great_tables.html`) helper functions to style
        the text as Markdown or to retain HTML elements in the text.

    Returns
    -------
    GT
        The GT object is returned. This is the same object that the method is called on so that we
        can facilitate method chaining.

    Examples
    --------
    Using a subset of the `exibble` dataset, let's create a new table. The missing values in two
    selections of columns will be given different variations of replacement text (across two
    separate calls of `sub_missing()`).

    ```{python}
    from great_tables import GT, md, html, exibble
    import polars as pl
    import polars.selectors as cs

    exibble_mini = pl.from_pandas(exibble).drop("row", "group", "fctr").slice(4, 8)

    (
        GT(exibble_mini)
        .sub_missing(
            columns=["num", "char"],
            missing_text="missing"
        )
        .sub_missing(
            columns=cs.contains(("date", "time")) | cs.by_name("currency"),
            missing_text="nothing"
        )
    )
    ```
    """

    subber = SubMissing(self._tbl_data, missing_text)
    return fmt(self, fns=subber.to_html, columns=columns, rows=rows, is_substitution=True)

    Substitute missing values in the table body.

    Wherever there is missing data (i.e., `None` values) customizable content may present better
    than the standard representation of missing values that would otherwise appear. The
    `sub_missing()` method allows for this replacement through its `missing_text=` argument.
    And by not supplying anything to `missing_text=`, an em dash will serve as a default indicator
    of missingness.

    Parameters
    ----------
    columns
        The columns to target. Can either be a single column name or a series of column names
        provided in a list.
    rows
        In conjunction with `columns=`, we can specify which of their rows should be scanned for
        missing values. The default is all rows, resulting in all rows in all targeted columns being
        considered for this substitution. Alternatively, we can supply a list of row indices.
    missing_text
        The text to be used in place of missing values in the rendered table. We can optionally use
        the [`md()`](`great_tables.md`) or [`html()`](`great_tables.html`) helper functions to style
        the text as Markdown or to retain HTML elements in the text.

    Returns
    -------
    GT
        The GT object is returned. This is the same object that the method is called on so that we
        can facilitate method chaining.

    Examples
    --------
    Using a subset of the `exibble` dataset, let's create a new table. The missing values in two
    selections of columns will be given different variations of replacement text (across two
    separate calls of `sub_missing()`).

    ```{python}
    from great_tables import GT, md, html, exibble
    import polars as pl
    import polars.selectors as cs

    exibble_mini = pl.from_pandas(exibble).drop("row", "group", "fctr").slice(4, 8)

    (
        GT(exibble_mini)
        .sub_missing(
            columns=["num", "char"],
            missing_text="missing"
        )
        .sub_missing(
            columns=cs.contains(("date", "time")) | cs.by_name("currency"),
            missing_text="nothing"
        )
    )
    ```
    
great_tables/_substitution.py