mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 10:38:11 +02:00
125 lines
4.9 KiB
Python
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
|