diff --git a/midas/cli/highlight.css b/midas/cli/highlight.css index a2f378c..1abed08 100644 --- a/midas/cli/highlight.css +++ b/midas/cli/highlight.css @@ -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; - } } \ No newline at end of file diff --git a/midas/cli/highlighter.py b/midas/cli/highlighter.py index e302c3a..051ae79 100644 --- a/midas/cli/highlighter.py +++ b/midas/cli/highlighter.py @@ -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 [ + " ", + ] 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] = [ "", '', @@ -34,9 +54,8 @@ class PythonHighlighter(Expr.Visitor[None]): ' ', ' ', " Highlighted file", - " ", + *base_css, + *extra_css, "", "", '
', @@ -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) diff --git a/midas/cli/hl_midas.css b/midas/cli/hl_midas.css new file mode 100644 index 0000000..e8adef6 --- /dev/null +++ b/midas/cli/hl_midas.css @@ -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; + } +} \ No newline at end of file diff --git a/midas/cli/hl_python.css b/midas/cli/hl_python.css new file mode 100644 index 0000000..e6dc43b --- /dev/null +++ b/midas/cli/hl_python.css @@ -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; + } +} \ No newline at end of file diff --git a/midas/cli/main.py b/midas/cli/main.py index 4dd79c0..7319226 100644 --- a/midas/cli/main.py +++ b/midas/cli/main.py @@ -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)