feat(assessment): auto comprehensive assessment - rules engine from college scoring standard, per-semester stats, manual overrides, word export
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .utils import ColorSpan, StyleRef, THEME, iter_display_chars, unbracket, wlen
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ContentFragment:
|
||||
"""A single display character with its visual width and style.
|
||||
|
||||
The body of ``>>> def greet`` becomes one fragment per character::
|
||||
|
||||
d e f g r e e t
|
||||
╰──┴──╯ ╰──┴──┴──┴──╯
|
||||
keyword (unstyled)
|
||||
|
||||
e.g. ``ContentFragment("d", 1, StyleRef(tag="keyword"))``.
|
||||
"""
|
||||
|
||||
text: str
|
||||
width: int
|
||||
style: StyleRef = StyleRef()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PromptContent:
|
||||
"""The prompt split into leading full-width lines and an inline portion.
|
||||
|
||||
For the common ``">>> "`` prompt (no newlines)::
|
||||
|
||||
>>> def greet(name):
|
||||
╰─╯
|
||||
text=">>> ", width=4, leading_lines=()
|
||||
|
||||
If ``sys.ps1`` contains newlines, e.g. ``"Python 3.13\\n>>> "``::
|
||||
|
||||
Python 3.13 ← leading_lines[0]
|
||||
>>> def greet(name):
|
||||
╰─╯
|
||||
text=">>> ", width=4
|
||||
"""
|
||||
|
||||
leading_lines: tuple[ContentFragment, ...]
|
||||
text: str
|
||||
width: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SourceLine:
|
||||
"""One logical line from the editor buffer, before styling.
|
||||
|
||||
Given this two-line input in the REPL::
|
||||
|
||||
>>> def greet(name):
|
||||
... return name
|
||||
▲ cursor
|
||||
|
||||
The buffer ``"def greet(name):\\n return name"`` yields::
|
||||
|
||||
SourceLine(lineno=0, text="def greet(name):",
|
||||
start_offset=0, has_newline=True)
|
||||
SourceLine(lineno=1, text=" return name",
|
||||
start_offset=17, cursor_index=14)
|
||||
"""
|
||||
|
||||
lineno: int
|
||||
text: str
|
||||
start_offset: int
|
||||
has_newline: bool
|
||||
cursor_index: int | None = None
|
||||
|
||||
@property
|
||||
def cursor_on_line(self) -> bool:
|
||||
return self.cursor_index is not None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ContentLine:
|
||||
"""A logical line paired with its prompt and styled body.
|
||||
|
||||
For ``>>> def greet(name):``::
|
||||
|
||||
>>> def greet(name):
|
||||
╰─╯ ╰──────────────╯
|
||||
prompt body: one ContentFragment per character
|
||||
"""
|
||||
|
||||
source: SourceLine
|
||||
prompt: PromptContent
|
||||
body: tuple[ContentFragment, ...]
|
||||
|
||||
|
||||
def process_prompt(prompt: str) -> PromptContent:
|
||||
r"""Return prompt content with width measured without zero-width markup."""
|
||||
|
||||
prompt_text = unbracket(prompt, including_content=False)
|
||||
visible_prompt = unbracket(prompt, including_content=True)
|
||||
leading_lines: list[ContentFragment] = []
|
||||
|
||||
while "\n" in prompt_text:
|
||||
leading_text, _, prompt_text = prompt_text.partition("\n")
|
||||
visible_leading, _, visible_prompt = visible_prompt.partition("\n")
|
||||
leading_lines.append(ContentFragment(leading_text, wlen(visible_leading)))
|
||||
|
||||
return PromptContent(tuple(leading_lines), prompt_text, wlen(visible_prompt))
|
||||
|
||||
|
||||
def build_body_fragments(
|
||||
buffer: str,
|
||||
colors: list[ColorSpan] | None,
|
||||
start_index: int,
|
||||
) -> tuple[ContentFragment, ...]:
|
||||
"""Convert a line's text into styled content fragments."""
|
||||
# Two separate loops to avoid the THEME() call in the common uncolored path.
|
||||
if colors is None:
|
||||
return tuple(
|
||||
ContentFragment(
|
||||
styled_char.text,
|
||||
styled_char.width,
|
||||
StyleRef(),
|
||||
)
|
||||
for styled_char in iter_display_chars(buffer, colors, start_index)
|
||||
)
|
||||
|
||||
theme = THEME()
|
||||
return tuple(
|
||||
ContentFragment(
|
||||
styled_char.text,
|
||||
styled_char.width,
|
||||
StyleRef.from_tag(styled_char.tag, theme[styled_char.tag])
|
||||
if styled_char.tag
|
||||
else StyleRef(),
|
||||
)
|
||||
for styled_char in iter_display_chars(buffer, colors, start_index)
|
||||
)
|
||||
Reference in New Issue
Block a user