#!/usr/bin/env python3
"""
Realce de sintaxe para HTML, CSS e JS, feito NA GERACAO.

Chamado por bin/fichas.py -- nao rode direto.

Por que aqui e nao no navegador: a pagina servida continua sem uma linha de
JavaScript de terceiro e sem custo de pintura. O codigo chega a ficha ja em
<span>, e a copia continua exata porque os spans nao acrescentam texto -- o
textContent do <code> e o arquivo, caractere a caractere.

Nao e um parser. E um tokenizador de uma passada, que acerta o que aparece
na biblioteca e erra com dignidade no resto: no pior caso um trecho sai sem
cor, nunca sai texto a mais ou a menos. A garantia que importa e essa, e o
proprio gerador a confere (ver `realcar`).

As classes sao da ficha (prefixo fi-): comentario, string, palavra-chave,
tag, atributo, propriedade, numero, custom property.
"""
import html, re

def _e(t):
    return html.escape(t, quote=True)

def _span(cls, t):
    return '<span class="fi-' + cls + '">' + _e(t) + "</span>" if t else ""

# ------------------------------------------------------------------ HTML
_HTML_TOK = re.compile(r"<!--.*?-->|<[^>]+>", re.S)
# O "=valor" é opcional: atributo booleano (data-rv, hidden, required) é nome
# sozinho, e sem isto ele caía na última alternativa e saía colorido LETRA A
# LETRA — um <span> por caractere de "data-rv".
_HTML_ATTR = re.compile(r'([^\s=/>]+)(?:(\s*=\s*)("[^"]*"|\'[^\']*\'|[^\s>]+))?|(\s+)|([^\s])')

def _tag(t):
    m = re.match(r"(</?)([\w:-]+)(.*?)(/?>)$", t, re.S)
    if not m:
        return _e(t)
    out = [_e(m.group(1)), _span("t", m.group(2))]
    resto = m.group(3)
    for a in _HTML_ATTR.finditer(resto):
        if a.group(1):
            out.append(_span("a", a.group(1)) + _e(a.group(2) or ""))
            if a.group(3):
                out.append(_span("s", a.group(3)))
        elif a.group(4):
            out.append(_e(a.group(4)))
        else:
            out.append(_span("a", a.group(5)))
    out.append(_e(m.group(4)))
    return "".join(out)

def html_(src):
    out, i = [], 0
    for m in _HTML_TOK.finditer(src):
        out.append(_e(src[i:m.start()]))
        t = m.group(0)
        out.append(_span("c", t) if t.startswith("<!--") else _tag(t))
        i = m.end()
    out.append(_e(src[i:]))
    return "".join(out)

# ------------------------------------------------------------------ CSS
_CSS_TOK = re.compile(r"/\*.*?\*/|\"(?:\\.|[^\"\\])*\"|'(?:\\.|[^'\\])*'|[{};]", re.S)
_CSS_VAL = re.compile(r"(--[\w-]+)|(#[0-9a-fA-F]{3,8}\b)|(-?\d*\.?\d+(?:[a-z%]+)?)|([\w-]+)(?=\()|(!important)")

def _valor(t):
    out, i = [], 0
    for m in _CSS_VAL.finditer(t):
        out.append(_e(t[i:m.start()]))
        if m.group(1):
            out.append(_span("v", m.group(1)))
        elif m.group(2) or m.group(3):
            out.append(_span("n", m.group(0)))
        elif m.group(4):
            out.append(_span("k", m.group(4)))
        else:
            out.append(_span("k", m.group(5)))
        i = m.end()
    out.append(_e(t[i:]))
    return "".join(out)

def _segmento(pedacos, fim):
    """Um trecho entre delimitadores. Quem decide o que ele e' e o delimitador
    que o fecha: `{` -> seletor ou @regra; `;`/`}` -> declaracao."""
    texto = "".join(t for k, t in pedacos if k == "x")
    out = []
    if fim == "{":
        for k, t in pedacos:
            if k == "c":
                out.append(_span("c", t))
            elif k == "s":
                out.append(_span("s", t))
            else:
                regra = texto.lstrip().startswith("@")
                for parte in re.split(r"(@[\w-]+)", t):
                    if parte.startswith("@"):
                        out.append(_span("k", parte))
                    elif regra:
                        out.append(_valor(parte))
                    else:
                        # espaco e quebra de linha fora do span: o seletor
                        # colorido nao arrasta a indentacao junto
                        m = re.match(r"(\s*)(.*?)(\s*)$", parte, re.S)
                        out.append(_e(m.group(1)) + _span("t", m.group(2)) + _e(m.group(3)))
        return "".join(out)
    feito = False
    for k, t in pedacos:
        if k == "c":
            out.append(_span("c", t)); continue
        if k == "s":
            out.append(_span("s", t)); continue
        if not feito and ":" in t:
            m = re.match(r"(\s*)([\w-]+)(\s*:)(.*)$", t, re.S)
            if m:
                cls = "v" if m.group(2).startswith("--") else "p"
                out.append(_e(m.group(1)) + _span(cls, m.group(2)) + _e(m.group(3)) + _valor(m.group(4)))
                feito = True
                continue
        out.append(_valor(t) if feito else _e(t))
    return "".join(out)

def css(src):
    out, buf, i = [], [], 0
    for m in _CSS_TOK.finditer(src):
        if m.start() > i:
            buf.append(("x", src[i:m.start()]))
        t = m.group(0)
        if t.startswith("/*"):
            buf.append(("c", t))
        elif t[0] in "\"'":
            buf.append(("s", t))
        else:
            out.append(_segmento(buf, t)); buf = []
            out.append(_e(t))
        i = m.end()
    if i < len(src):
        buf.append(("x", src[i:]))
    out.append(_segmento(buf, ""))
    return "".join(out)

# ------------------------------------------------------------------ JS
_JS_KW = ("var|let|const|function|return|if|else|for|while|do|break|continue|new|"
          "typeof|instanceof|in|of|this|null|undefined|true|false|try|catch|finally|"
          "throw|switch|case|default|delete|void")
_JS_TOK = re.compile(
    r"(/\*.*?\*/|//[^\n]*)"                                  # comentario
    r"|(\"(?:\\.|[^\"\\\n])*\"|'(?:\\.|[^'\\\n])*'|`(?:\\.|[^`\\])*`)"  # string
    r"|\b(" + _JS_KW + r")\b"                                # palavra-chave
    r"|\b(\d+\.?\d*|\.\d+)\b",                               # numero
    re.S)

def js(src):
    out, i = [], 0
    for m in _JS_TOK.finditer(src):
        out.append(_e(src[i:m.start()]))
        if m.group(1):
            out.append(_span("c", m.group(1)))
        elif m.group(2):
            out.append(_span("s", m.group(2)))
        elif m.group(3):
            out.append(_span("k", m.group(3)))
        else:
            out.append(_span("n", m.group(4)))
        i = m.end()
    out.append(_e(src[i:]))
    return "".join(out)

# ------------------------------------------------------------------ entrada
def realcar(src, lingua):
    """Devolve o HTML realcado. Confere que tirar as tags devolve o fonte
    exato -- se nao devolver, cai no texto sem cor em vez de publicar codigo
    alterado. Copiar da ficha tem de dar o arquivo, nao algo parecido."""
    f = {"html": html_, "css": css, "js": js}[lingua]
    feito = f(src)
    volta = html.unescape(re.sub(r"<[^>]+>", "", feito))
    return feito if volta == src else _e(src)
