Skip to content

Commit 9aeb774

Browse files
Storage and fixes (#423)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** - Added instructor and student build variants via the TYPE parameter for flexible documentation builds - Added Make Instructor and Make Student commands to Codio menu * **Documentation** - Updated installation instructions with specific package versions - Added new build command documentation for instructor and student variants * **Improvements** - Enhanced search configuration structure <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent a20d7ef commit 9aeb774

714 files changed

Lines changed: 1040 additions & 55 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.coderabbit.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ tone_instructions: |
66
Provide clear, concise, and professional feedback focused on improving documentation clarity, grammar, and consistency.
77
Suggest improvements for readability and structure, and highlight any potential ambiguities.
88
reviews:
9-
profile: "chill"
9+
profile: "assertive"
1010
request_changes_workflow: false
1111
high_level_summary: true
1212
poem: false

.codio

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// Run button configuration
44
"commands": {
55
"Make": "clear && make html",
6+
"Make Instructor": "clear && make html TYPE=instructor",
7+
"Make Student": "clear && make html TYPE=student",
68
"Start Ungit":"ungit --port=4000 --ungitBindIp 0.0.0.0"
79
},
810
// Preview button configuration

.github/workflows/deploy.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111
steps:
1212
- name: Setup env
13-
run: pip install sphinx==8.2.0 sphinxawesome-theme==5.3.2 sphinx-docsearch sphinx-sitemap sphinx_code_tabs recommonmark
13+
run: pip install sphinx==8.2.0 sphinxawesome-theme==5.3.2 sphinx-docsearch==0.1.0 sphinx-sitemap==2.9.0 sphinx_code_tabs==0.5.5 recommonmark==0.7.1
1414

1515
- name: Checkout
1616
uses: actions/checkout@v1

Makefile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,22 @@ help:
1313

1414
.PHONY: help Makefile
1515

16+
# Default TYPE if not provided: both (build student + instructor)
17+
ifeq ($(TYPE),)
18+
TYPE := both
19+
endif
20+
1621
# Catch-all target: route all unknown targets to Sphinx using the new
1722
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
1823
%: Makefile
1924
rm -rf "$(BUILDDIR)"
25+
ifeq ($(TYPE),student)
2026
@$(SPHINXBUILD) -M $@ "student-$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
27+
else ifeq ($(TYPE),instructor)
2128
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
22-
29+
else ifeq ($(TYPE),both)
30+
@$(SPHINXBUILD) -M $@ "student-$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
31+
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
32+
else
33+
$(error Invalid TYPE='$(TYPE)'. Use TYPE=student, TYPE=instructor, or TYPE=both)
34+
endif

README.md

Lines changed: 5 additions & 1 deletion

algolia-config.json

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
{
2-
"index_name": "CodioDocs",
3-
"sitemap_urls": [
4-
"https://docs.codio.com/sitemap.xml",
5-
"https://docs.codio.com/sitemap-student.xml"
6-
],
7-
"selectors": {
8-
"lvl0": "main h1",
9-
"lvl1": "main .section h2",
10-
"lvl2": "main .section h3",
11-
"lvl3": "main .section h4",
12-
"lvl4": "main .section h5",
13-
"lvl5": "main .section h6",
14-
"text": "main .section p,main .section blockquote"
15-
},
16-
"only_content_level": true
2+
"index_name": "CodioDocs",
3+
"sitemap_urls": [
4+
"https://docs.codio.com/sitemap.xml",
5+
"https://docs.codio.com/sitemap-student.xml"
6+
],
7+
"start_urls": [
8+
"https://docs.codio.com/",
9+
"https://docs.codio.com/students/"
10+
],
11+
"only_content_level": true,
12+
"selectors": {
13+
"lvl0": "main h1",
14+
"lvl1": "main h2",
15+
"lvl2": "main h3",
16+
"lvl3": "main h4",
17+
"lvl4": "main h5",
18+
"lvl5": "main h6",
19+
"text": "main p, main li, main blockquote, main td"
20+
}
1721
}

find_unused_images.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
'''
2+
python find_unused_images.py
3+
python find_unused_images.py --delete
4+
python find_unused_images.py --delete --yes
5+
python find_unused_images.py --debug
6+
'''
7+
8+
#!/usr/bin/env python3
9+
import argparse
10+
import csv
11+
import os
12+
import re
13+
from pathlib import Path
14+
from urllib.parse import urlparse
15+
16+
ROOT_DIRS = [
17+
Path(r"source"),
18+
Path(r"student-source"),
19+
]
20+
21+
# IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"} # .svg for logos.
22+
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
23+
24+
# Matches:
25+
# .. image:: /path.png
26+
# .. figure:: /path.png
27+
# .. |Alias| image:: /path.png
28+
# And when the directive appears inline (e.g., inside a table cell).
29+
# Accept plain, "quoted", or 'quoted' paths so we handle spaces.
30+
DIRECTIVE_RE = re.compile(
31+
r"""
32+
\.\.\s+ # leading '.. '
33+
(?:\|[^|]+\|\s+)? # optional '|Alias| '
34+
(?:image|figure)::\s+ # image:: or figure::
35+
(?P<path>"[^"]+"|'[^']+'|\S+) # the path (quoted or non-space)
36+
""",
37+
re.IGNORECASE | re.VERBOSE,
38+
)
39+
40+
SKIP_DIRS = {"_build", ".venv", "venv", "node_modules", ".git", "__pycache__"}
41+
42+
def is_url(s: str) -> bool:
43+
try:
44+
p = urlparse(s)
45+
return bool(p.scheme) and bool(p.netloc)
46+
except Exception:
47+
return False
48+
49+
def common_parent(paths: list[Path]) -> Path:
50+
parts = os.path.commonpath([str(p) for p in paths])
51+
return Path(parts)
52+
53+
def strip_quotes(s: str) -> str:
54+
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
55+
return s[1:-1]
56+
return s
57+
58+
def normalize_ref(s: str) -> str:
59+
# normalize backslashes, trim quotes, strip leading/trailing spaces
60+
s = strip_quotes(s.strip()).replace("\\", "/")
61+
return s
62+
63+
def rel_from_any_root(p: Path, roots_abs: list[Path]) -> tuple[Path, str] | None:
64+
"""Return (root, relative_posix) if p is under any root; else None."""
65+
for r in roots_abs:
66+
try:
67+
rel = p.relative_to(r).as_posix()
68+
return r, rel
69+
except ValueError:
70+
continue
71+
return None
72+
73+
def list_image_files(roots_abs: list[Path], debug=False) -> set[tuple[Path, str]]:
74+
"""
75+
Return a set of (root, rel_path) for all images under all roots.
76+
"""
77+
found: set[tuple[Path, str]] = set()
78+
for root in roots_abs:
79+
for dirpath, dirnames, filenames in os.walk(str(root)):
80+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
81+
for fn in filenames:
82+
if Path(fn).suffix.lower() in IMAGE_EXTS:
83+
full = (Path(dirpath) / fn).resolve()
84+
try:
85+
rel = full.relative_to(root).as_posix()
86+
found.add((root, rel))
87+
except ValueError:
88+
if debug:
89+
print(f"[skip-not-under-root] {full}")
90+
continue
91+
if debug:
92+
print(f"[debug] Found {len(found)} images on disk across {len(roots_abs)} root(s).")
93+
for root, rel in list(sorted(found))[:10]:
94+
print(f" - [{root.name}] {rel}")
95+
return found
96+
97+
def resolve_reference(rst_file: Path, ref: str, roots_abs: list[Path]) -> tuple[Path, str] | None:
98+
"""
99+
Resolve an image/figure reference to (root, relative_posix) across multiple roots.
100+
Strategy:
101+
* Absolute (/img/foo.png): try each root/<rel>.
102+
* Relative (img/foo.png or ../img/foo.png): resolve against rst_file and then map to a root.
103+
* If file doesn't exist, still attempt to map path to a root by anchor (best-effort).
104+
"""
105+
if is_url(ref):
106+
return None
107+
108+
ref = normalize_ref(ref)
109+
110+
if ref.startswith("/"):
111+
rel = ref.lstrip("/")
112+
for r in roots_abs:
113+
cand = (r / rel).resolve()
114+
if cand.exists():
115+
return r, rel
116+
for r in roots_abs:
117+
try:
118+
rst_file.relative_to(r)
119+
return r, rel
120+
except ValueError:
121+
continue
122+
return None
123+
124+
candidate = (rst_file.parent / ref)
125+
try:
126+
cand_abs = candidate.resolve()
127+
except FileNotFoundError:
128+
cand_abs = candidate.absolute()
129+
130+
mapped = rel_from_any_root(cand_abs, roots_abs)
131+
if mapped:
132+
return mapped
133+
134+
for r in roots_abs:
135+
try:
136+
rst_file.relative_to(r)
137+
try:
138+
rel = cand_abs.relative_to(r).as_posix()
139+
return r, rel
140+
except ValueError:
141+
return r, Path(ref).as_posix()
142+
except ValueError:
143+
continue
144+
return None
145+
146+
def parse_rst_image_refs(roots_abs: list[Path], debug=False) -> set[tuple[Path, str]]:
147+
"""
148+
Parse all .rst files under all roots and collect referenced images as (root, rel_path).
149+
"""
150+
refs: set[tuple[Path, str]] = set()
151+
for root in roots_abs:
152+
for dirpath, dirnames, filenames in os.walk(str(root)):
153+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
154+
for fn in filenames:
155+
if not fn.lower().endswith(".rst"):
156+
continue
157+
rst_path = Path(dirpath) / fn
158+
try:
159+
text = rst_path.read_text(encoding="utf-8")
160+
except UnicodeDecodeError:
161+
text = rst_path.read_text(encoding="latin-1")
162+
163+
for m in DIRECTIVE_RE.finditer(text):
164+
raw = m.group("path")
165+
raw = normalize_ref(raw)
166+
if not any(raw.lower().endswith(ext) for ext in IMAGE_EXTS):
167+
continue
168+
resolved = resolve_reference(rst_path.resolve(), raw, roots_abs)
169+
if resolved is not None:
170+
refs.add(resolved)
171+
elif debug:
172+
print(f"[warn] Could not resolve: {raw} in {rst_path}")
173+
174+
if debug:
175+
print(f"[debug] Found {len(refs)} image/figure references across {len(roots_abs)} root(s).")
176+
for root, rel in list(sorted(refs))[:10]:
177+
print(f" - [{root.name}] {rel}")
178+
return refs
179+
180+
def write_csv(unused: list[tuple[Path, str]], csv_path: Path):
181+
csv_path.parent.mkdir(parents=True, exist_ok=True)
182+
with csv_path.open("w", newline="", encoding="utf-8") as f:
183+
writer = csv.writer(f)
184+
writer.writerow(["root", "image_path"]) # which root + rel path
185+
for root, rel in unused:
186+
writer.writerow([str(root), rel])
187+
print(f"[info] CSV written: {csv_path}")
188+
189+
def delete_files(unused: list[tuple[Path, str]], yes=False):
190+
if not unused:
191+
print("[info] No unused images to delete.")
192+
return
193+
if not yes:
194+
confirm = input(f"⚠️ Delete {len(unused)} unused images across all roots? (y/N): ").strip().lower()
195+
if confirm != "y":
196+
print("[info] Deletion canceled.")
197+
return
198+
deleted = 0
199+
for root, rel in unused:
200+
try:
201+
(root / rel).unlink(missing_ok=True)
202+
deleted += 1
203+
except Exception as e:
204+
print(f"[error] Could not delete [{root.name}] {rel}: {e}")
205+
print(f"[info] Deleted {deleted} unused image(s).")
206+
207+
def main():
208+
parser = argparse.ArgumentParser(
209+
description="Find or delete unused images referenced via '.. image::', '.. figure::', or '.. |alias| image::' across multiple Sphinx roots."
210+
)
211+
parser.add_argument("--delete", action="store_true", help="Delete unused images instead of listing them")
212+
parser.add_argument("--yes", action="store_true", help="Skip confirmation prompt when deleting")
213+
parser.add_argument("--debug", action="store_true", help="Show debug info")
214+
args = parser.parse_args()
215+
216+
roots_abs = [p.resolve() for p in ROOT_DIRS]
217+
for r in roots_abs:
218+
print(f"[info] Root directory: {r}")
219+
220+
images_on_disk = list_image_files(roots_abs, debug=args.debug) # set[(root, rel)]
221+
referenced = parse_rst_image_refs(roots_abs, debug=args.debug) # set[(root, rel)]
222+
223+
referenced_rels = {rel for (_root, rel) in referenced}
224+
unused = sorted([(root, rel) for (root, rel) in images_on_disk if rel not in referenced_rels],
225+
key=lambda x: (str(x[0]), x[1]))
226+
227+
print(f"[result] Found {len(unused)} unused image(s) across all roots).")
228+
229+
out_csv = common_parent(roots_abs) / "unused_images.csv"
230+
231+
if args.delete:
232+
delete_files(unused, yes=args.yes)
233+
else:
234+
write_csv(unused, out_csv)
235+
if args.debug and unused:
236+
print("First few unused images:")
237+
for root, rel in unused[:10]:
238+
print(f" - [{root.name}] {rel}")
239+
240+
if __name__ == "__main__":
241+
main()

plag-button.png

-221 KB
Binary file not shown.

prefs-account-gh1.png

-138 KB
Binary file not shown.
-20.2 KB
Binary file not shown.

0 commit comments

Comments
 (0)