553 lines
19 KiB
Python
553 lines
19 KiB
Python
"""
|
|
Generates the static website
|
|
|
|
Uses Jinja2 (see https://jinja.palletsprojects.com/en/2.11.x/)
|
|
"""
|
|
|
|
# TODO index.html tiles, content
|
|
# TODO index.html image (maybe downloaded and assembled from osgameclones)
|
|
# TODO index.html only count games
|
|
# TODO Font awesome 5 (icons for OS, for Github, Gitlab and maybe others)
|
|
# TODO contribute.html tiles? content
|
|
# TODO games pages links to licenses (wikipedia)
|
|
# TODO indexes: make categories bold that have a certain amount of entries!
|
|
# TODO everywhere: style URLs (Github, Wikipedia, Internet archive, SourceForge, ...)
|
|
# TODO developers pages links to games and more information, styles
|
|
# TODO inspirations pages, add link to games and more information, styles
|
|
# TODO navbar add is active
|
|
# TODO statistics page: better and more statistics with links where possible
|
|
# TODO meaningful information (links, license, last updated with lower precision)
|
|
# TODO singular, plural everywhere (game, entries, items)
|
|
# TODO background and shadow for the boxes
|
|
# TODO line breaks and spaces in html source and output
|
|
# TODO rename fields (Home to Homepage, Inspirations to Inspiration)
|
|
# TODO developers contact expand to links to Github, Sourceforge
|
|
# TODO games keywords as labels (some as links)
|
|
# TODO games links to licenses and languages
|
|
# TODO platforms as labels and with links
|
|
# TODO split games in libraries/tools/frameworks and real games
|
|
# TODO statistics with nice graphics (pie charts in SVG) with matplotlib, seaborn, plotly?
|
|
# TODO statistics, get it from common statistics generator
|
|
# TODO optimize jinja for line breaks and indention
|
|
# TODO @notices in entries
|
|
|
|
import os
|
|
import shutil
|
|
import math
|
|
import datetime
|
|
from functools import partial
|
|
from utils import osg, constants as c, utils
|
|
from jinja2 import Environment, FileSystemLoader
|
|
import html5lib
|
|
|
|
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
extra = '0'
|
|
extended_alphabet = alphabet + extra
|
|
|
|
games_path = 'games'
|
|
inspirations_path = 'inspirations'
|
|
developers_path = 'developers'
|
|
|
|
plurals = {k: k+'s' for k in ('Assets license', 'Contact', 'Code language', 'Code license', 'Developer', 'Download', 'Inspiration', 'Game', 'Home', 'Organization', 'Platform')}
|
|
for k in ('Media', 'Play', 'Keywords'):
|
|
plurals[k] = k
|
|
for k in ('Code repository', 'Code dependency'):
|
|
plurals[k] = k[:-1] + 'ies'
|
|
|
|
def get_plural_or_singular(name, amount):
|
|
if not name in plurals.keys():
|
|
raise RuntimeError('"{}" not a known singular!'.format(name))
|
|
if amount == 1:
|
|
return name
|
|
return plurals[name]
|
|
|
|
html5parser = html5lib.HTMLParser(strict=True)
|
|
|
|
|
|
def write(text, file):
|
|
"""
|
|
|
|
:param text:
|
|
:param file:
|
|
"""
|
|
# validate text
|
|
try:
|
|
html5parser.parse(text)
|
|
except Exception as e:
|
|
utils.write_text(os.path.join(c.web_path, 'invalid.html'), text) # for further checking with https://validator.w3.org/
|
|
raise RuntimeError(e)
|
|
# output file
|
|
file = os.path.join(c.web_path, file)
|
|
# create output directory if necessary
|
|
containing_dir = os.path.dirname(file)
|
|
if not os.path.isdir(containing_dir):
|
|
os.mkdir(containing_dir)
|
|
# write text
|
|
utils.write_text(file, text)
|
|
|
|
|
|
def sort_into_categories(list, categories, fit, unknown_category_name=None):
|
|
"""
|
|
|
|
:param list:
|
|
:param categories:
|
|
:param fit:
|
|
:param unknown_category_name:
|
|
:return:
|
|
"""
|
|
categorized_sublists = {}
|
|
for category in categories:
|
|
sublist = [item for item in list if fit(item, category)]
|
|
categorized_sublists[category] = sublist
|
|
if unknown_category_name:
|
|
# now those that do not fit
|
|
sublist = [item for item in list if not any(fit(item, category) for category in categories)]
|
|
categorized_sublists[unknown_category_name] = sublist
|
|
return categorized_sublists
|
|
|
|
|
|
def divide_in_columns(categorized_lists, transform):
|
|
"""
|
|
|
|
:param categorized_lists:
|
|
:param key:
|
|
:return:
|
|
"""
|
|
number_entries = {category: len(categorized_lists[category]) for category in categorized_lists.keys()}
|
|
entries = {}
|
|
for category in categorized_lists.keys():
|
|
e = categorized_lists[category]
|
|
# transform entry
|
|
e = [transform(e) for e in e]
|
|
# divide in three equal lists
|
|
n = len(e)
|
|
n1 = math.ceil(n/3)
|
|
n2 = math.ceil(2*n/3)
|
|
e = [e[:n1], e[n1:n2], e[n2:]]
|
|
entries[category] = e
|
|
return {'number_entries': number_entries, 'entries': entries}
|
|
|
|
|
|
def url_to(current, target):
|
|
"""
|
|
|
|
:param current: Current path
|
|
:param target:
|
|
:return:
|
|
"""
|
|
# if it's an absolute url, just return
|
|
if any(target.startswith(x) for x in ('http://', 'https://')):
|
|
return target
|
|
# split by slash
|
|
if current:
|
|
current = current.split('/')
|
|
target = target.split('/')
|
|
# reduce by common elements
|
|
while len(current) > 0 and len(target) > 1 and current[0] == target[0]:
|
|
current = current[1:]
|
|
target = target[1:]
|
|
# add .. as often as length of current still left
|
|
target = ['..'] * len(current) + target
|
|
# join by slash again
|
|
url = '/'.join(target)
|
|
return url
|
|
|
|
|
|
def preprocess(list, key, path):
|
|
"""
|
|
|
|
:param list:
|
|
:param key:
|
|
:return:
|
|
"""
|
|
_ = set()
|
|
for item in list:
|
|
# add unique anchor ref
|
|
anchor = osg.canonical_name(item[key])
|
|
while anchor in _:
|
|
anchor += 'x'
|
|
_.add(anchor)
|
|
item['anchor-id'] = anchor
|
|
|
|
# for alphabetic sorting
|
|
start = item[key][0].upper()
|
|
if not start in alphabet:
|
|
start = extra
|
|
item['letter'] = start
|
|
item['href'] = os.path.join(path, '{}.html#{}'.format(start, anchor))
|
|
|
|
|
|
def game_index(entry):
|
|
e = {
|
|
'name': entry['Title'],
|
|
'href': entry['href'],
|
|
'anchor-id': entry['anchor-id']
|
|
}
|
|
tags = []
|
|
if 'beta' in entry['State']:
|
|
tags.append('beta')
|
|
if osg.is_inactive(entry):
|
|
tags.append('inactive since {}'.format(osg.extract_inactive_year(entry)))
|
|
if tags:
|
|
e['tags'] = ', '.join(tags)
|
|
return e
|
|
|
|
|
|
def inspiration_index(inspiration):
|
|
e = {
|
|
'name': inspiration['Name'],
|
|
'href': inspiration['href'],
|
|
'anchor-id': inspiration['anchor-id'],
|
|
}
|
|
n = len(inspiration['Inspired entries'])
|
|
if n > 1:
|
|
e['tags'] = n
|
|
return e
|
|
|
|
|
|
def developer_index(developer):
|
|
e = {
|
|
'name': developer['Name'],
|
|
'href': developer['href'],
|
|
'anchor-id': developer['anchor-id']
|
|
}
|
|
n = len(developer['Games'])
|
|
if n > 1:
|
|
e['tags'] = n
|
|
return e
|
|
|
|
def shortcut_url(url):
|
|
|
|
# gitlab
|
|
gl_prefix = 'https://gitlab.com/'
|
|
if url.startswith(gl_prefix):
|
|
return 'GL: ' + url[len(gl_prefix):]
|
|
# github
|
|
gh_prefix = 'https://github.com/'
|
|
if url.startswith(gh_prefix):
|
|
return 'GH: ' + url[len(gh_prefix):]
|
|
|
|
# sourceforge
|
|
sf_prefix = 'https://sourceforge.net/projects/'
|
|
if url.startswith(sf_prefix):
|
|
return 'SF: ' + url[len(sf_prefix):]
|
|
|
|
# archive link
|
|
ia_prefix = 'https://web.archive.org/web/'
|
|
if url.startswith(ia_prefix):
|
|
return 'Archive: ' + url[len(ia_prefix):]
|
|
|
|
# Wikipedia link
|
|
wp_prefix = 'https://en.wikipedia.org/wiki/'
|
|
if url.startswith(wp_prefix):
|
|
return 'WP: ' + url[len(wp_prefix):]
|
|
|
|
# cutoff common prefixes
|
|
for prefix in ('http://', 'https://'):
|
|
if url.startswith(prefix):
|
|
return url[len(prefix):]
|
|
# as is
|
|
return url
|
|
|
|
|
|
def convert_inspirations(inspirations, entries):
|
|
entries_references = {entry['Title']:entry['href'] for entry in entries}
|
|
for inspiration in inspirations:
|
|
fields = []
|
|
# media
|
|
if 'Media' in inspiration:
|
|
entries = inspiration['Media']
|
|
entries = [{'href': url, 'name': shortcut_url(url)} for url in entries]
|
|
field = {
|
|
'name': 'Media',
|
|
'entries': entries
|
|
}
|
|
fields.append(field)
|
|
# inspired entries (with links to them)
|
|
inspired_entries = inspiration['Inspired entries']
|
|
entries = [{'href': entries_references[entry], 'name': entry} for entry in inspired_entries]
|
|
field = {
|
|
'name': 'Inspired {}'.format(get_plural_or_singular('Game', len(entries)).lower()),
|
|
'entries': entries
|
|
}
|
|
fields.append(field)
|
|
inspiration['fields'] = fields
|
|
inspiration['name'] = inspiration['Name']
|
|
|
|
|
|
def convert_developers(developers, entries):
|
|
entries_references = {entry['Title']:entry['href'] for entry in entries}
|
|
for developer in developers:
|
|
fields = []
|
|
# games field
|
|
developed_entries = developer['Games']
|
|
entries = [{'href': entries_references[entry], 'name': entry} for entry in developed_entries]
|
|
field = {
|
|
'name': 'Open source {}'.format(get_plural_or_singular('Game', len(entries))),
|
|
'entries': entries
|
|
}
|
|
fields.append(field)
|
|
for field in c.optional_developer_fields:
|
|
if field in developer:
|
|
entries = developer[field]
|
|
if field in c.url_developer_fields:
|
|
entries = [{'href': entry, 'name': shortcut_url(entry)} for entry in entries]
|
|
else:
|
|
entries = [{'href': '', 'name': entry} for entry in entries]
|
|
field = {
|
|
'name': get_plural_or_singular(field, len(entries)),
|
|
'entries': entries
|
|
}
|
|
fields.append(field)
|
|
developer['fields'] = fields
|
|
developer['name'] = developer['Name']
|
|
|
|
|
|
def convert_entries(entries, inspirations, developers):
|
|
inspirations_references = {inspiration['Name']: inspiration['href'] for inspiration in inspirations}
|
|
developer_references = {developer['Name']: developer['href'] for developer in developers}
|
|
for entry in entries:
|
|
fields = []
|
|
for field in ('Home', 'Inspirations', 'Media', 'Download', 'Play', 'Developer', 'Keywords'):
|
|
if field in entry:
|
|
e = entry[field]
|
|
if field == 'Inspirations':
|
|
field = 'Inspiration' # TODO this is a bug, rename in entries
|
|
if isinstance(e[0], osg.osg_parse.ValueWithComment):
|
|
e = [x.value for x in e]
|
|
if field == 'Inspiration':
|
|
e = [{'href': inspirations_references[x], 'name': x} for x in e]
|
|
elif field == 'Developer':
|
|
e = [{'href': developer_references[x], 'name': x} for x in e]
|
|
elif field in c.url_fields:
|
|
e = [{'href': x, 'name': shortcut_url(x)} for x in e]
|
|
else:
|
|
e = [{'href': '', 'name': x} for x in e]
|
|
field = {
|
|
'title': {'name': get_plural_or_singular(field, len(entries))},
|
|
'entries': e
|
|
}
|
|
fields.append(field)
|
|
if 'Note' in entry:
|
|
fields.append({'entries': [{'href': '', 'name': entry['Note']}]})
|
|
fields.append({'title': 'Technical info', 'entries': []})
|
|
for field in ('Platform', 'Code language', 'Code license', 'Code repository', 'Code dependencies', 'Assets license'):
|
|
if field in entry:
|
|
e = entry[field]
|
|
if not e:
|
|
continue
|
|
if field == 'Code dependencies':
|
|
field = 'Code dependency' # bug, rename field
|
|
if isinstance(e[0], osg.osg_parse.ValueWithComment):
|
|
e = [x.value for x in e]
|
|
if field in c.url_fields:
|
|
e = [{'href': x, 'name': shortcut_url(x)} for x in e]
|
|
else:
|
|
e = [{'href': '', 'name': x} for x in e]
|
|
field = {
|
|
'title': {'name': get_plural_or_singular(field, len(entries))},
|
|
'entries': e
|
|
}
|
|
fields.append(field)
|
|
entry['fields'] = fields
|
|
entry['name'] = entry['Title']
|
|
|
|
def add_license_links_to_entries(entries):
|
|
for entry in entries:
|
|
licenses = entry['Code license']
|
|
licenses = [(c.license_urls.get(license.value, ''), license.value) for license in licenses]
|
|
entry['Code license'] = licenses
|
|
|
|
|
|
def generate(entries, inspirations, developers):
|
|
"""
|
|
|
|
:param entries:
|
|
:param inspirations:
|
|
:param developers:
|
|
"""
|
|
|
|
# preprocess
|
|
preprocess(entries, 'Title', games_path)
|
|
preprocess(inspirations, 'Name', inspirations_path)
|
|
preprocess(developers, 'Name', developers_path)
|
|
|
|
# set internal links up
|
|
convert_inspirations(inspirations, entries)
|
|
convert_developers(developers, entries)
|
|
convert_entries(entries, inspirations, developers)
|
|
|
|
# set external links up
|
|
add_license_links_to_entries(entries)
|
|
|
|
# sort into categories
|
|
games_by_alphabet = sort_into_categories(entries, extended_alphabet, lambda item, category: category == item['letter'])
|
|
inspirations_by_alphabet = sort_into_categories(inspirations, extended_alphabet, lambda item, category: category == item['letter'])
|
|
developers_by_alphabet = sort_into_categories(developers, extended_alphabet, lambda item, category: category == item['letter'])
|
|
|
|
genres = [keyword.capitalize() for keyword in c.recommended_keywords]
|
|
games_by_genre = sort_into_categories(entries, genres, lambda item, category: category.lower() in item['Keywords'])
|
|
games_by_platform = sort_into_categories(entries, c.valid_platforms, lambda item, category: category in item.get('Platform', []), 'Unspecified')
|
|
games_by_language = sort_into_categories(entries, c.known_languages, lambda item, category: category in item['Code language'])
|
|
|
|
# base dictionary
|
|
base = {
|
|
'title': 'OSGL',
|
|
'creation-date': datetime.datetime.utcnow()
|
|
}
|
|
|
|
# copy bulma css
|
|
os.mkdir(c.web_css_path)
|
|
shutil.copy2(os.path.join(c.web_template_path, 'bulma.min.css'), c.web_css_path)
|
|
|
|
# create Jinja Environment
|
|
environment = Environment(loader=FileSystemLoader(c.web_template_path), autoescape=True)
|
|
environment.globals['base'] = base
|
|
|
|
# multiple times used templates
|
|
template_categorical_index = environment.get_template('categorical_index.jinja')
|
|
template_listing = environment.get_template('listing.jinja')
|
|
|
|
# top level folder
|
|
base['url_to'] = partial(url_to, '')
|
|
|
|
# index.html
|
|
base['active_nav'] = 'index'
|
|
index = {'number_games': len(entries)}
|
|
template = environment.get_template('index.jinja')
|
|
write(template.render(index=index), 'index.html')
|
|
|
|
# contribute.html
|
|
base['active_nav'] = 'contribute'
|
|
template = environment.get_template('contribute.jinja')
|
|
write(template.render(), 'contribute.html')
|
|
|
|
# statistics
|
|
base['active_nav'] = 'statistics'
|
|
|
|
# preparation
|
|
template = environment.get_template('statistics.jinja')
|
|
data = {
|
|
'title': 'Statistics',
|
|
'sections': []
|
|
}
|
|
|
|
# build-systems
|
|
build_systems = []
|
|
field = 'Build system'
|
|
for entry in entries:
|
|
if field in entry['Building']:
|
|
build_systems.extend(entry['Building'][field])
|
|
build_systems = [x.value for x in build_systems]
|
|
|
|
unique_build_systems = set(build_systems)
|
|
unique_build_systems = [(l, build_systems.count(l)) for l in unique_build_systems]
|
|
unique_build_systems.sort(key=lambda x: str.casefold(x[0])) # first sort by name
|
|
unique_build_systems.sort(key=lambda x: -x[1]) # then sort by occurrence (highest occurrence first)
|
|
section = {
|
|
'title': 'Build system',
|
|
'items': ['{} ({})'.format(*item) for item in unique_build_systems]
|
|
}
|
|
data['sections'].append(section)
|
|
write(template.render(data=data), os.path.join('statistics.html'))
|
|
|
|
# games folder
|
|
base['url_to'] = partial(url_to, games_path)
|
|
base['active_nav'] = 'games'
|
|
|
|
# generate games pages
|
|
for letter in extended_alphabet:
|
|
listing = {
|
|
'title': 'Games starting with {}'.format(letter.capitalize()),
|
|
'items': games_by_alphabet[letter]
|
|
}
|
|
write(template_listing.render(listing=listing), os.path.join(games_path, '{}.html'.format(letter.capitalize())))
|
|
|
|
# generate games index
|
|
index = divide_in_columns(games_by_alphabet, game_index)
|
|
index['title'] = 'Games alphabetical index'
|
|
index['categories'] = extended_alphabet
|
|
write(template_categorical_index.render(index=index), os.path.join(games_path, 'index.html'))
|
|
|
|
# genres
|
|
base['active_nav'] = 'filter genres'
|
|
index = divide_in_columns(games_by_genre, game_index)
|
|
index['title'] = 'Games by genre'
|
|
index['categories'] = genres
|
|
write(template_categorical_index.render(index=index), os.path.join(games_path, 'genres.html'))
|
|
|
|
# games by language
|
|
base['active_nav'] = 'filter code language'
|
|
index = divide_in_columns(games_by_language, game_index)
|
|
index['title'] = 'Games by language'
|
|
index['categories'] = c.known_languages
|
|
write(template_categorical_index.render(index=index), os.path.join(games_path, 'languages.html'))
|
|
|
|
# games by platform
|
|
base['active_nav'] = 'filter platforms'
|
|
index = divide_in_columns(games_by_platform, game_index)
|
|
index['title'] = 'Games by platform'
|
|
index['categories'] = c.valid_platforms + ('Unspecified',)
|
|
write(template_categorical_index.render(index=index), os.path.join(games_path, 'platforms.html'))
|
|
|
|
# inspirations folder
|
|
base['url_to'] = partial(url_to, inspirations_path)
|
|
base['active_nav'] = 'filter inspirations'
|
|
|
|
# inspirations
|
|
|
|
# inspirations index
|
|
index = divide_in_columns(inspirations_by_alphabet, inspiration_index)
|
|
index['title'] = 'Inspirations alphabetical index'
|
|
index['categories'] = extended_alphabet
|
|
write(template_categorical_index.render(index=index), os.path.join(inspirations_path, 'index.html'))
|
|
|
|
# inspirations single pages
|
|
for letter in extended_alphabet:
|
|
listing = {
|
|
'title': 'Inspirations ({})'.format(letter.capitalize()),
|
|
'items': inspirations_by_alphabet[letter]
|
|
}
|
|
write(template_listing.render(listing=listing), os.path.join(inspirations_path, '{}.html'.format(letter.capitalize())))
|
|
|
|
# developers folder
|
|
base['url_to'] = partial(url_to, developers_path)
|
|
base['active_nav'] = 'developers'
|
|
|
|
# developers single pages
|
|
for letter in extended_alphabet:
|
|
listing = {
|
|
'title': 'Developers ({})'.format(letter.capitalize()),
|
|
'items': developers_by_alphabet[letter]
|
|
}
|
|
write(template_listing.render(listing=listing), os.path.join(developers_path, '{}.html'.format(letter.capitalize())))
|
|
|
|
# developers index
|
|
index = divide_in_columns(developers_by_alphabet, developer_index)
|
|
index['title'] = 'Developers alphabetical index'
|
|
index['categories'] = extended_alphabet
|
|
write(template_categorical_index.render(index=index), os.path.join(developers_path, 'index.html'))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
# clean the output directory
|
|
print('clean current static website')
|
|
utils.recreate_directory(c.web_path)
|
|
|
|
# load entries, inspirations and developers and sort them
|
|
print('load entries, inspirations and developers')
|
|
entries = osg.read_entries()
|
|
entries.sort(key=lambda x: str.casefold(x['Title']))
|
|
|
|
inspirations = osg.read_inspirations()
|
|
inspirations = list(inspirations.values())
|
|
inspirations.sort(key=lambda x: str.casefold(x['Name']))
|
|
|
|
developers = osg.read_developers()
|
|
developers = list(developers.values())
|
|
developers.sort(key=lambda x: str.casefold(x['Name']))
|
|
|
|
# re-generate static website
|
|
print('re-generate static website')
|
|
generate(entries, inspirations, developers) |