diff --git a/.azuredevops/README.md b/.azuredevops/README.md new file mode 100644 index 0000000..78b74a9 --- /dev/null +++ b/.azuredevops/README.md @@ -0,0 +1,157 @@ +# Markdown to PDF converter + +This is a Azure DevOps [pipeline](./markdown2pdf.yml) to publish a PDF artifact by converting Markdown files using a predefined template. + +## Get started + +To use the pipeline, several prerequisite steps are required: + +1. If needed, [create a repo](https://learn.microsoft.com/en-us/azure/devops/repos/git/create-new-repo?view=azure-devops#create-a-repo-using-the-web-portal). + +1. If needed, add [markdown](https://www.markdownguide.org/) documentation to the repo. It is recommended to keep the markdown files in a subfolder of a **"docs"** folder, for example **"docs/hld"**. + +1. Add a **"document.order"** file in the markdown folder. It is recommended to only keep one order in each markdown folder. + +1. Add markdown file names to **"document.order"**, one file name for each line. + + Ensure that the file names are in the correct order. This is how they will appear in the PDF file. + + Note that lines that start with the comment sign `#` will be ignored. + +1. Add the [markdown2pdf.yml](./markdown2pdf.yml) to a repo folder with a name that represent what it converts, e.g. **".pipelines/docs.dns.design.yml"**. + +1. Customize the variable values in the pipeline file and commit the changes. + +1. Go to the Azure DevOps **Pipelines** page. Then choose the action to create a **New pipeline**. + +1. Select **Azure Repos Git** as the location of the source code. + +1. When the list of repositories appears, select the repository. + +1. Select **Existing Azure Pipelines YAML file** and choose the YAML file, e.g. **"/.pipelines/docs.dns.design.yml"**. + +1. Save the pipeline without running it. + +1. Configure [branch policies](https://learn.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops&tabs=browser#configure-branch-policies) for the default/main branch. + +1. Add a [build validation branch policy](https://learn.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops&tabs=browser#build-validation). + +## Pipeline + +The pipeline converts markdown to PDF using a [script](./../tools/convert.sh) and a [LaTeX template](./../tools/designdoc.tex). + +### Template sections + +The template has the following sections: + +1. A **Title page** with a [cover logo](./../tools/designdoc-cover.png) and the document title and subtitle. + +1. A **Version history page**. The content of this page is generated based on the following: + + 1. If a **HistoryFile** exist; use the content of that file. + 1. Or, if **SkipGitCommitHistory** is set to `true` or a commit history don't exist; use the value of **MainAuthor** and **FirstChangeDescription**. + 1. Or, use git commit history. Limit the number of items to the value of **GitLogLimit**. + +1. A **Table of Content page**. + +1. **Content pages** with the converted markdown files. This content is generated based on the following: + + 1. Merge the markdown files listed in the **"OrderFile"** to one markdown file. + 1. Replace markdown links that have a relative path with a absolute path. + 1. If a **ReplaceFile** exist; update the markdown file and replace each string that match the key from **ReplaceFile**, with the matching value. + 1. Build metadata for the **"pandoc"** converter. + 1. Convert the markdown using **"pandoc"** and save it as an artifact to the job. + +1. **Page header** on all pages, except for the **Title page**: + + - Left side: [logo](./../tools/designdoc-logo.png) + - Center: **Project** and **Author(s)** + - Right side: Current date. + +1. **Page footer** on all pages, except for the **Title page**. + + - Center: page number and total number of pages. + +### Markdown tips + +The markdown can include admonition blocks for text that need special attention. + +The following blocks are supported: warning, important, note, caution, tip + +Example: + +```markdown +# A title + +This is some regular text. + +::: tip +This text is in a tip admonition block. +::: +``` + +### Issues + +The following are known issues: + +- Relative paths should start with `./` to ensure it can be replaced with absolute path before converting it to PDF. +- Markdown table columns can overlap in the PDF if the table is to wide, making text unreadable. To work around this: + - limit the text in the table. Typically it should be less than 80 characters wide. + - split the table up. + - use an ordered or unordered list. +- The template is designed for a standing A4 layout. It has no option to flip page orientation for one or several pages in a document. +- Titles are not automatically numbered. Titles can be manually numbered and maintained in the markdown document. To avoid having to manually update all titles, it is best to not use numbered titles if possible. + +## Variables + +- **FIRST_CHANGE_DESCRIPTION**: The first change description. This value will be used if a HistoryFile is not specified and a git commit comment is not available. + +- **FOLDER**: The repository folder where the order file exist. + +- **GIT_LOG_LIMIT**: Maximum entries to get from Git Log for version history. + +- **HISTORY_FILE**: The name of the history file. This is a file with the version history content. The file must contain the following structure: + + Version|Date|Author|Description + + Example content for a history.txt file: + + 1|Oct 26, 2023|Jane Doe|Initial draft + 2|Oct 28, 2023|John Doe|Added detailed design + +- **MAIN_AUTHOR**: The main author of the PDF content. This value will be used if a HistoryFile is not specified and author can't be retrieved from git commits. + +- **ORDER_FILE**: The name of the .order file. This is a text file with the name of each markdown file, one for each line, in the correct order. + + Example content for a document.order file: + + summary.md + details.md + faq.md + +- **OUT_FILE**: The name of the output file. This file will be uploaded to the job artifacts. + +- **PROJECT**: The project ID or name. + +- **REPLACE_FILE**: The name of the replace file. This is a JSON file with key and value strings. Each key will be searched for in the markdown files and replaced with the value before conversion to PDF. + +- **SKIP_GIT_COMMIT_HISTORY**: Skip using git commit history. When set to true, the change history will not be retrieved from the git commit log. + +- **SUBTITLE**: The document subtitle. + +- **TITLE**: The document title. + + Example content for a replace.json file: + + ```json + { + "{data center name}": "WE1", + "{organization name}": "contoso" + } + ``` + +- **WORKFLOW_VERSION**: The version of the markdown2pdf scripts to use. See . + +## License + +The code and documentation in this project are released under the [BSD 3-Clause License](./../LICENSE). diff --git a/.azuredevops/markdown2pdf.yml b/.azuredevops/markdown2pdf.yml new file mode 100644 index 0000000..a0ef48f --- /dev/null +++ b/.azuredevops/markdown2pdf.yml @@ -0,0 +1,108 @@ +trigger: none +pr: + autoCancel: true + drafts: false + +name: 📝 Convert Markdown + +variables: + FIRST_CHANGE_DESCRIPTION: Initial draft + FOLDER: docs + GIT_LOG_LIMIT: 15 + HISTORY_FILE: + MAIN_AUTHOR: Innofactor + ORDER_FILE: document.order + OUT_FILE: document.pdf + PROJECT: + REPLACE_FILE: + SKIP_GIT_COMMIT_HISTORY: false + SUBTITLE: Design Document + TITLE: DNS + WORKFLOW_VERSION: v3 + +pool: + vmImage: ubuntu-22.04 + +steps: + - checkout: self + displayName: Checkout + fetchDepth: 0 + + - task: Cache@2 + displayName: Cache TeX Live + inputs: + key: 'texlive | "$(Agent.OS)"' + path: /opt/texlive + + - task: Bash@3 + displayName: Install tools + env: + SCRIPT: install-tools + VERSION: ${{ variables.WORKFLOW_VERSION }} + inputs: + targetType: inline + script: | + files=( + "${SCRIPT}.sh" + texlive.profile + texlive_packages.txt + requirements.txt + ) + for file in "${files[@]}"; do + uri="https://github.com/innofactororg/markdown2pdf/raw/${VERSION}/tools/${file}" + HTTP_CODE=$(curl -sSL --remote-name --retry 4 \ + --write-out "%{response_code}" \ + --header 'Accept: application/vnd.github.raw' "${uri}" + ) + if [ "${HTTP_CODE}" -lt 200 ] || [ "${HTTP_CODE}" -gt 299 ]; then + echo "##[error]Unable to get ${uri}! Response code: ${HTTP_CODE}" + exit 1 + fi + done + chmod +x "${SCRIPT}.sh" + ./"${SCRIPT}.sh" + + - task: Bash@3 + displayName: Build PDF + env: + AUTHOR: ${{ variables.MAIN_AUTHOR }} + DESCRIPTION: ${{ variables.FIRST_CHANGE_DESCRIPTION }} + SKIP_GIT_HISTORY: ${{ variables.SKIP_GIT_COMMIT_HISTORY }} + HISTORY: ${{ variables.HISTORY_FILE }} + LIMIT: ${{ variables.GIT_LOG_LIMIT }} + SCRIPT: convert + VERSION: ${{ variables.WORKFLOW_VERSION }} + inputs: + targetType: inline + script: | + files=( + "${SCRIPT}.sh" + designdoc.tex + designdoc-cover.png + designdoc-logo.png + ) + for file in "${files[@]}"; do + uri="https://github.com/innofactororg/markdown2pdf/raw/${VERSION}/tools/${file}" + HTTP_CODE=$(curl -sSL --remote-name --retry 4 \ + --write-out "%{response_code}" \ + --header 'Accept: application/vnd.github.raw' "${uri}" + ) + if [ "${HTTP_CODE}" -lt 200 ] || [ "${HTTP_CODE}" -gt 299 ]; then + echo "##[error]Unable to get ${uri}! Response code: ${HTTP_CODE}" + exit 1 + fi + done + git config --global --add safe.directory '*' + chmod +x "${SCRIPT}.sh" + ./"${SCRIPT}.sh" -a "${AUTHOR}" -d "${DESCRIPTION}" -f "${FOLDER}" \ + -force "${SKIP_GIT_HISTORY}" -h "${HISTORY}" -l "${LIMIT}" \ + -o "${ORDER_FILE}" -out "${OUT_FILE}" -p "${PROJECT}" \ + -r "${REPLACE_FILE}" -s "${SUBTITLE}" -t "${TITLE}" \ + --template "designdoc" + + - task: PublishPipelineArtifact@1 + displayName: Publish PDF + inputs: + targetPath: $(Build.SourcesDirectory)/${{ variables.OUT_FILE }} + artifact: ${{ variables.OUT_FILE }} + publishLocation: pipeline diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index a52eb76..cd5aa9e 100755 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -1,4 +1,5 @@ #!/usr/bin/env sh +# shellcheck disable=SC2016 if [ -d "/var/run/docker.sock" ]; then # Grant access to the docker socket sudo chmod 666 /var/run/docker.sock @@ -8,19 +9,19 @@ if ! [ -d ~/.ssh ]; then if [ -d /tmp/.ssh-localhost ]; then command mkdir -p -- ~/.ssh sudo cp -R /tmp/.ssh-localhost/* ~/.ssh - sudo chown -R $(whoami):$(whoami) ~ || true ?>/dev/null - sudo chmod 400 ~/.ssh/* + sudo chown -R -- "$(whoami):$(whoami)" ~ || true -- ?>/dev/null + sudo chmod 400 -- ~/.ssh/* fi fi -apk add --no-cache git curl jq librsvg font-noto-cjk zsh bash starship -apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community font-carlito font-fira-code-nerd shellcheck +apk add --no-cache bash font-noto-cjk starship zsh +apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community font-fira-code-nerd shellcheck -pip install pre-commit +apk add --no-cache git curl jq librsvg +apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community font-carlito -tlmgr update --self -tlmgr option -- autobackup -1 -tlmgr install lastpage +#pip install --break-system-packages pre-commit +pip install pre-commit if [ -f ~/.gitconfig ]; then rm ~/.gitconfig diff --git a/.devcontainer/startup.sh b/.devcontainer/startup.sh index 053d9fa..7323a84 100755 --- a/.devcontainer/startup.sh +++ b/.devcontainer/startup.sh @@ -1,5 +1,6 @@ #!/usr/bin/env sh -if [ "${CODESPACES}" = "true" ]; then +# shellcheck disable=SC2016 +if [ "${CODESPACES}" = 'true' ]; then # Remove the default credential helper sudo sed -i -E 's/helper =.*//' /etc/gitconfig @@ -7,18 +8,20 @@ if [ "${CODESPACES}" = "true" ]; then git config --global credential.helper '!f() { sleep 1; echo "username=${GITHUB_USER}"; echo "password=${GH_TOKEN}"; }; f' fi -if [ "$(git config --get safe.directory)" != "*" ]; then - git config --global --add safe.directory "*" +if [ "$(git config --get safe.directory)" != '*' ]; then + git config --global --add safe.directory '*' fi -if [ "$(git config pull.rebase)" != "false" ]; then +if [ "$(git config pull.rebase)" != 'false' ]; then git config --global pull.rebase false fi -if [ "$(git config user.name)" = "" ]; then - echo "Warning: git user.name is not configured" +if [ "$(git config user.name)" = '' ]; then + echo 'Warning: git user.name is not configured' fi -if [ "$(git config user.email)" = "" ]; then - echo "Warning: git user.email is not configured" +if [ "$(git config user.email)" = '' ]; then + echo 'Warning: git user.email is not configured' fi -pre-commit install -pre-commit autoupdate +if type pre-commit > /dev/null 2>&1; then + pre-commit install + pre-commit autoupdate +fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c317e49..ed5c518 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,6 @@ jobs: contents: write steps: - name: "Release" - uses: innofactororg/code-release@v1 + uses: innofactororg/code-release@v2 with: tag: ${{ github.event.inputs.tag }} diff --git a/README.md b/README.md index fc80f52..113bb4a 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,72 @@ # Markdown to PDF converter -This composite action can be used to publish PDF from Markdown using a predefined latex template. +This repository includes a GitHub action and a Azure DevOps [pipeline](./.azuredevops/README.md) to publish a PDF artifact by converting Markdown files using a predefined template. -It use the following logic: +## Get started + +To set up a markdown2pdf workflow, several prerequisite steps are required: + +1. If needed, [create a repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-new-repository). + +1. If needed, add [markdown](https://www.markdownguide.org/) documentation to the repository. It is recommended to keep the markdown files in a subfolder of a **"docs"** folder, for example **"docs/hld"**. + +1. Add a **"document.order"** file in the markdown folder. It is recommended to only keep one order in each markdown folder. + +1. Add markdown file names to **"document.order"**, one file name for each line. + + Ensure that the file names are in the correct order. This is how they will appear in the PDF file. + + Note that lines that start with the comment sign `#` will be ignored. + +1. Add a workflow file for each order file in the **".github/workflows"** folder of the repository. For example **".github/workflows/docs.hld.dns.yml"**. + +1. Copy the sample workflow from [Usage](#usage) into each workflow file, customize it and commit the changes. + +1. The workflows can be started manually or automatically when creating or updating a pull request. + +## Workflow + +The workflow converts markdown to PDF using a [script](./tools/convert.sh) and a [LaTeX template](./tools/designdoc.tex). + +### Template sections + +The template has the following sections: + +1. A **Title page** with a [logo](./tools/designdoc-cover.png) and the document title and subtitle. + +1. A **Version history page**. The content of this page is generated based on the following: -1. Get version history: 1. If a **HistoryFile** exist; use the content of that file. 1. Or, if **SkipGitCommitHistory** is set to `true` or a commit history don't exist; use the value of **MainAuthor** and **FirstChangeDescription**. 1. Or, use git commit history. Limit the number of items to the value of **GitLogLimit**. -1. Merge the markdown files listed in the OrderFile to one markdown file. -1. Replace markdown links that have relative path with absolute path. -1. If a **ReplaceFile** exist; replace in the markdown each string that match the key from **ReplaceFile**, with the matching value. -1. Build and display metadata for the pandoc converter. -1. Convert the markdown using pandoc and save it as an artifact to the job. + +1. A **Table of Content page**. + +1. **Content pages** with the converted markdown files. This content is generated based on the following: + + 1. Merge the markdown files listed in the **"OrderFile"** to one markdown file. + 1. Replace markdown links that have a relative path with a absolute path. + 1. If a **ReplaceFile** exist; update the markdown file and replace each string that match the key from **ReplaceFile**, with the matching value. + 1. Build metadata for the **"pandoc"** converter. + 1. Convert the markdown using **"pandoc"** and save it as an artifact to the job. + +1. **Page header** on all pages, except for the **Title page**: + + - Left side: [logo](./tools/designdoc-logo.png) + - Center: **Project** and **Author(s)** + - Right side: Current date. + +1. **Page footer** on all pages, except for the **Title page**. + + - Center: page number and total number of pages. + +### Markdown tips The markdown can include admonition blocks for text that need special attention. -The following blocks are supported: warning, important, note, caution, tip +The following blocks are supported: **warning**, **important**, **note**, **caution**, **tip**. -Example: +For example: ```markdown # A title @@ -30,7 +78,7 @@ This text is in a tip admonition block. ::: ``` -## Issues +### Issues The following are known issues: @@ -41,12 +89,113 @@ The following are known issues: - use an ordered or unordered list. - The template is designed for a standing A4 layout. It has no option to flip page orientation for one or several pages in a document. - Titles are not automatically numbered. Titles can be manually numbered and maintained in the markdown document. To avoid having to manually update all titles, it is best to not use numbered titles if possible. -- Because the template is based on latex, backslash has a special meaning and can therefore cause an error from pandoc. -## Usage +### Action Inputs + +#### Required + +- **Title**: (Required) The document title. + +#### Optional + +- **FirstChangeDescription**: The first change description. + + This value will be used if a HistoryFile is not specified and a git commit comment is not available. + + Default: **"Initial draft"** + +- **Folder**: The repository folder where the order file exist. + + Default: **"docs"** + +- **GitLogLimit**: Maximum entries to get from Git Log for version history. + + Default: **"15"** + +- **HistoryFile**: The name of a history file. + + This is a file with version history content. The file must contain the following structure: + + `Version|Date|Author|Description` + + Example content for a history.txt file: + + ```text + 1|Oct 26, 2023|Jane Doe|Initial draft + 2|Oct 28, 2023|John Doe|Added detailed design + ``` + + Default: **""** + +- **MainAuthor**: + + The main author of the PDF content. + + This value will be used if a HistoryFile is not specified and author can't be retrieved from git commits. + + Default: **"Innofactor"** + +- **OrderFile**: The name of the .order file. + + This is a text file with the name of each markdown file, one for each line, in the correct order. + + Example content for a document.order file: + + ```text + summary.md + details.md + faq.md + ``` + + Default: **"document.order"** + +- **OutFile**: The name of the output file. + + This file will be uploaded to the job artifacts. + + Default: **"document.pdf"** + +- **Project**: The project ID or name. This value will be in the document header. + + Default: **""** + +- **ReplaceFile**: The name of the replace file. + + This is a JSON file with key and value strings. Each key will be searched for in the markdown files and replaced with the value before conversion to PDF. + + Example content for a replace.json file: + + ```json + { + "{data center name}": "WE1", + "{organization name}": "contoso" + } + ``` + + Default: **""** + +- **RetentionDays**: Number of days to retain job artifacts. + + Default: **"5"** + +- **SkipGitCommitHistory**: Skip using git commit history. + + When set to **true**, the change history will not be retrieved from the git commit log. + + Default: **"false"** + +- **Subtitle**: The document subtitle. + + Default: **""** + +- **Template**: The template name. Must be: designdoc. + + Default: **"designdoc"** + +### Usage ```yaml -name: 🧳 Convert Markdown +name: 📝 Convert Markdown on: workflow_dispatch: pull_request: @@ -68,117 +217,28 @@ jobs: image: pandoc/extra:edge-alpine steps: - name: Clone repository - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 with: fetch-depth: 0 - name: Build PDF uses: innofactororg/markdown2pdf@v3 with: - # The first change description. - # - # This value will be used if a HistoryFile - # is not specified and a git commit comment - # is not available. - # - # Default: Initial draft FirstChangeDescription: Initial draft - - # The repository folder where the order file exist. - # Default: docs Folder: docs/design - - # Maximum entries to get from Git Log for version history. - # Default: 15 GitLogLimit: 15 - - # The name of the history file. - # - # This is a file with the version history content. - # - # The file must contain the following structure: - # Version|Date|Author|Description - # - # Example content for a history.txt file: - # 1|Oct 26, 2023|Jane Doe|Initial draft - # 2|Oct 28, 2023|John Doe|Added detailed design - # - # Default: '' HistoryFile: history.txt - - # The main author of the PDF content. - # - # This value will be used if a HistoryFile - # is not specified and author can't be retrieved - # from git commits. - # - # Default: Innofactor MainAuthor: Innofactor - - # The name of the .order file. - # - # This is a text file with the name of each markdown file, - # one for each line, in the correct order. - # - # Example content for a document.order file: - # summary.md - # details.md - # faq.md - # - # Default: document.order OrderFile: document.order - - # The name of the output file. - # - # This file will be uploaded to the job artifacts. - # - # Default: document.pdf OutFile: Design.pdf - - # The project ID or name. - # Default: '' Project: 12345678 - - # Skip using git commit history. - # - # When set to true, the change history will not be - # retrieved from the git commit log. - # - # Default: false - SkipGitCommitHistory: false - - # The document subtitle. - # Default: '' - Subtitle: DESIGN DOCUMENT - - # The template name. Must be: designdoc. - # Default: designdoc - Template: designdoc - - # The document title. - # Required - Title: Design - - # The name of the replace file. - # - # This is a JSON file with key and value strings. - # Each key will be searched for in the markdown files and - # replaced with the value before conversion to PDF. - # - # Example content for a replace.json file: - # { - # "{data center name}": "WE1", - # "{organization name}": "contoso" - # } - # - # Default: ReplaceFile: replace.json - - # Number of days to retain job artifacts. - # Default: 5 days RetentionDays: 5 + SkipGitCommitHistory: false + Subtitle: DESIGN DOCUMENT + Title: DNS ``` ## License -The code and documentation in this project are released under the [BSD 3-Clause License](LICENSE). +The code and documentation in this project are released under the [BSD 3-Clause License](./LICENSE). diff --git a/action.yml b/action.yml index bba09e1..6efb1a9 100644 --- a/action.yml +++ b/action.yml @@ -77,6 +77,10 @@ inputs: required: false type: number default: 5 +outputs: + pdf: + description: The path to the PDF file. + value: ${{ github.workspace }}/${{ inputs.OutFile }} runs: using: composite steps: @@ -90,16 +94,22 @@ runs: echo "${toolsFolder}" >> "${GITHUB_PATH}" echo 'System path:' echo "${PATH}" + if test -f "${GITHUB_ACTION_PATH}/tools/requirements.txt"; then + cp -f "${GITHUB_ACTION_PATH}/tools/requirements.txt" ./ + fi echo '::endgroup::' + - name: Cache Tex Live + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 + with: + key: ${{ runner.os }}-texlive + path: /opt/texlive + - name: Install requirements shell: sh run: | echo "::group::Install requirements" - apk add --no-cache git curl jq librsvg font-noto-cjk - apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community font-carlito - tlmgr update --self - tlmgr install lastpage + install-tools.sh echo '::endgroup::' - name: Build PDF @@ -128,8 +138,30 @@ runs: --template "${TEMPLATE}" echo '::endgroup::' + - name: Show debug info + if: success() || failure() + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1 + with: + script: | + const fs = require('fs'); + const event = JSON.parse(fs.readFileSync(process.env['GITHUB_EVENT_PATH'])); + console.log('::group::environment variables'); + console.log('::stop-commands::77e6a57ef9854574'); + for (const [key, value] of Object.entries(process.env).sort()) { + if (key != 'INPUT_SCRIPT') { + console.log(`${key}=${value}`); + } + } + console.log('::77e6a57ef9854574::'); + console.log('::endgroup::'); + console.log('::group::github event'); + console.log('::stop-commands::77e6a57ef9854574'); + console.log(JSON.stringify(event, null, 2)); + console.log('::77e6a57ef9854574::'); + console.log('::endgroup::'); + - name: Publish PDF - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 #v4.3.0 + uses: actions/upload-artifact@ef09cdac3e2d3e60d8ccadda691f4f1cec5035cb #v4.3.1 + 3 commits with: if-no-files-found: error name: ${{ inputs.OutFile }} diff --git a/tools/convert.sh b/tools/convert.sh index 3b3ff0b..3bddecf 100755 --- a/tools/convert.sh +++ b/tools/convert.sh @@ -1,25 +1,22 @@ #!/usr/bin/env sh -set -Eeuo pipefail -trap 'error_handler $LINENO "$SCRIPT_COMMAND" $?' ERR 1 2 3 6 +# shellcheck disable=SC3043 +set -e trap cleanup EXIT -error_handler() { - error $1 "${2}" $3 -} cleanup() { - if [ -f "$DocsPath/metadata.json" ]; then - rm -f -- "$DocsPath/metadata.json" + if test -f "${DocsPath}/metadata.json"; then + rm -f "${DocsPath}/metadata.json" fi } error() { - local line="$1" - local message="$2" + local line="${1}" + local message="${2}" if [ $# -gt 2 ]; then - local code=$3 + local code="${3}" else local code=-1 fi local line_message="" - if [ "$line" != "" ]; then + if [ "$line" != '' ]; then line_message=" on or near line ${line}" fi if test -n "${message}"; then @@ -28,7 +25,7 @@ error() { message="Unspecified (exit code ${code})" fi command printf '\033[1;31mError%s\033[0m: %s\n' "${line_message}" "${message}" 1>&2 - exit ${code} + exit "${code}" } warning() { command printf '\033[1;33mWarning\033[0m: %s\n' "$1" 1>&2 @@ -42,7 +39,7 @@ info() { fi } test_arg() { - if [ $# -lt 4 ] || [ -z "${4}" ] || echo "${4}" | grep -Eq '^-.*'; then + if [ $# -lt 4 ] || test -z "${4}" || echo "${4}" | grep -Eq '^-.*'; then if [ "${1}" = 'true' ]; then echo "${2}" else @@ -58,64 +55,80 @@ test_true_false() { else local default='false' fi - local value=$(echo ${1} | awk '{ print tolower($0) }') - if [ -z "${value}" ]; then + local value + value="$(echo "${1}" | awk '{ print tolower($0) }')" + if test -z "${value}"; then echo "${default}" - elif [ "${value}" = 'true' ] || [ "${value}" = 'yes' ] || [ "${value}" == '1' ]; then + elif [ "${value}" = 'true' ] || [ "${value}" = 'yes' ] || [ "${value}" = '1' ]; then echo 'true' - elif [ "${value}" = 'false' ] || [ "${value}" = 'no' ] || [ "${value}" == '0' ]; then + elif [ "${value}" = 'false' ] || [ "${value}" = 'no' ] || [ "${value}" = '0' ]; then echo 'false' else echo "${default}" fi } get_file_path() { - if [ -z "${1}" ]; then + if test -z "${1}"; then echo '' - elif [ -e ${1} ]; then - echo $(readlink -f "${1}") - elif [ -e ${2}/${1} ]; then - echo $(readlink -f ${2}/${1}) + elif test -e "${1}"; then + readlink -f "${1}" + elif test -e "${2}/${1}"; then + readlink -f "${2}/${1}" else echo '' fi } get_version_history() { - if [ -n "${historyFilePath}" ]; then - if ! [ -f "${historyFilePath}" ]; then + if test -n "${historyFilePath}"; then + if ! test -f "${historyFilePath}"; then error '' "Unable to find history file ${historyFilePath}" 1 fi - mergeLogs=$(cat -- ${historyFilePath}) + mergeLogs=$(cat "${historyFilePath}") elif [ "${SkipGitCommitHistory}" = 'true' ]; then - mergeLogs=$(echo "tag: rel/repo/1.0.0|$currentDate|$MainAuthor|$FirstChangeDescription") + mergeLogs="tag: rel/repo/1.0.0|${currentDate}|${MainAuthor}|${FirstChangeDescription}" else - mergeLogs=$(git --no-pager log -$GitLogLimit --date-order --date=format:'%b %e, %Y' --no-merges --oneline --pretty=format:'%D|%ad|%an|%s' -- $DocsPath) + mergeLogs=$( + git --no-pager log "-${GitLogLimit}" --date-order --date=format:'%b %e, %Y' \ + --no-merges --oneline --pretty=format:'%D|%ad|%an|%s' "${DocsPath}" + ) fi - if [ -z "$mergeLogs" ]; then - mergeLogs=$(echo "tag: rel/repo/1.0.0|$currentDate|$MainAuthor|$FirstChangeDescription") + if test -z "${mergeLogs}"; then + mergeLogs="tag: rel/repo/1.0.0|${currentDate}|${MainAuthor}|${FirstChangeDescription}" fi - lineCount=$(echo "$mergeLogs" | wc -l) + lineCount=$(echo "${mergeLogs}" | wc -l) historyJson='[]' - while IFS= read -r line; do + printf '%s\n' "${mergeLogs}" | while read -r line; do lineCount=$((lineCount-1)) version="$(echo "$line" | cut -d'|' -f1 | rev | cut -d'/' -f1 | rev)" - if [ -z "$version" ] || ! echo $version | grep -Eq '^[0-9].*'; then - version="1.0.$lineCount" + if test -z "${version}" || ! echo "${version}" | grep -Eq '^[0-9].*'; then + version="1.0.${lineCount}" fi - date="$(echo "$line" | cut -d'|' -f2)" - author="$(echo "$line" | cut -d'|' -f3)" - description="$(echo "$line" | cut -d'|' -f4)" - historyJson=$(echo "$historyJson" | jq --arg version "$version" \ - --arg date "$date" \ - --arg author "$author" \ - --arg description "$description" \ - '. +=[{ version: $version, date: $date, author: $author, description: $description }]') - done < <(echo "$mergeLogs") - echo "$historyJson" + date="$(echo "${line}" | cut -d'|' -f2)" + author="$(echo "${line}" | cut -d'|' -f3)" + description="$(echo "${line}" | cut -d'|' -f4)" + if test -f tmp_history_41231.json; then + historyJson=$(jq '.' tmp_history_41231.json) + fi + printf '%s\n' "${historyJson}" | jq --arg version "${version}" \ + --arg date "${date}" \ + --arg author "${author}" \ + --arg description "${description}" \ + '. +=[{ version: $version, date: $date, author: $author, description: $description }]' > tmp_history_41231.json + done + if test -f tmp_history_41231.json; then + jq '.' tmp_history_41231.json + else + printf '%s\n' '[]' | jq --arg version '1.0.0' \ + --arg date "${currentDate}" \ + --arg author "${MainAuthor}" \ + --arg description "${FirstChangeDescription}" \ + '. +=[{ version: $version, date: $date, author: $author, description: $description }]' > tmp_history_41231.json + jq '.' tmp_history_41231.json + fi + rm -f tmp_history_41231.json } process_params() { - while [ $# -gt 0 ] - do + while [ $# -gt 0 ]; do local arg="$1" case "$arg" in -a|--author) @@ -195,61 +208,58 @@ ReplaceFile='' Subtitle='' Template='designdoc' Title='' -SCRIPT_COMMAND="$@" process_params "$@" -if [ -z "${Title}" ]; then +if test -z "${Title}"; then error '' 'Missing Title: Value not set for argument --title' 1 fi currentDate=$(date "+%B %d, %Y") currentPath=$(pwd) # Ensure OutFile has full path -if ! echo "$OutFile" | grep -Eq '^[a-zA-Z]:\\.*' && ! echo "$OutFile" | grep -Eq '^/.*'; then +if ! echo "${OutFile}" | grep -Eq '^[a-zA-Z]:\\.*' && ! echo "${OutFile}" | grep -Eq '^/.*'; then OutFile="${currentPath}/${OutFile}" fi -if ! echo "$DocsPath" | grep -Eq '^[a-zA-Z]:\\.*' && ! echo "$DocsPath" | grep -Eq '^/.*'; then +if ! echo "${DocsPath}" | grep -Eq '^[a-zA-Z]:\\.*' && ! echo "${DocsPath}" | grep -Eq '^/.*'; then DocsPath="${currentPath}/${DocsPath}" fi -if ! [ -d "${DocsPath}" ]; then +if ! test -d "${DocsPath}"; then error '' "Unable to find folder ${DocsPath}" 1 fi -scriptPath=$(dirname -- $(readlink -f "$0")) +scriptPath="$(dirname "$(readlink -f "$0")")" # Get path to docs files in the same folder as the docs -historyFilePath=$(get_file_path "$HistoryFile" $DocsPath) -orderFilePath=$(get_file_path "$OrderFile" $DocsPath) -if ! [ -f "${orderFilePath}" ]; then +historyFilePath=$(get_file_path "${HistoryFile}" "${DocsPath}") +orderFilePath=$(get_file_path "${OrderFile}" "${DocsPath}") +if ! test -f "${orderFilePath}"; then error '' "Unable to find order file ${orderFilePath}" 1 fi -replaceFilePath=$(get_file_path "$ReplaceFile" $DocsPath) +replaceFilePath=$(get_file_path "${ReplaceFile}" "${DocsPath}") # Get path to template files in the same folder as the script -templateFilePath=$(get_file_path "${Template}.tex" $scriptPath) -if ! [ -f "${templateFilePath}" ]; then +templateFilePath=$(get_file_path "${Template}.tex" "${scriptPath}") +if ! test -f "${templateFilePath}"; then error '' "Unable to find template file ${templateFilePath}" 1 fi -templateCoverFilePath=$(get_file_path "${Template}-cover.png" $scriptPath) -if ! [ -f "${templateCoverFilePath}" ]; then +templateCoverFilePath=$(get_file_path "${Template}-cover.png" "${scriptPath}") +if ! test -f "${templateCoverFilePath}"; then error '' "Unable to find template cover file ${templateCoverFilePath}" 1 fi -templateLogoFilePath=$(get_file_path "${Template}-logo.png" $scriptPath) -if ! [ -f "${templateLogoFilePath}" ]; then +templateLogoFilePath=$(get_file_path "${Template}-logo.png" "${scriptPath}") +if ! test -f "${templateLogoFilePath}"; then error '' "Unable to find template logo file ${templateLogoFilePath}" 1 fi info 'Get version history' versionHistory=$(get_version_history) -newLine=' -' -if [ "${OutFile: -3}" = '.md' ]; then +if [ "$(printf '%s' "${OutFile}" | tail -c 3)" = '.md' ]; then mdOutFile="${OutFile}" else mdOutFile="${OutFile}.md" fi info "Merge markdown files in ${orderFilePath}" -printf '%s\n' "$(cat -- "${orderFilePath}")" | while read line; do - if test -n "${line}" && ! [ "${line:0:1}" = '#' ]; then +printf '%s\n' "$(cat "${orderFilePath}")" | while read -r line; do + if test -n "${line}" && ! [ "$(printf '%s' "$line" | cut -c 1)" = '#' ]; then if ! test -f "${DocsPath}/${line}"; then error '' "Unable to find markdown file ${DocsPath}/${line}" 1 fi - mdFile=$(readlink -f -- "${DocsPath}/${line}") - mdPath=$(dirname -- $mdFile) + mdFile="$(readlink -f "${DocsPath}/${line}")" + mdPath="$(dirname "$mdFile")" tmpContent=$( printf '%s' "$(sed -e "s|\(\[.*\](\)\(\../\)\(.*)\)|\1${mdPath}/\2\3|g" "${mdFile}" | sed -e "s|\(\[.*\](\)\(\./\)\(.*)\)|\1${mdPath}/\3|g" | sed -e "s|\(\[.*\](\)\(asset\)\(.*)\)|\1${mdPath}/\2\3|g" | sed -e "s|\(\[.*\](\)\(attach\)\(.*)\)|\1${mdPath}/\2\3|g" | sed -e "s|\(\[.*\](\)\(image\)\(.*)\)|\1${mdPath}/\2\3|g" | sed -e "s|\(\[.*\](\)\(\.\)\(.*)\)|\1${mdPath}/\2\3|g")" ) @@ -262,7 +272,7 @@ printf '%s\n' "$(cat -- "${orderFilePath}")" | while read line; do fi fi else - info "Ignore $line" + info "Ignore ${line}" fi done info 'Done merging markdown files' @@ -271,28 +281,34 @@ if ! test -f "${mdOutFile}"; then exit 1 fi mdContent=$(cat "${mdOutFile}") -if [ -n "${ReplaceFile}" ]; then - if ! [ -f "${replaceFilePath}" ]; then +if test -n "${ReplaceFile}"; then + if ! test -f "${replaceFilePath}"; then error '' "Unable to find replace file ${replaceFilePath}" 1 fi info 'Perform replace in markdown' - while IFS=$'\t' read -r key value; do - mdContent=$(echo "${mdContent}" | sed -e "s/${key}/${value}/g") - done < <(jq -r 'to_entries[] | [.key, .value] | @tsv' $replaceFilePath) - echo "${mdContent}" > "${mdOutFile}" + tab_values=$(jq -r 'to_entries[] | [.key, .value] | @tsv' "${replaceFilePath}") + printf '%s\n' "${tab_values}" | while IFS="$(printf '\t')" read -r key value; do + printf '%s\n' "${mdContent}" | sed -e "s/${key}/${value}/g" > "${mdOutFile}" + done + mdContent="$(cat "${mdOutFile}")" fi authors=$(echo "${versionHistory}" | jq '.[].author' | uniq | sed ':a; N; $!ba; s/\n/,/g') -IFS= read -r -d '' metadataContent < "${DocsPath}/metadata.json" if test -n "${mdContent}"; then info "The markdown contains ${#mdContent} characters" - info "Create ${OutFile} using metadata:" - echo "${metadataContent}" | jq '.' - if ! [ "${OutFile: -3}" = '.md' ]; then + if ! [ "$(printf '%s' "${OutFile}" | tail -c 3)" = '.md' ]; then + info "Create ${OutFile} using metadata:" + printf '%s\n' "${metadataContent}" + printf '%s\n' "${metadataContent}" | jq '.' > "${DocsPath}/metadata.json" # We need to be in the docs path so image paths can be relative - cd $DocsPath + cd "${DocsPath}" echo "${mdContent}" | pandoc \ --standalone \ --listings \ @@ -340,12 +356,12 @@ if test -n "${mdContent}"; then --template="${templateFilePath}" \ --filter pandoc-latex-environment \ --output="${OutFile}" - cd $currentPath + cd "${currentPath}" fi - if [ ! -f $OutFile ]; then + if ! test -f "${OutFile}"; then warning "Unable to create ${OutFile}" else - size=$(expr $(stat -c '%s' $OutFile) / 1000) + size=$(($(stat -c '%s' "${OutFile}") / 1000)) info "Created ${OutFile} using ${size} KB" fi fi diff --git a/tools/designdoc.tex b/tools/designdoc.tex index 357bc2d..0e76680 100644 --- a/tools/designdoc.tex +++ b/tools/designdoc.tex @@ -374,7 +374,7 @@ $endif$ \usepackage{titling} \usepackage{fancyhdr} -\usepackage{lastpage} +% \usepackage{lastpage} \usepackage{amsfonts} \usepackage{sectsty} \sectionfont{\clearpage} diff --git a/tools/install-tools.sh b/tools/install-tools.sh new file mode 100755 index 0000000..6db21f7 --- /dev/null +++ b/tools/install-tools.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env sh +set -e +aptinstall() { + if [ "${apt_update}" -eq 0 ]; then + sudo apt-get -q --no-allow-insecure-repositories update + apt_update=1 + fi + sudo apt-get install --assume-yes --no-install-recommends "${1}" +} +if type apt-get > /dev/null 2>&1; then + texlivebin='' + export DEBIAN_FRONTEND=noninteractive + apt_update=0 + if ! type rsvg-convert > /dev/null 2>&1; then + aptinstall librsvg2-bin + fi + if ! test -f '/usr/share/fonts/truetype/crosextra/Carlito-Regular.ttf'; then + aptinstall fonts-crosextra-carlito + fi + if ! type pandoc > /dev/null 2>&1; then + aptinstall pandoc + fi + sudo rm -rf /var/lib/apt/lists/* > /dev/null 2>&1 + scriptPath="$(dirname "$(readlink -f "$0")")" + if test -f "/opt/texlive/texdir/install-tl"; then + texlivebin="$(dirname $(find /opt/texlive/texdir/bin -name tlmgr))" + export PATH="${texlivebin}:${PATH}" + sudo env "PATH=${PATH}" tlmgr path add + else + cd /tmp + uri='https://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz' + echo "Download ${uri}" + HTTP_CODE=$(curl -sSL --remote-name --retry 4 \ + --write-out "%{response_code}" "${uri}" + ) + if [ "${HTTP_CODE}" -lt 200 ] || [ "${HTTP_CODE}" -gt 299 ]; then + echo "##[error]Unable to get ${uri}! Response code: ${HTTP_CODE}" + exit 1 + fi + zcat < install-tl-unx.tar.gz | tar xf - + rm -f install-tl-unx.tar.gz + TLTMP=$(readlink -f install-tl-*) + TLPROFILE=$(readlink -f "${scriptPath}/texlive.profile") + sudo perl "${TLTMP}/install-tl" --no-interaction --no-doc-install --no-src-install --profile="${TLPROFILE}" + rm -rf "${TLTMP}" + texlivebin="$(dirname $(find /opt/texlive/texdir/bin -name tlmgr))" + export PATH="${texlivebin}:${PATH}" + sudo env "PATH=${PATH}" tlmgr init-usertree + TLPKG=$(readlink -f "${scriptPath}/texlive_packages.txt") + sed -e 's/ *#.*$//' -e '/^ *$/d' "${TLPKG}" | xargs sudo env "PATH=${PATH}" tlmgr install + sudo chmod -R o+w /opt/texlive/texdir/texmf-var + fi + if [ -n "${TF_BUILD-}" ]; then + echo "##vso[task.prependpath]${texlivebin}" + else + echo "${texlivebin}" >> "${GITHUB_PATH}" + fi + TLREQ=$(readlink -f "${scriptPath}/requirements.txt") + sudo env "PATH=${PATH}" pip3 --no-cache-dir install -r "${TLREQ}" +elif type apk > /dev/null 2>&1; then + if ! type git > /dev/null 2>&1; then + apk add --no-cache git + fi + if ! type curl > /dev/null 2>&1; then + apk add --no-cache curl + fi + if ! type jq > /dev/null 2>&1; then + apk add --no-cache jq + fi + if ! type rsvg-convert > /dev/null 2>&1; then + apk add --no-cache librsvg + fi + if ! test -f '/usr/share/fonts/carlito/Carlito-Regular.ttf'; then + apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community font-carlito + fi +# if type tlmgr > /dev/null 2>&1 && ! test -f '/opt/texlive/texdir/texmf-dist/tex/latex/lastpage/lastpage.sty'; then +# tlmgr update --self +# tlmgr option -- autobackup -1 +# tlmgr install lastpage +# else +# echo "Unable to find tlmgr in path: ${PATH}" +# exit 1 +# fi +else + echo 'Unable to find apt-get or apk!' + exit 1 +fi diff --git a/tools/requirements.txt b/tools/requirements.txt new file mode 100644 index 0000000..3032cef --- /dev/null +++ b/tools/requirements.txt @@ -0,0 +1,8 @@ +# +# Python filters +# +# We are freezing only the 2 first digits (e.g. `1.3`) +# because minor versions (e.g. `1.3.6`) _should_ not break things +# + +pandoc-latex-environment==1.1 diff --git a/tools/texlive.profile b/tools/texlive.profile new file mode 100644 index 0000000..0c68891 --- /dev/null +++ b/tools/texlive.profile @@ -0,0 +1,26 @@ +selected_scheme scheme-basic +TEXDIR /opt/texlive/texdir +TEXMFLOCAL /opt/texlive/texmf-local +TEXMFSYSVAR /opt/texlive/texdir/texmf-var +TEXMFSYSCONFIG /opt/texlive/texdir/texmf-config +TEXMFVAR ~/.texlive/texmf-var +TEXMFCONFIG ~/.texlive/texmf-config +TEXMFHOME ~/texmf +instopt_adjustpath 0 +instopt_adjustrepo 1 +instopt_letter 0 +instopt_portable 0 +instopt_write18_restricted 1 +tlpdbopt_autobackup 0 +tlpdbopt_backupdir tlpkg/backups +tlpdbopt_create_formats 1 +tlpdbopt_desktop_integration 1 +tlpdbopt_file_assocs 1 +tlpdbopt_generate_updmap 0 +tlpdbopt_install_docfiles 0 +tlpdbopt_install_srcfiles 0 +tlpdbopt_post_code 1 +tlpdbopt_sys_bin /usr/local/bin +tlpdbopt_sys_info /usr/local/share/info +tlpdbopt_sys_man /usr/local/share/man +tlpdbopt_w32_multi_user 1 diff --git a/tools/texlive_packages.txt b/tools/texlive_packages.txt new file mode 100644 index 0000000..5223a12 --- /dev/null +++ b/tools/texlive_packages.txt @@ -0,0 +1,193 @@ +# Packages listed in https://pandoc.org/MANUAL.html#creating-a-pdf + +# NOTE: search left hand side on CTAN to see for yourself: +# graphicx -> graphics +# grffile -> oberdiek +# longtable -> tools + +# Redundant, as included in `scheme-basic` +#amsfonts +#amsmath +#babel-english +#bibtex +#geometry +#graphics +#hyperref +#iftex +#lm +#luatex +#natbib +#oberdiek +#pdftexcmds +#tools # The LaTeX standard tools bundle; e.g., calc, longtable + +# Other basic packages +beamer +booktabs +caption # Customize captions in floating envs; required for beamer +cmap # Make PDF files searchable and copyable +euler # Use AMS Euler fonts for math +eurosym # Metafont and macros for Euro sign +fancyvrb +listings +lm-math +logreq +memoir +multirow # Tabular cells spanning multiple rows +parskip +pdflscape # landscape mode for single pages +pgf # for TikZ +setspace +ulem +unicode-math +xcolor + +# Required when using pandoc-crossref +cleveref # Intelligent cross-referencing +float # Improved interface for floating objects +subfig # Figures broken into subfigures + +# Needed for when `--highlight-style` is used with something other than +# pygments. +framed + +######################################################################### +# Extra packages for XeTex, LuaTex, and BibLaTex. +embedfile +fontspec +hyperxmp +ifmtarg +luacode +lualatex-math +luatexbase +mathspec +microtype +selnolig +upquote +xetex + +######################################################################### +# I18n and languages; the choice of selected languages is historic, +# those were the ones installed by texlive by default for a long time. +bidi +csquotes +#babel-basque +#babel-czech +babel-danish +#babel-dutch +babel-finnish +#babel-french +#babel-german +#babel-hungarian +#babel-italian +babel-norsk +#babel-polish +#babel-portuges +#babel-spanish +babel-swedish +#hyphen-basque +#hyphen-czech +hyphen-danish +#hyphen-dutch +hyphen-english +hyphen-finnish +#hyphen-french +#hyphen-german +#hyphen-hungarian +#hyphen-italian +hyphen-norwegian +#hyphen-polish +#hyphen-portuguese +#hyphen-spanish +hyphen-swedish +# no longer needed in newer pandoc versions +#polyglossia + +######################################################################### +# Reference backend options +biber +biblatex + +######################################################################### +# These packages were identified by the tests, they are likely +# dependencies of dependencies that are not encoded well. +footnotehyper +soul +xurl + +# +# Latex packages required by the templates and filters +# + +######################################################################### +# Required by pandoc-latex-environment filter +# Redundant, as included in above +#etoolbox +#pgf + +environ +tcolorbox +trimspaces + +######################################################################### +# Required by eisvogel template +# see https://github.com/Wandmalfarbe/pandoc-latex-template/blob/master/.github/workflows/build-examples.yml +# Redundant, as included in above or in scheme-basic +#babel-german +#bidi +#csquotes +#filehook +#framed +#letltxmacro +#ulem +#unicode-math +#upquote +#xurl + +abstract +adjustbox +awesomebox +background +catchfile +collectbox +everypage +fontawesome5 +footmisc +footnotebackref +fvextra +hardwrap +incgraph +lineno +listingsutf8 +ly1 +koma-script +mdframed +mweights +needspace +pagecolor +sectsty +sourcecodepro +sourcesanspro +titlesec +titling +transparent +ucharcat +xecjk +zref + +######################################################################### +# Completes Source family +sourceserifpro + +######################################################################### +# Required by Beamer/Metropolis +beamertheme-metropolis +pgfopts +tcolorbox +environ +tikzfill + +# https://github.com/pandoc/dockerfiles/issues/135 +enumitem + +lastpage