diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d680206 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.sif +*.tar diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ce37dd6..0000000 --- a/.gitmodules +++ /dev/null @@ -1,12 +0,0 @@ -[submodule "markdeep"] - path = markdeep-slides/markdeep - url = https://github.com/morgan3d/markdeep.git -[submodule "remark-cicero/cicero"] - path = remark-cicero/cicero - url = https://github.com/mlouhivu/cicero.git -[submodule "reveal.js"] - path = reveal.js - url = https://github.com/hakimel/reveal.js -[submodule "mathjax"] - path = mathjax - url = https://github.com/mathjax/MathJax.git diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..25622ed --- /dev/null +++ b/Dockerfile @@ -0,0 +1,87 @@ +FROM docker.io/alpine:3.17 AS slidefactory-files + +ARG VERSION + +ADD LICENSE /slidefactory/ +ADD fonts/ /slidefactory/fonts/ +ADD theme/ /slidefactory/theme/ +ADD slidefactory.py /slidefactory/ + +# Remove possible temporary files +RUN find /slidefactory -name '*~' -delete +RUN find /slidefactory -name '.*' -delete + +# Fix permissions +RUN chmod 755 /slidefactory && \ + find /slidefactory -type d -exec chmod 755 {} \; && \ + find /slidefactory -type f -exec chmod 644 {} \; && \ + chmod 755 /slidefactory/slidefactory.py + +# Add checksums +RUN cd /slidefactory && \ + find . -type f -print0 | xargs -0 sha256sum > /tmp/sha256sums_$VERSION && \ + mv /tmp/sha256sums_$VERSION /slidefactory/ + + +FROM docker.io/alpine:3.17 + +RUN apk update && \ + apk add --no-cache \ + ca-certificates \ + chromium \ + git \ + pandoc \ + font-freefont \ + python3 \ + py3-pandocfilters \ + py3-yaml \ + tar \ + zip \ + && \ + rm -rf /var/cache/apk/* + +# Reveal.js +RUN wget https://github.com/hakimel/reveal.js/archive/refs/tags/4.4.0.zip -O tmp.zip && \ + unzip tmp.zip 'reveal.js-4.4.0/LICENSE' -d /slidefactory && \ + unzip tmp.zip 'reveal.js-4.4.0/dist/*' -d /slidefactory && \ + unzip tmp.zip 'reveal.js-4.4.0/plugin/*' -d /slidefactory && \ + rm -f tmp.zip + +# MathJax +RUN wget https://github.com/mathjax/MathJax/archive/refs/tags/3.2.2.zip -O tmp.zip && \ + unzip tmp.zip 'MathJax-3.2.2/LICENSE' -d /slidefactory && \ + unzip tmp.zip 'MathJax-3.2.2/es5/tex-chtml-full.js' -d /slidefactory && \ + unzip tmp.zip 'MathJax-3.2.2/es5/input/tex/extensions/*' -d /slidefactory && \ + unzip tmp.zip 'MathJax-3.2.2/es5/output/chtml/fonts/woff-v2/*' -d /slidefactory && \ + unzip tmp.zip 'MathJax-3.2.2/es5/adaptors/*' -d /slidefactory && \ + rm -f tmp.zip + +# Fonts +RUN FONT_DIR=NotoSans && \ + mkdir -p /slidefactory/fonts/$FONT_DIR && \ + wget https://github.com/notofonts/latin-greek-cyrillic/releases/download/NotoSans-v2.013/NotoSans-v2.013.zip -O tmp.zip && \ + unzip -j tmp.zip 'NotoSans/googlefonts/ttf/*' -d /slidefactory/fonts/$FONT_DIR && \ + unzip -j tmp.zip 'OFL.txt' -d /slidefactory/fonts/$FONT_DIR && \ + rm tmp.zip + +RUN FONT_DIR=Inconsolata && \ + mkdir -p /slidefactory/fonts/$FONT_DIR && \ + wget https://github.com/googlefonts/Inconsolata/archive/refs/tags/v3.000.zip -O tmp.zip && \ + unzip -j tmp.zip 'Inconsolata-3.000/fonts/ttf/Inconsolata-*' -x '*Condensed*' '*Expanded*' -d /slidefactory/fonts/$FONT_DIR && \ + unzip -j tmp.zip 'Inconsolata-3.000/OFL.txt' -d /slidefactory/fonts/$FONT_DIR && \ + rm tmp.zip + +COPY --from=slidefactory-files /slidefactory/ /slidefactory/ + +# Create executable +RUN echo -e '#!/bin/sh\n\ +exec python3 /slidefactory/slidefactory.py "$@"\n\ +' > /usr/bin/slidefactory && \ + chmod a+x /usr/bin/slidefactory + +RUN mkdir /work + +WORKDIR /work + +ENTRYPOINT ["slidefactory"] +CMD ["-h"] diff --git a/INSTALL.md b/INSTALL.md deleted file mode 100644 index c0d6995..0000000 --- a/INSTALL.md +++ /dev/null @@ -1,93 +0,0 @@ -## Install - -Slidefactory consists of two parts: 1) a **git repo** containing files -defining the slide layout (aka themes), pandoc filters, and a convenience -script (`convert.py`) to ease the use of pandoc, and 2) a -**singularity container** with a self-contained software environment that is -tested to work with slidefactory. - - -### Download source code - -``` -git clone https://github.com/csc-training/slide-template -cd slide-template -``` - - -### Install full slidefactory - -To build the singularity container and install it together with the git -repository, you can simply say: -``` -make install -``` - -| Note: In order to build the container image, singularity needs to be -| installed (https://sylabs.io/guides/latest/admin-guide/installation.html) -| and you will need sudo rights. - -If you feel that `make` is too old skool (or your system doesn't have it), -there are also python scripts to do the same: -``` -python3 setup/build.py -python3 setup/install.py -``` - - -### Install only git repository (without container) - -If you prefer to use your local software environment, you can also just -install the git repo without the singularity container: -``` -make git -``` - -In order to use slidefactory, you need to also install all the dependencies: - - python3 - - python3-pandocfilters - - pandoc - - fonts-noto - - fonts-inconsolata - - pandoc-types (1.17.5.4) - - pandoc-emphasize-code - - chromium-browser - - -### Set environment variable SLIDEFACTORY - -After installation, you should edit your `.bashrc` or similar, to set the -environment variable `SLIDEFACTORY` to point to the installation location of -the git repository (as prompted by the installer) and to make sure the -directory containing the container image is in your `PATH`. - -For example: -``` -export SLIDEFACTORY=$HOME/lib/slidefactory -export PATH=$PATH:$HOME/bin -``` - - -### Custom installation location - -By default slidefactory will be installed under `bin/` and `lib/` in your -`$HOME` (i.e. `$HOME/bin/slidefactory.sif` and `$HOME/lib/slidefactory`). If -you prefer to install slidefactory in another location, you can use -environment variable `PREFIX` to point to your preferred location. - -For example to install under `/some/path/bin` and `/some/path/lib`, you can -say: -``` -PREFIX=/some/path make install -``` - -or to install just the git repo: -``` -PREFIX=/some/path make git -``` - -The python installation script has a similar option, please see -`setup/install.py --help` for more details. - -When uninstalling from a custom location, one needs to provide the same -`PREFIX` unless the environment variable `SLIDEFACTORY` is set correctly. diff --git a/LICENSE b/LICENSE index 4f1f1a9..d593fe7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2022 CSC - IT Center for Science Ltd. +Copyright (c) 2017-2023 CSC - IT Center for Science Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index d0aa87c..0c2c876 100644 --- a/Makefile +++ b/Makefile @@ -1,68 +1,24 @@ -DEF=slidefactory.def -SIF=slidefactory.sif +IMAGE_ROOT?=ghcr.io/csc-training +IMAGE=slidefactory +IMAGE_VERSION?=$(shell grep -m1 -oP '(?<=VERSION = ").+(?=")' slidefactory.py) -ifndef PREFIX - PREFIX=$(HOME) -endif -INSTALL_BIN=$(PREFIX)/bin -INSTALL_GIT=$(PREFIX)/lib/slidefactory -GIT=https://github.com/csc-training/slide-template -.PHONY: build clean install uninstall -.PHONY: check clone git +build: Dockerfile slidefactory.py + docker build \ + --label "org.opencontainers.image.source=https://github.com/csc-training/slidefactory" \ + --label "org.opencontainers.image.description=slidefactory" \ + --build-arg VERSION=${IMAGE_VERSION} \ + -t ${IMAGE_ROOT}/${IMAGE}:${IMAGE_VERSION} \ + . -build: $(SIF) +push: + docker push ${IMAGE_ROOT}/${IMAGE}:${IMAGE_VERSION} -clean: - rm $(SIF) - -check: - @if [ -e $(INSTALL_GIT) ]; then \ - echo "Already installed. Please run 'make uninstall' to remove old installation."; \ - exit 1; \ - fi - -clone: - git clone --recursive . $(INSTALL_GIT) - cd $(INSTALL_GIT) && git remote set-url origin $(GIT) && git fetch origin - -git: - @make -s check - @make -s clone - @echo "" - @echo "Installed:" - @echo " $(INSTALL_GIT)/" - @echo "" - @echo "Please add the following into your .bashrc or similar" - @echo " export SLIDEFACTORY=$(INSTALL_GIT)" +singularity: + rm -f $(IMAGE).sif $(IMAGE).tar + docker save $(IMAGE_ROOT)/$(IMAGE):$(IMAGE_VERSION) -o $(IMAGE).tar + singularity build $(IMAGE).sif docker-archive://$(IMAGE).tar + rm -f $(IMAGE).tar -install: build - @make -s check - @if [ ! -d $(INSTALL_BIN) ]; then \ - mkdir -p $(INSTALL_BIN); \ - fi - cp -i $(SIF) $(INSTALL_BIN)/ - @make -s clone - @echo "" - @echo "Installed:" - @echo " $(INSTALL_BIN)/$(SIF)" - @echo " $(INSTALL_GIT)/" - @echo "" - @echo "Please add the following into your .bashrc or similar" - @echo " export SLIDEFACTORY=$(INSTALL_GIT)" - -uninstall: - @echo "Removing:" - @if [ -e $(INSTALL_BIN)/$(SIF) ]; then \ - echo " $(INSTALL_BIN)/$(SIF)"; \ - fi - @echo " $(INSTALL_GIT)/" - @read -r -p "Proceed [Y/n]? " OK; \ - [ "$$OK" = "y" ] || [ "$$OK" = "Y" ] || [ "$$OK" = "" ] || (exit 1;) - @if [ -e $(INSTALL_BIN)/$(SIF) ]; then \ - rm -f $(INSTALL_BIN)/$(SIF); \ - fi - rm -rf $(INSTALL_GIT) - -%.sif: %.def - sudo singularity build $@ $< +clean: + rm -f $(IMAGE).sif $(IMAGE).tar diff --git a/README.md b/README.md index f7c1e7a..8780bee 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This repository contains the recipe to build a new *slidefactory* container image and the files needed by the tool to generate slides in CSC style. If you are looking for an example of how to write slides using slidefactory, -please have a look at the empty +please have a look at the [slidefactory template](https://github.com/csc-training/slidefactory-template) that can be used as a basis for new courses. Besides some convenience tooling, it also contains a syntax guide and an example slide set. @@ -14,112 +14,105 @@ it also contains a syntax guide and an example slide set. ## Usage -Convert slides from Markdown to HTML: -```bash -slidefactory.sif example.md -``` +The container can be run via singularity / apptainer or docker / podman. -or the same if not using the singularity container: -```bash -python3 $SLIDEFACTORY/convert.py example.md -``` +### Singularity / apptainer -To see more detailed information when running, such as configuration options -and the exact pandoc command, you can add `--verbose` to the commands above. +Fetch the slidefactory container image: -Use `--help` to see descriptions of all the available arguments. + singularity pull docker://ghcr.io/csc-training/slidefactory:VERSION +Convert the markdown slides to a PDF (default): -### option: `--pdf` + ./slidefactory_VERSION.sif slides --format pdf slides.md -To generate also a PDF version of the slides, add `--pdf` to the command -above, e.g.: +Convert slides to a regular HTML (an internet access required to display): -```bash -slidefactory.sif --pdf example.md -``` + ./slidefactory_VERSION.sif slides --format html slides.md +Convert slides to a local HTML ([a local version of resources](#local-slidefactory-installation) used - no internet access required): -### option: `--self-contained` + ./slidefactory_VERSION.sif slides --format html-local slides.md -One can also use the option `--self-contained` to embed images and other -assets into the HTML file with the aim to produce a file that is as -"self-contained" as possible. +Convert slides to an embedded HTML (images and other resources embedded within the file): -```bash -slidefactory.sif --self-contained example.md -``` + ./slidefactory_VERSION.sif slides --format html-embedded slides.md -Beware that **files produces in this way can become very large** and that not -everything will be contained in the HTML file, so e.g. math formulas will -require internet connection to work. +The embedded HTML files are rather large and [buggy](#known-issues) so +the pdf or the local HTML format is recommended for offline use. +The local HTML requires [a local slidefactory installation](#local-slidefactory-installation). +Change the theme with `--theme`: -## Install + ./slidefactory_VERSION.sif slides --theme .../path/to/any/theme slides.md -Slidefactory consists of two parts: 1) a **git repo** containing files -defining the slide layout (aka themes), pandoc filters, and a convenience -script (`convert.py`) to ease the use of pandoc, and 2) a -**singularity container** with a self-contained software environment that is -tested to work with slidefactory. +Use help for all other options: -Get the source code, build the singularity container and install -slidefactory: -``` -git clone https://github.com/csc-training/slide-template -cd slide-template -make -make install -``` + ./slidefactory_VERSION.sif slides --help -As prompted by the installer, please add the environment variable -`SLIDEFACTORY` to your `.bashrc` (or similar) and make sure that the directory -containing the container image is in the `PATH`. -If needed, please see [INSTALL.md](INSTALL.md) for more detailed installation -instructions (and alternative installation options). +#### Build pages for a project +Use pages sub-command to create an index page and convert all slides: -### Uninstall + ./slidefactory_VERSION.sif pages about.yml build -To uninstall slidefactory, you can say `slidefactory.sif --uninstall` (or -without the container `python3 $SLIDEFACTORY/setup/uninstall.py`). If one is -in the directory containing the source code, `make uninstall` will also work. +#### Local slidefactory installation -## Update +Copy slidefactory files from the container to a local directory: -The installed git repo can be updated (to get new themes etc.) with: -``` -slidefactory.sif --update -``` + ./slidefactory_VERSION.sif install my_slidefactory -or without the singularity container: -``` -python3 $SLIDEFACTORY/setup/update.py -``` +and follow the instructions. -or just simply by using git: -``` -cd $SLIDEFACTORY -git pull -cd - -``` -If you want to update the git repo included inside the container (used only if -no local installation is found), then you can add `--container` flag to the -command above. This will unpack the container, update it, and rebuild the -image. +### Docker / podman -If the container image definition has changed, you need to re-install -slidefactory to get a new version of the image. +Fetch the slidefactory container image: + docker pull ghcr.io/csc-training/slidefactory:VERSION -## Example: template for new courses +Convert the markdown slides to a PDF (default): -Examples and more information on how to write slides using slidefactory are -available in the -[slidefactory template](https://github.com/csc-training/slidefactory-template) -repository. The repository contains an empty template for slidefactory slides -that can be used as a basis for new courses. Please read the `README.md` -included in the repository for more details. + docker run -it --rm -v "$PWD:$PWD:Z" -w "$PWD" ghcr.io/csc-training/slidefactory:VERSION slides --format pdf slides.md + +All the options work the same way as for singularity +but using the above docker command instead. + + +## Known issues + +* Embedded HTML: incorrect math font + * Use local HTML or PDF instead +* Embedded and local HTML: Firefox displays incorrect fonts + * Use Chromium or Chrome instead + + +## Building and updating the container image + +The container recipe is encoded in `Dockerfile` and `Makefile`. + +If you don't have docker or podman, install using + + sudo apt install podman-docker + +If using podman, define + + export BUILDAH_FORMAT=docker + +Build the image + + make build + +Login using GitHub Personal Access Token in order to be able to push: + + docker login ghcr.io + +Push the image + + make push + +For testing, you can also convert the local image to singularity: + + make singularity diff --git a/convert.py b/convert.py deleted file mode 100755 index cc1816c..0000000 --- a/convert.py +++ /dev/null @@ -1,383 +0,0 @@ -#!/usr/bin/python -#---------------------------------------------------------------------------# -# Function: Convert a presentation from Markdown (or reStructuredText) to # -# reveal.js powered HTML5 using pandoc. # -# Usage: python convert.py talk.md # -# Help: python convert.py --help # -#---------------------------------------------------------------------------# -import argparse -import inspect -import os -import sys -import subprocess - -# reveal.js configuration -default_config = [ - 'width=1920', - 'height=1080', - 'history=true', - 'center=false', - 'controls=false', - 'transition=none', - 'backgroundTransition=none' - ] - -# online URLS for the javascript libs -url_reveal = 'https://mlouhivu.github.io/static-engine/reveal/3.5.0' -config_mathjax = '?config=TeX-AMS_HTML-full' -url_mathjax = ( - 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js' - + config_mathjax) - -def error(msg, code=1): - """Custom error messages""" - print('') - print(inspect.cleandoc(msg)) - print('') - if code == 1: # setup error (invalid path etc.) - print('Please see README.md for installation instructions.') - sys.exit(code) - -def remove_duplicates(config): - """Remove duplicate entries from a list of config options.""" - tmp = {} - order = [] - for item in config: - try: - key, value = item.split('=', 1) - except ValueError: - error('Malformed config option: %s' % item, code=2) - tmp[key] = value - if key not in order: - order.append(key) - return [key + '=' + tmp[key] for key in order] - -def check_environment(): - """Check environment variables.""" - for env in ['SLIDEFACTORY', 'SLIDEFACTORY_CONTAINER']: - path = os.environ.get(env, False) - if path and not os.path.isdir(path): - error('Invalid path in environment variable {env}: {path}'.format( - env=env, path=os.environ[env])) - -# global flags -no_local_theme = False -no_local_reveal = False -no_local_mathjax = False - -def get_paths(): - """ - Figure out the paths to files - order of preference: current working directory, environment variable, - default installation path, location of this script - """ - global no_local_theme - global no_local_reveal - global no_local_mathjax - in_container = bool(os.environ.get('SLIDEFACTORY_CONTAINER', False)) - not_installed = False - default_path = os.path.join( - os.environ.get('HOME', os.path.expanduser('~')), - 'lib/slidefactory') - cwd = os.getcwd() - # .. base path - for path in [os.environ.get('SLIDEFACTORY', False), - default_path, - os.environ.get('SLIDEFACTORY_CONTAINER', False)]: - if path and os.path.isdir(path): - break - else: - path = os.path.dirname( - os.path.abspath(inspect.getsourcefile(lambda: None))) - not_installed = True - if path == os.environ.get('SLIDEFACTORY_CONTAINER', False): - not_installed = True - # .. path to themes - path_themes = os.path.join(cwd, 'theme') - if not os.path.isdir(path_themes): - path_themes = os.path.join(path, 'theme') - if in_container and not_installed: - no_local_theme = True - if not os.path.isdir(path_themes): - error('Invalid theme path: {0}'.format( - os.path.join('.', os.path.relpath(path_themes, start=cwd)))) - # .. path to filters - for path_filters in [os.path.join(cwd, 'filter'), - os.path.join(path, 'filter')]: - if os.path.isdir(path_filters): - break - else: - error('Invalid filter path: {0}'.format( - os.path.join('.', os.path.relpath(path_filters, start=cwd)))) - # .. path to reveal - for path_reveal in [os.path.join(cwd, 'reveal.js'), - os.path.join(path, 'reveal.js')]: - if os.path.isdir(path_reveal): - break - else: - path_reveal = '' - no_local_reveal = True - # .. path to mathjax - for path_mathjax in [os.path.join(cwd, 'mathjax/MathJax.js'), - os.path.join(path, 'mathjax/MathJax.js')]: - if os.path.isfile(path_mathjax): - break - else: - path_mathjax = '' - no_local_mathjax = True - path_mathjax = path_mathjax + config_mathjax - # .. only files outside of a container are accessible afterwards - if in_container and not_installed: - if path_reveal != os.path.join(cwd, 'reveal.js'): - no_local_reveal = True - if path_mathjax != os.path.join(cwd, 'mathjax/MathJax.js'): - no_local_mathjax = True - return { - 'base': path, - 'themes': path_themes, - 'filters': path_filters, - 'reveal': path_reveal, - 'mathjax': path_mathjax, - } - -def get_filters(path_filters): - """Get paths to pandoc filters.""" - filters = [os.path.join(path_filters, x) for x in [ - 'contain-slide.py', 'background-image.py']] - if os.path.exists('/usr/local/bin/pandoc-emphasize-code'): - filters.append('/usr/local/bin/pandoc-emphasize-code') - return filters - -def get_themes(path_themes): - """Find existing presentation themes.""" - try: - themes = [x for x in os.listdir(path_themes) - if os.path.isdir(os.path.join(path_themes, x))] - except OSError: - error('Invalid theme path: {0}'.format(path_themes)) - return themes - -def get_highlight_styles(): - """Get highlight styles supported by pandoc.""" - output = subprocess.check_output('pandoc --list-highlight-styles', - shell=True) - return output.decode().split() - - -def run(): - global no_local_theme - global no_local_reveal - global no_local_mathjax - - # find out the environment we are running in - check_environment() - path = get_paths() - themes = get_themes(path['themes']) - filters = get_filters(path['filters']) - highlight_styles = get_highlight_styles() - - parser = argparse.ArgumentParser(description="""Convert a presentation - from Markdown (or reStructuredText) to reveal.js powered HTML5 using - pandoc.""") - parser.add_argument('input', metavar='input.md', nargs='+', - help='filename for presentation source (e.g. in Markdown)') - parser.add_argument('--output', metavar='prefix', - help='prefix for output filenames (by default uses the ' - 'basename of the input file, i.e. talk.md -> talk.html)') - parser.add_argument('-t', '--theme', default='csc-2016', - choices=themes, metavar='THEME', - help='presentation theme: ' + ', '.join(themes) \ - + ' (default: csc-2016)') - parser.add_argument('-s', '--style', default='pygments', - choices=highlight_styles, metavar='name', - help='code highlight style: ' + ', '.join(highlight_styles) \ - + ' (default: pygments)') - parser.set_defaults(html=True) - parser.add_argument('-p', '--pdf', action='store_true', default=False, - help='convert HTMLs to PDFs') - parser.add_argument('-c', '--self-contained', - action='store_true', default=False, - help='produce as self-contained HTMLs as possible') - parser.add_argument('-b', '--browser', default='chromium-browser', - help='browser to use for converting PDFs (default: %(default)s)') - parser.add_argument('--config', action='append', default=default_config, - metavar='key=value', - help='reveal.js config option (multiple allowed)') - parser.add_argument('--filter', action='append', default=filters, - metavar='filter.py', - help='pandoc filter script (multiple allowed)') - parser.add_argument('--reveal', help=argparse.SUPPRESS, default=None) - parser.add_argument('--mathjax', help=argparse.SUPPRESS, default=None) - parser.add_argument('--as-container', help=argparse.SUPPRESS, - action='store_true', default=False) - parser.add_argument('--dry-run', '--show-command', - action='store_true', default=False, - help='do nothing, only show the full pandoc command' - + ' (together with config options and filters used)') - parser.add_argument('--verbose', action='store_true', default=False, - help='be loud and noisy') - parser.add_argument('--debug', action='store_true', default=False, - help='show debug options') - args = parser.parse_args() - - # show hidden debug options and exit? - if args.debug: - parser.print_help() - print('\ndebug options:') - print(' --reveal URL of the reveal.js to use') - print(' : ' + args.reveal) - print(' --mathjax URL of the MathJax.js to use') - print(' : ' + args.mathjax) - parser.exit() - - # without a local theme, container can only produce PDFs - if args.as_container and no_local_theme: - args.pdf = True - args.html = False - if args.verbose: - print('Could not find a local installation of slidefactory. ' - + 'Converting to PDFs only.') - print('If you want HTMLs, please set correct path to the ' - + 'environment variable SLIDEFACTORY or install ' - + 'slidefactory with:') - print(' slidefactory.sif --install') - print('') - - # check if given URLs are actually paths - if args.reveal and os.path.isdir(args.reveal): - no_local_reveal = False - path['reveal'] = args.reveal - if args.mathjax and os.path.isfile(args.mathjax): - no_local_mathjax = False - path['mathjax'] = args.mathjax - - # select local or remote reveal and mathjax - if args.self_contained or no_local_reveal: - args.reveal = args.reveal or url_reveal - else: - args.reveal = args.reveal or path['reveal'] - if args.self_contained or no_local_mathjax: - args.mathjax = args.mathjax or url_mathjax - else: - args.mathjax = args.mathjax or path['mathjax'] - - # self contained HTML - if args.self_contained: - if no_local_reveal: - error('Local copy of reveal.js is needed for --self-contained.') - urlencode = os.path.join(path['filters'], 'url-encode.py') - if urlencode not in args.filter: - args.filter.append(urlencode) - contained = '--self-contained' - else: - contained = '' - - # check config options and remove duplicates (if any) - config = remove_duplicates(args.config) - - # meta variables to pandoc - meta = [ - 'theme=' + args.theme, - 'themepath=' + os.path.join(path['themes'], args.theme), - 'revealjs-url=' + args.reveal, - 'revealjs-css-url=' + ( - path['reveal'] if args.self_contained else args.reveal), - ] - - # extra template variables to pandoc - variables = [ - ] - - # prepare command-line arguments - flags = { - 'style': args.style, - 'input': args.input, - 'output': args.output, - 'meta': ' '.join('-M ' + x for x in meta), - 'vars': ' '.join('-V ' + x for x in variables), - 'config': ' '.join('-V ' + x for x in config), - 'filter': ' '.join('--filter ' + x for x in args.filter), - 'mathjax': args.mathjax, - 'template': os.path.join( - path['themes'], args.theme, 'template.html'), - 'contained': contained, - } - - # display extra info? - if args.verbose or args.dry_run: - print('Using theme from: ' + os.path.join(path['themes'], args.theme)) - print('\nReveal.js configuration:') - for x in config: - print(' {0}'.format(x)) - print('\nPandoc filters:') - if filters: - for x in filters: - print(' {0}'.format(x)) - else: - print(' (none)') - print('\nPandoc variables:') - if meta or variables: - for x in meta: - print(' -M {0}'.format(x)) - for x in variables: - print(' -V {0}'.format(x)) - else: - print(' (none)') - print('\nPandoc options:') - print(' --highlight-style={0}'.format(args.style)) - print(' --mathjax={0}'.format(args.mathjax)) - if args.self_contained: - print(' ' + contained) - - # convert files - for filename in args.input: - # figure out the output filename - base, ext = os.path.splitext(filename) - if args.output: - base = args.output + base - html = base + '.html' - # add filenames to the command-line arguments - flags['input'] = filename - flags['output'] = html - - # construct the pandoc command - cmd = ('pandoc {input} -s -f markdown-native_divs -t revealjs --template={template} ' - + '{meta} {vars} {config} {contained} ' - + '--mathjax={mathjax} --highlight-style={style} ' - + '{filter} -o {output}').format(**flags) - - # display pandoc command? - if args.verbose or args.dry_run: - print('\nPandoc command:') - print(' {0}\n'.format(cmd)) - - # execute pandoc - if not args.dry_run: - os.system(cmd) - - # convert to pdfs? - if args.pdf: - pdf = base + '.pdf' - flags = [ - '--headless', - '--virtual-time-budget=10000', - '--run-all-compositor-stages-before-draw', - ] - cmd = ('{browser} {flags} --print-to-pdf={pdf} ' - + 'file://{path}/{html}?print-pdf').format( - browser=args.browser, - flags=' '.join(flags), - pdf=pdf, - path=os.path.abspath(os.getcwd()), - html=html) - if args.verbose or args.dry_run: - print('') - print('Command to convert to PDF:') - print(' {0}'.format(cmd)) - print('') - if not args.dry_run: - subprocess.run(cmd, shell=True, stderr=subprocess.DEVNULL) - return 0 - -if __name__ == '__main__': - sys.exit(run()) diff --git a/filter/background-image.py b/filter/background-image.py deleted file mode 100755 index ffdbbd0..0000000 --- a/filter/background-image.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -from pandocfilters import toJSONFilter, Header, attributes - -def background(key, value, format, meta): - # language - try: - if meta['lang']['t'] == 'MetaInlines': - lang = ' '.join([x['c'] for x in meta['lang']['c']]) - else: - lang = meta['lang']['c'] - except: - lang = 'en' - # setup a template with correct path to the theme - try: - path = meta['themepath']['c'] - except: - path = 'theme' - template = u'{0}/img/%s.png'.format(path) - # set background image for title slide - if 'title_bg' not in meta: - filename = template % 'title-{0}'.format(lang) - meta['title_bg'] = {'t': 'MetaString', 'c': filename} - # markdown: special class name triggers the setting of a data background - # image unless already present - if key == 'Header' and value[0] == 1: - if 'data-background-image' not in [x[0] for x in value[1][2]]: - for key in ['title', 'author', 'section']: - if key in value[1][1]: - if key == 'title': - key = key + '-' + lang - value[1][2].append( - [u'data-background-image', template % key]) - break - return Header(value[0], value[1], value[2]) - # reST: special class name in a container Div triggers the same as above, - # but only the modified Header is returned - elif key == 'Div' and value[1][0]['t'] == 'Header': - header = value[1][0]['c'] - if 'data-background-image' not in [x[0] for x in header[1][2]]: - for key in ['title', 'author', 'section']: - if key in value[0][1]: - header[1][1].append(key) - if key == 'title': - key = key + '-' + lang - header[1][2].append( - [u'data-background-image', template % key]) - break - return Header(header[0], header[1], header[2]) - - -if __name__ == '__main__': - toJSONFilter(background) diff --git a/filter/contain-slide.py b/filter/contain-slide.py deleted file mode 100755 index ba50ab0..0000000 --- a/filter/contain-slide.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -from pandocfilters import toJSONFilter, Header, attributes - -def contain(key, value, format, meta): -# raise ValueError, 'key=%s, value=%s, format=%s, meta=%s' % \ -# (repr(key), repr(value), repr(format), repr(meta)) - if key == 'Header' and value[0] == 1: - if 'data-background-size' not in [x[0] for x in value[1][2]]: - value[1][2].append([u'data-background-size', u'contain']) - return Header(value[0], value[1], value[2]) - elif key == 'HorizontalRule': - name = "section" - attr = [[u'data-background', u'empty-slide'], - [u'data-background-size', u'contain']] - return Header(1, (name, [], attr), []) - -if __name__ == '__main__': - toJSONFilter(contain) diff --git a/filter/url-encode.py b/filter/url-encode.py deleted file mode 100755 index c3f930b..0000000 --- a/filter/url-encode.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -from pandocfilters import toJSONFilter, Header, attributes -import base64 - -def _encode(filename): - with open(filename, 'rb') as fp: - data = base64.b64encode(fp.read()) - return 'data:image/png;base64,' + data.decode() - -def urlencode(key, value, format, meta): - # urlencode title background - if 'title_bg' in meta and 'title_bg_encoded' not in meta: - meta['title_bg'] = { - 't': 'MetaString', - 'c': _encode(meta['title_bg']['c']) - } - meta['title_bg_encoded'] = {'t': 'MetaString', 'c': 'yes'} - # markdown: urlencode images - if key == 'Header' and value[0] == 1: - for attribute in value[1][2]: - if attribute[0] == 'data-background-image': - attribute[1] = _encode(attribute[1]) - return Header(value[0], value[1], value[2]) - # reST: urlencode images - elif key == 'Div' and value[1][0]['t'] == 'Header': - header = value[1][0]['c'] - for attribute in header[1][2]: - if attribute[0] == 'data-background-image': - attribute[1] = _encode(attribute[1]) - return Header(header[0], header[1], header[2]) - -if __name__ == '__main__': - toJSONFilter(urlencode) diff --git a/fonts/fonts.css b/fonts/fonts.css new file mode 100644 index 0000000..f431b27 --- /dev/null +++ b/fonts/fonts.css @@ -0,0 +1,47 @@ +@font-face { + font-family: 'Noto Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + src: url(NotoSans/NotoSans-Regular.ttf) format(truetype); +} + +@font-face { + font-family: 'Noto Sans'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + src: url(NotoSans/NotoSans-Bold.ttf) format(truetype); +} + +@font-face { + font-family: 'Noto Sans'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + src: url(NotoSans/NotoSans-Italic.ttf) format(truetype); +} + +@font-face { + font-family: 'Noto Sans'; + font-style: italic; + font-weight: 700; + font-stretch: 100%; + src: url(NotoSans/NotoSans-BoldItalic.ttf) format(truetype); +} + +@font-face { + font-family: 'Inconsolata'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + src: url(Inconsolata/Inconsolata-Regular.ttf) format(truetype); +} + +@font-face { + font-family: 'Inconsolata'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + src: url(Inconsolata/Inconsolata-Bold.ttf) format(truetype); +} diff --git a/mathjax b/mathjax deleted file mode 160000 index d71cc40..0000000 --- a/mathjax +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d71cc40666d213dceeb9353822a3b530656d9a4b diff --git a/reveal.js b/reveal.js deleted file mode 160000 index 360bc94..0000000 --- a/reveal.js +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 360bc940062711db9b8020ce4e848f6c37014481 diff --git a/setup/build.py b/setup/build.py deleted file mode 100755 index b27860d..0000000 --- a/setup/build.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/python3 -#---------------------------------------------------------------------------# -# Function: Build slidefactory singularity container. # -# Help: ./build.sh --help # -#---------------------------------------------------------------------------# -import argparse -import sys -import os - -def run(): - desc = 'Build slidefactory singularity container' - parser = argparse.ArgumentParser(description=desc) - parser.add_argument('output', nargs='?', - default='slidefactory.sif', metavar='image.sif', - help='filename for the container image to be built ' \ - + '(default: %(default)s)') - parser.add_argument('-v', '--verbose', action='store_true', default=False, - help='display additional information while running') - parser.add_argument('-d', '--definition', - default='slidefactory.def', metavar='image.def', - help='singularity definition file for the container ' \ - + '(default: %(default)s)') - - args = parser.parse_args() - - # be noisy? - if args.verbose: - print('Image file: {0}'.format(args.output)) - print('Definition: {0}'.format(args.definition)) - print('') - - # check files - if not os.path.isfile(args.definition): - print("Definition file '{0}' missing.".format(args.definition)) - return 1 - if os.path.exists(args.output): - # is the existing container image newer than the definition? - try: - time_def = os.path.getmtime(args.definition) - time_out = os.path.getmtime(args.output) - if time_def < time_out: - if args.verbose: - print('Nothing to do.') - return 0 - except Exception: - if args.verbose: - print('Warning: unable to determine file modification time') - # remove existing file? - yn = input("File '{0}' exists. Overwrite [y/N]? ".format(args.output)) - if yn.lower() in ['y', 'yes']: - try: - os.remove(args.output) - except Exception: - print("Unable to remove '{0}'. Maybe it is not a file?".format( - args.output)) - return 1 - else: - print('Abort.') - return 2 - - # build command - cmd = 'sudo singularity build {0} {1}'.format(args.output, args.definition) - - # execute - if args.verbose: - print('Building image...') - print(' ' + cmd) - print('') - os.system(cmd) - - # the end. - return 0 - -if __name__ == '__main__': - sys.exit(run()) diff --git a/setup/install.py b/setup/install.py deleted file mode 100755 index 62406ac..0000000 --- a/setup/install.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/python3 -#---------------------------------------------------------------------------# -# Function: Install slidefactory container and repository # -# Help: ./install.sh --help # -#---------------------------------------------------------------------------# -import argparse -import sys -import os -import shutil -import subprocess - -def get_version(image): - cmd = [image, '--version'] - proc = subprocess.run(cmd, capture_output=True, encoding='utf-8') - if proc.returncode or not proc.stdout: - print("Couldn't determine the version of image {0}".format(image)) - sys.exit(3) - return proc.stdout.strip() - -def run(): - desc = 'Install slidefactory container and repository' - parser = argparse.ArgumentParser(description=desc) - parser.add_argument('-p', '--prefix', - default=os.environ.get('HOME', os.path.expanduser('~')), - help='install files into this path (under bin/ and lib/)' \ - + ' (default: %(default)s)') - parser.add_argument('-i', '--image', metavar='SIF', - default='slidefactory.sif', - help='container image to install (default: %(default)s)') - parser.add_argument('-r', '--repository', metavar='URL', - default='https://github.com/csc-training/slide-template', - help='URL to the git repository to install (default: %(default)s)') - parser.add_argument('-v', '--verbose', action='store_true', default=False, - help='display additional information while running') - - args = parser.parse_args() - - # install paths - install_git = os.path.join(args.prefix, 'lib/slidefactory') - install_bin = os.path.join(args.prefix, 'bin') - install_sif = os.path.join(install_bin, args.image) - - # be noisy? - if args.verbose: - print('Image file: {0}'.format(args.image)) - print('Repository: {0}'.format(args.repository)) - print('Install paths:') - print(' {0}'.format(install_bin)) - print(' {0}'.format(install_git)) - print('') - - # check files - if not os.path.isfile(args.image): - print("Image file '{0}' missing.".format(args.definition)) - return 1 - if os.path.exists(install_sif): - version_sif = get_version(install_sif) - version_image = get_version(os.path.join('.', args.image)) - if version_image < version_sif: - print() - print(("Newer version of the image installed already " - "({0} vs. {1}).").format(version_sif, version_image)) - print() - print(("To update the installed git repository, please run " - "'setup/update.py'")) - return 1 - yn = input("File '{0}' exists already. Remove [y/N]? ".format( - install_sif)) - if yn.lower() in ['y', 'yes']: - try: - os.remove(install_sif) - except Exception: - print("Unable to remove '{0}'. Maybe it is not a file?".format( - install_sif)) - return 1 - else: - print('Abort.') - return 2 - if os.path.exists(install_git): - yn = input("Path '{0}' exists already. Remove [y/N]? ".format( - install_git)) - if yn.lower() in ['y', 'yes']: - try: - shutil.rmtree(install_git) - except Exception: - print("Unable to remove '{0}'.") - return 1 - else: - print('Abort.') - return 2 - - # copy image - cmd = 'cp -i {0} {1}'.format(args.image, install_sif) - if args.verbose: - print('Copying image...') - print(' ' + cmd) - os.system(cmd) - - # clone git repository - cmd = 'git clone --recursive {0} {1}'.format(args.repository, install_git) - if args.verbose: - print('Cloning repository...') - print(' ' + cmd) - print('') - os.system(cmd) - - print('') - print('Installed:') - print(' {0}'.format(install_sif)) - print(' {0}'.format(install_git)) - print('') - print('Please add the following into your .bashrc or similar:') - print(' export SLIDEFACTORY={0}'.format(install_git)) - - # check if bin in PATH - path = os.environ.get('PATH', '').split(':') - if not install_bin in path: - print('') - print("Please make sure that '{0}' is in your PATH:".format(install_bin)) - print(' export PATH=$PATH:{0}'.format(install_bin)) - - # the end. - return 0 - -if __name__ == '__main__': - sys.exit(run()) diff --git a/setup/uninstall.py b/setup/uninstall.py deleted file mode 100755 index 1f9dd7a..0000000 --- a/setup/uninstall.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/python3 -#---------------------------------------------------------------------------# -# Function: Uninstall slidefactory container and repository # -# Help: ./install.sh --help # -#---------------------------------------------------------------------------# -import argparse -import sys -import os -import shutil - -def get_install_path(): - if 'SLIDEFACTORY' in os.environ: - return os.path.abspath( - os.path.join(os.environ['SLIDEFACTORY'], '../..')) - else: - return os.environ.get('HOME', os.path.expanduser('~')) - -def run(): - desc = 'Uninstall slidefactory container and repository' - parser = argparse.ArgumentParser(description=desc) - parser.add_argument('-p', '--prefix', - default=get_install_path(), - help='uninstall files installed at this path (under bin/ and lib/)' - + ' (default: %(default)s)') - parser.add_argument('-i', '--image', metavar='SIF', - default='slidefactory.sif', - help='name of the container image (default: %(default)s)') - parser.add_argument('-v', '--verbose', action='store_true', default=False, - help='display additional information while running') - - args = parser.parse_args() - - # install paths - install_git = os.path.join(args.prefix, 'lib/slidefactory') - install_bin = os.path.join(args.prefix, 'bin') - install_sif = os.path.join(install_bin, args.image) - - # be noisy? - if args.verbose: - print('Image file: {0}'.format(args.image)) - print('Uninstall paths:') - print(' {0}'.format(install_bin)) - print(' {0}'.format(install_git)) - print('') - - # are there files to remove? - if not os.path.exists(install_sif) and not os.path.exists(install_git): - print('Nothing to uninstall.') - - print('Removing:') - print(' {0}'.format(install_sif)) - print(' {0}'.format(install_git)) - yn = input('Proceed [Y/n]? ') - if yn.lower() not in ['y', 'yes', '']: - print('Abort.') - return 2 - try: - pass - os.remove(install_sif) - shutil.rmtree(install_git) - except Exception: - print('Unable to remove all files.') - return 1 - - # the end. - return 0 - -if __name__ == '__main__': - sys.exit(run()) diff --git a/setup/update.py b/setup/update.py deleted file mode 100755 index 4a2b4d4..0000000 --- a/setup/update.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/python3 -#---------------------------------------------------------------------------# -# Function: Update slidefactory repository (and maybe container) # -# Help: ./update.sh --help # -#---------------------------------------------------------------------------# -import argparse -import sys -import os -import tempfile -from contextlib import contextmanager - -def get_install_path(): - if 'SLIDEFACTORY' in os.environ: - return os.path.abspath( - os.path.join(os.environ['SLIDEFACTORY'], '../..')) - else: - path = os.environ.get('HOME', os.path.expanduser('~')) - if os.path.isdir(os.path.join(path, 'lib/slidefactory')): - return path - raise Exception - -@contextmanager -def change_dir(path): - cwd = os.getcwd() - try: - os.chdir(path) - yield - finally: - os.chdir(cwd) - -def run(): - desc = 'Update slidefactory repository (and maybe container)' - parser = argparse.ArgumentParser(description=desc) - parser.set_defaults(image='slidefactory.sif') - parser.add_argument('--as-container', action='store_true', default=False, - help=argparse.SUPPRESS) - parser.add_argument('-c', '--container', action='store_true', default=False, - help='update also container') - parser.add_argument('-v', '--verbose', action='store_true', default=False, - help='display additional information while running') - - args = parser.parse_args() - - # figure out install paths - try: - install_prefix = get_install_path() - except Exception: - print('Could not find files to update. Please set correct path ' - + 'to the environment variable SLIDEFACTORY.') - print("If you haven't yet installed slidefactory, please first run:") - if args.as_container: - print(' slidefactory.sif --install') - else: - print(' python3 setup/install.py') - return 1 - install_git = os.path.join(install_prefix, 'lib/slidefactory') - install_sif = os.path.join(install_prefix, 'bin', args.image) - - # check files - if not os.path.isdir(install_git) or \ - not os.path.isdir(os.path.join(install_git, '.git')): - print("Invalid git repository path: {0}".format(install_git)) - return 1 - if args.container: - if not os.path.isfile(install_sif): - print("Invalid image file: {0}".format(install_sif)) - return 1 - - # be noisy? - if args.verbose: - print('Repository: {0}'.format(install_git)) - if args.container: - print('Container: {0}'.format(install_sif)) - print('') - - # update repository - cmd = 'git pull && git submodule update --init' - if args.verbose: - print('Updating repository...') - print(' ' + cmd) - print('') - try: - with change_dir(install_git): - os.system(cmd) - except Exception: - print("Unable to update repository: {0}".format(install_git)) - - # update container - if args.container: - with tempfile.TemporaryDirectory(prefix='slidefactory-') as tmp: - sandbox = os.path.join(tmp, 'sandbox') - # create sandbox from the existing container - cmd = 'singularity build --sandbox {0} {1}'.format(sandbox, - install_sif) - if args.verbose: - print('Unpacking container...') - print(' ' + cmd) - try: - os.system(cmd) - except Exception: - print('Unable to unpack container: {0}'.format(install_sif)) - # update the git repo - path = os.path.join(sandbox, 'slidefactory') - cmd = 'git pull && git submodule update --init' - if args.verbose: - print('Updating repository (in container)...') - print(' ' + cmd) - try: - with change_dir(path): - os.system(cmd) - except Exception: - print("Unable to update the repository in the container.") - # create a new container from the sandbox - cmd = 'singularity build --force {0} {1}'.format(install_sif, - sandbox) - if args.verbose: - print('Building container...') - print(' ' + cmd) - print('') - try: - os.system(cmd) - except Exception: - print('Unable to build container: {0}'.format(install_sif)) - - print('Updated:') - print(' {0}'.format(install_git)) - if args.container: - print(' {0}'.format(install_sif)) - - # the end. - return 0 - -if __name__ == '__main__': - sys.exit(run()) diff --git a/slidefactory.def b/slidefactory.def deleted file mode 100644 index 8fcc676..0000000 --- a/slidefactory.def +++ /dev/null @@ -1,46 +0,0 @@ -BootStrap: library -From: ubuntu:20.04 - -%environment - export SLIDEFACTORY_CONTAINER=/lib/slidefactory - export SLIDEFACTORY_VERSION=1.x - -%post - apt update && apt install -y software-properties-common && apt update - add-apt-repository universe - add-apt-repository ppa:phd/chromium-browser - apt update - apt install -y python3-pip pandoc python3-pandocfilters - apt install -y fonts-noto fonts-inconsolata - apt install -y git - apt install -y chromium-browser - apt install -y cabal-install nvi - cabal update - mkdir -p /root/.cabal/bin - cabal install pandoc-types-1.17.5.4 - cabal install pandoc-emphasize-code - apt-get remove -y --autoremove cabal-install - url=https://github.com/csc-training/slide-template - git clone --recursive $url $SINGULARITY_ROOTFS/lib/slidefactory - -%runscript - _SLIDEFACTORY_PATH=$SLIDEFACTORY - if test "$_SLIDEFACTORY_PATH" = "" - then - _SLIDEFACTORY_PATH=$SLIDEFACTORY_CONTAINER - fi - if test "$1" = "--version" - then - shift - echo $SLIDEFACTORY_VERSION - elif test "$1" = "--update" - then - shift - exec python3 $_SLIDEFACTORY_PATH/setup/update.py --as-container "$@" - elif test "$1" = "--install" - then - shift - exec python3 $_SLIDEFACTORY_PATH/setup/install.py "$@" - else - exec python3 $_SLIDEFACTORY_PATH/convert.py --as-container "$@" - fi diff --git a/slidefactory.py b/slidefactory.py new file mode 100755 index 0000000..c59b753 --- /dev/null +++ b/slidefactory.py @@ -0,0 +1,637 @@ +#!/usr/bin/python +# ------------------------------------------------------------------------- # +# Function: Convert a presentation from Markdown (or reStructuredText) to # +# reveal.js powered HTML5 using pandoc. # +# Usage: python slidefactory.py talk.md # +# Help: python slidefactory.py --help # +# ------------------------------------------------------------------------- # +import argparse +import copy +import functools +import hashlib +import html.parser +import inspect +import os +import re +import shlex +import shutil +import sys +import subprocess +import tempfile +import yaml +from collections import namedtuple +from contextlib import contextmanager +from urllib.parse import quote as urlquote, urlparse +from pathlib import Path + + +VERSION = "3.1.0-beta.6" +SLIDEFACTORY_ROOT = Path(__file__).absolute().parent +IN_CONTAINER = SLIDEFACTORY_ROOT == Path('/slidefactory') + +# Modify version string if this file has been edited +with open(__file__, 'rb') as f: + CHECKSUM = hashlib.sha256(f.read()).hexdigest() + + +def __read_checksum_reference(): + checksum_fpath = SLIDEFACTORY_ROOT / f'sha256sums_{VERSION}' + if not checksum_fpath.exists(): + return None + with open(checksum_fpath, 'r') as f: + for line in f: + chk, fpath = line.strip().split(' ', 1) + if fpath == f'./{Path(__file__).name}': + return chk + return None + + +REF_CHECKSUM = __read_checksum_reference() +if CHECKSUM != REF_CHECKSUM: + VERSION += '-edited' + + +URL_KEYS = ( + 'defaults_fpath', + 'template_fpath', + 'theme_url', + 'revealjs_url', + 'mathjax_url', + 'fonts_url', + ) + +Theme = namedtuple('Theme', ['name', 'dpath', 'is_custom']) + + +def get_default_url(key: str, format: str, theme: Theme): + assert key in URL_KEYS + use_local_resources = format in ['pdf', 'html-local', 'html-embedded'] + root_url = f'file://{urlquote(str(SLIDEFACTORY_ROOT))}' + if key == 'theme_url': + if theme.is_custom or not IN_CONTAINER or use_local_resources: + return f'file://{urlquote(str(theme.dpath.absolute()))}/csc.css' # noqa: E501 + else: + return f'https://cdn.jsdelivr.net/gh/csc-training/slidefactory@3.1.0-beta.4/theme/{theme.name}/csc.css' # noqa: E501 + + elif key == 'defaults_fpath': + return theme.dpath / "defaults.yaml" + + elif key == 'template_fpath': + return theme.dpath / "template.html" + + elif key == 'revealjs_url': + if use_local_resources: + return f'{root_url}/reveal.js-4.4.0' + else: + return 'https://cdn.jsdelivr.net/npm/reveal.js@4.4.0' # noqa: E501 + + elif key == 'mathjax_url': + if use_local_resources: + return f'{root_url}/MathJax-3.2.2/es5/tex-chtml-full.js' # noqa: E501 + else: + return 'https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-chtml-full.js' # noqa: E501 + + elif key == 'fonts_url': + if use_local_resources: + return f'{root_url}/fonts/fonts.css' + else: + return 'https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wdth,wght@0,100,400;0,100,700;1,100,400;1,100,700&family=Inconsolata:wght@400;700' # noqa: E501 + + +@contextmanager +def named_ctx(name): + """Context manager that returns an object + with object.name being the given name.""" + yield namedtuple('Named', ['name'])(name) + + +class HTMLParser(html.parser.HTMLParser): + def __init__(self): + super().__init__() + self.sources = set() + + def handle_starttag(self, tag, attrs): + if tag == 'img': + for key, value in attrs: + if key == 'data-src': + if urlparse(value).scheme == '': + self.sources.add(value) + + +def run_template(run_args, *, dry_run): + run_args = [str(a) for a in run_args] + + if dry_run: + info(shlex.join(run_args)) + return + + verbose_info(shlex.join(run_args)) + p = subprocess.run(run_args, + check=False, shell=False, + capture_output=True) + + verbose_info(p.stdout.decode()) + + if p.returncode != 0: + error(f'error: {repr(run_args[0])} failed ' + f'with exit code {p.returncode}:\n' + f'{p.stderr.decode()}') + + +def info_template(msg, *, quiet): + if not quiet: + print(msg, flush=True) + + +def error(msg, code=1): + """Custom error messages""" + print('') + print(inspect.cleandoc(msg), + file=sys.stderr, flush=True) + print('') + sys.exit(code) + + +def get_available_themes(theme_root): + available_themes = sorted([str(x.name) for x in theme_root.iterdir() + if x.is_dir()]) + return available_themes + + +def find_theme(name): + is_custom = False + if os.sep in str(name): + is_custom = True + p = Path(name) + name = p.name + if not p.is_dir(): + error(f'Nonexistent theme directory {p.absolute()}') + else: + theme_root = SLIDEFACTORY_ROOT / 'theme' + p = theme_root / name + if not p.is_dir(): + available_themes = get_available_themes(theme_root) + error(f'Invalid theme {name}.' + f' Available themes: {", ".join(available_themes)}.') + for fname in ['defaults.yaml', 'template.html', 'csc.css']: + if not (p / fname).is_file(): + error(f'File {fname} missing from the theme directory' + f' {p.absolute()}') + return Theme(name, p, is_custom) + + +def create_html(input_fpath, html_fpath, *, + defaults_fpath, + template_fpath, + pandoc_vars, + filters=[], + pandoc_args=[], + ): + run_args = [ + 'pandoc', + f'--defaults={defaults_fpath}', + f'--template={template_fpath}', + ] + for key, value in pandoc_vars.items(): + run_args += [f'--variable={key}:{value}'] + run_args += pandoc_args + run_args += [f'--filter={f}' for f in filters] + run_args += [ + f'--output={html_fpath}', + input_fpath, + ] + run(run_args) + + +def copy_html_externals(input_fpath, html_fpath): + # Find external file paths + parser = HTMLParser() + with open(html_fpath, 'r') as f: + parser.feed(f.read()) + externals = parser.sources + + # Check that files exist + for fname in externals: + fpath = input_fpath.parent / fname + if not fpath.exists(): + error(f'Linked file missing: {fpath}') + + # Copy files to output path + if input_fpath.parent.resolve() != html_fpath.parent.resolve(): + for fname in externals: + ext_fpath = input_fpath.parent / fname + tgt_fpath = html_fpath.parent / fname + verbose_info(f'cp {ext_fpath} {tgt_fpath}') + tgt_fpath.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(ext_fpath, tgt_fpath) + + +def create_pdf(html_fpath, pdf_fpath): + run_args = [ + 'chromium-browser', + '--no-sandbox', + '--headless', + '--disable-gpu', + '--disable-software-rasterizer', + '--hide-scrollbars', + '--virtual-time-budget=2147483647', + '--run-all-compositor-stages-before-draw', + f'--print-to-pdf={pdf_fpath}', + f'file://{html_fpath.absolute()}?print-pdf' + ] + run(run_args) + + +def create_index_page(fpath, title, info_content, html_content, pdf_content): + info(f'Create {fpath}') + with fpath.open("w") as fd: + csc_ui_version = '2.1.11' + fd.write(f""" + + +
+ +{}
') + content += f'