feat(cli): add midas highlighter
This commit is contained in:
@@ -50,32 +50,4 @@ span {
|
||||
--border: 2px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&.base-type {
|
||||
--col: 108, 233, 108;
|
||||
}
|
||||
|
||||
&.param {
|
||||
--col: 103, 192, 224;
|
||||
}
|
||||
|
||||
&.constraint-type {
|
||||
--col: 174, 200, 195;
|
||||
}
|
||||
|
||||
&.frame-column {
|
||||
--col: 216, 231, 81;
|
||||
}
|
||||
|
||||
&.frame-type {
|
||||
--col: 231, 46, 40;
|
||||
}
|
||||
|
||||
&.function {
|
||||
--col: 215, 103, 224;
|
||||
}
|
||||
|
||||
&.argument {
|
||||
--col: 103, 192, 224;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import TextIO
|
||||
from typing import Generic, Optional, Protocol, TextIO, TypeVar
|
||||
|
||||
from midas.ast.python import (
|
||||
BaseType,
|
||||
ConstraintType,
|
||||
Expr,
|
||||
FrameColumn,
|
||||
FrameType,
|
||||
Function,
|
||||
FunctionArgument,
|
||||
)
|
||||
from midas.ast.location import Location
|
||||
import midas.ast.midas as m
|
||||
import midas.ast.python as p
|
||||
|
||||
H = TypeVar("H", bound="Highlighter", contravariant=True)
|
||||
|
||||
|
||||
class PythonHighlighter(Expr.Visitor[None]):
|
||||
CSS_PATH: Path = Path(__file__).parent / "highlight.css"
|
||||
class Highlightable(Protocol, Generic[H]):
|
||||
def accept(self, visitor: H): ...
|
||||
|
||||
|
||||
class Locatable(Protocol):
|
||||
@property
|
||||
@abstractmethod
|
||||
def location(self) -> Optional[Location]: ...
|
||||
|
||||
|
||||
class Highlighter(ABC):
|
||||
BASE_CSS_PATH: Path = Path(__file__).parent / "highlight.css"
|
||||
EXTRA_CSS_PATH: Optional[Path] = None
|
||||
|
||||
def __init__(self, source: str) -> None:
|
||||
self.source: str = source
|
||||
@@ -21,12 +31,22 @@ class PythonHighlighter(Expr.Visitor[None]):
|
||||
self.openings: dict[tuple[int, int], list[str]] = {}
|
||||
self.closings: dict[tuple[int, int], list[str]] = {}
|
||||
|
||||
def highlight(self, node: Expr):
|
||||
node.accept(self)
|
||||
def format_css(self, path: Path) -> list[str]:
|
||||
css: str = path.read_text()
|
||||
css = "\n".join((" " + line).rstrip() for line in css.splitlines())
|
||||
return [
|
||||
" <style>",
|
||||
css,
|
||||
" </style>",
|
||||
]
|
||||
|
||||
def dump(self, buf: TextIO):
|
||||
css: str = self.CSS_PATH.read_text()
|
||||
css = "\n".join((" " + line).rstrip() for line in css.splitlines())
|
||||
base_css: list[str] = self.format_css(self.BASE_CSS_PATH)
|
||||
extra_css: list[str] = (
|
||||
self.format_css(self.EXTRA_CSS_PATH)
|
||||
if self.EXTRA_CSS_PATH is not None
|
||||
else []
|
||||
)
|
||||
lines: list[str] = [
|
||||
"<!DOCTYPE html>",
|
||||
'<html lang="en">',
|
||||
@@ -34,9 +54,8 @@ class PythonHighlighter(Expr.Visitor[None]):
|
||||
' <meta charset="UTF-8">',
|
||||
' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
||||
" <title>Highlighted file</title>",
|
||||
" <style>",
|
||||
css,
|
||||
" </style>",
|
||||
*base_css,
|
||||
*extra_css,
|
||||
"</head>",
|
||||
"<body>",
|
||||
' <div id="code">',
|
||||
@@ -64,7 +83,7 @@ class PythonHighlighter(Expr.Visitor[None]):
|
||||
|
||||
buf.write("\n".join(lines))
|
||||
|
||||
def wrap(self, node: Expr, cls: str):
|
||||
def wrap(self, node: Locatable, cls: str):
|
||||
if node.location is None:
|
||||
return
|
||||
if node.location.end_lineno is None or node.location.end_col_offset is None:
|
||||
@@ -84,32 +103,125 @@ class PythonHighlighter(Expr.Visitor[None]):
|
||||
self.closings.setdefault((l, c), []).insert(0, closing)
|
||||
self.openings.setdefault((l + 1, 0), []).append(opening)
|
||||
|
||||
def visit_base_type(self, node: BaseType) -> None:
|
||||
|
||||
class PythonHighlighter(Highlighter, p.Expr.Visitor[None]):
|
||||
EXTRA_CSS_PATH: Optional[Path] = Path(__file__).parent / "hl_python.css"
|
||||
|
||||
def highlight(self, node: Highlightable[PythonHighlighter]):
|
||||
node.accept(self)
|
||||
|
||||
def visit_base_type(self, node: p.BaseType) -> None:
|
||||
self.wrap(node, "base-type")
|
||||
if node.param is not None:
|
||||
self.wrap(node.param, "param")
|
||||
node.param.accept(self)
|
||||
|
||||
def visit_constraint_type(self, node: ConstraintType) -> None:
|
||||
def visit_constraint_type(self, node: p.ConstraintType) -> None:
|
||||
self.wrap(node, "constraint-type")
|
||||
node.type.accept(self)
|
||||
|
||||
def visit_frame_column(self, node: FrameColumn) -> None:
|
||||
def visit_frame_column(self, node: p.FrameColumn) -> None:
|
||||
self.wrap(node, "frame-column")
|
||||
if node.type is not None:
|
||||
node.type.accept(self)
|
||||
|
||||
def visit_frame_type(self, node: FrameType) -> None:
|
||||
def visit_frame_type(self, node: p.FrameType) -> None:
|
||||
self.wrap(node, "frame-type")
|
||||
for column in node.columns:
|
||||
column.accept(self)
|
||||
|
||||
def visit_function(self, node: Function) -> None:
|
||||
def visit_function(self, node: p.Function) -> None:
|
||||
self.wrap(node, "function")
|
||||
for arg in node.posonlyargs + node.args + node.kwonlyargs:
|
||||
arg.accept(self)
|
||||
|
||||
def visit_function_argument(self, node: FunctionArgument) -> None:
|
||||
def visit_function_argument(self, node: p.FunctionArgument) -> None:
|
||||
self.wrap(node, "argument")
|
||||
if node.type is not None:
|
||||
node.type.accept(self)
|
||||
|
||||
|
||||
class MidasHighlighter(Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None]):
|
||||
EXTRA_CSS_PATH: Optional[Path] = Path(__file__).parent / "hl_midas.css"
|
||||
|
||||
def highlight(self, node: Highlightable[MidasHighlighter]):
|
||||
node.accept(self)
|
||||
|
||||
def visit_simple_type_stmt(self, stmt: m.SimpleTypeStmt) -> None:
|
||||
self.wrap(stmt, "simple-type")
|
||||
if stmt.template is not None:
|
||||
stmt.template.accept(self)
|
||||
stmt.base.accept(self)
|
||||
if stmt.constraint is not None:
|
||||
self.wrap(stmt.constraint, "constraint")
|
||||
stmt.constraint.accept(self)
|
||||
|
||||
def visit_complex_type_stmt(self, stmt: m.ComplexTypeStmt) -> None:
|
||||
self.wrap(stmt, "complex-type")
|
||||
if stmt.template is not None:
|
||||
stmt.template.accept(self)
|
||||
for prop in stmt.properties:
|
||||
prop.accept(self)
|
||||
|
||||
def visit_property_stmt(self, stmt: m.PropertyStmt) -> None:
|
||||
self.wrap(stmt, "property")
|
||||
stmt.type.accept(self)
|
||||
if stmt.constraint is not None:
|
||||
self.wrap(stmt.constraint, "constraint")
|
||||
stmt.constraint.accept(self)
|
||||
|
||||
def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None:
|
||||
self.wrap(stmt, "extend")
|
||||
stmt.type.accept(self)
|
||||
for op in stmt.operations:
|
||||
op.accept(self)
|
||||
|
||||
def visit_op_stmt(self, stmt: m.OpStmt) -> None:
|
||||
self.wrap(stmt, "op")
|
||||
stmt.operand.accept(self)
|
||||
stmt.result.accept(self)
|
||||
|
||||
def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None:
|
||||
self.wrap(stmt, "predicate")
|
||||
stmt.type.accept(self)
|
||||
stmt.condition.accept(self)
|
||||
|
||||
def visit_simple_type_expr(self, expr: m.SimpleTypeExpr) -> None:
|
||||
self.wrap(expr, "simple-type-expr")
|
||||
|
||||
def visit_logical_expr(self, expr: m.LogicalExpr) -> None:
|
||||
self.wrap(expr, "logical-expr")
|
||||
expr.left.accept(self)
|
||||
expr.right.accept(self)
|
||||
|
||||
def visit_binary_expr(self, expr: m.BinaryExpr) -> None:
|
||||
self.wrap(expr, "binary-expr")
|
||||
expr.left.accept(self)
|
||||
expr.right.accept(self)
|
||||
|
||||
def visit_unary_expr(self, expr: m.UnaryExpr) -> None:
|
||||
self.wrap(expr, "unary-expr")
|
||||
expr.right.accept(self)
|
||||
|
||||
def visit_get_expr(self, expr: m.GetExpr) -> None:
|
||||
self.wrap(expr, "get-expr")
|
||||
expr.expr.accept(self)
|
||||
|
||||
def visit_variable_expr(self, expr: m.VariableExpr) -> None:
|
||||
self.wrap(expr, "variable")
|
||||
|
||||
def visit_grouping_expr(self, expr: m.GroupingExpr) -> None:
|
||||
expr.expr.accept(self)
|
||||
|
||||
def visit_literal_expr(self, expr: m.LiteralExpr) -> None: ...
|
||||
|
||||
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> None: ...
|
||||
|
||||
def visit_template_expr(self, expr: m.TemplateExpr) -> None:
|
||||
self.wrap(expr, "template")
|
||||
expr.type.accept(self)
|
||||
|
||||
def visit_type_expr(self, expr: m.TypeExpr) -> None:
|
||||
self.wrap(expr, "type")
|
||||
if expr.template is not None:
|
||||
expr.template.accept(self)
|
||||
|
||||
55
midas/cli/hl_midas.css
Normal file
55
midas/cli/hl_midas.css
Normal file
@@ -0,0 +1,55 @@
|
||||
span {
|
||||
&.comment {
|
||||
--col: 200, 200, 200;
|
||||
color: rgb(110, 110, 110);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.simple-type {
|
||||
--col: 108, 233, 108;
|
||||
}
|
||||
|
||||
&.complex-type {
|
||||
--col: 233, 206, 108;
|
||||
}
|
||||
|
||||
&.constraint {
|
||||
--col: 233, 108, 108;
|
||||
}
|
||||
|
||||
&.property {
|
||||
--col: 233, 108, 176;
|
||||
}
|
||||
|
||||
&.extend {
|
||||
--col: 108, 197, 233;
|
||||
}
|
||||
|
||||
&.op {
|
||||
--col: 108, 148, 233;
|
||||
}
|
||||
|
||||
&.predicate {
|
||||
--col: 193, 108, 233;
|
||||
}
|
||||
|
||||
&.simple-type-expr {
|
||||
--col: 150, 150, 150;
|
||||
}
|
||||
|
||||
&.logical-expr,
|
||||
&.binary-expr,
|
||||
&.unary-expr,
|
||||
&.get-expr {
|
||||
--col: 123, 215, 193;
|
||||
}
|
||||
|
||||
&.template {
|
||||
--col: 163, 117, 71;
|
||||
}
|
||||
|
||||
&.type {
|
||||
--col: 200, 200, 200;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
29
midas/cli/hl_python.css
Normal file
29
midas/cli/hl_python.css
Normal file
@@ -0,0 +1,29 @@
|
||||
span {
|
||||
&.base-type {
|
||||
--col: 108, 233, 108;
|
||||
}
|
||||
|
||||
&.param {
|
||||
--col: 103, 192, 224;
|
||||
}
|
||||
|
||||
&.constraint-type {
|
||||
--col: 174, 200, 195;
|
||||
}
|
||||
|
||||
&.frame-column {
|
||||
--col: 216, 231, 81;
|
||||
}
|
||||
|
||||
&.frame-type {
|
||||
--col: 231, 46, 40;
|
||||
}
|
||||
|
||||
&.function {
|
||||
--col: 215, 103, 224;
|
||||
}
|
||||
|
||||
&.argument {
|
||||
--col: 103, 192, 224;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,13 @@ from typing import Optional, TextIO
|
||||
|
||||
import click
|
||||
|
||||
from midas.ast.location import Location
|
||||
import midas.ast.midas as m
|
||||
from midas.ast.printer import PythonAstPrinter
|
||||
from midas.cli.highlighter import PythonHighlighter
|
||||
from midas.cli.highlighter import Highlighter, MidasHighlighter, PythonHighlighter
|
||||
from midas.lexer.midas import MidasLexer
|
||||
from midas.lexer.token import Token, TokenType
|
||||
from midas.parser.midas import MidasParser
|
||||
from midas.parser.python import PythonParser
|
||||
|
||||
|
||||
@@ -59,18 +64,53 @@ def dump_ast(output: Optional[TextIO], parse: bool, file: TextIO):
|
||||
output.write(dump)
|
||||
|
||||
|
||||
@utils.command()
|
||||
@click.option("-o", "--output", type=click.File("w"), default="-")
|
||||
@click.argument("file", type=click.File("r"))
|
||||
def highlight(output: TextIO, file: TextIO):
|
||||
source: str = file.read()
|
||||
tree: ast.Module = ast.parse(source, filename=file.name)
|
||||
def highlight_python(source: str, path: str) -> Highlighter:
|
||||
tree: ast.Module = ast.parse(source, filename=path)
|
||||
parser = PythonParser()
|
||||
parser.visit(tree)
|
||||
highlighter: PythonHighlighter = PythonHighlighter(source)
|
||||
highlighter = PythonHighlighter(source)
|
||||
for _, annotation in parser.annotations:
|
||||
if annotation is not None:
|
||||
highlighter.highlight(annotation)
|
||||
for func in parser.functions:
|
||||
highlighter.highlight(func)
|
||||
return highlighter
|
||||
|
||||
|
||||
def highlight_midas(source: str, path: str) -> Highlighter:
|
||||
lexer = MidasLexer(source, file=path)
|
||||
tokens: list[Token] = lexer.process()
|
||||
parser = MidasParser(tokens)
|
||||
stmts: list[m.Stmt] = parser.parse()
|
||||
highlighter = MidasHighlighter(source)
|
||||
|
||||
class LocatableToken:
|
||||
def __init__(self, token: Token):
|
||||
self.token: Token = token
|
||||
|
||||
@property
|
||||
def location(self) -> Location:
|
||||
return self.token.get_location()
|
||||
|
||||
for token in tokens:
|
||||
if token.type == TokenType.COMMENT:
|
||||
highlighter.wrap(LocatableToken(token), "comment")
|
||||
for stmt in stmts:
|
||||
highlighter.highlight(stmt)
|
||||
return highlighter
|
||||
|
||||
|
||||
@utils.command()
|
||||
@click.option("-o", "--output", type=click.File("w"), default="-")
|
||||
@click.argument("file", type=click.File("r"))
|
||||
def highlight(output: TextIO, file: TextIO):
|
||||
source: str = file.read()
|
||||
highlighter: Highlighter
|
||||
|
||||
if file.name.endswith(".py"):
|
||||
highlighter = highlight_python(source, file.name)
|
||||
elif file.name.endswith(".midas"):
|
||||
highlighter = highlight_midas(source, file.name)
|
||||
else:
|
||||
raise ValueError("Unsupported file type")
|
||||
highlighter.dump(output)
|
||||
|
||||
Reference in New Issue
Block a user