# Copyright (c) 2023 Christophe Dufaza <chris@openmarl.org>
#
# SPDX-License-Identifier: Apache-2.0

"""SVG contents format for command output redirection.

Rationale:
- the SVG documents we can generate directly with the Textualize's rich library
  API do not really fit the DTSh use cases
- we need an additional abstraction layer to properly support the "append" mode
"""


from typing import Tuple, List, Type, TypeVar, Optional

import os
import re

from rich.console import Console
from rich.terminal_theme import TerminalTheme

from dtsh.shell import DTShError


SVGText = List[str]

TFragment = TypeVar("TFragment", bound="SVGFragment")


class SVGFormat:
    """Describes the SVG format used by DTSh
    when formatting command outputs to SVG.

    Rich SVG format
    ---------------
    Reverse engineering of the SVG text generated by the rich library
    API rich.console.Console.export_svg().

    * SVG container

    The SVG container set the document view's dimensions:

        <svg viewBox="0 0 {width} {height}">

    * CSS style element

    The CSS style element includes:
    - the CSS specifications for the "Fira Code" font family
    - the CSS base styles for the SVG boxes that will represent
      the command outputs

        <style>
        @font-face {{
            ...
        }}
        <!-- CSS style for output container, the table -->
        .terminal-2641550630-matrix {
        }
        <!-- CSS styles line by line, the rows -->
        .terminal-3755984749-r1 { ... }
        .terminal-3755984749-r2 { ... }
        </style>

    * SVG clipping paths

    Represent the line drawing for the terminal rows:

        <defs>
        <!-- First, the terminal -->
        <clipPath id="terminal-4015336181-clip-terminal">
            <rect x="0" y="0" width="1211.0" height="145.39999999999998" />
        </clipPath>
        <!-- Then the geometry line by line -->
        <clipPath id="terminal-4015336181-line-0">
            <rect x="0" y="1.5" width="1212" height="24.65"/></clipPath>
        </clipPath>
        ...
        <defs>

    * The background rectangle (aka chrome)

    Rendered within the SVG container viewBox, with TBLR margin of 1:
    - x = y = 1
    - width: viewBox.width -1 -1
    - height: viewBox.height -1 -1

        <rect fill="#292929" ... width=".." height="...""/>

    * Title bar

    An SVG box, containing:
    - macOs-like window buttons (left)
    - an optional title (center)

    * The terminal output GBox

    SVG box rendering the captured output, referencing the above
    styles and clipping paths:

        <g transform="translate(9, 12)" clip-path="url(#terminal-4015336181-clip-terminal)">
            <g class="terminal-4015336181-matrix">
                <text clip-path="...">text</text>
                ...
            </g>
        </g>

    * SVG epilogue

    Closing the SVG document with </svg>.

    DTSh SVG format
    ---------------
    Slightly adapt the original format of the rich library
    to better fit our use cases, especially:
    - append mode: when appending commands outputs to formatted SVG,
      we can't simply concatenate the outputs: we must generate
      a new structured content based on the existing file,
      and on the capture of the last command
    - formatting the generated SVG files as individual figures into
      external documents (HTML, PDF/LaTeX): there, we are not necessarily
      fans of the mac-OS like buttons with which the rich library decorates
      the generated output

    * SVG container

    There's still a single container.
    It's geometry is updated in append mode to match the widest output.

    * CSS style element

    We:
    - split the Fira Code font face specification and the CSS base styles
      into separate <style> elements
    - in append mode, aggregate all CSS base styles within a single <style>

    * SVG clipping paths

    In append mode, aggregate all clipping paths
    within a single <defs> element.

    * The background rectangle (aka chrome)

    It's geometry is updated:
    - to compensate for the title bar if we remove it
    - in append mode, to match the widest output

    * Title bar

    The macOs-like buttons are hidden by default
    (see "pref.svg.decorations" in dtsh.ini).
    If there's neither buttons nor title, the title bar is removed,
    and we must update the SVG document geometry.

    * The terminal output GBox

    It's geometry is updated to compensate for the title bar if we removed it.
    In append mode, new SVG boxes are computed for each output.

    See also:
    - DTSH_META_SVG_FORMAT
    - DTSH_SVG_STYLE_FONT_FACES
    """

    class Error(DTShError):
        """Unexpected or invalid SVG content."""

        def __init__(self, cause: str) -> None:
            super().__init__(f"SVG format error: {cause}")

    # Match the text line which sets the SVG viewBox.
    RE_VIEWBOX: re.Pattern[str] = re.compile(
        r'\s*<svg\s+class=".*" viewBox="0 0 (?P<w>[\d.]+) (?P<h>[\d.]+)"'
    )

    # Match the text line which starts the base CSS styles.
    # Note the type="": valid syntax we use to distinguish this
    # from the <style> adding the CSS font faces.
    RE_STYLE_TYPE = re.compile(r'\s*<style type="">')
    RE_STYLE_CLOSE = re.compile(r"\s*</style>")

    # Match the text line which starts the clipping paths.
    RE_DEFS = re.compile(r"\s*<defs>")
    RE_DEFS_CLOSE = re.compile(r"\s*</defs>")

    # Match the text line which starts chrome rectangle.
    RE_CHROME = re.compile(
        r'\s*<rect\s+.*x="1" y="1" width="(?P<w>[\d.]+)" height="(?P<h>[\d.]+)"'
    )

    # Match the text line which starts macOS-like buttons.
    RE_GBOX_CIRCLES = re.compile(r'\s*<g transform="translate\(26,22\)">')

    # Match the text line starting an SVG box representing
    # the captured output at rows.
    RE_GBOX_TERM = re.compile(
        r'\s*<g transform="translate\((?P<x>[\d.]+), (?P<y>[\d.]+)\)" clip-path="url(.*)">\s*$'
    )
    RE_GBOX_END: re.Pattern[str] = re.compile(r"\s*</g>\s*$")

    # Margins left plus right.
    MARGIN_WIDTH: int = 1 + 1

    # Margins top plus bottom.
    MARGIN_HEIGHT: int = 1 + 1

    # Y coordinate of the macOS-like buttons (vertically centered).
    GCIRCLES_Y: int = 22

    # Radius of the macOS-like buttons.
    GCIRCLES_R: int = 7

    # Y coordinate of the SVG title bar.
    GTITLE_Y: int = GCIRCLES_Y - GCIRCLES_R

    # Top padding relative to the viewBox,
    # down to the captured output (aka the "Terminal group").
    PADDING_TOP: int = GTITLE_Y - 1

    # Bottom pading.
    PADDING_BOTTOM: int = 8

    # Y coordinate of the SVG group representing the actual capture.
    # as generated by the rich library (aka the "Terminal group").
    GTERM_Y = 41

    # Vertical space occupied by the title bar when present.
    VSPAN_TITLE_BAR: int = GTERM_Y - GCIRCLES_Y + 1 + GCIRCLES_R

    @staticmethod
    def ifind(
        svg_txt: SVGText, pattern: re.Pattern[str], start: int
    ) -> Tuple[int, re.Match[str]]:
        """Find an RE pattern in SVG text.

        Stops on the first matching line.

        Args:
            svg_txt: The SVG text as lines.
            pattern: The pattern to match.
            start: Start the search from here.

        Returns: A tuple containing the index of the matching text line
            and the RE match.

        Raises:
            SVGFormat: Pattern not found.
        """
        for i, txt in enumerate(svg_txt[start:]):
            m = pattern.match(txt)
            if m:
                return (i + start, m)
        raise SVGFormat.Error(pattern.pattern)

    @staticmethod
    def mk_format(font_family: str) -> str:
        """Prepare the SVG format string for a given font family.

        Args:
            font_family: A font family (see pref.svg.font_family).

        Returns:
            A format string compatible with the rich library API.
        """
        if font_family:
            font_family = f"{font_family},monospace"
        else:
            font_family = "monospace"

        fmt = DTSH_META_SVG_FORMAT.replace("|font_family|", font_family)
        return fmt


