Files
SaraView/Client/Variables.py
T

125 lines
4.9 KiB
Python

"""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