Module refinery.units.sinks.hl

Expand source code Browse git
from __future__ import annotations

import pathlib
import typing

import colorama

if True:
    colorama.init()

from refinery.lib.id import get_text_format
from refinery.lib.meta import metavars
from refinery.lib.types import Param
from refinery.units import Arg, Unit

if typing.TYPE_CHECKING:
    from io import TextIOBase
    from pygments.token import _TokenType as TokenType


class hl(Unit):
    """
    This unit uses the Pygments library for syntax highlighting.

    It expects plain text code as input and outputs ANSI-colored text.
    """
    _pygments_name_completion = {
        'js'  : 'JavaScript',
        'py'  : 'Python',
        'vb'  : 'VisualBasic',
        'vba' : 'VisualBasic',
        'vbs' : 'VisualBasic',
    }

    def __init__(
        self,
        language: Param[str | None, Arg.String(
            help='Optionally specify the input language to be highlighted.')] = None,
        style: Param[str | None, Arg.String('-s', group='STYLE',
            help='Optionally specify a color style supported by Pygments.')] = None,
        github: Param[bool, Arg.Switch('-G', group='STYLE',
            help='Use github-flavored styling.')] = False,
        solarized: Param[bool, Arg.Switch('-S', group='STYLE',
            help='Use solarized-flavored styling.')] = False,
        gruvbox: Param[bool, Arg.Switch('-B', group='STYLE',
            help='Use gruvbox-flavored styling.')] = False,
        dark: Param[bool, Arg.Switch('-d',
            help='Assume a dark brackground.')] = False,
        light: Param[bool, Arg.Switch('-l',
            help='Assume a light background.')] = False,
    ):
        if dark and light:
            raise ValueError('The "dark" and "light" options cannot simultaneously be set.')
        if sum(1 for opt in (github, solarized, gruvbox, style) if opt) > 1:
            raise ValueError('More than one styling option was set.')
        return super().__init__(
            language=language,
            style=style,
            dark=dark,
            light=light,
            github=github,
            solarized=solarized,
            gruvbox=gruvbox,
        )

    @Unit.Requires('Pygments', 3)
    def _pygments():
        import pygments
        import pygments.formatter
        import pygments.formatters
        import pygments.lexers
        import pygments.token
        return pygments

    def _style_variant(self):
        return 'light' if self.args.light else 'dark'

    def process(self, data):
        lib = self._pygments
        language: str = self.args.language

        if language:
            lexer = lib.lexers.get_lexer_by_name(
                self._pygments_name_completion.get(language, language))
        else:
            guesses = []
            meta = metavars(data)
            if format := get_text_format(data):
                guesses.append(format.extension)
            guesses.append(str(meta['ext']))
            if path := meta.get('path'):
                guesses.append(pathlib.Path(str(path)).suffix.lstrip('.'))
            for guess in guesses:
                try:
                    lexer = lib.lexers.get_lexer_by_name(guess)
                except Exception:
                    pass
                else:
                    break
            else:
                lexer = lib.lexers.guess_lexer(data)

        self.log_info(F'using lexer for {lexer.name.lower()}')
        t256 = lib.formatters.Terminal256Formatter

        if self.args.github:
            tf = t256(style=F'github-{self._style_variant()}')
        elif self.args.solarized:
            tf = t256(style=F'solarized-{self._style_variant()}')
        elif self.args.gruvbox:
            tf = t256(style=F'gruvbox-{self._style_variant()}')
        elif self.args.light:
            tf = lib.formatters.TerminalFormatter(bg='light')
        elif self.args.dark:
            tf = lib.formatters.TerminalFormatter(bg='dark')
        elif (style := self.args.style):
            tf = t256(colorscheme=style)
        else:
            FG = colorama.Fore
            ST = colorama.Style
            t = lib.token

            R = FG.LIGHTRED_EX
            C = FG.CYAN
            W = FG.WHITE
            B = FG.LIGHTBLACK_EX

            ANSI_MAP = {
                # Base layer
                t.Text             : W,
                # Comments (background noise)
                t.Comment          : B,
                t.Comment.Preproc  : B,
                # Structure (dominant visual signal)
                t.Keyword          : R + ST.BRIGHT,
                t.Keyword.Constant : R,
                t.Keyword.Type     : R,
                t.Operator         : R,
                t.Operator.Word    : R,
                # Names (keep calm and readable)
                t.Name             : W,
                t.Name.Variable    : W,
                t.Name.Function    : W,
                # Rare highlight (just enough contrast)
                t.Name.Class       : W + ST.BRIGHT,
                # Keep palette tight (no extra hues)
                t.Name.Namespace   : B,
                t.Name.Attribute   : B,
                t.Name.Property    : B,
                # Structural accents
                t.Name.Builtin     : R,
                t.Name.Decorator   : R,
                t.Name.Tag         : R,
                # Data (controlled green usage)
                t.String           : C,
                t.String.Doc       : C,
                t.String.Escape    : C,
                t.String.Interpol  : C,
                t.Number           : C,
                t.Literal          : C,
                t.Name.Constant    : C,
                # Background structure
                t.Punctuation      : B,
                # Errors (high visibility)
                t.Error            : R + ST.BRIGHT,
            }

            class ColoramaFormatter(lib.formatter.Formatter):
                def format(self, tokensource: list[tuple[TokenType, str]], outfile: TextIOBase):
                    for tt, value in tokensource:
                        cc = None
                        while tt and cc is None:
                            cc = ANSI_MAP.get(tt)
                            tt = tt.parent
                        outfile.write(cc or FG.WHITE)
                        outfile.write(value)
                        outfile.write(ST.RESET_ALL)

            tf = ColoramaFormatter()

        out = lib.highlight(data, lexer, tf)
        out = F'{out}{colorama.Style.RESET_ALL}'
        return out.encode(self.codec)