class SVGFragment:
    """Base for SVG fragments."""

    RE_BEGIN: re.Pattern[str]
    """RE matching the beginning of the fragment."""

    RE_END: Optional[re.Pattern[str]] = None
    """RE matching the end of the fragment (multi-line fragments only)."""

    @classmethod
    def ifind(
        cls: Type[TFragment], svg_txt: SVGText, start: int = 0
    ) -> TFragment:
        """Find an SVG fragment.

        Args:
            svg_txt: The SVG text to search.
            start: Start the search from here.

        Returns: A tuple containing the index of the line matching
            the end of the fragment, and the matched fragment.

        Raises:
            SVGFormat: Fragment not found.
        """
        i_begin, matched = SVGFormat.ifind(svg_txt, cls.RE_BEGIN, start)
        if cls.RE_END:
            i_end, _ = SVGFormat.ifind(svg_txt, cls.RE_END, i_begin + 1)
        else:
            i_end = i_begin
        return cls(i_end, svg_txt[i_begin : i_end + 1], matched)

    @staticmethod
    def newline(svg_txt: SVGText) -> None:
        """Append an empty line to SVG text.

        NOTE: This, and the title feature, would instead deserve a "echo"
        built-in command (keeping this as an available entry level
        contribution/PR for now).
        """
        svg_txt.append("")

    _endl: int
    _content: SVGText

    def __init__(
        self,
        endl: int,
        content: SVGText,
        matched: re.Match[str],  # pylint: disable=unused-argument
    ) -> None:
        """Initialize fragment.

        Args:
            content: The fragment's content.
            matched: The RE match opening this fragment.
        """
        self._endl = endl
        self._content = content

    @property
    def i_end(self) -> int:
        """Index of the last SVG line parsed within this fragment."""
        return self._endl

    @property
    def content(self) -> SVGText:
        """The fragment's content."""
        return self._content

    def __repr__(self) -> str:
        return os.linesep.join(self.content)


