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