From 0cd98873e0e4688f152d938bea1fe9ee7de286d2 Mon Sep 17 00:00:00 2001 From: SRHTNIN Date: Mon, 21 Sep 2026 23:07:17 +0200 Subject: [PATCH] Added children, siblings, and parents as variables. Also % as for. --- Backend/Portals.py | 101 ++++++++++++++++++++++++++++++++++++++++++- Frontend/src/App.jsx | 12 ++++- 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/Backend/Portals.py b/Backend/Portals.py index d42e3ef..9aa6f51 100644 --- a/Backend/Portals.py +++ b/Backend/Portals.py @@ -3,12 +3,13 @@ from __future__ import annotations import re from Core import NormalizeRelativePath, ResolvePath -from Explorer import ClassifyFile, ListDirectory +from Explorer import ClassifyFile, ListDirectory, NoteFilePrefix from Locks import IsPathAccessible from Preview import ReadTextFile PortalFileTypes = {"markdown", "text"} +LoopSources = ("children", "siblings", "parents") MarkerPattern = re.compile( r"\{(?:(?P[^\s{}']+)\s+)?\*\*\*(?P[^\s*{}]+)" @@ -19,6 +20,11 @@ MarkerPattern = re.compile( r"|\{\*(?P[^\s*{}]+)\}" ) +LoopMarkerPattern = re.compile( + r"\{(?Pchildren|siblings|parents)\s+%\s+(?P[A-Za-z_][A-Za-z0-9_]*)\}" + r"|\{(?Pchildren|siblings|parents)\s+%\}" +) + def _LineBounds(Content: str, Position: int) -> tuple[int, int]: LineStart = Content.rfind("\n", 0, Position) + 1 @@ -94,6 +100,95 @@ def _ResolveLocalPortals(Content: str) -> str: return "".join(Output) +def _ListAccessibleEntryNames( + RootName: str, DirectoryRelativePath: str, Username: str, UserGroups: list[str], Exclude: str | None +) -> list[str]: + if not IsPathAccessible(RootName, DirectoryRelativePath, Username, UserGroups): + return [] + + try: + Entries = ListDirectory(RootName, DirectoryRelativePath, False, False) + except (ValueError, NotADirectoryError, FileNotFoundError): + return [] + + Names: list[str] = [] + + for Entry in Entries: + if Entry["Name"] == Exclude: + continue + + EntryPath = ( + f"{DirectoryRelativePath}/{Entry['Name']}" if DirectoryRelativePath else Entry["Name"] + ) + + if IsPathAccessible(RootName, EntryPath, Username, UserGroups): + Names.append(Entry["Name"]) + + return Names + + +def _ComputeLoopLists( + RootName: str, RelativePath: str, Username: str, UserGroups: list[str] +) -> dict[str, list[str]]: + FileName = RelativePath.rsplit("/", 1)[-1] if RelativePath else "" + ParentDirectory = RelativePath.rsplit("/", 1)[0] if "/" in RelativePath else "" + ParentBaseName = ParentDirectory.rsplit("/", 1)[-1] if ParentDirectory else "" + + IsDirectoryNote = FileName.startswith(f"{NoteFilePrefix}{ParentBaseName}.") + + Children = ( + _ListAccessibleEntryNames(RootName, ParentDirectory, Username, UserGroups, FileName) + if IsDirectoryNote + else [] + ) + Siblings = _ListAccessibleEntryNames(RootName, ParentDirectory, Username, UserGroups, FileName) + Parents = [Segment for Segment in ParentDirectory.split("/") if Segment] + + return {"children": Children, "siblings": Siblings, "parents": Parents} + + +def _ResolveLoops(Content: str, Lists: dict[str, list[str]]) -> str: + Matches = list(LoopMarkerPattern.finditer(Content)) + Output: list[str] = [] + Cursor = 0 + Index = 0 + + while Index < len(Matches): + Match = Matches[Index] + + if not Match.group("LoopSource"): + Index += 1 + continue + + Source = Match.group("LoopSource") + LoopVar = Match.group("LoopVar") + + CloseMatch = None + for Candidate in Matches[Index + 1 :]: + if Candidate.group("CloseSource") == Source: + CloseMatch = Candidate + break + + BodyEnd = CloseMatch.start() if CloseMatch is not None else len(Content) + BodyTemplate = Content[Match.end() : BodyEnd] + Items = Lists.get(Source, []) + VariablePattern = re.compile(rf"%{re.escape(LoopVar)}\b") + + Rendered = "\n".join(VariablePattern.sub(Item, BodyTemplate) for Item in Items) + + Output.append(Content[Cursor : Match.start()]) + Output.append(Rendered) + Cursor = CloseMatch.end() if CloseMatch is not None else len(Content) + + if CloseMatch is not None: + Index = Matches.index(CloseMatch) + 1 + else: + break + + Output.append(Content[Cursor:]) + return "".join(Output) + + def _ReadAccessiblePortalSource( RootName: str, RelativePath: str, Username: str, UserGroups: list[str] ) -> str | None: @@ -224,7 +319,9 @@ def _ResolveLineNumberReference( def ResolvePortals( RootName: str, RelativePath: str, Content: str, Username: str, UserGroups: list[str] ) -> str: - LocallyResolved = _ResolveLocalPortals(Content) + LoopLists = _ComputeLoopLists(RootName, RelativePath, Username, UserGroups) + WithLoopsResolved = _ResolveLoops(Content, LoopLists) + LocallyResolved = _ResolveLocalPortals(WithLoopsResolved) RefererDirectory = RelativePath.rsplit("/", 1)[0] if "/" in RelativePath else "" Matches = list(MarkerPattern.finditer(LocallyResolved)) diff --git a/Frontend/src/App.jsx b/Frontend/src/App.jsx index 7f4f8be..58e9787 100644 --- a/Frontend/src/App.jsx +++ b/Frontend/src/App.jsx @@ -239,7 +239,15 @@ export default function App() { const Created = await EnsureDirectoryNote(ActiveRoot, DirectoryPath); HandleSelectDirectoryNote(DirectoryPath, Created.Name, Created.FileType); } catch (Error) { - SetErrorMessage(Error.message); + HandleNavigateDirectory(DirectoryPath); + } + } + + function HandleBreadcrumbNavigate(Path) { + if (!Path) { + HandleNavigateDirectory(""); + } else { + OpenDirectoryNote(Path, null); } } @@ -333,7 +341,7 @@ export default function App() { RootName={ActiveRoot} FilePath={ViewPath} IsDirectory={ViewIsDirectory || Boolean(ViewNoteFilePath)} - OnNavigate={HandleNavigateDirectory} + OnNavigate={HandleBreadcrumbNavigate} /> ) : (