class SVGFragmentViewBox(SVGFragment):
    """Fragment for the viewBox geometry."""

    RE_BEGIN: re.Pattern[str] = SVGFormat.RE_VIEWBOX

    # SVG container dimensions.
    _width: float
    _height: float

    def __init__(
        self, endl: int, content: SVGText, matched: re.Match[str]
    ) -> None:
        """Initialize SVG container geometry."""
        super().__init__(endl, content, matched)
        raw_width: str = matched.group("w")
        raw_height: str = matched.group("h")

        width = float(raw_width)
        height = float(raw_height)
        self.set_width_height(width, height)

    @property
    def width(self) -> float:
        """SVG container width."""
        return self._width

    @property
    def height(self) -> float:
        """SVG container height."""
        return self._height

    def set_width_height(self, width: float, height: float) -> None:
        """Update container geometry.

        Args:
            width: new width.
            height: new height.
        """
        self._content[0] = re.sub(
            r'viewBox="0 0 (?P<w>[\d.]+) (?P<h>[\d.]+)"',
            f'viewBox="0 0 {width} {height}"',
            self._content[0],
        )
        self._width = width
        self._height = height


class SVGFragmentStyle(SVGFragment):
    """Fragment for the CSS styles."""

    RE_BEGIN = SVGFormat.RE_STYLE_TYPE
    RE_END = SVGFormat.RE_STYLE_CLOSE

    def append(self, fragment: "SVGFragmentStyle") -> None:
        """Append CSS styles from another fragment (append nmode).

        Args:
            fragment: The fragment from which we're appending styles.
        """
        offset = len(self._content) - 1
        self._content = (
            self._content[:offset]
            + [""]
            + fragment.content[1:-1]
            + self._content[offset:]
        )


class SVGFragmentDefs(SVGFragment):
    """Fragment for the SVG clipping paths."""

    RE_BEGIN = SVGFormat.RE_DEFS
    RE_END = SVGFormat.RE_DEFS_CLOSE

    # RE matching the first <rect> that will appear within
    # the SVG <defs> element.
    # See _workaround_rich_issue_3576().
    RE_RECT_3576 = re.compile(
        r'\s*<rect\s+.*x="0" y="0" width="[\d.]+" height="(?P<h>[\d.]+)"'
    )

    def __init__(
        self, endl: int, content: SVGText, matched: re.Match[str]
    ) -> None:
        """Initialize rectangle geometry."""
        super().__init__(endl, content, matched)
        self._workaround_rich_issue_3576()

    def _workaround_rich_issue_3576(self) -> None:
        # Workaround upstream rich library issue 3576:
        # Underscores on the last captured line are clipped
        # when exporting to SVG.
        #
        # The main <clipPath> height computed
        # by the rich library in Console.export_svg() is a bit too small,
        # and the bottom of the last captured output line may be truncated,
        # depending on the font and the printed characters (e.g. underscores,
        # but also the bottom of the letter 'y').
        #
        # This workaround adds the bottom padding to the height computed
        # by the rich library.
        #
        # Ref:
        # - https://github.com/Textualize/rich/issues/3576
        matched: Optional[re.Match[str]] = None
        i_rect: int = 0
        for i_rect, txt in enumerate(self._content):
            matched = SVGFragmentDefs.RE_RECT_3576.match(txt)
            if matched:
                break
        if not matched:
            raise SVGFormat.Error(SVGFragmentDefs.RE_RECT_3576.pattern)

        height = float(matched.group("h"))
        # Taking account for the missing bottom padding.
        height += SVGFormat.PADDING_BOTTOM
        self._content[i_rect] = re.sub(
            r'height="[\d.]+"', f'height="{height}"', self._content[i_rect]
        )

    def append(self, fragment: "SVGFragmentDefs") -> None:
        """Append SVG clipping paths from another fragment (append nmode).

        Args:
            fragment: The fragment from which we're appending clipping paths.
        """
        offset = len(self._content) - 1
        self._content = (
            self._content[:offset]
            + [""]
            + fragment.content[1:-1]
            + self._content[offset:]
        )


