diff --git a/README.md b/README.md index e07d5a7..2d7f631 100644 --- a/README.md +++ b/README.md @@ -13,22 +13,28 @@ Everyone is welcome to contribute! Simply send in a pull request with your useful link, and if it passes quality control, it will be merged and made available to the public. -To add a link, add the shortcut link and description to the relevant section in -[`src/index.html`][3]. You can also add new sections if none of the existing -sections fit the bill. +To add a link, add find the relevant section in under [`src/links.yml`][3], +and under the `links` key, add a new item for your link. This item should be a +mapping with three keys: -To add the redirect, add it to [`src/redirects.conf`][4], into the same place -as you did in `src/index.html`. `redirects.conf` is included inside an -[nginx `map` block][5], and the syntax is: +* `name`: the shortcut link, starting with `/`, followed by letters, numbers, + and `-`; +* `target`: the URL to redirect to; and +* `description`: the description of the link shown on the home page. -``` -/shortcut "https://example.com/long/url"; -``` +To be able to run the python scripts locally, run +`pip install -r requirements.txt` to install our dependencies. + +To verify that your changes follow the correct format, run automatic sanity +checks with [`python3 check.py`][4]. + +To generate the HTML for the site, run [`python3 build.py`][5]. Output will be +generated in a directory called `dist`. Thank you for contributing. [1]: https://uwat.cf [2]: https://uwat.cf/exams - [3]: src/index.html - [4]: src/redirects.conf - [5]: https://nginx.org/en/docs/http/ngx_http_map_module.html + [3]: src/links.yml + [4]: check.py + [5]: build.py diff --git a/build.py b/build.py index cca0f2d..63156ea 100644 --- a/build.py +++ b/build.py @@ -1,7 +1,10 @@ +#!/usr/bin/env python3 import errno import os +from html import escape from hashlib import sha256 +import yaml from rcssmin import cssmin DIR = os.path.dirname(__file__) @@ -10,8 +13,6 @@ DIST_DIR = os.path.join(DIR, 'dist') ASSETS_SRC = os.path.join(SRC_DIR, 'assets') ASSETS_DIST = os.path.join(DIST_DIR, 'assets') -bytes = type(b'') - def build_assets(): name_map = [] @@ -26,29 +27,62 @@ def build_assets(): dist_name = '%s-%s%s' % (name, hash, ext) if ext == '.css': - content = cssmin(content) + content = cssmin(content.decode('utf-8')).encode('utf-8') with open(os.path.join(ASSETS_DIST, dist_name), 'wb') as f: f.write(content) - name_map.append((bytes(asset), bytes(dist_name))) + name_map.append((asset, dist_name)) return name_map -def build_files(html_replace): - for name in os.listdir(SRC_DIR): - src_path = os.path.join(SRC_DIR, name) - if not os.path.isfile(src_path): - continue +def build_links(links): + output = [] + for section in links['sections']: + output.append('

%s

' % (section['id'], escape(section['name']))) + output.append(' ') + output.append('') - with open(os.path.join(DIST_DIR, name), 'wb') as f: - f.write(content) + return '\n'.join(output) + + +def build_redirects(links): + output = [] + + def build_link(link): + output.append('%s "%s";' % (link['name'], link['target'])) + + if 'other_links' in links: + for link in links['other_links']: + build_link(link) + output.append('') + + for section in links['sections']: + output.append('# %s' % (section['name'],)) + for link in section['links']: + build_link(link) + output.append('') + + with open(os.path.join(DIST_DIR, 'redirects.conf'), 'w', encoding='utf-8') as f: + f.write('\n'.join(output)) + + +def build_index(html_replace, links): + with open(os.path.join(SRC_DIR, 'index.html'), encoding='utf-8') as f: + content = f.read() + + for old, new in html_replace: + content = content.replace(old, new) + content = content.replace('{listing}', build_links(links)) + + with open(os.path.join(DIST_DIR, 'index.html'), 'w', encoding='utf-8') as f: + f.write(content) def main(): @@ -58,8 +92,13 @@ def main(): if e.errno != errno.EEXIST: raise + with open(os.path.join(SRC_DIR, 'links.yml'), encoding='utf-8') as f: + links = yaml.safe_load(f) + name_map = build_assets() - build_files(name_map) + build_index(name_map, links) + build_redirects(links) + if __name__ == '__main__': main() diff --git a/check.py b/check.py new file mode 100644 index 0000000..3825d7f --- /dev/null +++ b/check.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import os +import re + +import yaml + +SRC_DIR = os.path.join(os.path.dirname(__file__), 'src') + + +def ensure(cond, output): + if not cond: + raise SystemExit(output) + + +def check_link(link, description=True): + ensure(isinstance(link, dict), 'a link must be dict, not: %s' % (link,)) + + ensure('name' in link, 'a link must contain a name: %s' % (link,)) + ensure(isinstance(link['name'], str), 'key "name" under link must be string: %s' % (link,)) + ensure(link['name'].startswith('/'), 'the name of a link must start with /: %s' % (link,)) + ensure(re.match('^/[a-z0-9-]+$', link['name']), + 'the name of a link must be / followed by letters, numbers, and -, not %s' % (link['name'],)) + + ensure('target' in link, 'link "%s" must contain a target' % (link['name'],)) + ensure(isinstance(link['target'], str), 'key "target" under link "%s" must be string' % (link['name'],)) + + if description: + ensure('description' in link, 'link "%s" must contain a description' % (link['name'],)) + ensure(isinstance(link['description'], str), + 'key "description" under link "%s" must be string' % (link['name'],)) + + +def main(): + with open(os.path.join(SRC_DIR, 'links.yml'), encoding='utf-8') as f: + links = yaml.safe_load(f) + + unique = set() + + ensure('sections' in links, 'links.yml should contain key "sections"') + ensure(isinstance(links['sections'], list), 'key "sections" should map to a list') + + for section in links['sections']: + ensure(isinstance(section, dict), 'every item in "sections" should be a dict') + + ensure('id' in section, 'every section must have an id') + ensure(isinstance(section['id'], str), 'section IDs must be strings') + ensure(re.match('^[a-z-]+$', section['id']), 'section IDs should only contain lowercase letters and -') + + ensure('name' in section, 'every section must have a name') + ensure(isinstance(section['name'], str), 'section names must be strings') + + ensure('links' in section, 'every section must have links') + ensure(isinstance(section['links'], list), 'links under %s must be a list' % (section['id'],)) + + for link in section['links']: + check_link(link) + + if link['name'] in unique: + raise SystemExit('duplicate link "%s"' % link['name']) + unique.add(link['name']) + + if 'other_links' in links: + ensure(isinstance(links['other_links'], list), 'other_links must be a list') + + for link in links['other_links']: + check_link(link, description=False) + + if link['name'] in unique: + raise SystemExit('duplicate link "%s"' % link['name']) + unique.add(link['name']) + + with open(os.path.join(SRC_DIR, 'index.html'), encoding='utf-8') as f: + contents = f.read() + ensure('{listing}' in contents, 'index.html should have {listing}') + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5ab8c00 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyyaml +rcssmin diff --git a/src/index.html b/src/index.html index 1a0dfe3..8c06e33 100644 --- a/src/index.html +++ b/src/index.html @@ -12,36 +12,7 @@