class hlg(hl, docs='{0}{s}This variant uses GitHub styling.'):
    def __init__(self, lexer: str | None = None, dark: bool = False, light: bool = False):
        super().__init__(lexer, dark=dark, light=light, github=True)


class hls(hl, docs='{0}{s}This variant uses solarized styling.'):
    def __init__(self, lexer: str | None = None, dark: bool = False, light: bool = False):
        super().__init__(lexer, dark=dark, light=light, solarized=True)


class hlb(hl, docs='{0}{s}This variant uses gruvbox styling.'):
    def __init__(self, lexer: str | None = None, dark: bool = False, light: bool = False):
        super().__init__(lexer, dark=dark, light=light, gruvbox=True)

Classes

class hl (language=None, style=None, github=False, solarized=False, gruvbox=False, dark=False, light=False)

This unit uses the Pygments library for syntax highlighting.

It expects plain text code as input and outputs ANSI-colored text.

Expand source code Browse git
class hl(Unit):
    """
    This unit uses the Pygments library for syntax highlighting.

    It expects plain text code as input and outputs ANSI-colored text.
    """
    _pygments_name_completion = {
        'js'  : 'JavaScript',
        'py'  : 'Python',
        'vb'  : 'VisualBasic',
        'vba' : 'VisualBasic',
        'vbs' : 'VisualBasic',
    }

    def __init__(
        self,
        language: Param[str | None, Arg.String(
            help='Optionally specify the input language to be highlighted.')] = None,
        style: Param[str | None, Arg.String('-s', group='STYLE',
            help='Optionally specify a color style supported by Pygments.')] = None,
        github: Param[bool, Arg.Switch('-G', group='STYLE',
            help='Use github-flavored styling.')] = False,
        solarized: Param[bool, Arg.Switch('-S', group='STYLE',
            help='Use solarized-flavored styling.')] = False,
        gruvbox: Param[bool, Arg.Switch('-B', group='STYLE',
            help='Use gruvbox-flavored styling.')] = False,
        dark: Param[bool, Arg.Switch('-d',
            help='Assume a dark brackground.')] = False,
        light: Param[bool, Arg.Switch('-l',
            help='Assume a light background.')] = False,
    ):
        if dark and light:
            raise ValueError('The "dark" and "light" options cannot simultaneously be set.')
        if sum(1 for opt in (github, solarized, gruvbox, style) if opt) > 1:
            raise ValueError('More than one styling option was set.')
        return super().__init__(
            language=language,
            style=style,
            dark=dark,
            light=light,
            github=github,
            solarized=solarized,
            gruvbox=gruvbox,
        )

    @Unit.Requires('Pygments', 3)
    def _pygments():
        import pygments
        import pygments.formatter
        import pygments.formatters
        import pygments.lexers
        import pygments.token
        return pygments

    def _style_variant(self):
        return 'light' if self.args.light else 'dark'

    def process(self, data):
        lib = self._pygments
        language: str = self.args.language

        if language:
            lexer = lib.lexers.get_lexer_by_name(
                self._pygments_name_completion.get(language, language))
        else:
            guesses = []
            meta = metavars(data)
            if format := get_text_format(data):
                guesses.append(format.extension)
            guesses.append(str(meta['ext']))
            if path := meta.get('path'):
                guesses.append(pathlib.Path(str(path)).suffix.lstrip('.'))
            for guess in guesses:
                try:
                    lexer = lib.lexers.get_lexer_by_name(guess)
                except Exception:
                    pass
                else:
                    break
            else:
                lexer = lib.lexers.guess_lexer(data)

        self.log_info(F'using lexer for {lexer.name.lower()}')
        t256 = lib.formatters.Terminal256Formatter

        if self.args.github:
            tf = t256(style=F'github-{self._style_variant()}')
        elif self.args.solarized:
            tf = t256(style=F'solarized-{self._style_variant()}')
        elif self.args.gruvbox:
            tf = t256(style=F'gruvbox-{self._style_variant()}')
        elif self.args.light:
            tf = lib.formatters.TerminalFormatter(bg='light')
        elif self.args.dark:
            tf = lib.formatters.TerminalFormatter(bg='dark')
        elif (style := self.args.style):
            tf = t256(colorscheme=style)
        else:
            FG = colorama.Fore
            ST = colorama.Style
            t = lib.token

            R = FG.LIGHTRED_EX
            C = FG.CYAN
            W = FG.WHITE
            B = FG.LIGHTBLACK_EX

            ANSI_MAP = {
                # Base layer
                t.Text             : W,
                # Comments (background noise)
                t.Comment          : B,
                t.Comment.Preproc  : B,
                # Structure (dominant visual signal)
                t.Keyword          : R + ST.BRIGHT,
                t.Keyword.Constant : R,
                t.Keyword.Type     : R,
                t.Operator         : R,
                t.Operator.Word    : R,
                # Names (keep calm and readable)
                t.Name             : W,
                t.Name.Variable    : W,
                t.Name.Function    : W,
                # Rare highlight (just enough contrast)
                t.Name.Class       : W + ST.BRIGHT,
                # Keep palette tight (no extra hues)
                t.Name.Namespace   : B,
                t.Name.Attribute   : B,
                t.Name.Property    : B,
                # Structural accents
                t.Name.Builtin     : R,
                t.Name.Decorator   : R,
                t.Name.Tag         : R,
                # Data (controlled green usage)
                t.String           : C,
                t.String.Doc       : C,
                t.String.Escape    : C,
                t.String.Interpol  : C,
                t.Number           : C,
                t.Literal          : C,
                t.Name.Constant    : C,
                # Background structure
                t.Punctuation      : B,
                # Errors (high visibility)
                t.Error            : R + ST.BRIGHT,
            }

            class ColoramaFormatter(lib.formatter.Formatter):
                def format(self, tokensource: list[tuple[TokenType, str]], outfile: TextIOBase):
                    for tt, value in tokensource:
                        cc = None
                        while tt and cc is None:
                            cc = ANSI_MAP.get(tt)
                            tt = tt.parent
                        outfile.write(cc or FG.WHITE)
                        outfile.write(value)
                        outfile.write(ST.RESET_ALL)

            tf = ColoramaFormatter()

        out = lib.highlight(data, lexer, tf)
        out = F'{out}{colorama.Style.RESET_ALL}'
        return out.encode(self.codec)

