# Source code for fontTools.misc.xmlWriter

    """xmlWriter.py -- Simple XML authoring class"""

    from __future__ import annotations

    from typing import BinaryIO, Callable, TextIO
    from fontTools.misc.textTools import byteord, strjoin, tobytes, tostr
    import sys
    import os
    import string
    import logging
    import itertools

    INDENT = "  "
    TTX_LOG = logging.getLogger("fontTools.ttx")
    REPLACEMENT = "?"
    ILLEGAL_XML_CHARS = dict.fromkeys(
        itertools.chain(
            range(0x00, 0x09),
            (0x0B, 0x0C),
            range(0x0E, 0x20),
            range(0xD800, 0xE000),
            (0xFFFE, 0xFFFF),
        ),
        REPLACEMENT,
    )

    class XMLWriter(object):
        def __init__(
            self,
            fileOrPath: str | os.PathLike[str] | BinaryIO | TextIO,
            indentwhite: str = INDENT,
            idlefunc: Callable[[], None] | None = None,
            encoding: str = "utf_8",
            newlinestr: str | bytes = "\n",
        ) -> None:
            if encoding.lower().replace("-", "").replace("_", "") != "utf8":
                raise Exception("Only UTF-8 encoding is supported.")
            if fileOrPath == "-":
                fileOrPath = sys.stdout
            self.filename: str | os.PathLike[str] | None
            if not hasattr(fileOrPath, "write"):
                if not isinstance(fileOrPath, (str, os.PathLike)):
                    raise TypeError(
                        "fileOrPath must be a file path (str or PathLike) if it isn't an object with a `write` method."
                    )
                self.filename = fileOrPath
                self.file = open(fileOrPath, "wb")
                self._closeStream = True
            else:
                self.filename = None
                # assume writable file object
                self.file = fileOrPath
                self._closeStream = False

            # Figure out if writer expects bytes or unicodes
            try:
                # The bytes check should be first.  See:
                # https://github.com/fonttools/fonttools/pull/233
                self.file.write(b"")
                self.totype = tobytes
            except TypeError:
                # This better not fail.
                self.file.write("")
                self.totype = tostr
            self.indentwhite = self.totype(indentwhite)
            if newlinestr is None:
                self.newlinestr = self.totype(os.linesep)
            else:
                self.newlinestr = self.totype(newlinestr)
            self.indentlevel = 0
            self.stack = []
            self.needindent = 1
            self.idlefunc = idlefunc
            self.idlecounter = 0
            self._writeraw('<?xml version="1.0" encoding="UTF-8"?>')
            self.newline()

        def __enter__(self):
            return self

        def __exit__(self, exception_type, exception_value, traceback):
            self.close()

        def close(self) -> None:
            if self._closeStream:
                assert not isinstance(self.file, (str, os.PathLike))
                self.file.close()

        def write(self, string, indent=True):
            """Writes text."""
            self._writeraw(escape(string), indent=indent)

        def writecdata(self, string):
            """Writes text in a CDATA section."""
            self._writeraw("<![CDATA[" + string + "]]>")

        def write8bit(self, data, strip=False):
            """Writes a bytes() sequence into the XML, escaping
            non-ASCII bytes.  When this is read in xmlReader,
            the original bytes can be recovered by encoding to
            'latin-1'."""
            self._writeraw(escape8bit(data), strip=strip)

        def write_noindent(self, string):
            """Writes text without indentation."""
            self._writeraw(escape(string), indent=False)

        def _writeraw(self, data, indent=True, strip=False):
            """Writes bytes, possibly indented."""
            if indent and self.needindent:
                self.file.write(self.indentlevel * self.indentwhite)
                self.needindent = 0
            s = self.totype(data, encoding="utf_8")
            if strip:
                s = s.strip()
            self.file.write(s)

        def newline(self):
            self.file.write(self.newlinestr)
            self.needindent = 1
            idlecounter = self.idlecounter
            if not idlecounter % 100 and self.idlefunc is not None:
                self.idlefunc()
            self.idlecounter = idlecounter + 1

        def comment(self, data):
            data = escape(data)
            lines = data.split("\n")
            self._writeraw("<!-- " + lines[0])
            for line in lines[1:]:
                self.newline()
                self._writeraw("     " + line)
            self._writeraw(" -->")

        def simpletag(self, _TAG_, *args, **kwargs):
            attrdata = self.stringifyattrs(*args, **kwargs)
            data = "<%s%s/>" % (_TAG_, attrdata)
            self._writeraw(data)

        def begintag(self, _TAG_, *args, **kwargs):
            attrdata = self.stringifyattrs(*args, **kwargs)
            data = "<%s%s>" % (_TAG_, attrdata)
            self._writeraw(data)
            self.stack.append(_TAG_)
            self.indent()

        def endtag(self, _TAG_):
            assert self.stack and self.stack[-1] == _TAG_, "nonmatching endtag"
            del self.stack[-1]
            self.dedent()
            data = "</%s>" % _TAG_
            self._writeraw(data)

        def dumphex(self, data):
            linelength = 16
            hexlinelength = linelength * 2
            chunksize = 8
            for i in range(0, len(data), linelength):
                hexline = hexStr(data[i : i + linelength])
                line = ""
                white = ""
                for j in range(0, hexlinelength, chunksize):
                    line = line + white + hexline[j : j + chunksize]
                    white = " "
                self._writeraw(line)
                self.newline()

        def indent(self):
            self.indentlevel = self.indentlevel + 1

        def dedent(self):
            assert self.indentlevel > 0
            self.indentlevel = self.indentlevel - 1

        def stringifyattrs(self, *args, **kwargs):
            if kwargs:
                assert not args
                attributes = sorted(kwargs.items())
            elif args:
                assert len(args) == 1
                attributes = args[0]
            else:
                return ""
            data = ""
            for attr, value in attributes:
                if not isinstance(value, (bytes, str)):
                    value = str(value)
                data = data + ' %s="%s"' % (attr, escapeattr(value))
            return data

    def escape(data):
        """Escape characters not allowed in `XML 1.0 <https://www.w3.org/TR/xml/#NT-Char>`_."""
        data = tostr(data, "utf_8")
        data = data.replace("&", "&amp;")
        data = data.replace("<", "&lt;")
        data = data.replace(">", "&gt;")
        data = data.replace("\r", "&#13;")

        newData = data.translate(ILLEGAL_XML_CHARS)
        if newData != data:
            maxLen = 10
            preview = repr(data)
            if len(data) > maxLen:
                preview = repr(data[:maxLen])[1:-1] + "..."
            TTX_LOG.warning(
                "Illegal XML character(s) found; replacing offending string %r with %r",
                preview,
                REPLACEMENT,
            )
        return newData

    def escapeattr(data):
        data = escape(data)
        data = data.replace('"', "&quot;")
        return data

    def escape8bit(data):
        """Input is Unicode string."""

        def escapechar(c):
            n = ord(c)
            if 32 <= n <= 127 and c not in "<&>":
                return c
            else:
                return "&#" + repr(n) + ";"

        return strjoin(map(escapechar, data.decode("latin-1")))

    def hexStr(s):
        h = string.hexdigits
        r = ""
        for c in s:
            i = byteord(c)
            r = r + h[(i >> 4) & 0xF] + h[i & 0xF]
        return r