Useful UWaterloo Links

All /links can be accessed as uwat.cf/link.

-

General

- - -

Faculty of Mathematics

- - -

Faculty of Engineering

- - -

Co-operative Education

- - -

Finances

- +{listing}

About

Want to add more links? Send us a pull request on GitHub!

diff --git a/src/links.yml b/src/links.yml new file mode 100644 index 0000000..d856321 --- /dev/null +++ b/src/links.yml @@ -0,0 +1,57 @@ +other_links: + - name: /link + target: / + +sections: + - id: general + name: General + links: + - name: /learn + target: https://learn.uwaterloo.ca/d2l/home + description: LEARN + - name: /quest + target: https://quest.pecs.uwaterloo.ca/psp/SS/ACADEMIC/SA/?cmd=login&languageCd=ENG + description: Quest + - name: /wp + target: https://idm.uwaterloo.ca/search/authen/ + description: WatIAM white pages + - name: /watcard + target: https://watcard.uwaterloo.ca/OneWeb/Account/LogOn + description: Manage my WatCard + - name: /exams + target: https://odyssey.uwaterloo.ca/teaching/schedule + description: Exam seating and schedules + + - id: math + name: Faculty of Mathematics + links: + - name: /marmoset + target: https://marmoset.student.cs.uwaterloo.ca/ + description: Marmoset + - name: /mathexams + target: http://mathsoc.uwaterloo.ca/exambank + description: MathSoc Exam Bank + + - id: eng + name: Faculty of Engineering + links: + - name: /engexams + target: https://exams.engsoc.uwaterloo.ca/ + description: EngSoc Exam Bank + - name: /engrank + target: https://engug.uwaterloo.ca/ + description: Undergrad student rankings + + - id: coop + name: Co-operative Education + links: + - name: /coopcal + target: https://uwaterloo.ca/co-operative-education/important-dates + description: Co-op important dates + + - id: finance + name: Finances + links: + - name: /endow + target: https://uwaterloo.ca/forms/finance/user?destination=endowment_request + description: Endowment refund diff --git a/src/redirects.conf b/src/redirects.conf deleted file mode 100644 index caf91bd..0000000 --- a/src/redirects.conf +++ /dev/null @@ -1,23 +0,0 @@ -# Example link -/link "https://uwat.cf/"; - -# General -/learn "https://learn.uwaterloo.ca/d2l/home"; -/quest "https://quest.pecs.uwaterloo.ca/psp/SS/ACADEMIC/SA/?cmd=login&languageCd=ENG"; -/wp "https://idm.uwaterloo.ca/search/authen/"; -/watcard "https://watcard.uwaterloo.ca/OneWeb/Account/LogOn"; -/exams "https://odyssey.uwaterloo.ca/teaching/schedule"; - -# Faculty of Math -/marmoset "https://marmoset.student.cs.uwaterloo.ca/"; -/mathexams "http://mathsoc.uwaterloo.ca/exambank"; - -# Faculty of Engineering -/engexams "https://exams.engsoc.uwaterloo.ca/"; -/engrank "https://engug.uwaterloo.ca/"; - -# Co-op -/coopcal "https://uwaterloo.ca/co-operative-education/important-dates"; - -# Finances -/endow "https://uwaterloo.ca/forms/finance/user?destination=endowment_request";