class SVGFragmentChrome(SVGFragment):
    """Fragment for the SVG background rectangle (aka chrome)."""

    RE_BEGIN = SVGFormat.RE_CHROME

    # Rectangle dimensions.
    _width: float
    _height: float

    def __init__(
        self, endl: int, content: SVGText, matched: re.Match[str]
    ) -> None:
        """Initialize rectangle geometry."""
        super().__init__(endl, content, matched)
        raw_width: str = matched.group("w")
        raw_height: str = matched.group("h")

        width = float(raw_width)
        height = float(raw_height)
        self.set_width_height(width, height)

    @property
    def width(self) -> float:
        """SVG rectangle width."""
        return self._width

    @property
    def height(self) -> float:
        """SVG rectangle height."""
        return self._height

    def set_width_height(self, width: float, height: float) -> None:
        """Update rectangle geometry.

        Args:
            width: new width.
            height: new height.
        """
        self._content[0] = re.sub(
            r'width="(?P<w>[\d.]+)" height="(?P<h>[\d.]+)"',
            f'width="{width}" height="{height}"',
            self._content[0],
        )
        self._width = width
        self._height = height


class SVGFragmentGCircles(SVGFragment):
    """Fragment for the SVG colored circles (aka macOS-like buttons)."""

    RE_BEGIN = SVGFormat.RE_GBOX_CIRCLES
    RE_END = SVGFormat.RE_GBOX_END


class SVGFragmentGTerminal(SVGFragment):
    """SVG fragment for the captured output (aka terminal GBox)."""

    RE_BEGIN = SVGFormat.RE_GBOX_TERM

    @classmethod
    def ifind(cls, svg_txt: SVGText, start: int = 0) -> "SVGFragmentGTerminal":
        """Overrides SVGFragment.ifind()."""
        i_begin, matched = SVGFormat.ifind(svg_txt, cls.RE_BEGIN, start)
        i_end, _ = SVGFormat.ifind(svg_txt, SVGFormat.RE_GBOX_END, i_begin + 1)
        # Note: the fragment actually ends by closing two lines of GBox (</g>).
        i_end += 1
        return cls(i_end, svg_txt[i_begin : i_end + 1], matched)

    @classmethod
    def ifind_list(
        cls, svg_txt: SVGText, start: int = 0
    ) -> List["SVGFragmentGTerminal"]:
        """Find successive SVG boxes.

        Args:
            svg_txt: The SVG text to search.
            start: Start the search from here.

        Returns: A tuple containing the index of the line matching
            the end of the last box, and the matched boxes.
        """
        gterminal = cls.ifind(svg_txt, start)
        gterminals: List[SVGFragmentGTerminal] = [gterminal]

        try:
            while True:
                gterminal = cls.ifind(svg_txt, gterminal.i_end + 1)
                gterminals.append(gterminal)
        except SVGFormat.Error:
            # Last GBox.
            pass

        return gterminals

    # GBox coordinates (translation).
    _x: float
    _y: float

    def __init__(
        self, endl: int, content: SVGText, matched: re.Match[str]
    ) -> None:
        """Initialize GBox geometry."""
        super().__init__(endl, content, matched)
        raw_x: str = matched.group("x")
        raw_y: str = matched.group("y")

        x = float(raw_x)
        y = float(raw_y)
        self.set_xy(x, y)

    @property
    def x(self) -> float:
        """Horizontal translation."""
        return self._x

    @property
    def y(self) -> float:
        """Vertical translation."""
        return self._y

    def set_xy(self, x: float, y: float) -> None:
        """Update translation.

        Args:
            x: new horizontal translation.
            y: new vertical translation.
        """
        self._content[0] = re.sub(
            r"translate\((?P<x>[\d.]+), (?P<y>[\d.]+)\)",
            f"translate({x}, {y})",
            self._content[0],
        )
        self._x = x
        self._y = y