Ancestors

Subclasses

Class variables

var reverse

The type of the None singleton.

Inherited members

class hlg (lexer=None, dark=False, light=False)

This unit uses the Pygments library for syntax highlighting.

It expects plain text code as input and outputs ANSI-colored text. This variant uses GitHub styling.

Expand source code Browse git
class hlg(hl, docs='{0}{s}This variant uses GitHub styling.'):
    def __init__(self, lexer: str | None = None, dark: bool = False, light: bool = False):
        super().__init__(lexer, dark=dark, light=light, github=True)

Ancestors

Subclasses

Inherited members

class hls (lexer=None, dark=False, light=False)

This unit uses the Pygments library for syntax highlighting.

It expects plain text code as input and outputs ANSI-colored text. This variant uses solarized styling.

Expand source code Browse git
class hls(hl, docs='{0}{s}This variant uses solarized styling.'):
    def __init__(self, lexer: str | None = None, dark: bool = False, light: bool = False):
        super().__init__(lexer, dark=dark, light=light, solarized=True)

Ancestors

Subclasses

Inherited members

class hlb (lexer=None, dark=False, light=False)

This unit uses the Pygments library for syntax highlighting.

It expects plain text code as input and outputs ANSI-colored text. This variant uses gruvbox styling.

Expand source code Browse git
class hlb(hl, docs='{0}{s}This variant uses gruvbox styling.'):
    def __init__(self, lexer: str | None = None, dark: bool = False, light: bool = False):
        super().__init__(lexer, dark=dark, light=light, gruvbox=True)

Ancestors

Subclasses

Inherited members