git » git-arr » next » tree

[next] / utils.py

"""
Miscellaneous utilities.

These are mostly used in templates, for presentation purposes.
"""

try:
    import pygments  # type: ignore
    from pygments import highlight  # type: ignore
    from pygments import lexers  # type: ignore
    from pygments.formatters import HtmlFormatter  # type: ignore

    _html_formatter = HtmlFormatter(
        encoding="utf-8",
        cssclass="source_code",
        linenos="table",
        anchorlinenos=True,
        lineanchors="line",
    )
except ImportError:
    pygments = None

try:
    import markdown  # type: ignore
    import markdown.treeprocessors  # type: ignore
except ImportError:
    markdown = None

import base64
import functools
import mimetypes
import string
import os.path

import git


def shorten(s: str, width=60):
    if len(s) < 60:
        return s
    return s[:57] + "..."


@functools.lru_cache
def can_colorize(s: str):
    """True if we can colorize the string, False otherwise."""
    if pygments is None:
        return False

    # Pygments can take a huge amount of time with long files, or with very
    # long lines; these are heuristics to try to avoid those situations.
    if len(s) > (512 * 1024):
        return False

    # If any of the first 5 lines is over 300 characters long, don't colorize.
    start = 0
    for i in range(5):
        pos = s.find("\n", start)
        if pos == -1:
            break

        if pos - start > 300:
            return False
        start = pos + 1

    return True


def can_markdown(repo: git.Repo, fname: str):
    """True if we can process file through markdown, False otherwise."""
    if markdown is None:
        return False

    if not repo.info.embed_markdown:
        return False

    return fname.endswith(".md")


def can_embed_image(repo, fname):
    """True if we can embed image file in HTML, False otherwise."""
    if not repo.info.embed_images:
        return False

    return ("." in fname) and (
        fname.split(".")[-1].lower() in ["jpg", "jpeg", "png", "gif"]
    )


@functools.lru_cache
def colorize_diff(s: str) -> str:
    lexer = lexers.DiffLexer(encoding="utf-8")
    formatter = HtmlFormatter(encoding="utf-8", cssclass="source_code")

    return highlight(s, lexer, formatter)


@functools.lru_cache
def colorize_blob(fname, s: str) -> str:
    try:
        lexer = lexers.guess_lexer_for_filename(fname, s, encoding="utf-8")
    except lexers.ClassNotFound:
        # Only try to guess lexers if the file starts with a shebang,
        # otherwise it's likely a text file and guess_lexer() is prone to
        # make mistakes with those.
        lexer = lexers.TextLexer(encoding="utf-8")
        if s.startswith("#!"):
            try:
                lexer = lexers.guess_lexer(s[:80], encoding="utf-8")
            except lexers.ClassNotFound:
                pass

    return highlight(s, lexer, _html_formatter)


def embed_image_blob(fname: str, image_data: bytes) -> str:
    mimetype = mimetypes.guess_type(fname)[0]
    b64img = base64.b64encode(image_data).decode("ascii")
    return '<img style="max-width:100%;" src="data:{0};base64,{1}" />'.format(
        mimetype, b64img
    )


@functools.lru_cache
def is_binary(b: bytes):
    # Git considers a blob binary if NUL in first ~8KB, so do the same.
    return b"\0" in b[:8192]


@functools.lru_cache
def hexdump(s: bytes):
    graph = string.ascii_letters + string.digits + string.punctuation + " "
    b = s.decode("latin1")
    offset = 0
    while b:
        t = b[:16]
        hexvals = ["%.2x" % ord(c) for c in t]
        text = "".join(c if c in graph else "." for c in t)
        yield offset, " ".join(hexvals[:8]), " ".join(hexvals[8:]), text
        offset += 16
        b = b[16:]


if markdown:

    class RewriteLocalLinks(markdown.treeprocessors.Treeprocessor):
        """Rewrites relative links to files, to match git-arr's links.

        A link of "[example](a/file.md)" will be rewritten such that it links to
        "a/f=file.md.html".

        Note that we're already assuming a degree of sanity in the HTML, so we
        don't re-check that the path is reasonable.
        """

        def run(self, root):
            for child in root:
                if child.tag == "a":
                    self.rewrite_href(child)

                # Continue recursively.
                self.run(child)

        def rewrite_href(self, tag):
            """Rewrite an <a>'s href."""
            target = tag.get("href")
            if not target:
                return
            if "://" in target or target.startswith("/"):
                return

            head, tail = os.path.split(target)
            new_target = os.path.join(head, "f=" + tail + ".html")
            tag.set("href", new_target)

    class RewriteLocalLinksExtension(markdown.Extension):
        def extendMarkdown(self, md):
            md.treeprocessors.register(
                RewriteLocalLinks(), "RewriteLocalLinks", 1000
            )

    _md_extensions = [
        "markdown.extensions.fenced_code",
        "markdown.extensions.tables",
        RewriteLocalLinksExtension(),
    ]

    @functools.lru_cache
    def markdown_blob(s: str) -> str:
        return markdown.markdown(s, extensions=_md_extensions)

else:

    @functools.lru_cache
    def markdown_blob(s: str) -> str:
        raise RuntimeError("markdown_blob() called without markdown support")