diff options
| author | Kévin Le Gouguec <kevin.legouguec@gmail.com> | 2021-10-31 23:35:35 +0100 |
|---|---|---|
| committer | Kévin Le Gouguec <kevin.legouguec@gmail.com> | 2022-02-07 00:34:51 +0100 |
| commit | 214f6aa6553ecdfa11d2ec63bb1cef1a49e8538d (patch) | |
| tree | 1d81f5b56ac8c3b52a0839b3a87f87280309e7e3 /build-concerts.py | |
| parent | 7dbc6d60c4f8da78c2ed8f9e6dde694b5a9d0d3e (diff) | |
| download | quatuorbellefeuille.com-214f6aa6553ecdfa11d2ec63bb1cef1a49e8538d.tar.xz | |
Add simplified concerts syntax
Diffstat (limited to 'build-concerts.py')
| -rwxr-xr-x | build-concerts.py | 358 |
1 files changed, 358 insertions, 0 deletions
diff --git a/build-concerts.py b/build-concerts.py new file mode 100755 index 0000000..3581421 --- /dev/null +++ b/build-concerts.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 + +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import datetime +import locale +from operator import attrgetter +from pathlib import Path +import re +from sys import argv +from typing import Iterator, Optional + +from helpers import relative_path + + +# TODO: change some jargon: +# - event => concert +# - canceled => warning + + +LICENSE_URLS = { + 'CC0': 'https://creativecommons.org/publicdomain/zero', + 'CC BY': 'https://creativecommons.org/licenses/by', + 'CC BY-SA': 'https://creativecommons.org/licenses/by-sa', +} + +LICENSE_RE = re.compile( + '('+'|'.join(LICENSE_URLS.keys())+')' + ' ([0-9.]+)' +) + + +@dataclass +class LicenseInfo: + tag: str + version: str + + @classmethod + def deserialize(cls, info): + if info is None: + return None + return cls(*LICENSE_RE.fullmatch(info).groups()) + + def format(self): + url = f'{LICENSE_URLS[self.tag]}/{self.version}/' + + return f'<a href="{url}" target="_blank">{self.tag}</a>' + + +@dataclass +class Illustration: + file: str + alt_text: str + source_name: str + source_link: Optional[str] + license_info: Optional[LicenseInfo] + + @classmethod + def deserialize(cls, d): + return cls(d['pic_file'], + d['pic_alt'], + d['pic_src'], + d['pic_link'], + LicenseInfo.deserialize(d['pic_license'])) + + +@dataclass +class Concert: + time: datetime + place: str + address: str + pieces: Iterator[str] + instructions: str + illustration: Illustration + warning: Optional[str] + + @classmethod + def deserialize(cls, d): + return cls( + time=datetime.strptime(d['time'], '%d/%m/%Y %Hh%M'), + place=d['place'], + address=d['address'], + pieces=d['pieces'], + instructions=d['instructions'], + illustration=Illustration.deserialize(d), + warning=d['warning'] + ) + + +def optional(line): + return f'(?:{line})?' + + +CONCERT_LINES = ( + r'QUAND : (?P<time>[^\n]+)\n', + r'O[UÙ] : (?P<place>[^\n]+)\n', + 'ADRESSE :\n', + '(?P<address>.+?)\n', + 'PROGRAMME :\n', + '(?P<pieces>.+?)\n', + 'INSTRUCTIONS :\n', + '(?P<instructions>.+?)\n', + 'ILLUSTRATION :\n', + r'fichier : (?P<pic_file>[^\n]+)\n', + r'légende : (?P<pic_alt>[^\n]+)\n', + r'source : (?P<pic_src>[^\n]+)\n', + optional(r'lien : (?P<pic_link>[^\n]+)\n'), + optional(r'licence : (?P<pic_license>[^\n]+)\n'), + optional(r'AVERTISSEMENT : (?P<warning>[^\n]+)\n'), +) + +CONCERT_RE = re.compile(''.join(CONCERT_LINES), flags=re.DOTALL) + + +def guess_language(filename): + parent = str(Path(filename).parent) + if parent == '.': + return 'fr' + return parent + + +def read_concerts(filename): + with open(filename) as f: + concerts = ( + Concert.deserialize(match) + for match in re.finditer(CONCERT_RE, f.read()) + ) + return tuple(sorted(concerts, key=attrgetter('time'))) + + +def split_concerts(concerts, threshold): + for i, c in enumerate(concerts): + if c.time > threshold: + return concerts[:i], concerts[i:] + + return concerts, () + + +LOCALIZED_TEXT = { + 'en': { + 'past': 'Past concerts', + 'next': 'Next concerts', + 'alt': 'Illustration:', + 'hint': 'Click on a concert to obtain more information.', + }, + 'fr': { + 'past': 'Concerts passés', + 'next': 'Prochains concerts', + 'alt': 'Illustration :', + 'hint': "Cliquez sur un concert pour obtenir plus d'informations.", + } +} + +THUMBNAILS_TEMPLATE = '''\ + <h1>{heading}</h1> + <div class="events {time}"> +{thumbnails} + </div>\ +''' + +THUMBNAIL_TEMPLATE = '''\ + <div class="{eventclasses}"> + <a class="thumbnail" href="#{eventid}"> + <img src="{pic_file}" alt="{pic_alt}"> + <p class="summary"> + {summary} + </p> + </a> + <div class="credits"> + <span> + {credits} + </span> + </div> + </div>\ +''' + + +@contextmanager +def tmplocale(lang): + old_lang, encoding = locale.getlocale() + try: + locale.setlocale(locale.LC_TIME, (lang, encoding)) + yield + finally: + locale.setlocale(locale.LC_TIME, (old_lang, encoding)) + + +def format_credits(illustration): + credits = illustration.source_name + + if illustration.source_link is not None: + credits = (f'<a href="{illustration.source_link}" target="_blank">' + f'{illustration.source_name}' + '</a>') + + if illustration.license_info is not None: + credits += ' / ' + illustration.license_info.format() + + return credits + + +def format_thumbnail(concert, imgdir, lang): + eventclasses = ('event',) + with tmplocale(lang): + day = f'{concert.time.day} {concert.time.strftime("%B %Y")}' + summary = f'{concert.place}<br>{day}' + + if concert.warning is not None: + eventclasses += ('canceled',) + summary = (f'<span class="canceled">{concert.warning}</span>\n' + f' {summary}') + + alt_prefix = LOCALIZED_TEXT[lang]['alt'] + + return THUMBNAIL_TEMPLATE.format_map({ + 'eventclasses': ' '.join(eventclasses), + 'eventid': f'concert-{concert.time.strftime("%F")}', + 'pic_file': Path(imgdir, 'concerts', concert.illustration.file), + 'pic_alt': f'{alt_prefix} {concert.illustration.alt_text}', + 'summary': summary, + 'credits': format_credits(concert.illustration) + }) + + +def print_thumbnails_section(concerts, imgdir, section, lang): + if not concerts: + return + + thumbnails = '\n'.join( + format_thumbnail(c, imgdir, lang) for c in concerts + ) + + print(THUMBNAILS_TEMPLATE.format(heading=LOCALIZED_TEXT[lang][section], + time=section, + thumbnails=thumbnails)) + + +def print_thumbnails(concerts, imgdir, lang): + today = datetime.fromordinal( + datetime.today().date().toordinal() + ) + past_concerts, next_concerts = split_concerts(concerts, today) + + print('<div id="event-list">') + print_thumbnails_section(next_concerts, imgdir, 'next', lang) + print_thumbnails_section(past_concerts, imgdir, 'past', lang) + print('</div>') + + +DETAILS_TEMPLATE = '''\ + <div class="{concertclasses}" id="{concertid}"> +{details} + </div>\ +''' + + +DATE_FORMATTERS = { + 'en': { + 'date': lambda d: d.strftime('%A %B %-d, %Y'), + 'time': lambda d: d.strftime('%I:%M %p'), + }, + 'fr': { + 'date': lambda d: d.strftime('%A %-d %B %Y').capitalize(), + 'time': lambda d: d.strftime('%Hh%M'), + }, +} + + +def detail_block(tag, classes, content): + opener = f'<{tag} class="{" ".join(classes)}">' + closer = f'</{tag}>' + + if isinstance(content, str): + return f' {opener}{content}{closer}' + + return '\n'.join(( + ' '+opener, + *(' '+line for line in content), + ' '+closer, + )) + + +def break_lines(lines): + return tuple(line+'<br>' for line in lines[:-1]) + (lines[-1],) + + +TOUCHUPS = ( + (re.compile('([0-9])(st|nd|rd|th|er|ère|nde|ème)'), r'\1<sup>\2</sup>'), + (re.compile('(https://[^ ]+)'), r'<a href="\1" target="_blank">\1</a>'), + (re.compile('([^ ]+@[^ ]+)'), r'<a href="mailto:\1">\1</a>'), +) + + +def touchup_plaintext(plaintext): + text = plaintext + for regexp, repl in TOUCHUPS: + text = regexp.sub(repl, text) + return text + + +def print_concert_details(concert, lang): + concert_id = f'concert-{concert.time.strftime("%F")}' + classes = ('details',) + blocks = [] + + if concert.warning is not None: + classes += ('canceled',) + blocks.append( + detail_block('p', ('canceled',), concert.warning) + ) + + with tmplocale(lang): + day = DATE_FORMATTERS[lang]['date'](concert.time) + hour = DATE_FORMATTERS[lang]['time'](concert.time) + + address_lines = break_lines(concert.address.splitlines()) + piece_list = tuple( + f'<li>{touchup_plaintext(line)}</li>' + for line in concert.pieces.splitlines() + ) + + blocks.extend(( + detail_block('p', ('detail', 'date'), day), + detail_block('p', ('detail', 'time'), hour), + detail_block('p', ('detail', 'place'), address_lines), + detail_block('ol', ('detail', 'program'), piece_list), + )) + + instructions = [ + f' <p>{touchup_plaintext(line)}</p>' + for line in concert.instructions.splitlines() + ] + + print(f' <div class="{" ".join(classes)}" id="{concert_id}">') + print('\n'.join(blocks+instructions)) + print(' </div>') + + +def print_details(concerts, lang): + print('<div id="event-details">') + print(f' <p class="hint">{LOCALIZED_TEXT[lang]["hint"]}</p>') + + for c in concerts: + print_concert_details(c, lang) + + print('</div>') + + +def main(concerts_src): + imgdir = relative_path(to='images', ref=concerts_src) + lang = guess_language(concerts_src) + + concerts = read_concerts(concerts_src) + print_thumbnails(concerts, imgdir, lang) + print_details(concerts, lang) + + +if __name__ == '__main__': + main(argv[1]) |
