mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 10:38:11 +02:00
Client: variables resolved in the live editor and reading view
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
eec8fd4b79
commit
ee904540c6
7 files changed
+169
-16
No files matched your search
@@ -0,0 +1,124 @@
|
||||
"""SaraView's variables (portals, definitions, loops) in the client: shown as
|
||||
what they resolve to, like the web view. A server says where each one is and
|
||||
what it becomes (POST /api/file/spans); in a local folder the client works it
|
||||
out itself with the server's own engine, Backend/VarEngine.py, which is
|
||||
installed beside the client so both read a note the same way.
|
||||
|
||||
Spans are [{"From", "To", "Text"}] in characters of the whole note; Text ""
|
||||
is a definition's own marker, which just disappears."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from Sources import SourceError
|
||||
|
||||
# VarEngine.py lives in Backend/, next to Client/ (in the repository, and in
|
||||
# what SaraSuite installs).
|
||||
_BackendDir = Path(__file__).resolve().parent.parent / "Backend"
|
||||
if _BackendDir.is_dir() and str(_BackendDir) not in sys.path:
|
||||
sys.path.append(str(_BackendDir))
|
||||
try:
|
||||
import VarEngine
|
||||
except ImportError:
|
||||
VarEngine = None # an install without it: variables stay as written
|
||||
|
||||
Span = dict
|
||||
LineMap = list[Optional[int]]
|
||||
|
||||
|
||||
class _LocalAccess:
|
||||
"""VarEngine's file access for a local folder (LocalSource): everything
|
||||
in it can be read, as there are no locks."""
|
||||
|
||||
def __init__(self, Source, Root: str):
|
||||
self.Source = Source
|
||||
self.Root = Root
|
||||
|
||||
def ReadText(self, RelativePath: str) -> Optional[str]:
|
||||
try:
|
||||
FilePath = self.Source.LocalFile(self.Root, RelativePath)
|
||||
if not FilePath.is_file() or FilePath.suffix.lower() not in (".md", ".markdown", ".txt"):
|
||||
return None
|
||||
return FilePath.read_text(encoding="utf-8")
|
||||
except (SourceError, OSError, UnicodeDecodeError, ValueError):
|
||||
return None
|
||||
|
||||
def List(self, DirectoryPath: str) -> Optional[list[dict]]:
|
||||
try:
|
||||
Entries = self.Source.List(self.Root, DirectoryPath)
|
||||
except SourceError:
|
||||
return None
|
||||
# Like the server's engine: hidden files and .sv_ notes never count,
|
||||
# whatever the file list is set to show.
|
||||
return [{"Name": Item.Name, "IsDirectory": Item.IsDirectory, "FileType": Item.FileType}
|
||||
for Item in Entries if not Item.Name.startswith(".")]
|
||||
|
||||
def IsDirectory(self, RelativePath: str) -> bool:
|
||||
try:
|
||||
return self.Source.LocalFile(self.Root, RelativePath).is_dir()
|
||||
except (SourceError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def FindSpans(TheApp, Root: str, RelativePath: str, Content: str) -> list[Span]:
|
||||
"""Where the variables in Content are and what they resolve to ([] if
|
||||
that can't be worked out: then they show as written)."""
|
||||
if "{" not in Content:
|
||||
return []
|
||||
Source = TheApp.Source
|
||||
try:
|
||||
if Source.Kind == "Server":
|
||||
return Source.Spans(Root, RelativePath, Content)
|
||||
if VarEngine is None:
|
||||
return []
|
||||
return VarEngine.ResolveSpans(RelativePath, Content, _LocalAccess(Source, Root))
|
||||
except (SourceError, OSError, ValueError, RecursionError):
|
||||
return []
|
||||
|
||||
|
||||
def Substitute(Text: str, Start: int, Spans: list[Span]) -> tuple[str, LineMap]:
|
||||
"""Text (which starts at Start in the note) with its variables shown as
|
||||
their results, and for each line of that, which line of Text it is
|
||||
(None for a line with a result in it: a checkbox there isn't the note's).
|
||||
A result shows where its variable starts; a variable running on past
|
||||
Text's end (a loop over several blocks) is cut there, and the part of one
|
||||
that started earlier just disappears."""
|
||||
Pieces: list[tuple[str, bool]] = [] # (text, is a result)
|
||||
SkippedLines: list[int] = [] # source newlines a result replaced, per result piece
|
||||
Cursor = 0
|
||||
for Item in Spans:
|
||||
From, To = Item["From"] - Start, Item["To"] - Start
|
||||
if To <= 0 or From >= len(Text) or From < Cursor and From >= 0:
|
||||
continue
|
||||
From, To = max(From, 0), min(To, len(Text))
|
||||
Pieces.append((Text[Cursor:From], False))
|
||||
Pieces.append((Item["Text"] if Item["From"] >= Start else "", True))
|
||||
SkippedLines.append(Text.count("\n", From, To))
|
||||
Cursor = To
|
||||
Pieces.append((Text[Cursor:], False))
|
||||
|
||||
Shown: list[str] = []
|
||||
Map: LineMap = []
|
||||
SourceLine = 0
|
||||
LineStart = 0 # the source line the current shown line began on
|
||||
Tainted = False
|
||||
Skips = iter(SkippedLines)
|
||||
for Piece, IsResult in Pieces:
|
||||
Shown.append(Piece)
|
||||
if IsResult:
|
||||
Tainted = Tainted or Piece != ""
|
||||
for _ in range(Piece.count("\n")):
|
||||
Map.append(None)
|
||||
Skipped = next(Skips)
|
||||
SourceLine += Skipped
|
||||
Tainted = Tainted or Skipped > 0 # this line now runs on from a later one
|
||||
continue
|
||||
for Character in Piece:
|
||||
if Character == "\n":
|
||||
Map.append(None if Tainted else LineStart)
|
||||
SourceLine += 1
|
||||
LineStart = SourceLine
|
||||
Tainted = False
|
||||
Map.append(None if Tainted else LineStart)
|
||||
return "".join(Shown), Map
|
||||
Reference in new issue
Block a user