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)