from __future__ import annotations import re from Core import ResolvePath from Explorer import ClassifyFile, ListDirectory from Locks import IsPathAccessible from Preview import ReadTextFile PortalFileTypes = {"markdown", "text"} MarkerPattern = re.compile( r"\{(?:(?P[^\s{}']+)\s+)?\*\*\*(?P[^\s*{}]+)" r"(?:\s+(?Phead|tail)\s+(?P\d+))?\}" r"|\{(?P[^\s{}']+)\s+(?Phead|tail)\s+(?P\d+)\}" r"|\{(?P[^\s{}']+)\s+'(?P\d+)'\}" r"|\{\*\*(?P[^\s*{}]+)\}" r"|\{\*(?P[^\s*{}]+)\}" ) def _LineBounds(Content: str, Position: int) -> tuple[int, int]: LineStart = Content.rfind("\n", 0, Position) + 1 LineEnd = Content.find("\n", Position) if LineEnd == -1: LineEnd = len(Content) return LineStart, LineEnd def _FindLocalDefinitions(Content: str) -> dict[str, str]: Matches = list(MarkerPattern.finditer(Content)) LineDefinitions: dict[str, str] = {} BlockOccurrences: dict[str, list[re.Match]] = {} for Match in Matches: if Match.group("LineName"): Name = Match.group("LineName") if Name not in LineDefinitions: LineStart, LineEnd = _LineBounds(Content, Match.start()) Line = Content[LineStart:LineEnd] Captured = ( Line[: Match.start() - LineStart] + Line[Match.end() - LineStart :] ).strip() LineDefinitions[Name] = _ResolveLocalPortals(Captured) elif Match.group("BlockName"): Name = Match.group("BlockName") BlockOccurrences.setdefault(Name, []).append(Match) BlockDefinitions: dict[str, str] = {} for Name, Occurrences in BlockOccurrences.items(): Open = Occurrences[0] if len(Occurrences) >= 2: Close = Occurrences[1] Captured = Content[Open.end() : Close.start()].strip("\n") else: Captured = Content[Open.end() :].lstrip("\n") BlockDefinitions[Name] = _ResolveLocalPortals(Captured) return {**LineDefinitions, **BlockDefinitions} def _ResolveLocalPortals(Content: str) -> str: Matches = list(MarkerPattern.finditer(Content)) Definitions = _FindLocalDefinitions(Content) LineSeenCount: dict[str, int] = {} BlockSeenCount: dict[str, int] = {} Output: list[str] = [] Cursor = 0 for Match in Matches: Replacement = None if Match.group("LineName"): Name = Match.group("LineName") Occurrence = LineSeenCount.get(Name, 0) LineSeenCount[Name] = Occurrence + 1 Replacement = "" if Occurrence == 0 else Definitions.get(Name, "") elif Match.group("BlockName"): Name = Match.group("BlockName") Occurrence = BlockSeenCount.get(Name, 0) BlockSeenCount[Name] = Occurrence + 1 Replacement = "" if Occurrence in (0, 1) else Definitions.get(Name, "") if Replacement is not None: Output.append(Content[Cursor : Match.start()]) Output.append(Replacement) Cursor = Match.end() Output.append(Content[Cursor:]) return "".join(Output) def _NormalizeRelativePath(BaseDirectory: str, RawPath: str) -> str: Combined = f"{BaseDirectory}/{RawPath}" if BaseDirectory else RawPath Parts: list[str] = [] for Part in Combined.split("/"): if Part in ("", "."): continue if Part == "..": if Parts: Parts.pop() continue Parts.append(Part) return "/".join(Parts) def _ReadAccessiblePortalSource( RootName: str, RelativePath: str, Username: str, UserGroups: list[str] ) -> str | None: if not IsPathAccessible(RootName, RelativePath, Username, UserGroups): return None try: FilePath = ResolvePath(RootName, RelativePath) except ValueError: return None if not FilePath.is_file() or ClassifyFile(FilePath) not in PortalFileTypes: return None try: return ReadTextFile(FilePath) except (OSError, UnicodeDecodeError): return None def _FindNamedPortalInDirectory( RootName: str, DirectoryRelativePath: str, Name: str, Username: str, UserGroups: list[str] ) -> str | None: if not IsPathAccessible(RootName, DirectoryRelativePath, Username, UserGroups): return None try: Entries = ListDirectory(RootName, DirectoryRelativePath, False, False) except (ValueError, NotADirectoryError, FileNotFoundError): return None for Entry in Entries: if Entry["IsDirectory"] or Entry["FileType"] not in PortalFileTypes: continue EntryPath = ( f"{DirectoryRelativePath}/{Entry['Name']}" if DirectoryRelativePath else Entry["Name"] ) Content = _ReadAccessiblePortalSource(RootName, EntryPath, Username, UserGroups) if Content is None: continue Definitions = _FindLocalDefinitions(Content) if Name in Definitions: return Definitions[Name] return None def _ResolveCrossFileReference( RootName: str, RefererDirectory: str, PathText: str | None, Name: str, Username: str, UserGroups: list[str], ) -> str | None: if not PathText: return _FindNamedPortalInDirectory(RootName, RefererDirectory, Name, Username, UserGroups) TargetRelativePath = _NormalizeRelativePath(RefererDirectory, PathText) try: TargetPath = ResolvePath(RootName, TargetRelativePath) except ValueError: return None if TargetPath.is_dir(): return _FindNamedPortalInDirectory( RootName, TargetRelativePath, Name, Username, UserGroups ) Content = _ReadAccessiblePortalSource(RootName, TargetRelativePath, Username, UserGroups) if Content is None: return None return _FindLocalDefinitions(Content).get(Name) def _ApplyPipe(Text: str, Mode: str, Count: int) -> str: Lines = Text.splitlines() Selected = Lines[:Count] if Mode == "head" else Lines[-Count:] if Count > 0 else [] return "\n".join(Selected) def _ResolveWholeFilePipe( RootName: str, RefererDirectory: str, PathText: str, Mode: str, Count: int, Username: str, UserGroups: list[str], ) -> str | None: TargetRelativePath = _NormalizeRelativePath(RefererDirectory, PathText) Content = _ReadAccessiblePortalSource(RootName, TargetRelativePath, Username, UserGroups) if Content is None: return None return _ApplyPipe(_ResolveLocalPortals(Content), Mode, Count) def _ResolveLineNumberReference( RootName: str, RefererDirectory: str, PathText: str, LineNumber: int, Username: str, UserGroups: list[str], ) -> str | None: TargetRelativePath = _NormalizeRelativePath(RefererDirectory, PathText) Content = _ReadAccessiblePortalSource(RootName, TargetRelativePath, Username, UserGroups) if Content is None: return None Lines = Content.splitlines() Index = LineNumber - 1 if 0 <= Index < len(Lines): return Lines[Index] return None def ResolvePortals( RootName: str, RelativePath: str, Content: str, Username: str, UserGroups: list[str] ) -> str: LocallyResolved = _ResolveLocalPortals(Content) RefererDirectory = RelativePath.rsplit("/", 1)[0] if "/" in RelativePath else "" Matches = list(MarkerPattern.finditer(LocallyResolved)) Output: list[str] = [] Cursor = 0 for Match in Matches: Replacement = None if Match.group("CrossName"): Replacement = _ResolveCrossFileReference( RootName, RefererDirectory, Match.group("CrossPath"), Match.group("CrossName"), Username, UserGroups, ) if Replacement is not None and Match.group("CrossPipe"): Replacement = _ApplyPipe( Replacement, Match.group("CrossPipe"), int(Match.group("CrossPipeCount")) ) elif Match.group("PipeMode"): Replacement = _ResolveWholeFilePipe( RootName, RefererDirectory, Match.group("PipePath"), Match.group("PipeMode"), int(Match.group("PipeCount")), Username, UserGroups, ) elif Match.group("LineNumber"): Replacement = _ResolveLineNumberReference( RootName, RefererDirectory, Match.group("LinePath"), int(Match.group("LineNumber")), Username, UserGroups, ) if Replacement is not None: Output.append(LocallyResolved[Cursor : Match.start()]) Output.append(Replacement) Cursor = Match.end() Output.append(LocallyResolved[Cursor:]) return "".join(Output)