-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfiletree.py
153 lines (133 loc) · 5.04 KB
/
filetree.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""Craft/edit a simple file tree.
"""
from typing import cast
from document import HighlightSquare
from modifiers import (
AnonymousPlaceHolder,
ListBuilder,
MakePlaceHolder,
PlaceHolder,
TextModifier,
render_method,
)
FileTreeLineModifier, FileTreeLine = MakePlaceHolder(
"FileTreeLine",
r"<type>/<mod>/<name>/<filename>",
)
FileTreeLines = ListBuilder(FileTreeLine, ",\n", tail=True)
class FileTree(TextModifier):
"""One chain of files, arranged top-down.
Keep it simple as subsubfolders and 'step out' are not exactly used/implemented yet.
"""
def __init__(self, input: str):
intro, files = input.split("{\n", 1)
self.intro = AnonymousPlaceHolder(
r"\FileTree[<name>]{<location>}", "parse", intro
)
files = files.rsplit("}", 1)[0]
self.list = FileTreeLines.parse(files)
self._sub = False # Raise when in subfolder.
@property
def name(self):
return self.intro.name
@render_method
def render(self) -> str:
return self.intro.render() + "{\n" + self.list.render() + "}\n"
def clear(self) -> "FileTree":
self.list.clear()
self._sub = False
return self
def populate(self, filetree: "FileTree") -> "FileTree":
"""Import/copy all files from another value."""
self.clear()
for file in filetree.list:
self.list.append(file.copy())
self._sub = filetree._sub
return self
def append(
self,
filename: str | PlaceHolder, # FileTreeLine
# Use only 'file' or 'folder',
# with possible 'in' additional keyword
# for the first file stepping in a subfolder chain.
keywords: str = "file",
mod: str = "0",
# Name for the file node, otherwise defaults to uncapitalized filename base.
name: str | None = None,
) -> PlaceHolder: # FileTreeLine
if type(filename) is str:
# Construct an actual file and pass again recursively to the same function.
words = set(keywords.split())
allow_list = set("file folder stepin".split())
if invalid := words - allow_list:
raise ValueError(
f"Invalid keyword in file type: {repr(next(iter(invalid)))}."
)
if "file" in words and "folder" in words:
raise ValueError("Cannot be both 'file' and 'folder' type.")
if not ("file" in words or "folder" in words):
words.add("file")
if name is None:
if "." in filename:
name, ext = (s.lower().strip() for s in filename.rsplit(".", 1))
if not name: # Useful for special names like '.git'.
name = ext
else:
name = filename.lower().strip()
# Avoid tikz special chars and duplicates.
assert "." not in name
assert not any(f.name == name for f in self.list)
file = FileTreeLine.new(" ".join(words), mod, name, filename)
return self.append(file)
file = cast(PlaceHolder, filename) # FileTreeLine
words = set(file.type.split())
if "stepin" in words:
self._sub = True
words -= {"connect"}
elif self._sub:
words.add("connect")
else:
words -= {"connect"}
file.type = " ".join(words)
return self.list.append(file)
@staticmethod
def remove_from_type(file: PlaceHolder, keyword: str):
file.type = " ".join(set(file.type.split()) - {keyword})
@staticmethod
def add_to_type(file: PlaceHolder, keyword: str):
kws = set(file.type.split())
kws.add(keyword)
file.type = " ".join(kws)
def pop(self, file: PlaceHolder | str) -> PlaceHolder: # FileTreeLine
"""Remove from the chain, taking care of preserving the structure."""
if type(file) is str:
file = self[file]
file = cast(PlaceHolder, file)
i = self.list.list.index(file)
removed = self.list.list.pop(i)
if "stepin" in removed.type:
self._sub = False
return removed
def __getitem__(self, name: str) -> PlaceHolder: # FileTreeLine
"""Search and retrieve file by name (*not* filename)."""
for file in self.list:
if file.name == name:
return file
raise KeyError(f"No such file in file tree: {repr(name)}.")
def all_mod(self, mod: str) -> "FileTree":
"""Set all content to the same mode,
Reset with mod='0'.
"""
assert self.list.list
for file in self.list:
file.mod = mod
return self
def highlight(self, name, pad=1.2) -> PlaceHolder: # HighlightSquare
"""Highlight one file in particular."""
return self.add_epilog(
HighlightSquare.new(
f"{name}-icon.south west",
f"{name}-filename.east |- {name}-icon.north",
padding=pad,
)
)