class SVGDocument:
    """Formatted SVG document."""

    @classmethod
    def capture(
        cls,
        console: Console,
        /,
        *,
        theme: TerminalTheme,
        font_family: str,
        font_ratio: float,
        title: str,
        show_gcircles: bool,
    ) -> "SVGDocument":
        """Capture SVG document from console.

        Args:
            console: The capture console.
            theme: Terminal theme to apply (see pref.svg.theme).
            font_family: CSS font family (see pref.svg.font_family).
            font_ratio: Font aspect ration (see pref.svg.font_ratio).
            title: Title or empty string.
            show_gcircles: Whether to show the macOS-like buttons.
        """
        svg_fmt = SVGFormat.mk_format(font_family)
        contents: List[str] = console.export_svg(
            theme=theme,
            title=title,
            code_format=svg_fmt,
            font_aspect_ratio=font_ratio,
        ).splitlines()
        return cls(contents, len(title) > 0, show_gcircles)

    # Configuration.
    _has_title: bool
    _show_gcircles: bool

    # This SVG document's fragments.
    _viewbox: SVGFragmentViewBox
    _style: SVGFragmentStyle
    _defs: SVGFragmentDefs
    _rect: SVGFragmentChrome
    _gcircles: Optional[SVGFragmentGCircles] = None
    _gterminals: List[SVGFragmentGTerminal]

    def __init__(
        self, svg_txt: SVGText, has_title: bool, show_gcircles: bool
    ) -> None:
        """
        Args:
            svg_txt: Valid SVG document.
            title: Title or empty string.
            show_gcircles: Whether to show the macOS-like buttons.
        """

        self._has_title = has_title
        self._show_gcircles = show_gcircles

        # Parse content into fragments.
        offset: int

        self._viewbox = SVGFragmentViewBox.ifind(svg_txt)
        offset = self._viewbox.i_end + 1

        self._style = SVGFragmentStyle.ifind(svg_txt, offset)
        offset = self._style.i_end + 1

        self._defs = SVGFragmentDefs.ifind(svg_txt, offset)
        offset = self._defs.i_end + 1

        self._rect = SVGFragmentChrome.ifind(svg_txt, offset)
        offset = self._rect.i_end + 1

        if self._show_gcircles:
            self._gcircles = SVGFragmentGCircles.ifind(svg_txt, offset)
            offset = self._gcircles.i_end + 1

        self._gterminals = SVGFragmentGTerminal.ifind_list(svg_txt, offset)

        if not self.has_titlebar and (self.gterminal.y == SVGFormat.GTERM_Y):
            # No title bar: if we're parsing SVG text generated
            # by the rich library (not one we've already formatted
            # and to which we're appending), update the geometry
            # to compensate for the missing objects (vertical space only).
            self._update_geometry_no_titlebar()

    @property
    def viewbox(self) -> SVGFragmentViewBox:
        """SVG container."""
        return self._viewbox

    @property
    def style(self) -> SVGFragmentStyle:
        """CSS styles."""
        return self._style

    @property
    def defs(self) -> SVGFragmentDefs:
        """SVG clipping paths."""
        return self._defs

    @property
    def rect(self) -> SVGFragmentChrome:
        """Chrome rectangle."""
        return self._rect

    @property
    def gcircles(self) -> Optional[SVGFragmentGCircles]:
        """mocOS-lke buttons."""
        return self._gcircles

    @property
    def gterminal(self) -> SVGFragmentGTerminal:
        """Captured output (last command)."""
        return self._gterminals[-1]

    @property
    def gterminals(self) -> List[SVGFragmentGTerminal]:
        """Captured outputs (append mode)."""
        return self._gterminals

    @property
    def has_titlebar(self) -> bool:
        """True when the SVG should show the macOS-like buttons or a title."""
        return self._show_gcircles or self._has_title

    @property
    def content(self) -> SVGText:
        """Assemble fragments into SVG content."""
        svg_txt: SVGText = []
        svg_txt.extend(self.viewbox.content)
        SVGFragment.newline(svg_txt)

        svg_txt.append(DTSH_SVG_STYLE_FONT_FACES)
        SVGFragment.newline(svg_txt)

        svg_txt.extend(self.style.content)
        SVGFragment.newline(svg_txt)

        svg_txt.extend(self.defs.content)
        SVGFragment.newline(svg_txt)

        svg_txt.extend(self.rect.content)
        if self._gcircles:
            svg_txt.extend(self._gcircles.content)
        SVGFragment.newline(svg_txt)

        for gterm in self.gterminals:
            svg_txt.extend(gterm.content)
        SVGFragment.newline(svg_txt)

        svg_txt.append("</svg>")
        return svg_txt

    def append(self, svg_next: "SVGDocument") -> None:
        """Append another SVG doument to this.

        Aggregate fragments and update geometry.

        Args:
            svg_next: The SVG document to append to this one.
        """
        # Put all base CSS into a single <style>.
        self.style.append(svg_next.style)
        # Put all clipping paths into a single <defs>.
        self.defs.append(svg_next.defs)

        # Recompute dimensions.
        viewbox_width = max(self.viewbox.width, svg_next.viewbox.width)
        viewbox_height = self.viewbox.height + svg_next.viewbox.height
        # Remove top padding added by the next SVG,
        # here rely only on the documents' vertical margins
        # to separate captured commands.
        # This permits to handle that how we want later in client code, e.g.:
        # - separate commands by writing a blank line to the capturing console
        #   before appending
        # - configurable padding
        # - an "echo" built-in command to use in batch mode
        viewbox_height -= SVGFormat.PADDING_TOP
        if self.has_titlebar:
            viewbox_height -= SVGFormat.VSPAN_TITLE_BAR

        rect_width = viewbox_width - SVGFormat.MARGIN_WIDTH
        rect_height = viewbox_height - SVGFormat.MARGIN_HEIGHT

        # Translate the captured output.
        next_term_x = self.gterminal.x
        next_term_y = svg_next.gterminal.y + self.viewbox.height
        next_term_y -= SVGFormat.PADDING_TOP
        if self.has_titlebar:
            next_term_y -= SVGFormat.VSPAN_TITLE_BAR

        # Update dimensions.
        self.viewbox.set_width_height(viewbox_width, viewbox_height)
        self.rect.set_width_height(rect_width, rect_height)
        # Append captured command output.
        svg_next.gterminal.set_xy(next_term_x, next_term_y)
        self._gterminals.append(svg_next.gterminal)

    def _update_geometry_no_titlebar(self) -> None:
        self.viewbox.set_width_height(
            self.viewbox.width,
            self.viewbox.height - SVGFormat.VSPAN_TITLE_BAR,
        )
        self.rect.set_width_height(
            self.rect.width, self.rect.height - SVGFormat.VSPAN_TITLE_BAR
        )
        self.gterminal.set_xy(
            self.gterminal.x, self.gterminal.y - SVGFormat.VSPAN_TITLE_BAR
        )


