Skip to content

Commit 9d5a540

Browse files
Script and GitHub Action to maintain a Wiki frontend (#303)
* Created workflow to update wiki with documents from repository * Allowed for manual running of action * Included Adding of petshop.md * Added petshop surface-level files * Excluded template from wiki * Changed files to match consistency of other markdown files * Added pet shop files to path * Excluded all example code folders and pruned empty folders * Added steps to run python script * Created Python script to form files for Wiki from repo * Changed to double-quote string * Combined methods with similar logic * Turned comment into doc string * Returned to single quotes * Simplified enumeration and string strip * Added type hints and docs * Added docs temp folder to gitignore * Added type hint * Moved call to slugify to extract title * Simplified markdown mapping * Created dataclass structure * Added parent to markdownfile * Created second structure class, FilePath * Made MarkdownFile subclass FilePath * Changed dataclass to use properties and store dirpath * Ran linter on files * Made dirpath not private * Used dataclass provided init * Restructured with dataclasses and simplified logic * Minor rename * Removed redundant variables * Fixed markdown extension * Added type hints * Passed link to variable to reduce code * File refactors and helper methods * Removed comments * Deleted old file instead of renaming and then deleting in case of case-only change * Added check for system's case sensitivity * Added info as parameter and moved method out of loop * Removed name maps since links cannot work unless relative or absolute * Simplified with dictionary comprehension * Inlined method * Returned name map for markdown * Returned back to tuple and name map * Created initial _Sidebar.md, which is included in Wiki * Added markdown newline spacers * Added markdown newline spacers * Removed pathing from push, as all files work together * Added spaces lost to merge * Simplified to always delete old_path before writing to new file if name changed at all * Remove makedirs since only file renaming happens * Markdown lint * Made links to repo folders link to base repo * Added extra base cases for no extension links * Added extra base cases for no extension links * Changed where Home base case is handled * Changed Home to early return * Added print statements to rewrite * Added boolean prints * Updated boolean prints * Changed to compare to empty string instead of None * Added phone number base case to links * Added explicit group 0 * Added case for links within angle brackets * Removed example-code case from direct link * Changed to link to any extension found * Simplified boolean logic * Removed CODE_EXTS * Changed base cases to a regex match array to simplify extensibility * Added regex comment on groups * Removed unnecessary parameter * Removed if blocks * Added a dash to titles with Phase additions * Created TupleMapName for simpler map creation and accessing * Added prints to code to see if partition is necessary * Changed structure to do a string replacement instead of rebuilding * Removed default dict values * Returned to old link structure * Returned from TupleNameMap structure * Added more useful prints than previous * Only print if due to sep being empty * Only print if not caught by any case * Added to else block to actually only print if passed * Added Home with anchor as base case * Added local anchor base case * Joined base cases * Removed case of No new link * Returned to modified link changing * Simplified local anchor base case * Added wrong case prints * Added an re.escape to re.sub * Returned test TupleNameMap structure * Added secondary structure to test * Added actual new value to prints * Moved print to init * Added prints of whole structures * Moved prints to get method * Added None base case * Printed results * Fixed tuple building * Returned to new structure * Minor refactors * Added more supported embed extensions * Created file to contain repo-specific rewrite rules * Moved EMBED_EXTS to rules file * Moved logic to name the file from extracted data to rules * Added module docstring * Made wiki_page_title add the file extension * Added EDIT_FILE_EXTS * Changed extension removal to account for any given extensions * Renamed elements to match generalization * Added class docstrings * Moved slugify to structure * Made title not slugified until after helper function * Added access to rewrite_rules to slugify if needed * Extracted logic for getting link to second helper method to use match return * Made extrac_title_and_body part of rules file * Took imports from structure's shared code * Added os and re imports per file * Changed so Wiki structure is kept within its own folder * Added -a flag to copy * Added dot to path * Moved python scripts to subfolder * Made Sidebar links relative to root * Renamed Sidebar links * Fixed link not updated * Used Em Dash for Getting Started rename * Added links to the Auto-Grader and Help Queue * Added some Phase 3 relevant links * Changed to Hyphen character * Added char const for special hypen * Made title link to base page * Changed to title being added * Added newline spaces * Removed temporary Phase 3 _Sidebar
1 parent de5027d commit 9d5a540

File tree

6 files changed

+288
-0
lines changed

6 files changed

+288
-0
lines changed

.github/scripts/rewrite.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env python3
2+
import sys
3+
import os
4+
import re
5+
from structure import ContentFilePath, FilePath, TupleNameMap, slugify
6+
from rewrite_rules import (LINK_BASE_CASES, EMBED_EXTS, EDIT_FILE_EXTS,
7+
wiki_page_title, extract_title_and_body)
8+
9+
10+
def get_ext(path: str):
11+
"""Get the file extension, if present"""
12+
return path.lower().rsplit('.', 1)[-1] if '.' in path else ''
13+
14+
def find_files_with_exts(root: str, *exts: str):
15+
"""Yield FilePaths for files of the given extensions."""
16+
for path_from_root, _, files in os.walk(root):
17+
for f in files:
18+
if get_ext(f) in exts:
19+
yield FilePath(path_from_root, f)
20+
21+
def main(root: str, code_base: str):
22+
mapping: dict[str, ContentFilePath] = {}
23+
for file_path in find_files_with_exts(root, *EDIT_FILE_EXTS):
24+
title, body = extract_title_and_body(file_path.full_path)
25+
filename = slugify(wiki_page_title(title, body, file_path))
26+
27+
mapping[file_path.full_path] = ContentFilePath(file_path.dirpath, filename, body)
28+
29+
edited_map = TupleNameMap(
30+
(info.parent, os.path.basename(old_path), info.filename)
31+
for (old_path, info) in mapping.items())
32+
33+
embed_map = TupleNameMap(
34+
(file_path.parent, file_path.filename, file_path.relative_from(root))
35+
for file_path in find_files_with_exts(root, *EMBED_EXTS))
36+
37+
# Groups: 1) Links inside <>, 2) All other links
38+
link_re = re.compile(r'\[(?:[^\]]+)\]\((?:<([^>]+)>|((?:[^()\\]|\\[()])+))\)')
39+
40+
base_re = re.compile('|'.join(LINK_BASE_CASES))
41+
42+
clean_ext_pattern = r'\.(' + '|'.join(EDIT_FILE_EXTS) + r')$'
43+
44+
def extract_new_link(info: ContentFilePath, path_part: str) -> str:
45+
dirname = os.path.basename(os.path.dirname(path_part))
46+
basename = os.path.basename(path_part)
47+
48+
match get_ext(basename):
49+
case embed if embed in EMBED_EXTS:
50+
return embed_map.get(dirname, info.parent, basename)
51+
case edit if edit in EDIT_FILE_EXTS:
52+
new_base = edited_map.get(dirname, info.parent, basename)
53+
return re.sub(clean_ext_pattern, '', new_base, flags=re.IGNORECASE)
54+
case _:
55+
abs_path = os.path.normpath(os.path.join(info.dirpath, path_part))
56+
rel_to_root = os.path.relpath(abs_path, root)
57+
58+
if code_base.startswith(('http://', 'https://')):
59+
return code_base.rstrip('/') + '/' + rel_to_root
60+
return os.path.normpath(os.path.join(code_base, rel_to_root))
61+
62+
def rewrite_line(line: str, info: ContentFilePath) -> str:
63+
def repl(m: re.Match[str]) -> str:
64+
target = m.group(1) or m.group(2)
65+
66+
if base_re.search(target):
67+
return m.group(0)
68+
69+
path_part = target.partition('#')[0]
70+
new_link = extract_new_link(info, path_part)
71+
72+
return re.sub(re.escape(path_part), new_link, m.group(0), 1)
73+
74+
return link_re.sub(repl, line)
75+
76+
for old_path, info in mapping.items():
77+
new_body = [rewrite_line(ln, info) for ln in info.body]
78+
79+
if old_path != info.full_path:
80+
os.remove(old_path)
81+
82+
with open(info.full_path, 'w', encoding='utf-8') as f:
83+
f.writelines(new_body)
84+
85+
if __name__ == '__main__':
86+
if len(sys.argv) != 3:
87+
print("Usage: rewrite.py <root-markdown-dir> <code-base-path-or-URL>")
88+
sys.exit(1)
89+
main(sys.argv[1], sys.argv[2])

.github/scripts/rewrite_rules.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
This module provides a common place to change the specific information of the rewrite script
3+
to allow its reuse in other repositories as desired.
4+
"""
5+
import re
6+
from structure import FilePath, slugify # pylint: disable=W0611
7+
8+
GHWT_HYPHEN = '‐'
9+
"""Holds the U+2010 hyphen character, which correctly renders
10+
as a hyphen when used inside a GitHub Wiki page title"""
11+
12+
LINK_BASE_CASES = [r':\/\/', r'^Home#?', r'^tel:.*', r'^mailto:.*', r'^#']
13+
"""List of regex strings that will leave a markdown link unchanged
14+
when transformed for wiki usage."""
15+
16+
EMBED_EXTS = {'png', 'gif', 'jpg', 'jpeg', 'svg', 'webp', 'uml', 'mp4', 'mov', 'webm'}
17+
"""File extensions that are used inside Markdown files to embed."""
18+
19+
EDIT_FILE_EXTS = {'md'}
20+
"""File extensions that will be modified by the script."""
21+
22+
def wiki_page_title(title: str | None, body: list[str], file_path: FilePath) -> str:
23+
# pylint: disable=W0613
24+
"""
25+
Takes in the extracted file title if found, the file's remaining text, and the file path info.
26+
27+
Returns the new title for the editing file, which will be slugified and used to name the page
28+
when used in the wiki, including the file extension.
29+
30+
The logic to find the title is defined in extract_title_and_body.
31+
"""
32+
if title:
33+
phase_dir = re.search(r'(\d+)', file_path.parent)
34+
if file_path.filename == 'getting-started.md' and phase_dir:
35+
return f"{title} {GHWT_HYPHEN} Phase {phase_dir.group(1)}.md"
36+
return title + '.md'
37+
return file_path.filename
38+
39+
def extract_title_and_body(path: str) -> tuple[str | None, list[str]]:
40+
"""
41+
Takes in the full path to the file being accessed.
42+
43+
Returns (title, body_lines) as extracted from the file, and the rest of the file.
44+
- title can be None if file's name must remain unchanged or doesn't match pattern.
45+
"""
46+
with open(path, encoding='utf-8') as f:
47+
lines = f.readlines()
48+
49+
line_number, first_line = next((i, ln) for (i, ln) in enumerate(lines) if ln.strip())
50+
if not first_line.lstrip().startswith('# '):
51+
return None, lines
52+
title = first_line.strip()[2:]
53+
54+
j = line_number + 1
55+
while j < len(lines) and lines[j].strip() == '':
56+
j += 1
57+
return title, lines[j:]

.github/scripts/structure.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from dataclasses import dataclass
2+
import os
3+
import re
4+
from typing import List, Iterator
5+
6+
7+
@dataclass
8+
class FilePath:
9+
"""Represents the path to a file from the source of the project."""
10+
dirpath: str
11+
"""Path from root to parent directory of file"""
12+
filename: str
13+
"""Name of the file"""
14+
15+
@property
16+
def full_path(self) -> str:
17+
"""Full path from root to the file"""
18+
return os.path.join(self.dirpath, self.filename)
19+
20+
@property
21+
def parent(self) -> str:
22+
"""Immediate parent directory"""
23+
return os.path.basename(self.dirpath)
24+
25+
def relative_from(self, root: str):
26+
return os.path.relpath(self.full_path, root)
27+
28+
29+
@dataclass
30+
class ContentFilePath(FilePath):
31+
"""Represents a FilePath with a reference to the contents of such file."""
32+
body: List[str]
33+
"""All content of the file as separate lines"""
34+
35+
36+
@dataclass(init=False)
37+
class TupleNameMap:
38+
"""Represents a conjoined map from an old filename to it's new name,
39+
relative to room or just by name alone."""
40+
_tuple_map: dict[tuple[str, str], str]
41+
_name_map: dict[str, str]
42+
43+
def __init__(self, mapper: Iterator[tuple[str, str, str]]):
44+
self._tuple_map = {}
45+
self._name_map = {}
46+
for parent, name, value in mapper:
47+
self._tuple_map[(parent, name)] = value
48+
self._name_map[name] = value
49+
50+
def get(self, dirpath: str, parent: str, name: str) -> str | None:
51+
return (self._tuple_map.get((dirpath, name)) or
52+
self._tuple_map.get((parent, name)) or
53+
self._name_map.get(name))
54+
55+
56+
def slugify(name: str) -> str:
57+
"""Remove filesystem-unfriendly chars"""
58+
name = name.strip()
59+
name = re.sub(r'[\\/:"*?<>|]+', '', name)
60+
name = re.sub(r'\s+', '-', name)
61+
return name

.github/structure/_Sidebar.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[**Home**](Home)
2+
[**Schedule**](Home#outcomes) <!--If structure for schedules is made, they could be moved to wiki-->
3+
[**Syllabus**](/instruction/syllabus/syllabus.md)
4+
[**Pet Shop**](/petshop/petshop.md)
5+
6+
[**Auto-Grader**](https://cs240.click)
7+
[**Help Queue**](https://help.cs240.click)
8+
9+
## Chess
10+
11+
[**Chess Application**](/chess/chess.md)
12+
<!--Chess Assignments-->
13+
[**GitHub Repository**](/chess/chess-github-repository/chess-github-repository.md)
14+
[**Phase 0**](/chess/0-chess-moves/chess-moves.md)
15+
[**Phase 1**](/chess/1-chess-game/chess-game.md)
16+
[**Phase 2**](/chess/2-server-design/server-design.md)
17+
[**Phase 3**](/chess/3-web-api/web-api.md)
18+
[**Phase 4**](/chess/4-database/database.md)
19+
[**Phase 5**](/chess/5-pregame/pregame.md)
20+
[**Phase 6**](/chess/6-gameplay/gameplay.md)
21+
<!--I don't think we need to link to getting started directly through here?-->
22+
23+
## Instruction
24+
25+
[**Instructional topics**](/instruction/modules.md)
26+
<!--Write out topics in either alphabetical or instruction order, or only parent links within modules.md-->
27+
28+
<!--Files not listed:
29+
- Chess: Code Quality Rubric
30+
- Phase 0: Game of Chess
31+
- Phase 3: TA Tips
32+
- Phase 4: Debugging Tips
33+
- Chess Phases: Getting Started (6)
34+
- All 40 instruction topics
35+
-->
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Wiki Content Sync
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
repository_dispatch:
8+
types: [docs]
9+
workflow_dispatch:
10+
gollum:
11+
12+
env:
13+
GIT_AUTHOR_NAME: GitHub Action
14+
GIT_AUTHOR_EMAIL: action@github.com
15+
GITHUB_REPO: ${{ github.server_url }}/${{ github.repository }}/blob/main
16+
17+
jobs:
18+
sync-content-to-wiki:
19+
if: always() && format('refs/heads/{0}', github.event.repository.default_branch) == github.ref && github.event_name != 'gollum'
20+
runs-on: [ ubuntu-latest ]
21+
steps:
22+
- name: Checkout Repo
23+
uses: actions/checkout@v4
24+
- name: Group Document files
25+
run: |
26+
rsync -av --exclude='.git*' --exclude='LICENSE' --include='petshop/*' --exclude='petshop/*/*' --exclude='*/example-code/' --exclude='schedule' --exclude='README.md' --exclude='instruction/template' ./ docs/
27+
find docs -empty -type d -delete
28+
cp README.md docs/Home.md
29+
cp -ar .github/structure/. docs/
30+
- name: Install dependencies for renaming
31+
run: |
32+
python3 -m pip install --upgrade pip
33+
- name: Rewrite markdown titles, filenames, and links
34+
run: |
35+
python3 .github/scripts/rewrite.py docs $GITHUB_REPO
36+
- name: Sync docs to wiki
37+
uses: newrelic/wiki-sync-action@v1.0.1
38+
with:
39+
source: docs
40+
destination: wiki
41+
token: ${{ secrets.WIKI_SYNC_SECRET }}
42+
gitAuthorName: ${{ env.GIT_AUTHOR_NAME }}
43+
gitAuthorEmail: ${{ env.GIT_AUTHOR_EMAIL }}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
.DS_Store
22
.idea/
3+
4+
# Used to store files while they're modified for Wiki.
5+
docs

0 commit comments

Comments
 (0)