DTSH_META_SVG_FORMAT = """\
<svg class="rich-terminal" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">

    <style type="">
    .{unique_id}-matrix {{
        font-family: |font_family|;
        font-size: {char_height}px;
        line-height: {line_height}px;
        font-variant-east-asian: full-width;
    }}

    .{unique_id}-title {{
        font-size: 18px;
        font-weight: bold;
        font-family: arial;
    }}

    {styles}
    </style>

    <defs>
    <clipPath id="{unique_id}-clip-terminal">
      <rect x="0" y="0" width="{terminal_width}" height="{terminal_height}" />
    </clipPath>
    {lines}
    </defs>

    {chrome}
    <g transform="translate({terminal_x}, {terminal_y})" clip-path="url(#{unique_id}-clip-terminal)">
    {backgrounds}
    <g class="{unique_id}-matrix">
    {matrix}
    </g>
    </g>

</svg>
"""

DTSH_SVG_STYLE_FONT_FACES = """\
    <style>
    @font-face {
        font-family: "Fira Code";
        src: local("FiraCode-Regular"),
                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
        font-style: normal;
        font-weight: 400;
    }
    @font-face {
        font-family: "Fira Code";
        src: local("FiraCode-Bold"),
                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
        font-style: bold;
        font-weight: 700;
    }
    </style>
"""
