From 66dc8fa292c533c60eec59c3431631c96344cabd Mon Sep 17 00:00:00 2001 From: Yutao ZHU <740876080@qq.com> Date: Tue, 12 Mar 2024 10:58:17 +0800 Subject: [PATCH] Initial commit --- .docker/Dockerfile | 30 ++ .docker/entrypoint.sh | 27 ++ .docker/run.sh | 37 ++ .github/DISCUSSION_TEMPLATE/general.yaml | 35 ++ .github/DISCUSSION_TEMPLATE/q-a.yaml | 35 ++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/issue.yaml | 38 ++ .github/pull_request_template.md | 13 + .github/user_pull_request_template.md | 4 + .github/workflows/build-preview.yaml | 58 +++ .github/workflows/build-site.yaml | 56 +++ .github/workflows/first-time-setup.yaml | 118 ++++++ .github/workflows/on-pages.yaml | 27 ++ .github/workflows/on-pull-request.yml | 22 ++ .github/workflows/on-push.yml | 24 ++ .github/workflows/on-schedule.yaml | 27 ++ .github/workflows/update-citations.yaml | 84 +++++ .github/workflows/update-url.yaml | 69 ++++ .github/workflows/versioning.yaml | 135 +++++++ .gitignore | 15 + 404.md | 11 + CHANGELOG.md | 146 ++++++++ CITATION.cff | 14 + Gemfile | 16 + Gemfile.lock | 157 ++++++++ LICENSE.md | 29 ++ README.md | 30 ++ _cite/.cache/cache.db | Bin 0 -> 49152 bytes _cite/cite.py | 188 ++++++++++ _cite/plugins/google-scholar.py | 61 ++++ _cite/plugins/orcid.py | 109 ++++++ _cite/plugins/pubmed.py | 46 +++ _cite/plugins/sources.py | 6 + _cite/requirements.txt | 7 + _cite/util.py | 234 ++++++++++++ _config.yaml | 73 ++++ _data/citations.yaml | 247 +++++++++++++ _data/orcid.yaml | 1 + _data/projects.yaml | 47 +++ _data/sources.yaml | 23 ++ _data/types.yaml | 216 +++++++++++ _includes/alert.html | 10 + _includes/analytics.html | 3 + _includes/button.html | 24 ++ _includes/card.html | 47 +++ _includes/citation.html | 109 ++++++ _includes/cols.html | 6 + _includes/content.html | 32 ++ _includes/fallback.html | 1 + _includes/feature.html | 27 ++ _includes/figure.html | 25 ++ _includes/float.html | 11 + _includes/fonts.html | 17 + _includes/footer.html | 42 +++ _includes/grid.html | 3 + _includes/head.html | 8 + _includes/header.html | 59 +++ _includes/icon.html | 10 + _includes/list.html | 58 +++ _includes/manubot.svg | 78 ++++ _includes/meta.html | 99 +++++ _includes/portrait.html | 45 +++ _includes/post-excerpt.html | 60 ++++ _includes/post-info.html | 37 ++ _includes/post-nav.html | 18 + _includes/scripts.html | 10 + _includes/search-box.html | 16 + _includes/search-info.html | 1 + _includes/section.html | 10 + _includes/site-search.html | 6 + _includes/styles.html | 28 ++ _includes/tags.html | 29 ++ _includes/verification.html | 3 + _layouts/default.html | 11 + _layouts/member.html | 51 +++ _layouts/post.html | 24 ++ _members/jane-smith.md | 19 + _members/john-doe.md | 10 + _members/sarah-johnson.md | 11 + _plugins/array.rb | 25 ++ _plugins/file.rb | 20 ++ _plugins/hash.rb | 28 ++ _plugins/misc.rb | 87 +++++ _plugins/regex.rb | 28 ++ _posts/2019-01-07-example-post-1.md | 10 + _posts/2021-09-30-example-post-2.md | 6 + _posts/2023-02-23-example-post-3.md | 8 + _scripts/anchors.js | 47 +++ _scripts/dark-mode.js | 25 ++ _scripts/fetch-tags.js | 67 ++++ _scripts/search.js | 215 +++++++++++ _scripts/site-search.js | 14 + _scripts/table-wrap.js | 25 ++ _scripts/tooltip.js | 41 +++ _styles/-theme.scss | 54 +++ _styles/alert.scss | 37 ++ _styles/all.scss | 11 + _styles/anchor.scss | 24 ++ _styles/background.scss | 21 ++ _styles/body.scss | 15 + _styles/bold.scss | 7 + _styles/button.scss | 51 +++ _styles/card.scss | 52 +++ _styles/checkbox.scss | 6 + _styles/citation.scss | 103 ++++++ _styles/code.scss | 38 ++ _styles/cols.scss | 39 ++ _styles/dark-toggle.scss | 31 ++ _styles/feature.scss | 53 +++ _styles/figure.scss | 26 ++ _styles/float.scss | 38 ++ _styles/font.scss | 5 + _styles/footer.scss | 25 ++ _styles/form.scss | 9 + _styles/grid.scss | 54 +++ _styles/header.scss | 166 +++++++++ _styles/heading.scss | 50 +++ _styles/highlight.scss | 7 + _styles/icon.scss | 16 + _styles/image.scss | 7 + _styles/link.scss | 16 + _styles/list.scss | 24 ++ _styles/main.scss | 8 + _styles/paragraph.scss | 8 + _styles/portrait.scss | 76 ++++ _styles/post-excerpt.scss | 69 ++++ _styles/post-info.scss | 33 ++ _styles/post-nav.scss | 39 ++ _styles/quote.scss | 16 + _styles/rule.scss | 9 + _styles/search-box.scss | 26 ++ _styles/search-info.scss | 9 + _styles/section.scss | 39 ++ _styles/table.scss | 18 + _styles/tags.scss | 34 ++ _styles/textbox.scss | 17 + _styles/tooltip.scss | 65 ++++ _styles/util.scss | 14 + blog/index.md | 21 ++ contact/index.md | 77 ++++ images/background.jpg | Bin 0 -> 82867 bytes images/fallback.svg | 10 + images/icon.png | Bin 0 -> 19174 bytes images/logo.svg | 68 ++++ images/photo.jpg | Bin 0 -> 7310 bytes images/share.jpg | Bin 0 -> 40015 bytes index.md | 94 +++++ projects/index.md | 27 ++ research/index.md | 27 ++ team/index.md | 35 ++ testbed.md | 437 +++++++++++++++++++++++ 151 files changed, 6488 insertions(+) create mode 100644 .docker/Dockerfile create mode 100755 .docker/entrypoint.sh create mode 100755 .docker/run.sh create mode 100644 .github/DISCUSSION_TEMPLATE/general.yaml create mode 100644 .github/DISCUSSION_TEMPLATE/q-a.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/issue.yaml create mode 100644 .github/pull_request_template.md create mode 100644 .github/user_pull_request_template.md create mode 100644 .github/workflows/build-preview.yaml create mode 100644 .github/workflows/build-site.yaml create mode 100644 .github/workflows/first-time-setup.yaml create mode 100644 .github/workflows/on-pages.yaml create mode 100644 .github/workflows/on-pull-request.yml create mode 100644 .github/workflows/on-push.yml create mode 100644 .github/workflows/on-schedule.yaml create mode 100644 .github/workflows/update-citations.yaml create mode 100644 .github/workflows/update-url.yaml create mode 100644 .github/workflows/versioning.yaml create mode 100644 .gitignore create mode 100644 404.md create mode 100644 CHANGELOG.md create mode 100644 CITATION.cff create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 _cite/.cache/cache.db create mode 100644 _cite/cite.py create mode 100644 _cite/plugins/google-scholar.py create mode 100644 _cite/plugins/orcid.py create mode 100644 _cite/plugins/pubmed.py create mode 100644 _cite/plugins/sources.py create mode 100644 _cite/requirements.txt create mode 100644 _cite/util.py create mode 100644 _config.yaml create mode 100644 _data/citations.yaml create mode 100644 _data/orcid.yaml create mode 100644 _data/projects.yaml create mode 100644 _data/sources.yaml create mode 100644 _data/types.yaml create mode 100644 _includes/alert.html create mode 100644 _includes/analytics.html create mode 100644 _includes/button.html create mode 100644 _includes/card.html create mode 100644 _includes/citation.html create mode 100644 _includes/cols.html create mode 100644 _includes/content.html create mode 100644 _includes/fallback.html create mode 100644 _includes/feature.html create mode 100644 _includes/figure.html create mode 100644 _includes/float.html create mode 100644 _includes/fonts.html create mode 100644 _includes/footer.html create mode 100644 _includes/grid.html create mode 100644 _includes/head.html create mode 100644 _includes/header.html create mode 100644 _includes/icon.html create mode 100644 _includes/list.html create mode 100644 _includes/manubot.svg create mode 100644 _includes/meta.html create mode 100644 _includes/portrait.html create mode 100644 _includes/post-excerpt.html create mode 100644 _includes/post-info.html create mode 100644 _includes/post-nav.html create mode 100644 _includes/scripts.html create mode 100644 _includes/search-box.html create mode 100644 _includes/search-info.html create mode 100644 _includes/section.html create mode 100644 _includes/site-search.html create mode 100644 _includes/styles.html create mode 100644 _includes/tags.html create mode 100644 _includes/verification.html create mode 100644 _layouts/default.html create mode 100644 _layouts/member.html create mode 100644 _layouts/post.html create mode 100644 _members/jane-smith.md create mode 100644 _members/john-doe.md create mode 100644 _members/sarah-johnson.md create mode 100644 _plugins/array.rb create mode 100644 _plugins/file.rb create mode 100644 _plugins/hash.rb create mode 100644 _plugins/misc.rb create mode 100644 _plugins/regex.rb create mode 100644 _posts/2019-01-07-example-post-1.md create mode 100644 _posts/2021-09-30-example-post-2.md create mode 100644 _posts/2023-02-23-example-post-3.md create mode 100644 _scripts/anchors.js create mode 100644 _scripts/dark-mode.js create mode 100644 _scripts/fetch-tags.js create mode 100644 _scripts/search.js create mode 100644 _scripts/site-search.js create mode 100644 _scripts/table-wrap.js create mode 100644 _scripts/tooltip.js create mode 100644 _styles/-theme.scss create mode 100644 _styles/alert.scss create mode 100644 _styles/all.scss create mode 100644 _styles/anchor.scss create mode 100644 _styles/background.scss create mode 100644 _styles/body.scss create mode 100644 _styles/bold.scss create mode 100644 _styles/button.scss create mode 100644 _styles/card.scss create mode 100644 _styles/checkbox.scss create mode 100644 _styles/citation.scss create mode 100644 _styles/code.scss create mode 100644 _styles/cols.scss create mode 100644 _styles/dark-toggle.scss create mode 100644 _styles/feature.scss create mode 100644 _styles/figure.scss create mode 100644 _styles/float.scss create mode 100644 _styles/font.scss create mode 100644 _styles/footer.scss create mode 100644 _styles/form.scss create mode 100644 _styles/grid.scss create mode 100644 _styles/header.scss create mode 100644 _styles/heading.scss create mode 100644 _styles/highlight.scss create mode 100644 _styles/icon.scss create mode 100644 _styles/image.scss create mode 100644 _styles/link.scss create mode 100644 _styles/list.scss create mode 100644 _styles/main.scss create mode 100644 _styles/paragraph.scss create mode 100644 _styles/portrait.scss create mode 100644 _styles/post-excerpt.scss create mode 100644 _styles/post-info.scss create mode 100644 _styles/post-nav.scss create mode 100644 _styles/quote.scss create mode 100644 _styles/rule.scss create mode 100644 _styles/search-box.scss create mode 100644 _styles/search-info.scss create mode 100644 _styles/section.scss create mode 100644 _styles/table.scss create mode 100644 _styles/tags.scss create mode 100644 _styles/textbox.scss create mode 100644 _styles/tooltip.scss create mode 100644 _styles/util.scss create mode 100644 blog/index.md create mode 100644 contact/index.md create mode 100644 images/background.jpg create mode 100644 images/fallback.svg create mode 100644 images/icon.png create mode 100644 images/logo.svg create mode 100644 images/photo.jpg create mode 100644 images/share.jpg create mode 100644 index.md create mode 100644 projects/index.md create mode 100644 research/index.md create mode 100644 team/index.md create mode 100644 testbed.md diff --git a/.docker/Dockerfile b/.docker/Dockerfile new file mode 100644 index 0000000..caab63b --- /dev/null +++ b/.docker/Dockerfile @@ -0,0 +1,30 @@ +# start with official ruby docker image as base +FROM ruby:3.1.2 + +# set working directory within container +WORKDIR /usr/src/app + +# pull in ruby (jekyll) and python (cite process) package info +COPY Gemfile Gemfile.lock _cite/requirements.txt ./ + +# install ruby packages +RUN VERSION=$(grep -A 1 'BUNDLED WITH' Gemfile.lock | tail -n 1 | xargs); \ + gem install bundler --version ${VERSION} && \ + bundle _${VERSION}_ install + +# install python +RUN apt update && apt install -y python3 python3-pip + +# install python packages +RUN python3 -m pip install --no-cache-dir --upgrade --requirement requirements.txt + +# install python package for listening for file changes +RUN pip install "watchdog[watchmedo]==3.0.0" + +# ports used by jekyll +EXPOSE 4000 +EXPOSE 35729 + +# run jekyll and cite process +COPY .docker/entrypoint.sh /var +CMD [ "/var/entrypoint.sh" ] diff --git a/.docker/entrypoint.sh b/.docker/entrypoint.sh new file mode 100755 index 0000000..77c697a --- /dev/null +++ b/.docker/entrypoint.sh @@ -0,0 +1,27 @@ +#! /bin/bash + +# print folder contents for debugging +echo "Contents:" +echo "" +ls +echo "" + +# run cite process +python3 _cite/cite.py + +# run jekyll serve in hot-reload mode +# rerun whenever _config.yaml changes (jekyll hot-reload doesn't work with this file) +watchmedo auto-restart \ + --debug-force-polling \ + --patterns="_config.yaml" \ + --signal SIGTERM \ + -- bundle exec jekyll serve --open-url --force_polling --livereload --trace --host=0.0.0.0 \ + | sed "s/LiveReload address.*//g;s/0.0.0.0/localhost/g" & + +# rerun cite process whenever _data files change +watchmedo shell-command \ + --debug-force-polling \ + --recursive \ + --wait \ + --command="python3 _cite/cite.py" \ + --patterns="_data/sources*;_data/orcid*;_data/pubmed*;_data/google-scholar*" \ diff --git a/.docker/run.sh b/.docker/run.sh new file mode 100755 index 0000000..34ffd05 --- /dev/null +++ b/.docker/run.sh @@ -0,0 +1,37 @@ +#! /bin/bash + +# name of image +IMAGE=lab-website-renderer:latest + +# name of running container +CONTAINER=lab-website-renderer + +# choose platform flag +PLATFORM="" + +# default vars +DOCKER_RUN="docker run" +WORKING_DIR=$(pwd) + +# fix windows faux linux shells/tools +if [[ $OSTYPE == msys* ]] || [[ $OSTYPE == cygwin* ]]; then + DOCKER_RUN="winpty docker run" + WORKING_DIR=$(cmd //c cd) +fi + +# build docker image +docker build ${PLATFORM} \ + --tag ${IMAGE} \ + --file ./.docker/Dockerfile . && \ + +# run built docker image +${DOCKER_RUN} ${PLATFORM} \ + --name ${CONTAINER} \ + --init \ + --rm \ + --interactive \ + --tty \ + --publish 4000:4000 \ + --publish 35729:35729 \ + --volume "${WORKING_DIR}:/usr/src/app" \ + ${IMAGE} "$@" diff --git a/.github/DISCUSSION_TEMPLATE/general.yaml b/.github/DISCUSSION_TEMPLATE/general.yaml new file mode 100644 index 0000000..66b2c49 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/general.yaml @@ -0,0 +1,35 @@ +body: + - type: checkboxes + attributes: + label: Checks + options: + - label: I have searched **[the docs](https://greene-lab.gitbook.io/lab-website-template-docs)**, [existing issues](https://github.com/greenelab/lab-website-template/issues), and [existing discussions](https://github.com/greenelab/lab-website-template/discussions) for answers first. + required: true + + - type: input + id: repo + attributes: + label: Link to your website repo + description: "In almost all cases, **we cannot help you if you don't provide this**." + placeholder: ex. https://github.com/greenelab/greenelab.com + validations: + required: true + + - type: input + id: version + attributes: + label: Version of Lab Website Template you are using + description: See your `CITATION.cff` file. + placeholder: ex. 1.0.0 + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: | + Describe your issue in as much detail as possible. For example: What happened? What did you expect to happen? How can we reproduce the problem? What browser are you seeing the problem in? + placeholder: Description + validations: + required: true diff --git a/.github/DISCUSSION_TEMPLATE/q-a.yaml b/.github/DISCUSSION_TEMPLATE/q-a.yaml new file mode 100644 index 0000000..66b2c49 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/q-a.yaml @@ -0,0 +1,35 @@ +body: + - type: checkboxes + attributes: + label: Checks + options: + - label: I have searched **[the docs](https://greene-lab.gitbook.io/lab-website-template-docs)**, [existing issues](https://github.com/greenelab/lab-website-template/issues), and [existing discussions](https://github.com/greenelab/lab-website-template/discussions) for answers first. + required: true + + - type: input + id: repo + attributes: + label: Link to your website repo + description: "In almost all cases, **we cannot help you if you don't provide this**." + placeholder: ex. https://github.com/greenelab/greenelab.com + validations: + required: true + + - type: input + id: version + attributes: + label: Version of Lab Website Template you are using + description: See your `CITATION.cff` file. + placeholder: ex. 1.0.0 + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: | + Describe your issue in as much detail as possible. For example: What happened? What did you expect to happen? How can we reproduce the problem? What browser are you seeing the problem in? + placeholder: Description + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..7d0b7de --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: 💬 Start a discussion + url: https://github.com/greenelab/lab-website-template/discussions + about: I need help, I have a question, or other discussion. + - name: 📚 Docs issue + url: https://github.com/greenelab/lab-website-template-docs/issues + about: I have a question or issue related to the template documentation. diff --git a/.github/ISSUE_TEMPLATE/issue.yaml b/.github/ISSUE_TEMPLATE/issue.yaml new file mode 100644 index 0000000..d812555 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue.yaml @@ -0,0 +1,38 @@ +name: 🐞 Create an issue +description: I think I've discovered a bug, I want to request a feature/change, or other issue. + +body: + - type: checkboxes + attributes: + label: Checks + options: + - label: I have searched **[the docs](https://greene-lab.gitbook.io/lab-website-template-docs)**, [existing issues](https://github.com/greenelab/lab-website-template/issues), and [existing discussions](https://github.com/greenelab/lab-website-template/discussions) for answers first. + required: true + + - type: input + id: repo + attributes: + label: Link to your website repo + description: "In almost all cases, **we cannot help you if you don't provide this**." + placeholder: ex. https://github.com/greenelab/greenelab.com + validations: + required: true + + - type: input + id: version + attributes: + label: Version of Lab Website Template you are using + description: See your `CITATION.cff` file. + placeholder: ex. 1.0.0 + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: | + Describe your issue in as much detail as possible. For example: What happened? What did you expect to happen? How can we reproduce the problem? What browser are you seeing the problem in? + placeholder: Description + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..6aaf76b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +STOP!!! + +You are about to open this pull request against THE TEMPLATE ITSELF. You probably meant to open it against your own website repo. + +--- + +FOR THE TEMPLATE MAINTAINER(S) + +New template version checklist: + +- [ ] I have updated CITATION and CHANGELOG as appropriate. +- [ ] I have updated lab-website-template-docs as appropriate. +- [ ] I have checked the testbed as appropriate. diff --git a/.github/user_pull_request_template.md b/.github/user_pull_request_template.md new file mode 100644 index 0000000..00a0e71 --- /dev/null +++ b/.github/user_pull_request_template.md @@ -0,0 +1,4 @@ +This website is based on the Lab Website Template. +See its documentation for working with this site: + +https://greene-lab.gitbook.io/lab-website-template-docs diff --git a/.github/workflows/build-preview.yaml b/.github/workflows/build-preview.yaml new file mode 100644 index 0000000..3ef0404 --- /dev/null +++ b/.github/workflows/build-preview.yaml @@ -0,0 +1,58 @@ +name: build-preview +run-name: build pull request preview + +on: + # run when called from another workflow + workflow_call: + + # run if user manually requests it + workflow_dispatch: + +# variables +env: + PREVIEWS_FOLDER: preview + +permissions: + contents: write + pull-requests: write + +jobs: + build-preview: + runs-on: ubuntu-latest + + steps: + # for debugging + - uses: crazy-max/ghaction-dump-context@v2 + + - name: Checkout branch contents + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.head_ref }} + + - name: Install Ruby packages + if: github.event.action != 'closed' + uses: ruby/setup-ruby@v1.172.0 + with: + ruby-version: "3.1" + bundler-cache: true + + - name: Get Pages url + if: github.event.action != 'closed' + id: pages + uses: actions/configure-pages@v4 + + # for debugging + - if: runner.debug == '1' + uses: mxschmitt/action-tmate@v3 + + - name: Build preview version of site + if: github.event.action != 'closed' + run: | + JEKYLL_ENV=production bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path || '' }}/${{ env.PREVIEWS_FOLDER }}/pr-${{ github.event.number }}" + + - name: Commit preview to Pages branch + uses: rossjrw/pr-preview-action@v1.4.7 + with: + source-dir: _site + umbrella-dir: ${{ env.PREVIEWS_FOLDER }} diff --git a/.github/workflows/build-site.yaml b/.github/workflows/build-site.yaml new file mode 100644 index 0000000..6aa3e5a --- /dev/null +++ b/.github/workflows/build-site.yaml @@ -0,0 +1,56 @@ +name: build-site +run-name: build live site + +on: + # run when called from another workflow + workflow_call: + + # run if user manually requests it + workflow_dispatch: + +# variables +env: + PREVIEWS_FOLDER: preview + +permissions: + contents: write + +jobs: + build-site: + runs-on: ubuntu-latest + + steps: + # for debugging + - uses: crazy-max/ghaction-dump-context@v2 + + - name: Checkout branch contents + uses: actions/checkout@v4 + + - name: Install Ruby packages + uses: ruby/setup-ruby@v1.172.0 + with: + ruby-version: "3.1" + bundler-cache: true + + - name: Get Pages url + id: pages + uses: actions/configure-pages@v4 + + # for debugging + - if: runner.debug == '1' + uses: mxschmitt/action-tmate@v3 + + - name: Set root url + run: | + echo "\n\nurl: ${{ steps.pages.outputs.origin }}" >> _config.yaml + + - name: Build live version of site + run: | + JEKYLL_ENV=production bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path || '' }}" + + - name: Commit live site to Pages branch + uses: JamesIves/github-pages-deploy-action@v4.5.0 + with: + folder: _site + clean-exclude: ${{ env.PREVIEWS_FOLDER }} + force: false diff --git a/.github/workflows/first-time-setup.yaml b/.github/workflows/first-time-setup.yaml new file mode 100644 index 0000000..51d3273 --- /dev/null +++ b/.github/workflows/first-time-setup.yaml @@ -0,0 +1,118 @@ +name: first-time-setup +run-name: first time setup of repo + +on: + # run if user manually requests it + workflow_dispatch: + +permissions: + contents: write + +jobs: + first-time-setup: + runs-on: ubuntu-latest + + steps: + # for debugging + - uses: crazy-max/ghaction-dump-context@v2 + + - name: Create Pages branch + uses: peterjgrainger/action-create-branch@v3.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + branch: "gh-pages" + + - name: Checkout Pages branch + uses: actions/checkout@v4 + with: + ref: gh-pages + + # for debugging + - if: runner.debug == '1' + uses: mxschmitt/action-tmate@v3 + + # clean slate, as if starting from orphan branch + - name: Clear Pages branch + run: rm -rf * .github .docker .gitignore + + # prevent GitHub from running Jekyll a second time after build + - name: Make .nojekyll file + run: touch .nojekyll + + - name: Make placeholder homepage + run: echo "Placeholder homepage" > index.html + + - name: Commit changes to Pages branch + uses: stefanzweifel/git-auto-commit-action@v5 + with: + branch: gh-pages + commit_message: "Clear branch" + + - name: Checkout main branch + uses: actions/checkout@v4 + + - name: Remove files user doesn't need + run: | + rm -rf \ + CHANGELOG.md \ + testbed.md \ + .github/ISSUE_TEMPLATE \ + .github/DISCUSSION_TEMPLATE \ + .github/workflows/versioning.yaml \ + .github/pull_request_template.md \ + + - name: Rename files + run: | + mv -f .github/user_pull_request_template.md .github/pull_request_template.md + + - name: Set vars for personalization + run: | + user="${{ github.repository_owner }}" + description="An engaging 1-3 sentence description of your lab." + echo "USER=${user}" >> $GITHUB_ENV + echo "DESCRIPTION=${description}" >> $GITHUB_ENV + + - name: Personalize readme for user + run: | + echo " + # ${{ env.USER }}'s Website + + Visit **[website url](#)** 🚀 + + _Built with [Lab Website Template](https://greene-lab.gitbook.io/lab-website-template-docs)_ + " > README.md + + - name: Personalize Jekyll config for user + uses: actions/github-script@v7 + with: + script: | + const { readFileSync, writeFileSync } = require("fs"); + const file = "_config.yaml"; + const contents = readFileSync(file) + .toString() + .replace(/(^title: ).*$/m, "$1${{ env.USER }}") + .replace(/(^subtitle: ).*$/m, "$1") + .replace(/(^description: ).*$/m, "$1${{ env.DESCRIPTION }}") + .replace(/(^ email: ).*$/m, "$1contact@${{ env.USER }}.com") + .replace(/(^ github: ).*$/m, "$1${{ env.USER }}") + .replace(/(^ twitter: ).*$/m, "$1${{ env.USER }}") + .replace(/(^ youtube: ).*$/m, "$1${{ env.USER }}"); + writeFileSync(file, contents); + + - name: Personalize homepage for user + uses: actions/github-script@v7 + with: + script: | + const { readFileSync, writeFileSync } = require("fs"); + const file = "index.md"; + let contents = readFileSync(file).toString(); + const find = /\# Lab Website Template[\s\S]+({% include section\.html)/; + const replace = `# ${{ env.USER }}'s Website\n\n${{ env.DESCRIPTION }}\n\n$1`; + contents = contents.replace(find, replace); + writeFileSync(file, contents); + + - name: Commit changed files + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "Setup repo" diff --git a/.github/workflows/on-pages.yaml b/.github/workflows/on-pages.yaml new file mode 100644 index 0000000..b61c13b --- /dev/null +++ b/.github/workflows/on-pages.yaml @@ -0,0 +1,27 @@ +name: on-pages +run-name: on pages deploy + +on: + workflow_run: + workflows: [pages-build-deployment] + types: + - completed + branches: + - gh-pages + + # run if user manually requests it + workflow_dispatch: + +permissions: + contents: write + +jobs: + update-url: + # only run on user instance of template, not template itself + if: github.repository != 'greenelab/lab-website-template' + uses: ./.github/workflows/update-url.yaml + + build-site: + needs: update-url + if: needs.update-url.outputs.changed == 'true' + uses: ./.github/workflows/build-site.yaml diff --git a/.github/workflows/on-pull-request.yml b/.github/workflows/on-pull-request.yml new file mode 100644 index 0000000..5cdf21a --- /dev/null +++ b/.github/workflows/on-pull-request.yml @@ -0,0 +1,22 @@ +name: on-pull-request +run-name: on pull request activity + +on: + pull_request_target: + types: + - opened + - reopened + - synchronize + - closed + +permissions: + contents: write + pull-requests: write + +jobs: + update-citations: + uses: ./.github/workflows/update-citations.yaml + + build-preview: + needs: update-citations + uses: ./.github/workflows/build-preview.yaml diff --git a/.github/workflows/on-push.yml b/.github/workflows/on-push.yml new file mode 100644 index 0000000..06dd001 --- /dev/null +++ b/.github/workflows/on-push.yml @@ -0,0 +1,24 @@ +name: on-push +run-name: on push to main + +on: + push: + branches: + - main + + # run if user manually requests it + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-citations: + # skip first run because nothing enabled or setup yet + if: github.run_number != 1 + uses: ./.github/workflows/update-citations.yaml + + build-site: + needs: update-citations + uses: ./.github/workflows/build-site.yaml diff --git a/.github/workflows/on-schedule.yaml b/.github/workflows/on-schedule.yaml new file mode 100644 index 0000000..ad1fe95 --- /dev/null +++ b/.github/workflows/on-schedule.yaml @@ -0,0 +1,27 @@ +name: on-schedule +run-name: on schedule + +on: + schedule: + # weekly + - cron: "0 0 * * 1" + + # run if user manually requests it + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-citations: + # only run on user instance of template, not template itself + if: github.repository != 'greenelab/lab-website-template' + uses: ./.github/workflows/update-citations.yaml + with: + open-pr: true + + build-preview: + needs: update-citations + if: needs.update-citations.outputs.changed == 'true' + uses: ./.github/workflows/build-preview.yaml diff --git a/.github/workflows/update-citations.yaml b/.github/workflows/update-citations.yaml new file mode 100644 index 0000000..f6ff6ec --- /dev/null +++ b/.github/workflows/update-citations.yaml @@ -0,0 +1,84 @@ +name: update-citations +run-name: update citations + +on: + # run when called from another workflow + workflow_call: + inputs: + open-pr: + type: boolean + outputs: + changed: + value: ${{ jobs.update-citations.outputs.changed }} + + # run if user manually requests it + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +env: + FORCE_COLOR: true + GOOGLE_SCHOLAR_API_KEY: ${{ secrets.GOOGLE_SCHOLAR_API_KEY }} + +jobs: + update-citations: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + # for debugging + - uses: crazy-max/ghaction-dump-context@v2 + + - name: Checkout branch contents + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.head_ref }} + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + cache-dependency-path: "**/requirements.txt" + + - name: Install Python packages + run: | + python -m pip install --upgrade --requirement ./_cite/requirements.txt + + # for debugging + - if: runner.debug == '1' + uses: mxschmitt/action-tmate@v3 + + - name: Build updated citations + run: python _cite/cite.py + timeout-minutes: 15 + + - name: Check if citations changed + id: changed + uses: tj-actions/verify-changed-files@v18 + with: + files: | + _data/citations.yaml + + - name: Commit updated citations to branch + if: | + steps.changed.outputs.files_changed == 'true' && + inputs.open-pr != true + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "Update citations" + + - name: Open pull request with updated citations + if: | + steps.changed.outputs.files_changed == 'true' && + inputs.open-pr == true + uses: peter-evans/create-pull-request@v6 + with: + branch: citation-update + title: Periodic citation update + + outputs: + changed: ${{ steps.changed.outputs.files_changed }} diff --git a/.github/workflows/update-url.yaml b/.github/workflows/update-url.yaml new file mode 100644 index 0000000..b3573a5 --- /dev/null +++ b/.github/workflows/update-url.yaml @@ -0,0 +1,69 @@ +name: update-url +run-name: update site after url change + +on: + # run when called from another workflow + workflow_call: + outputs: + changed: + value: ${{ jobs.update-url.outputs.changed }} + + # run if user manually requests it + workflow_dispatch: + +permissions: + contents: write + +jobs: + update-url: + runs-on: ubuntu-latest + + steps: + # for debugging + - uses: crazy-max/ghaction-dump-context@v2 + + - name: Get Pages url + id: pages + uses: actions/configure-pages@v4 + + - name: Checkout branch contents + uses: actions/checkout@v4 + + # for debugging + - if: runner.debug == '1' + uses: mxschmitt/action-tmate@v3 + + # update link to site in readme + - name: Update readme + uses: actions/github-script@v7 + with: + script: | + const { readFileSync, writeFileSync } = require("fs"); + const file = "README.md"; + let contents = readFileSync(file).toString(); + const find = /\*\*\[.*\]\(.*\)\*\*/; + const host = "${{ steps.pages.outputs.host }}"; + const path = "${{ steps.pages.outputs.base_path }}"; + const url = "${{ steps.pages.outputs.base_url }}"; + const replace = `**[${host}${path}](${url})**`; + if (contents.match(find)) + contents = contents.replace(find, replace); + else + contents = `Visit ${replace} 🚀\n\n` + contents; + writeFileSync(file, contents); + + - name: Check if readme changed + id: changed + uses: tj-actions/verify-changed-files@v18 + with: + files: | + README.md + + - name: Commit changed files + if: steps.changed.outputs.files_changed == 'true' + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "Update url" + + outputs: + changed: ${{ steps.changed.outputs.files_changed }} diff --git a/.github/workflows/versioning.yaml b/.github/workflows/versioning.yaml new file mode 100644 index 0000000..e148f36 --- /dev/null +++ b/.github/workflows/versioning.yaml @@ -0,0 +1,135 @@ +name: versioning +run-name: versioning tasks + +on: + pull_request: + branches: + - main + push: + branches: + - main + +permissions: + contents: write + +jobs: + pull-request: + # only run on template itself, not user instance of template + if: | + github.repository == 'greenelab/lab-website-template' && + github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + # for debugging + - uses: crazy-max/ghaction-dump-context@v2 + - if: runner.debug == '1' + uses: mxschmitt/action-tmate@v3 + + - name: Checkout base branch contents + uses: actions/checkout@v4 + with: + ref: main + path: base + + - name: Checkout pr branch contents + uses: actions/checkout@v4 + with: + path: pr + + - name: Install packages + run: npm install yaml semver + + - name: Check version, date, changelog + uses: actions/github-script@v7 + with: + script: | + const { readFileSync } = require("fs"); + const { lte, valid } = require("semver"); + const { parse } = require("yaml"); + + // load and parse file contents + const { version: oldVersion, "date-released": oldDate } = parse( + readFileSync("base/CITATION.cff").toString() + ); + const { version: newVersion, "date-released": newDate } = parse( + readFileSync("pr/CITATION.cff").toString() + ); + const changelog = readFileSync("pr/CHANGELOG.md") + .toString() + .split(/^## /m) + .map((section) => { + const [heading, ...body] = section.split("\n"); + return [heading.trim(), body.join("\n").trim()]; + }); + + // check version + if (!valid(newVersion)) throw Error("Version not valid"); + if (lte(newVersion, oldVersion)) throw Error("Version not updated"); + + // check date + if (new Date(newDate).toISOString().split("T")[0] !== newDate) + throw Error("Date not valid"); + if (new Date(newDate) <= new Date(oldDate)) throw Error("Date not updated"); + + // check changelog + const newSection = changelog.find( + ([heading, body]) => + heading.includes(newVersion) && heading.includes(newDate) && body + ); + if (!newSection) throw Error("Changelog not updated or not valid"); + + push: + # only run on template itself, not user instance of template + if: | + github.repository == 'greenelab/lab-website-template' && + github.event_name == 'push' + runs-on: ubuntu-latest + steps: + # for debugging + - uses: crazy-max/ghaction-dump-context@v2 + + - name: Checkout branch contents + uses: actions/checkout@v4 + + - name: Install packages + run: npm install yaml semver + + # for debugging + - if: runner.debug == '1' + uses: mxschmitt/action-tmate@v3 + + - name: Get version and body + id: version + uses: actions/github-script@v7 + with: + script: | + const { readFileSync } = require("fs"); + const { parse } = require("yaml"); + + // load and parse file contents + const { version, "date-released": date } = parse( + readFileSync("CITATION.cff").toString() + ); + const changelog = readFileSync("CHANGELOG.md") + .toString() + .split(/^## /m) + .map((section) => { + const [heading, ...body] = section.split("\n"); + return [heading.trim(), body.join("\n").trim()]; + }); + + // find changelog body for version + const [, body = ""] = + changelog.find( + ([heading]) => heading.includes(version) && heading.includes(date) + ) || []; + + return { version, body }; + + - name: Create GitHub release + uses: ncipollo/release-action@v1.14.0 + with: + commit: ${{ github.ref }} + tag: v${{ fromJson(steps.version.outputs.result).version }} + name: v${{ fromJson(steps.version.outputs.result).version }} + body: ${{ fromJson(steps.version.outputs.result).body }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3511ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +_site +.sass-cache +.jekyll-cache +.jekyll-metadata +vendor +debug.log +__pycache__ +.cache +!cache.db +.DS_STORE +.env* +package.json +package-lock.json +yarn.lock +node_modules diff --git a/404.md b/404.md new file mode 100644 index 0000000..64b5a4a --- /dev/null +++ b/404.md @@ -0,0 +1,11 @@ +--- +title: 404 +permalink: /404.html +--- + +## {% include icon.html icon="fa-solid fa-heart-crack" %} Page Not Found + +Try searching the whole site for the content you want: +{:.center} + +{% include site-search.html %} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d702c3e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,146 @@ +# Changelog + +Reference: common-changelog.org + +## 1.2.0 - 2024-03-08 + +### Changed + +- Update all GitHub Actions to fix "Node v16 deprecated" warnings. +- Sources that Manubot doesn't know how to cite (e.g. wosuid:12345) are now ignored by default if they're from metasources. +- Fix bug where passing tags to tags component manually doesn't work. +- Fix bug in citation (and other) components when `lookup` is blank. +- Fix nested tables bug. +- Dark mode tweaks. +- Various CSS tweaks and fixes. + +### Added + +- Add `image` param to support blog post thumbnails. +- Add `html-proofer` plugin that checks for broken images/links/etc. +- Add `remove` flag to remove a source from a metasource. + +## 1.1.6 - 2023-10-06 + +### Changed + +- Use latest minor versions of Python packages in auto-cite script. + +## 1.1.5 - 2023-05-19 + +### Changed + +- Fix ORCID plugin bug and other cite process tweaks. + +## 1.1.4 - 2023-04-28 + +### Changed + +- Fix ORCID plugin and other cite process bugs. + +## 1.1.3 - 2023-04-20 + +### Changed + +- Fix first-time-setup mv bug. +- Fix citation, float, and portrait component CSS. +- Filter and trim citation info fields. + +## 1.1.2 - 2023-04-11 + +### Changed + +- Fix first-time-setup rm bug. + +## 1.1.1 - 2023-04-06 + +### Changed + +- Change member profile page from col layout to float. +- Fix first time setup. Preserve config formatting and comments. +- Improve Docker cite process behavior. +- Fix post excerpt component start/end markers and special search attr chars. +- Fix misc CSS. + +### Added + +- Add show-title and show-subtitle site config options. +- Include site subtitle in description meta tag. +- Add user pull request template. +- Add title and link fallbacks to citation component. + +## 1.1.0 - 2023-03-17 + +Add alert component, Docker support, accessibility fixes. + +### Changed + +- Fix Lighthouse accessibility issues. +- De-href components when link isn't provided (no hand cursor icon on hover or nav on click). +- In search script, limit highlights by total count instead of char length. +- Grid and link style tweaks. +- Take ORCID icon from Font Awesome. +- Misc bug fixes in tags script, float component. + +### Added + +- Add Docker configuration and scripts for local previewing. +- Add alert component and types. +- Role icon in portrait component hoisted to top left. + +## 1.0.0 - 2023-02-28 + +First official release. + +High-level comparison with pre-releases: + +- Simpler configuration. +- More automation, less setup. +- More customization and flexibility. +- Redesigned components. +- New docs. +- Complete rewrite. +- Culmination of years of feedback. + +### Changed + +- Template is no longer limited to GitHub Pages white-listed Jekyll plugins. Any plugins possible. +- Pull request previews happen right within GitHub instead of needing Netlify. +- Better versioning. `CITATION.cff` file now source of truth for version, and tags/releases enforced. +- Citation-related files in `/_data` must now be named prefixed with the cite plugin they are to be run with, e.g. `sources-2020.yaml` or `orcid-students.yaml`. +- Folder renames for clarity and for better separation of template and user content: `/auto-cite` → `/_cite`, `/css` → `/_styles`, `/js` → `/_scripts`. +- Rename "Tools" page to "Projects" to be more clear and general purpose. +- Rename `extra-links` to `buttons` in `sources.yaml` files. +- Rename `theme.scss` to `-theme.scss`. +- Rename/repurpose components: link → button, two-col → cols, gallery → grid. +- Combine "link" and "role" data lists into single `types.yaml` map. +- Redesign components, change parameters and behavior. +- Update Font Awesome icon names from v5 to v6. +- Change placeholder text, images, and other images. +- Use CSS variables instead of Sass variables. +- Simplify caching method in cite process. +- Simplify Liquid code by including custom Ruby plugins. +- Simplify styles and scripts. + +### Added + +- New docs at greene-lab.gitbook.io/lab-website-template-docs. +- Add automations for first time setup and URL change. +- Write PubMed and Google Scholar automatic citation plugins. +- Automatic citations through GitHub Actions should now work from (most) forks. +- Add optional description and type params for citations. +- Add periodic cite process run that opens a pull request. +- List component filters can now accept arbitrary regex. +- Add light/dark mode toggle. +- Pre-install selection of useful Jekyll plugins, namely Jekyll Spaceship. +- Add author portrait and updated date for blog posts. +- Add richer metadata for SEO. +- Google Fonts link determined automatically from theme file. + +### Removed + +- Remove options from `_config.yaml` to simplify configuration: `baseurl`, `auto-cite`, `logo`. +- Remove `/favicons` folder, hardcode files for logo, icon, and share in `/images`. +- Remove `palettes.scss` and `mixins.scss`. +- Remove banner component (same thing can be achieved with full width section and figure components). +- Remove role component. Combine with portrait component. diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..dcb59dc --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,14 @@ +# citation metadata for the template itself + +title: "Lab Website Template" +version: 1.2.0 +date-released: 2024-03-08 +url: "https://github.com/greenelab/lab-website-template" +authors: + - family-names: "Rubinetti" + given-names: "Vincent" + orcid: "https://orcid.org/0000-0002-4655-3773" + - family-names: "Greene" + given-names: "Casey" + orcid: "https://orcid.org/0000-0001-8713-9213" +cff-version: 1.2.0 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..cbdd53f --- /dev/null +++ b/Gemfile @@ -0,0 +1,16 @@ +source "https://rubygems.org" + +# jekyll +gem "jekyll", "~> 4.3" +gem "webrick", "~> 1.7" + +gem "html-proofer", "~> 5.0" + +# plugins +group :jekyll_plugins do + gem "jekyll-spaceship" + gem "jekyll-sitemap" + gem "jekyll-redirect-from" + gem "jekyll-feed" + gem "jekyll-last-modified-at" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..ee2cb12 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,157 @@ +GEM + remote: https://rubygems.org/ + specs: + Ascii85 (1.1.0) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + afm (0.2.2) + async (2.8.1) + console (~> 1.10) + fiber-annotation + io-event (~> 1.1) + timers (~> 4.1) + colorator (1.1.0) + concurrent-ruby (1.2.2) + console (1.23.4) + fiber-annotation + fiber-local + json + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + ethon (0.16.0) + ffi (>= 1.15.0) + eventmachine (1.2.7) + ffi (1.15.5) + ffi (1.15.5-x64-mingw-ucrt) + fiber-annotation (0.2.0) + fiber-local (1.0.0) + forwardable-extended (2.6.0) + gemoji (3.0.1) + google-protobuf (3.22.0) + google-protobuf (3.22.0-arm64-darwin) + google-protobuf (3.22.0-x64-mingw-ucrt) + hashery (2.1.2) + html-proofer (5.0.8) + addressable (~> 2.3) + async (~> 2.1) + nokogiri (~> 1.13) + pdf-reader (~> 2.11) + rainbow (~> 3.0) + typhoeus (~> 1.3) + yell (~> 2.0) + zeitwerk (~> 2.5) + http_parser.rb (0.8.0) + i18n (1.12.0) + concurrent-ruby (~> 1.0) + io-event (1.4.4) + jekyll (4.3.2) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) + jekyll-watch (~> 2.0) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (>= 0.3.6, < 0.5) + pathutil (~> 0.9) + rouge (>= 3.0, < 5.0) + safe_yaml (~> 1.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-feed (0.17.0) + jekyll (>= 3.7, < 5.0) + jekyll-last-modified-at (1.3.0) + jekyll (>= 3.7, < 5.0) + posix-spawn (~> 0.3.9) + jekyll-redirect-from (0.16.0) + jekyll (>= 3.3, < 5.0) + jekyll-sass-converter (3.0.0) + sass-embedded (~> 1.54) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-spaceship (0.10.2) + gemoji (~> 3.0) + jekyll (>= 3.6, < 5.0) + nokogiri (~> 1.6) + rainbow (~> 3.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + json (2.7.1) + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.8.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.4.0) + mini_portile2 (2.8.1) + nokogiri (1.13.10) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) + nokogiri (1.13.10-arm64-darwin) + racc (~> 1.4) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + pdf-reader (2.12.0) + Ascii85 (~> 1.0) + afm (~> 0.2.1) + hashery (~> 2.0) + ruby-rc4 + ttfunk + posix-spawn (0.3.15) + public_suffix (5.0.1) + racc (1.6.2) + rainbow (3.1.1) + rake (13.1.0) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) + rexml (3.2.5) + rouge (3.30.0) + ruby-rc4 (0.1.5) + safe_yaml (1.0.5) + sass-embedded (1.58.3) + google-protobuf (~> 3.21) + rake (>= 10.0.0) + sass-embedded (1.58.3-arm64-darwin) + google-protobuf (~> 3.21) + sass-embedded (1.58.3-x64-mingw-ucrt) + google-protobuf (~> 3.21) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + timers (4.3.5) + ttfunk (1.7.0) + typhoeus (1.4.1) + ethon (>= 0.9.0) + unicode-display_width (2.4.2) + webrick (1.8.1) + yell (2.2.2) + zeitwerk (2.6.13) + +PLATFORMS + aarch64-linux + linux + universal-darwin-21 + universal-darwin-22 + x64-mingw-ucrt + x64-mingw32 + x64-unknown + x86_64-linux + +DEPENDENCIES + html-proofer (~> 5.0) + jekyll (~> 4.3) + jekyll-feed + jekyll-last-modified-at + jekyll-redirect-from + jekyll-sitemap + jekyll-spaceship + webrick (~> 1.7) + +BUNDLED WITH + 2.5.6 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..70c6b2a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2020, Greene Laboratory +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..29bf9a2 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +
+
+
z&=XiI31Bci7>$J3W=*{ivw$_@vs
+ {{ include.description | markdownify | remove: " " | remove: " "
+ | remove: " {{ include.title }} " | remove: "
+ {{ excerpt }}
+
+
+ Search for {{ page.name | default: page.title }}'s papers on the Research page
+
+ ZIXbWm^Fo$`KefPrZc(zxecNJnvLl
zDWXj^%| I@eQkzu#vqj*&CEhbp
znSlqn+x^bMx<@tlu@P#hx?eE$l9$wThSIh
+
+
+
+
+ {% endif %}
+
+
+
+
+
+ {% if include.caption %}
+
+ {% endif %}
+
+ {% endif %}
+ {% if site.logo-text != false %}
+
+ {% if site.title and site.show-title != false %}
+ {{ site.title }}
+ {% endif %}
+ {% if site.subtitle and site.show-subtitle != false %}
+ {{ site.subtitle }}
+ {% endif %}
+
+ {% endif %}
+
+
+
+
+
+
{{ year.name }}
+ {% assign data = data | sort: "date" | reverse %}
+ {% endif %}
+
+ {% for d in data %}
+ {% assign style = d.style | default: include.style %}
+
+ {%
+ include {{ include.component | append: ".html" }}
+ author=d.author
+ authors=d.authors
+ buttons=d.buttons
+ caption=d.caption
+ content=d.content
+ date=d.date
+ description=d.description
+ excerpt=d.excerpt
+ height=d.height
+ icon=d.icon
+ id=d.id
+ image=d.image
+ last_modified_at=d.last_modified_at
+ link=d.link
+ lookup=d.lookup
+ name=d.name
+ publisher=d.publisher
+ repo=d.repo
+ role=d.role
+ slug=d.slug
+ style=style
+ subtitle=d.subtitle
+ tags=d.tags
+ text=d.text
+ title=d.title
+ tooltip=d.tooltip
+ type=d.type
+ url=d.url
+ width=d.width
+ %}
+ {% endfor %}
+{% endfor %}
diff --git a/_includes/manubot.svg b/_includes/manubot.svg
new file mode 100644
index 0000000..24b0c2e
--- /dev/null
+++ b/_includes/manubot.svg
@@ -0,0 +1,78 @@
+
diff --git a/_includes/meta.html b/_includes/meta.html
new file mode 100644
index 0000000..1336e30
--- /dev/null
+++ b/_includes/meta.html
@@ -0,0 +1,99 @@
+{% assign filename = page.path | split: "/" | last %}
+{% if page.name and page.name != filename %}
+ {% assign title = page.name %}
+{% elsif page.title %}
+ {% assign title = page.title %}
+{% else %}
+ {% assign title = nil %}
+{% endif %}
+
+{% assign fulltitle = "" | split: "," %}
+{% if title %}
+ {% assign fulltitle = fulltitle | push: title %}
+{% endif %}
+{% if site.title %}
+ {% assign fulltitle = fulltitle | push: site.title %}
+{% endif %}
+{% assign fulltitle = fulltitle | join: " | " %}
+
+{% assign subtitle = site.subtitle %}
+
+{% assign description = page.description | default: site.description %}
+{% if site.subtitle %}
+ {% capture description -%}
+ {{ site.subtitle }}. {{ description }}
+ {%- endcapture %}
+{% endif %}
+{% capture url -%}
+ {{ site.url }}{{ site.baseurl }}
+{%- endcapture %}
+
+{% assign png = "images/icon.png" | file_exists %}
+{% assign jpg = "images/icon.jpg" | file_exists %}
+{% assign icon = png | default: jpg | relative_url %}
+
+{% assign jpg = "images/share.jpg" | file_exists %}
+{% assign png = "images/share.png" | file_exists %}
+{% assign share = jpg | default: png | relative_url %}
+
+{% assign published = page.date | date_to_xmlschema %}
+{% assign updated = page.last_modified_at | date_to_xmlschema %}
+
+{% assign feed = "feed.xml" | absolute_url %}
+
+
+
+
+
+ {% endif %}
+
+
+ {% endfor %}
+{{ page.title }}
+
+{%
+ include post-info.html
+ author=page.author
+ member=page.member
+ published=page.date
+ updated=page.last_modified_at
+ tags=page.tags
+%}
+
+{% include section.html %}
+
+{{ content }}
+
+{% include section.html %}
+
+{% include post-nav.html post=page %}
diff --git a/_members/jane-smith.md b/_members/jane-smith.md
new file mode 100644
index 0000000..f4cae89
--- /dev/null
+++ b/_members/jane-smith.md
@@ -0,0 +1,19 @@
+---
+name: Jane Smith
+image: images/photo.jpg
+role: pi
+aliases:
+ - J. Smith
+ - J Smith
+links:
+ home-page: https://janesmith.com
+ orcid: 0000-0001-8713-9213
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+Faucibus purus in massa tempor nec feugiat nisl pretium fusce.
+Elit at imperdiet dui accumsan.
+Duis tristique sollicitudin nibh sit amet commodo nulla facilisi.
+Vitae elementum curabitur vitae nunc sed velit dignissim sodales.
+Lacinia at quis risus sed vulputate odio ut.
+Magna eget est lorem ipsum.
diff --git a/_members/john-doe.md b/_members/john-doe.md
new file mode 100644
index 0000000..0e441c6
--- /dev/null
+++ b/_members/john-doe.md
@@ -0,0 +1,10 @@
+---
+name: John Doe
+image: images/photo.jpg
+role: phd
+group: alum
+links:
+ github: john-doe
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
diff --git a/_members/sarah-johnson.md b/_members/sarah-johnson.md
new file mode 100644
index 0000000..b0d97b4
--- /dev/null
+++ b/_members/sarah-johnson.md
@@ -0,0 +1,11 @@
+---
+name: Sarah Johnson
+image: images/photo.jpg
+description: Lead Programmer
+role: programmer
+links:
+ email: sarah.johnson@gmail.com
+ twitter: sarahjohnson
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
diff --git a/_plugins/array.rb b/_plugins/array.rb
new file mode 100644
index 0000000..871626b
--- /dev/null
+++ b/_plugins/array.rb
@@ -0,0 +1,25 @@
+require 'liquid'
+
+module Jekyll
+ module ArrayFilters
+ # filter out empty and trim entries in array
+ def array_filter(array)
+ return array
+ .map{|x| x.is_a?(String) ? x.strip() : x}
+ .select{|x| x and x != ""}
+ end
+
+ # omit middle items of array with ellipsis, leave N items on either side
+ def array_carve(array, length = 3)
+ if array.length <= length * 2
+ return array
+ else
+ left = array.slice(0, length) || []
+ right = array.slice(-length, length) || []
+ return [left, "...", right].flatten()
+ end
+ end
+ end
+end
+
+Liquid::Template.register_filter(Jekyll::ArrayFilters)
diff --git a/_plugins/file.rb b/_plugins/file.rb
new file mode 100644
index 0000000..32a0af2
--- /dev/null
+++ b/_plugins/file.rb
@@ -0,0 +1,20 @@
+require 'liquid'
+
+module Jekyll
+ module FileFilters
+ # check if file exists
+ def file_exists(file)
+ path = File.join(Dir.getwd, file)
+ # pass back filename if exists
+ return File.file?(path) ? file : nil
+ end
+
+ # read text contents of file
+ def file_read(file)
+ path = File.join(Dir.getwd, file)
+ return File.file?(path) ? File.read(path) : nil
+ end
+ end
+end
+
+Liquid::Template.register_filter(Jekyll::FileFilters)
diff --git a/_plugins/hash.rb b/_plugins/hash.rb
new file mode 100644
index 0000000..2344589
--- /dev/null
+++ b/_plugins/hash.rb
@@ -0,0 +1,28 @@
+require 'liquid'
+
+module Jekyll
+ module HashFilters
+ # merge main hash with another hash of defaults
+ def hash_default(hash, defaults)
+ if not hash.is_a?(Hash) or not defaults.is_a?(Hash)
+ return hash
+ end
+ defaults.each do |key, value|
+ # substitute main string into default string and set main item
+ if value.is_a?(String) and value.include?"$VALUE"
+ if hash[key].is_a?(String)
+ hash[key] = value.sub"$VALUE", hash[key]
+ end
+ # set main item to default item if not defined
+ else
+ if hash[key] == nil or !hash.key?(key)
+ hash[key] = value
+ end
+ end
+ end
+ return hash
+ end
+ end
+end
+
+Liquid::Template.register_filter(Jekyll::HashFilters)
diff --git a/_plugins/misc.rb b/_plugins/misc.rb
new file mode 100644
index 0000000..fe19bd8
--- /dev/null
+++ b/_plugins/misc.rb
@@ -0,0 +1,87 @@
+require 'liquid'
+require 'html-proofer'
+
+module Jekyll
+ module MiscFilters
+ # fallback if value unspecified
+ def is_nil(value, fallback)
+ return value == nil ? fallback : value
+ end
+
+ # get list of hash keys or array entries
+ def object_items(object)
+ if object.is_a?(Hash)
+ return object.keys
+ elsif object.is_a?(Array)
+ return object
+ end
+ return object
+ end
+
+ # filter a list of hashes by comma-sep'd field:value pairs
+ def data_filter(data, filters)
+ if not data.is_a?(Array) or not filters.is_a?(String)
+ return data
+ end
+ data = data.clone
+ for filter in array_filter(filters.split(","))
+ key, value = array_filter(filter.split(":"))
+ # find unspecified fields
+ if value == nil
+ data.select!{|d| d[key] == nil}
+ # find fields that match regex
+ elsif value.is_a?(String)
+ data.select!{|d| d[key].to_s =~ /#{value}/m}
+ end
+ end
+ return data
+ end
+
+ # from css text, find font family definitions and construct google font url
+ def google_fonts(css)
+ names = regex_scan(css, '--\S*:\s*"(.*)",?.*;', false, true).sort.uniq
+ weights = regex_scan(css, '--\S*:\s(\d{3});', false, true).sort.uniq
+ url = "https://fonts.googleapis.com/css2?display=swap&"
+ for name in names do
+ name.sub!" ", "+"
+ url += "&family=#{name}:ital,wght@"
+ for ital in [0, 1] do
+ for weight in weights do
+ url += "#{ital},#{weight};"
+ end
+ end
+ url.delete_suffix!(";")
+ end
+ return url
+ end
+ end
+
+ # based on https://github.com/episource/jekyll-html-proofer
+ module HtmlProofer
+ priority = Jekyll::Hooks::PRIORITY_MAP[:high] + 1000
+
+ Jekyll::Hooks.register(:site, :post_write, priority: priority) do |site|
+ if not site.config["proofer"] == false
+ options = {
+ allow_missing_href: true,
+ enforce_https: false,
+ ignore_files: [/.*testbed.html/],
+ ignore_urls: [
+ /fonts\.gstatic\.com/,
+ /localhost:/,
+ /0\.0\.0\.0:/,
+ ],
+ }
+
+ begin
+ HTMLProofer.check_directory(site.dest, options).run
+ rescue Exception => error
+ STDERR.puts error
+ # raise error
+ end
+ end
+ end
+ end
+end
+
+Liquid::Template.register_filter(Jekyll::MiscFilters)
diff --git a/_plugins/regex.rb b/_plugins/regex.rb
new file mode 100644
index 0000000..f7cd02e
--- /dev/null
+++ b/_plugins/regex.rb
@@ -0,0 +1,28 @@
+require 'liquid'
+
+module Jekyll
+ module RegexFilters
+ # search string for regex capture group, return first or all matches
+ def regex_scan(string, search, multi = false, all = false)
+ regex = multi ? /#{search}/m : /#{search}/
+ matches = string.scan(regex).flatten
+ if matches.length
+ return all ? matches : matches[0]
+ else
+ return ""
+ end
+ end
+
+ # find regex capture group in string and replace
+ def regex_replace(string, search, replace)
+ return string.gsub(/#{search}/m, replace)
+ end
+
+ # strip all non-letter and non-number characters from string
+ def regex_strip(string)
+ return string.gsub(/[^\p{L}\p{N}]/u, " ")
+ end
+ end
+end
+
+Liquid::Template.register_filter(Jekyll::RegexFilters)
diff --git a/_posts/2019-01-07-example-post-1.md b/_posts/2019-01-07-example-post-1.md
new file mode 100644
index 0000000..d586270
--- /dev/null
+++ b/_posts/2019-01-07-example-post-1.md
@@ -0,0 +1,10 @@
+---
+title: Example post 1
+author: sarah-johnson
+tags:
+ - biology
+ - medicine
+ - big data
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
diff --git a/_posts/2021-09-30-example-post-2.md b/_posts/2021-09-30-example-post-2.md
new file mode 100644
index 0000000..889c897
--- /dev/null
+++ b/_posts/2021-09-30-example-post-2.md
@@ -0,0 +1,6 @@
+---
+title: Example post 2
+author: jane-smith
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
diff --git a/_posts/2023-02-23-example-post-3.md b/_posts/2023-02-23-example-post-3.md
new file mode 100644
index 0000000..0aa5a15
--- /dev/null
+++ b/_posts/2023-02-23-example-post-3.md
@@ -0,0 +1,8 @@
+---
+title: Example post 3
+image: images/photo.jpg
+author: john-doe
+tags: biology, medicine
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
diff --git a/_scripts/anchors.js b/_scripts/anchors.js
new file mode 100644
index 0000000..58daabc
--- /dev/null
+++ b/_scripts/anchors.js
@@ -0,0 +1,47 @@
+/*
+ creates link next to each heading that links to that section.
+*/
+
+{
+ const onLoad = () => {
+ // for each heading
+ const headings = document.querySelectorAll(
+ "h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]"
+ );
+ for (const heading of headings) {
+ // create anchor link
+ const link = document.createElement("a");
+ link.classList.add("icon", "fa-solid", "fa-link", "anchor");
+ link.href = "#" + heading.id;
+ link.setAttribute("aria-label", "link to this section");
+ heading.append(link);
+
+ // if first heading in the section, move id to parent section
+ if (heading.matches("section > :first-child")) {
+ heading.parentElement.id = heading.id;
+ heading.removeAttribute("id");
+ }
+ }
+ };
+
+ // scroll to target of url hash
+ const scrollToTarget = () => {
+ const id = window.location.hash.replace("#", "");
+ const target = document.getElementById(id);
+
+ if (!target) return;
+ const offset = document.querySelector("header").clientHeight || 0;
+ window.scrollTo({
+ top: target.getBoundingClientRect().top + window.scrollY - offset,
+ behavior: "smooth",
+ });
+ };
+
+ // after page loads
+ window.addEventListener("load", onLoad);
+ window.addEventListener("load", scrollToTarget);
+ window.addEventListener("tagsfetched", scrollToTarget);
+
+ // when hash nav happens
+ window.addEventListener("hashchange", scrollToTarget);
+}
diff --git a/_scripts/dark-mode.js b/_scripts/dark-mode.js
new file mode 100644
index 0000000..b75b25e
--- /dev/null
+++ b/_scripts/dark-mode.js
@@ -0,0 +1,25 @@
+/*
+ manages light/dark mode.
+*/
+
+{
+ // immediately load saved (or default) mode before page renders
+ document.documentElement.dataset.dark =
+ window.localStorage.getItem("dark-mode") ?? "false";
+
+ const onLoad = () => {
+ // update toggle button to match loaded mode
+ document.querySelector(".dark-toggle").checked =
+ document.documentElement.dataset.dark === "true";
+ };
+
+ // after page loads
+ window.addEventListener("load", onLoad);
+
+ // when user toggles mode button
+ window.onDarkToggleChange = (event) => {
+ const value = event.target.checked;
+ document.documentElement.dataset.dark = value;
+ window.localStorage.setItem("dark-mode", value);
+ };
+}
diff --git a/_scripts/fetch-tags.js b/_scripts/fetch-tags.js
new file mode 100644
index 0000000..c843b67
--- /dev/null
+++ b/_scripts/fetch-tags.js
@@ -0,0 +1,67 @@
+/*
+ fetches tags (aka "topics") from a given GitHub repo and adds them to row of
+ tag buttons. specify repo in data-repo attribute on row.
+*/
+
+{
+ const onLoad = async () => {
+ // get tag rows with specified repos
+ const rows = document.querySelectorAll("[data-repo]");
+
+ // for each repo
+ for (const row of rows) {
+ // get props from tag row
+ const repo = row.dataset.repo.trim();
+ const link = row.dataset.link.trim();
+
+ // get tags from github
+ if (!repo) continue;
+ let tags = await fetchTags(repo);
+
+ // filter out tags already present in row
+ let existing = [...row.querySelectorAll(".tag")].map((tag) =>
+ window.normalizeTag(tag.innerText)
+ );
+ tags = tags.filter((tag) => !existing.includes(normalizeTag(tag)));
+
+ // add tags to row
+ for (const tag of tags) {
+ const a = document.createElement("a");
+ a.classList.add("tag");
+ a.innerHTML = tag;
+ a.href = `${link}?search="tag: ${tag}"`;
+ a.dataset.tooltip = `Show items with the tag "${tag}"`;
+ row.append(a);
+ }
+
+ // delete tags container if empty
+ if (!row.innerText.trim()) row.remove();
+ }
+
+ // emit "tags done" event for other scripts to listen for
+ window.dispatchEvent(new Event("tagsfetched"));
+ };
+
+ // after page loads
+ window.addEventListener("load", onLoad);
+
+ // GitHub topics endpoint
+ const api = "https://api.github.com/repos/REPO/topics";
+ const headers = new Headers();
+ headers.set("Accept", "application/vnd.github+json");
+
+ // get tags from GitHub based on repo name
+ const fetchTags = async (repo) => {
+ const url = api.replace("REPO", repo);
+ try {
+ const response = await (await fetch(url)).json();
+ if (response.names) return response.names;
+ else throw new Error(JSON.stringify(response));
+ } catch (error) {
+ console.groupCollapsed("GitHub fetch tags error");
+ console.log(error);
+ console.groupEnd();
+ return [];
+ }
+ };
+}
diff --git a/_scripts/search.js b/_scripts/search.js
new file mode 100644
index 0000000..fa23ca4
--- /dev/null
+++ b/_scripts/search.js
@@ -0,0 +1,215 @@
+/*
+ filters elements on page based on url or search box.
+ syntax: term1 term2 "full phrase 1" "full phrase 2" "tag: tag 1"
+ match if: all terms AND at least one phrase AND at least one tag
+*/
+{
+ // elements to filter
+ const elementSelector = ".card, .citation, .post-excerpt";
+ // search box element
+ const searchBoxSelector = ".search-box";
+ // results info box element
+ const infoBoxSelector = ".search-info";
+ // tags element
+ const tagSelector = ".tag";
+
+ // split search query into terms, phrases, and tags
+ const splitQuery = (query) => {
+ // split into parts, preserve quotes
+ const parts = query.match(/"[^"]*"|\S+/g) || [];
+
+ // bins
+ const terms = [];
+ const phrases = [];
+ const tags = [];
+
+ // put parts into bins
+ for (let part of parts) {
+ if (part.startsWith('"')) {
+ part = part.replaceAll('"', "").trim();
+ if (part.startsWith("tag:"))
+ tags.push(normalizeTag(part.replace(/tag:\s*/, "")));
+ else phrases.push(part.toLowerCase());
+ } else terms.push(part.toLowerCase());
+ }
+
+ return { terms, phrases, tags };
+ };
+
+ // normalize tag string for comparison
+ window.normalizeTag = (tag) =>
+ tag.trim().toLowerCase().replaceAll(/-|\s+/g, " ");
+
+ // get data attribute contents of element and children
+ const getAttr = (element, attr) =>
+ [element, ...element.querySelectorAll(`[data-${attr}]`)]
+ .map((element) => element.dataset[attr])
+ .join(" ");
+
+ // determine if element should show up in results based on query
+ const elementMatches = (element, { terms, phrases, tags }) => {
+ // tag elements within element
+ const tagElements = [...element.querySelectorAll(".tag")];
+
+ // check if text content exists in element
+ const hasText = (string) =>
+ (
+ element.innerText +
+ getAttr(element, "tooltip") +
+ getAttr(element, "search")
+ )
+ .toLowerCase()
+ .includes(string);
+ // check if text matches a tag in element
+ const hasTag = (string) =>
+ tagElements.some((tag) => normalizeTag(tag.innerText) === string);
+
+ // match logic
+ return (
+ (terms.every(hasText) || !terms.length) &&
+ (phrases.some(hasText) || !phrases.length) &&
+ (tags.some(hasTag) || !tags.length)
+ );
+ };
+
+ // loop through elements, hide/show based on query, and return results info
+ const filterElements = (parts) => {
+ let elements = document.querySelectorAll(elementSelector);
+
+ // results info
+ let x = 0;
+ let n = elements.length;
+ let tags = parts.tags;
+
+ // filter elements
+ for (const element of elements) {
+ if (elementMatches(element, parts)) {
+ element.style.display = "";
+ x++;
+ } else element.style.display = "none";
+ }
+
+ return [x, n, tags];
+ };
+
+ // highlight search terms
+ const highlightMatches = async ({ terms, phrases }) => {
+ // make sure Mark library available
+ if (typeof Mark === "undefined") return;
+
+ // reset
+ new Mark(document.body).unmark();
+
+ // limit number of highlights to avoid slowdown
+ let counter = 0;
+ const filter = () => counter++ < 100;
+
+ // highlight terms and phrases
+ new Mark(elementSelector)
+ .mark(terms, { separateWordSearch: true, filter })
+ .mark(phrases, { separateWordSearch: false, filter });
+ };
+
+ // update search box based on query
+ const updateSearchBox = (query = "") => {
+ const boxes = document.querySelectorAll(searchBoxSelector);
+
+ for (const box of boxes) {
+ const input = box.querySelector("input");
+ const button = box.querySelector("button");
+ const icon = box.querySelector("button i");
+ input.value = query;
+ icon.className = input.value.length
+ ? "icon fa-solid fa-xmark"
+ : "icon fa-solid fa-magnifying-glass";
+ button.disabled = input.value.length ? false : true;
+ }
+ };
+
+ // update info box based on query and results
+ const updateInfoBox = (query, x, n) => {
+ const boxes = document.querySelectorAll(infoBoxSelector);
+
+ if (query.trim()) {
+ // show all info boxes
+ boxes.forEach((info) => (info.style.display = ""));
+
+ // info template
+ let info = "";
+ info += `Showing ${x.toLocaleString()} of ${n.toLocaleString()} results
`;
+ info += "Clear search";
+
+ // set info HTML string
+ boxes.forEach((el) => (el.innerHTML = info));
+ }
+ // if nothing searched
+ else {
+ // hide all info boxes
+ boxes.forEach((info) => (info.style.display = "none"));
+ }
+ };
+
+ // update tags based on query
+ const updateTags = (query) => {
+ const { tags } = splitQuery(query);
+ document.querySelectorAll(tagSelector).forEach((tag) => {
+ // set active if tag is in query
+ if (tags.includes(normalizeTag(tag.innerText)))
+ tag.setAttribute("data-active", "");
+ else tag.removeAttribute("data-active");
+ });
+ };
+
+ // run search with query
+ const runSearch = (query = "") => {
+ const parts = splitQuery(query);
+ const [x, n] = filterElements(parts);
+ updateSearchBox(query);
+ updateInfoBox(query, x, n);
+ updateTags(query);
+ highlightMatches(parts);
+ };
+
+ // update url based on query
+ const updateUrl = (query = "") => {
+ const url = new URL(window.location);
+ let params = new URLSearchParams(url.search);
+ params.set("search", query);
+ url.search = params.toString();
+ window.history.replaceState(null, null, url);
+ };
+
+ // search based on url param
+ const searchFromUrl = () => {
+ const query =
+ new URLSearchParams(window.location.search).get("search") || "";
+ runSearch(query);
+ };
+
+ // return func that runs after delay
+ const debounce = (callback, delay = 250) => {
+ let timeout;
+ return (...args) => {
+ window.clearTimeout(timeout);
+ timeout = window.setTimeout(() => callback(...args), delay);
+ };
+ };
+
+ // when user types into search box
+ const debouncedRunSearch = debounce(runSearch, 1000);
+ window.onSearchInput = (target) => {
+ debouncedRunSearch(target.value);
+ updateUrl(target.value);
+ };
+
+ // when user clears search box with button
+ window.onSearchClear = () => {
+ runSearch();
+ updateUrl();
+ };
+
+ // after page loads
+ window.addEventListener("load", searchFromUrl);
+ // after tags load
+ window.addEventListener("tagsfetched", searchFromUrl);
+}
diff --git a/_scripts/site-search.js b/_scripts/site-search.js
new file mode 100644
index 0000000..caff0a6
--- /dev/null
+++ b/_scripts/site-search.js
@@ -0,0 +1,14 @@
+/*
+ for site search component. searches site/domain via google.
+*/
+
+{
+ // when user submits site search form/box
+ window.onSiteSearchSubmit = (event) => {
+ event.preventDefault();
+ const google = "https://www.google.com/search?q=site:";
+ const site = window.location.origin;
+ const query = event.target.elements.query.value;
+ window.location = google + site + " " + query;
+ };
+}
diff --git a/_scripts/table-wrap.js b/_scripts/table-wrap.js
new file mode 100644
index 0000000..4c5bddd
--- /dev/null
+++ b/_scripts/table-wrap.js
@@ -0,0 +1,25 @@
+/*
+ put a wrapper around each table to allow scrolling.
+*/
+
+{
+ const onLoad = () => {
+ // for each top-level table
+ const tables = document.querySelectorAll("table:not(table table)");
+ for (const table of tables) {
+ // create wrapper with scroll
+ const wrapper = document.createElement("div");
+ wrapper.style.overflowX = "auto";
+
+ // undo css force-text-wrap
+ table.style.overflowWrap = "normal";
+
+ // add wrapper around table
+ table.parentNode.insertBefore(wrapper, table);
+ wrapper.appendChild(table);
+ }
+ };
+
+ // after page loads
+ window.addEventListener("load", onLoad);
+}
diff --git a/_scripts/tooltip.js b/_scripts/tooltip.js
new file mode 100644
index 0000000..49eccfc
--- /dev/null
+++ b/_scripts/tooltip.js
@@ -0,0 +1,41 @@
+/*
+ shows a popup of text on hover/focus of any element with the data-tooltip
+ attribute.
+*/
+
+{
+ const onLoad = () => {
+ // make sure Tippy library available
+ if (typeof tippy === "undefined") return;
+
+ // get elements with non-empty tooltips
+ const elements = [...document.querySelectorAll("[data-tooltip]")].filter(
+ (element) => element.dataset.tooltip.trim() && !element._tippy
+ );
+
+ // add tooltip to elements
+ tippy(elements, {
+ content: (element) => element.dataset.tooltip.trim(),
+ delay: [200, 0],
+ offset: [0, 20],
+ allowHTML: true,
+ interactive: true,
+ appendTo: () => document.body,
+ aria: {
+ content: "describedby",
+ expanded: null,
+ },
+ onShow: ({ reference, popper }) => {
+ const dark = reference.closest("[data-dark]")?.dataset.dark;
+ if (dark === "false") popper.dataset.dark = true;
+ if (dark === "true") popper.dataset.dark = false;
+ },
+ // onHide: () => false, // debug
+ });
+ };
+
+ // after page loads
+ window.addEventListener("load", onLoad);
+ // after tags load
+ window.addEventListener("tagsfetched", onLoad);
+}
diff --git a/_styles/-theme.scss b/_styles/-theme.scss
new file mode 100644
index 0000000..0caecc6
--- /dev/null
+++ b/_styles/-theme.scss
@@ -0,0 +1,54 @@
+---
+---
+
+// colors
+[data-dark="false"] {
+ --primary: #0795d9;
+ --secondary: #7dd3fc;
+ --text: #000000;
+ --background: #ffffff;
+ --background-alt: #fafafa;
+ --light-gray: #e0e0e0;
+ --gray: #808080;
+ --dark-gray: #404040;
+ --overlay: #00000020;
+}
+[data-dark="true"] {
+ --primary: #0795d9;
+ --secondary: #075985;
+ --text: #ffffff;
+ --background: #181818;
+ --background-alt: #1c1c1c;
+ --light-gray: #404040;
+ --gray: #808080;
+ --dark-gray: #b0b0b0;
+ --overlay: #ffffff10;
+}
+
+:root {
+ // font families
+ --title: "Barlow", sans-serif;
+ --heading: "Barlow", sans-serif;
+ --body: "Barlow", sans-serif;
+ --code: "Roboto Mono", monospace;
+
+ // font sizes
+ --large: 1.2rem;
+ --xl: 1.4rem;
+ --xxl: 1.6rem;
+
+ // font weights
+ --thin: 200;
+ --regular: 400;
+ --semi-bold: 500;
+ --bold: 600;
+
+ // text line spacing
+ --spacing: 2;
+ --compact: 1.5;
+
+ // effects
+ --rounded: 3px;
+ --shadow: 0 0 10px 0 var(--overlay);
+ --transition: 0.2s ease;
+}
diff --git a/_styles/alert.scss b/_styles/alert.scss
new file mode 100644
index 0000000..6e77eec
--- /dev/null
+++ b/_styles/alert.scss
@@ -0,0 +1,37 @@
+---
+---
+
+.alert {
+ position: relative;
+ display: flex;
+ gap: 20px;
+ align-items: center;
+ margin: 20px 0;
+ padding: 20px;
+ border-radius: var(--rounded);
+ overflow: hidden;
+ text-align: left;
+ line-height: var(--spacing);
+}
+
+.alert:before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ opacity: 0.1;
+ background: var(--color);
+ z-index: -1;
+}
+
+.alert > .icon {
+ color: var(--color);
+ font-size: var(--large);
+}
+
+.alert-content > :first-child {
+ margin-top: 0;
+}
+
+.alert-content > :last-child {
+ margin-bottom: 0;
+}
diff --git a/_styles/all.scss b/_styles/all.scss
new file mode 100644
index 0000000..a8aeeaa
--- /dev/null
+++ b/_styles/all.scss
@@ -0,0 +1,11 @@
+---
+---
+
+*,
+::before,
+::after {
+ box-sizing: border-box;
+ -moz-text-size-adjust: none;
+ -webkit-text-size-adjust: none;
+ text-size-adjust: none;
+}
diff --git a/_styles/anchor.scss b/_styles/anchor.scss
new file mode 100644
index 0000000..65c18d8
--- /dev/null
+++ b/_styles/anchor.scss
@@ -0,0 +1,24 @@
+---
+---
+
+.anchor {
+ display: inline-block;
+ position: relative;
+ width: 0;
+ margin: 0;
+ left: 0.5em;
+ color: var(--primary) !important;
+ opacity: 0;
+ font-size: 0.75em;
+ text-decoration: none;
+ transition: opacity var(--transition), color var(--transition);
+}
+
+:hover > .anchor,
+.anchor:focus {
+ opacity: 1;
+}
+
+.anchor:hover {
+ color: var(--text) !important;
+}
diff --git a/_styles/background.scss b/_styles/background.scss
new file mode 100644
index 0000000..15a7ba4
--- /dev/null
+++ b/_styles/background.scss
@@ -0,0 +1,21 @@
+---
+---
+
+.background {
+ position: relative;
+ background: var(--background);
+ color: var(--text);
+ z-index: 1;
+}
+
+.background:before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background-image: var(--image);
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: 50% 50%;
+ opacity: 0.25;
+ z-index: -1;
+}
diff --git a/_styles/body.scss b/_styles/body.scss
new file mode 100644
index 0000000..91ecffc
--- /dev/null
+++ b/_styles/body.scss
@@ -0,0 +1,15 @@
+---
+---
+
+body {
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ padding: 0;
+ min-height: 100vh;
+ background: var(--background);
+ color: var(--text);
+ font-family: var(--body);
+ text-align: center;
+ line-height: var(--compact);
+}
diff --git a/_styles/bold.scss b/_styles/bold.scss
new file mode 100644
index 0000000..01c72f6
--- /dev/null
+++ b/_styles/bold.scss
@@ -0,0 +1,7 @@
+---
+---
+
+b,
+strong {
+ font-weight: var(--bold);
+}
diff --git a/_styles/button.scss b/_styles/button.scss
new file mode 100644
index 0000000..ed497f0
--- /dev/null
+++ b/_styles/button.scss
@@ -0,0 +1,51 @@
+---
+---
+
+button {
+ cursor: pointer;
+}
+
+.button-wrapper {
+ display: contents;
+}
+
+.button {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ max-width: calc(100% - 5px - 5px);
+ margin: 5px;
+ padding: 10px 15px;
+ border: none;
+ border-radius: var(--rounded);
+ background: var(--primary);
+ color: var(--background);
+ text-align: center;
+ font: inherit;
+ font-family: var(--heading);
+ font-weight: var(--semi-bold);
+ text-decoration: none;
+ vertical-align: middle;
+ appearance: none;
+ transition: background var(--transition), color var(--transition);
+}
+
+.button:hover {
+ background: var(--text);
+ color: var(--background);
+}
+
+.button[data-style="bare"] {
+ padding: 5px;
+ background: none;
+ color: var(--primary);
+
+ &:hover {
+ color: var(--text);
+ }
+}
+
+.button[data-flip] {
+ flex-direction: row-reverse;
+}
diff --git a/_styles/card.scss b/_styles/card.scss
new file mode 100644
index 0000000..d95888e
--- /dev/null
+++ b/_styles/card.scss
@@ -0,0 +1,52 @@
+---
+---
+
+.card {
+ display: inline-flex;
+ justify-content: stretch;
+ align-items: center;
+ flex-direction: column;
+ width: 350px;
+ max-width: calc(100% - 20px - 20px);
+ margin: 20px;
+ background: var(--background);
+ border-radius: var(--rounded);
+ overflow: hidden;
+ box-shadow: var(--shadow);
+ vertical-align: top;
+}
+
+.card[data-style="small"] {
+ width: 250px;
+}
+
+.card-image img {
+ aspect-ratio: 3 / 2;
+ object-fit: cover;
+ width: 100%;
+ // box-shadow: var(--shadow);
+}
+
+.card-text {
+ display: inline-flex;
+ justify-content: flex-start;
+ align-items: center;
+ flex-direction: column;
+ gap: 20px;
+ max-width: 100%;
+ padding: 20px;
+}
+
+.card-text > * {
+ margin: 0 !important;
+}
+
+.card-title {
+ font-family: var(--heading);
+ font-weight: var(--semi-bold);
+}
+
+.card-subtitle {
+ margin-top: -10px !important;
+ font-style: italic;
+}
diff --git a/_styles/checkbox.scss b/_styles/checkbox.scss
new file mode 100644
index 0000000..e5dbda8
--- /dev/null
+++ b/_styles/checkbox.scss
@@ -0,0 +1,6 @@
+---
+---
+
+input[type="checkbox"] {
+ cursor: pointer;
+}
diff --git a/_styles/citation.scss b/_styles/citation.scss
new file mode 100644
index 0000000..dc6c95e
--- /dev/null
+++ b/_styles/citation.scss
@@ -0,0 +1,103 @@
+---
+---
+
+$thumb-size: 180px;
+$wrap: 800px;
+
+.citation-container {
+ container-type: inline-size;
+}
+
+.citation {
+ display: flex;
+ margin: 20px 0;
+ border-radius: var(--rounded);
+ background: var(--background);
+ overflow: hidden;
+ box-shadow: var(--shadow);
+}
+
+.citation-image {
+ position: relative;
+ width: $thumb-size;
+ flex-shrink: 0;
+ // box-shadow: var(--shadow);
+}
+
+.citation-image img {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+.citation-text {
+ position: relative;
+ display: inline-flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ max-width: 100%;
+ height: min-content;
+ padding: 20px;
+ padding-left: 30px;
+ text-align: left;
+ overflow-wrap: break-word;
+ z-index: 0;
+}
+
+.citation-title,
+.citation-authors,
+.citation-details,
+.citation-description {
+ width: 100%;
+}
+
+.citation-title {
+ font-weight: var(--semi-bold);
+}
+
+.citation-text > .icon {
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ color: var(--light-gray);
+ opacity: 0.5;
+ font-size: 30px;
+ z-index: -1;
+}
+
+.citation-publisher {
+ text-transform: capitalize;
+}
+
+.citation-description {
+ color: var(--gray);
+}
+
+.citation-buttons {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.citation-buttons .button {
+ margin: 0;
+}
+
+.citation-text > .tags {
+ display: inline-flex;
+ justify-content: flex-start;
+ margin: 0;
+}
+
+@container (max-width: #{$wrap}) {
+ .citation {
+ flex-direction: column;
+ }
+
+ .citation-image {
+ width: unset;
+ height: $thumb-size;
+ }
+}
diff --git a/_styles/code.scss b/_styles/code.scss
new file mode 100644
index 0000000..4a50657
--- /dev/null
+++ b/_styles/code.scss
@@ -0,0 +1,38 @@
+---
+---
+
+pre,
+code,
+pre *,
+code * {
+ font-family: var(--code);
+}
+
+// inline code
+code.highlighter-rouge {
+ padding: 2px 6px;
+ background: var(--light-gray);
+ border-radius: var(--rounded);
+}
+
+// code block
+div.highlighter-rouge {
+ width: 100%;
+ margin: 40px 0;
+ border-radius: var(--rounded);
+ overflow-x: auto;
+ overflow-y: auto;
+ text-align: left;
+
+ div.highlight {
+ display: contents;
+
+ pre.highlight {
+ width: fit-content;
+ min-width: 100%;
+ margin: 0;
+ padding: 20px;
+ color: var(--white);
+ }
+ }
+}
diff --git a/_styles/cols.scss b/_styles/cols.scss
new file mode 100644
index 0000000..a3500b3
--- /dev/null
+++ b/_styles/cols.scss
@@ -0,0 +1,39 @@
+---
+---
+
+$two: 750px;
+$one: 500px;
+
+.cols {
+ display: grid;
+ --repeat: min(3, var(--cols));
+ grid-template-columns: repeat(var(--repeat), 1fr);
+ align-items: flex-start;
+ gap: 40px;
+ margin: 40px 0;
+}
+
+.cols > * {
+ min-width: 0;
+ min-height: 0;
+}
+
+.cols > div > :first-child {
+ margin-top: 0 !important;
+}
+
+.cols > div > :last-child {
+ margin-bottom: 0 !important;
+}
+
+@media (max-width: $two) {
+ .cols {
+ --repeat: min(2, var(--cols));
+ }
+}
+
+@media (max-width: $one) {
+ .cols {
+ --repeat: min(1, var(--cols));
+ }
+}
diff --git a/_styles/dark-toggle.scss b/_styles/dark-toggle.scss
new file mode 100644
index 0000000..ade9c05
--- /dev/null
+++ b/_styles/dark-toggle.scss
@@ -0,0 +1,31 @@
+---
+---
+
+.dark-toggle {
+ position: relative;
+ width: 40px;
+ height: 25px;
+ margin: 0;
+ border-radius: 999px;
+ background: var(--primary);
+ appearance: none;
+ transition: background var(--transition);
+}
+
+.dark-toggle:after {
+ content: "\f185";
+ position: absolute;
+ left: 12px;
+ top: 50%;
+ color: var(--text);
+ font-size: 15px;
+ font-family: "Font Awesome 6 Free";
+ font-weight: 900;
+ transform: translate(-50%, -50%);
+ transition: left var(--transition);
+}
+
+.dark-toggle:checked:after {
+ content: "\f186";
+ left: calc(100% - 12px);
+}
diff --git a/_styles/feature.scss b/_styles/feature.scss
new file mode 100644
index 0000000..3d2a53f
--- /dev/null
+++ b/_styles/feature.scss
@@ -0,0 +1,53 @@
+---
+---
+
+$wrap: 800px;
+
+.feature {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 40px;
+ margin: 40px 0;
+}
+
+.feature-image {
+ flex-shrink: 0;
+ width: 40%;
+ aspect-ratio: 3 / 2;
+ border-radius: var(--rounded);
+ overflow: hidden;
+ box-shadow: var(--shadow);
+}
+
+.feature-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.feature-text {
+ flex-grow: 1;
+}
+
+.feature-title {
+ font-size: var(--large);
+ text-align: center;
+ font-family: var(--heading);
+ font-weight: var(--semi-bold);
+}
+
+.feature[data-flip] {
+ flex-direction: row-reverse;
+}
+
+@media (max-width: $wrap) {
+ .feature {
+ flex-direction: column !important;
+ }
+
+ .feature-image {
+ width: 100%;
+ max-width: calc($wrap / 2);
+ }
+}
diff --git a/_styles/figure.scss b/_styles/figure.scss
new file mode 100644
index 0000000..3b3c6ef
--- /dev/null
+++ b/_styles/figure.scss
@@ -0,0 +1,26 @@
+---
+---
+
+.figure {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ gap: 10px;
+ margin: 40px 0;
+}
+
+.figure-image {
+ display: contents;
+}
+
+.figure-image img {
+ border-radius: var(--rounded);
+ overflow: hidden;
+ box-shadow: var(--shadow);
+}
+
+.figure-caption {
+ font-style: italic;
+ text-align: center;
+}
diff --git a/_styles/float.scss b/_styles/float.scss
new file mode 100644
index 0000000..ba8d9e2
--- /dev/null
+++ b/_styles/float.scss
@@ -0,0 +1,38 @@
+---
+---
+
+$wrap: 600px;
+
+.float {
+ margin-bottom: 20px;
+ max-width: 50%;
+}
+
+.float > * {
+ margin: 0 !important;
+}
+
+.float:not([data-flip]) {
+ float: left;
+ margin-right: 40px;
+}
+
+.float[data-flip] {
+ float: right;
+ margin-left: 40px;
+}
+
+.float[data-clear] {
+ float: unset;
+ clear: both;
+ margin: 0;
+}
+
+@media (max-width: $wrap) {
+ .float {
+ float: unset !important;
+ clear: both !important;
+ margin: auto !important;
+ max-width: unset;
+ }
+}
diff --git a/_styles/font.scss b/_styles/font.scss
new file mode 100644
index 0000000..162db3d
--- /dev/null
+++ b/_styles/font.scss
@@ -0,0 +1,5 @@
+---
+---
+
+@font-face {
+}
diff --git a/_styles/footer.scss b/_styles/footer.scss
new file mode 100644
index 0000000..d0d5277
--- /dev/null
+++ b/_styles/footer.scss
@@ -0,0 +1,25 @@
+---
+---
+
+footer {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ gap: 20px;
+ padding: 40px;
+ line-height: var(--spacing);
+ box-shadow: var(--shadow);
+}
+
+footer a {
+ color: var(--text) !important;
+}
+
+footer a:hover {
+ color: var(--primary) !important;
+}
+
+footer .icon {
+ font-size: var(--xl);
+}
diff --git a/_styles/form.scss b/_styles/form.scss
new file mode 100644
index 0000000..ce6129a
--- /dev/null
+++ b/_styles/form.scss
@@ -0,0 +1,9 @@
+---
+---
+
+form {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+}
diff --git a/_styles/grid.scss b/_styles/grid.scss
new file mode 100644
index 0000000..8ff6d2e
--- /dev/null
+++ b/_styles/grid.scss
@@ -0,0 +1,54 @@
+---
+---
+
+$two: 750px;
+$one: 500px;
+
+.grid {
+ display: grid;
+ --repeat: 3;
+ grid-template-columns: repeat(var(--repeat), 1fr);
+ justify-content: center;
+ align-items: flex-start;
+ gap: 40px;
+ margin: 40px 0;
+}
+
+.grid > * {
+ min-width: 0;
+ min-height: 0;
+ width: 100%;
+ // max-height: 50vh;
+ margin: 0 !important;
+}
+
+@media (max-width: $two) {
+ .grid {
+ --repeat: 2;
+ }
+}
+
+@media (max-width: $one) {
+ .grid {
+ --repeat: 1;
+ }
+}
+
+.grid[data-style="square"] {
+ align-items: center;
+
+ & > * {
+ aspect-ratio: 1 / 1;
+ }
+
+ & img {
+ aspect-ratio: 1 / 1;
+ object-fit: cover;
+ max-width: unset;
+ max-height: unset;
+ }
+}
+
+.grid > :where(h1, h2, h3, h4, h5, h6) {
+ display: none;
+}
diff --git a/_styles/header.scss b/_styles/header.scss
new file mode 100644
index 0000000..045e293
--- /dev/null
+++ b/_styles/header.scss
@@ -0,0 +1,166 @@
+---
+---
+
+$logo-big: 80px;
+$logo: 40px;
+$big-padding: 100px;
+$collapse: 700px;
+$sticky: true;
+
+header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 20px;
+ padding: 20px;
+ box-shadow: var(--shadow);
+
+ @if $sticky {
+ position: sticky !important;
+ top: 0;
+ z-index: 10 !important;
+ }
+}
+
+header a {
+ color: var(--text);
+ text-decoration: none;
+}
+
+.home {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 10px;
+ flex-basis: 0;
+ flex-grow: 1;
+ max-width: 100%;
+}
+
+.logo {
+ height: $logo;
+}
+
+.logo > * {
+ height: 100%;
+}
+
+.title-text {
+ display: flex;
+ justify-content: flex-start;
+ align-items: baseline;
+ flex-wrap: wrap;
+ gap: 5px;
+ min-width: 0;
+ font-family: var(--title);
+ text-align: left;
+}
+
+.title {
+ font-size: var(--large);
+}
+
+.subtitle {
+ opacity: 0.65;
+ font-weight: var(--thin);
+}
+
+.nav-toggle {
+ display: none;
+ position: relative;
+ width: 30px;
+ height: 30px;
+ margin: 0;
+ color: var(--text);
+ appearance: none;
+ transition: background var(--transition);
+}
+
+.nav-toggle:after {
+ content: "\f0c9";
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ color: var(--text);
+ font-size: 15px;
+ font-family: "Font Awesome 6 Free";
+ font-weight: 900;
+ transform: translate(-50%, -50%);
+}
+
+.nav-toggle:checked:after {
+ content: "\f00d";
+}
+
+nav {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 10px;
+ font-family: var(--heading);
+ text-transform: uppercase;
+}
+
+nav > a {
+ padding: 5px;
+}
+
+nav > a:hover {
+ color: var(--primary);
+}
+
+header:not([data-big]) {
+ @media (max-width: $collapse) {
+ justify-content: flex-end;
+
+ .nav-toggle {
+ display: flex;
+ }
+
+ .nav-toggle:not(:checked) + nav {
+ display: none;
+ }
+
+ nav {
+ align-items: flex-end;
+ flex-direction: column;
+ width: 100%;
+ }
+ }
+}
+
+header[data-big] {
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ padding: $big-padding 20px;
+
+ @if $sticky {
+ top: unset;
+ }
+
+ .home {
+ flex-direction: column;
+ flex-grow: 0;
+ }
+
+ .logo {
+ height: $logo-big;
+ }
+
+ .title-text {
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ }
+
+ .title {
+ font-size: var(--xxl);
+ }
+
+ .subtitle {
+ font-size: var(--large);
+ }
+}
diff --git a/_styles/heading.scss b/_styles/heading.scss
new file mode 100644
index 0000000..2ea35a4
--- /dev/null
+++ b/_styles/heading.scss
@@ -0,0 +1,50 @@
+---
+---
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin: 40px 0 20px 0;
+ font-family: var(--heading);
+ font-weight: var(--semi-bold);
+ text-align: left;
+ letter-spacing: 1px;
+}
+
+h1 {
+ font-size: 1.6rem;
+ font-weight: var(--regular);
+ text-transform: uppercase;
+ text-align: center;
+}
+
+h2 {
+ font-size: 1.6rem;
+ padding-bottom: 5px;
+ border-bottom: solid 1px var(--light-gray);
+ font-weight: var(--regular);
+}
+
+h3 {
+ font-size: 1.5rem;
+}
+
+h4 {
+ font-size: 1.3rem;
+}
+
+h5 {
+ font-size: 1.15rem;
+}
+
+h6 {
+ font-size: 1rem;
+}
+
+:where(h1, h2, h3, h4, h5, h6) > .icon {
+ margin-right: 1em;
+ color: var(--light-gray);
+}
diff --git a/_styles/highlight.scss b/_styles/highlight.scss
new file mode 100644
index 0000000..d41524a
--- /dev/null
+++ b/_styles/highlight.scss
@@ -0,0 +1,7 @@
+---
+---
+
+mark {
+ background: #fef08a;
+ color: #000000;
+}
diff --git a/_styles/icon.scss b/_styles/icon.scss
new file mode 100644
index 0000000..c434ff1
--- /dev/null
+++ b/_styles/icon.scss
@@ -0,0 +1,16 @@
+---
+---
+
+.icon {
+ font-size: 1em;
+}
+
+span.icon {
+ line-height: 1;
+}
+
+span.icon > svg {
+ position: relative;
+ top: 0.1em;
+ height: 1em;
+}
diff --git a/_styles/image.scss b/_styles/image.scss
new file mode 100644
index 0000000..d288263
--- /dev/null
+++ b/_styles/image.scss
@@ -0,0 +1,7 @@
+---
+---
+
+img {
+ max-width: 100%;
+ max-height: 100%;
+}
diff --git a/_styles/link.scss b/_styles/link.scss
new file mode 100644
index 0000000..41230d3
--- /dev/null
+++ b/_styles/link.scss
@@ -0,0 +1,16 @@
+---
+---
+
+a {
+ color: var(--primary);
+ transition: color var(--transition);
+ overflow-wrap: break-word;
+}
+
+a:hover {
+ color: var(--text);
+}
+
+a:not([href]) {
+ color: var(--text);
+}
diff --git a/_styles/list.scss b/_styles/list.scss
new file mode 100644
index 0000000..d769a6a
--- /dev/null
+++ b/_styles/list.scss
@@ -0,0 +1,24 @@
+---
+---
+
+ul,
+ol {
+ margin: 20px 0;
+ padding-left: 40px;
+}
+
+ul {
+ list-style-type: square;
+}
+
+li {
+ margin: 5px 0;
+ padding-left: 10px;
+ text-align: justify;
+ line-height: var(--spacing);
+
+ ul,
+ ol {
+ margin: 0;
+ }
+}
diff --git a/_styles/main.scss b/_styles/main.scss
new file mode 100644
index 0000000..36a8a79
--- /dev/null
+++ b/_styles/main.scss
@@ -0,0 +1,8 @@
+---
+---
+
+main {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+}
diff --git a/_styles/paragraph.scss b/_styles/paragraph.scss
new file mode 100644
index 0000000..08b05a3
--- /dev/null
+++ b/_styles/paragraph.scss
@@ -0,0 +1,8 @@
+---
+---
+
+p {
+ margin: 20px 0;
+ text-align: justify;
+ line-height: var(--spacing);
+}
diff --git a/_styles/portrait.scss b/_styles/portrait.scss
new file mode 100644
index 0000000..fa0c039
--- /dev/null
+++ b/_styles/portrait.scss
@@ -0,0 +1,76 @@
+---
+---
+
+.portrait-wrapper {
+ display: contents;
+}
+
+.portrait {
+ position: relative;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ gap: 20px;
+ margin: 20px;
+ width: 175px;
+ max-width: calc(100% - 20px - 20px);
+ text-decoration: none;
+}
+
+.portrait[data-style="small"] {
+ width: 100px;
+}
+
+.portrait[data-style="tiny"] {
+ flex-direction: row;
+ gap: 15px;
+ width: unset;
+ text-align: left;
+}
+
+.portrait-image {
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ border-radius: 999px;
+ object-fit: cover;
+ box-shadow: var(--shadow);
+}
+
+.portrait[data-style="tiny"] .portrait-image {
+ width: 50px;
+}
+
+.portrait[data-style="tiny"] .portrait-role {
+ display: none;
+}
+
+.portrait-text {
+ display: flex;
+ flex-direction: column;
+}
+
+.portrait-name {
+ font-family: var(--heading);
+ font-weight: var(--semi-bold);
+}
+
+.portrait-role .icon {
+ position: absolute;
+ left: 0;
+ top: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: calc(20px + 10%);
+ aspect-ratio: 1 / 1;
+ border-radius: 999px;
+ background: var(--background);
+ box-shadow: var(--shadow);
+ transform: translate(14%, 14%);
+}
+
+.portrait[data-style="small"] .portrait-role .icon {
+ left: -2px;
+ top: -2px;
+}
diff --git a/_styles/post-excerpt.scss b/_styles/post-excerpt.scss
new file mode 100644
index 0000000..27c7a1d
--- /dev/null
+++ b/_styles/post-excerpt.scss
@@ -0,0 +1,69 @@
+---
+---
+
+$thumb-size: 200px;
+$wrap: 800px;
+
+.post-excerpt-container {
+ container-type: inline-size;
+}
+
+.post-excerpt {
+ display: flex;
+ margin: 20px 0;
+ border-radius: var(--rounded);
+ background: var(--background);
+ overflow: hidden;
+ box-shadow: var(--shadow);
+}
+
+.post-excerpt-image {
+ position: relative;
+ width: $thumb-size;
+ flex-shrink: 0;
+ // box-shadow: var(--shadow);
+}
+
+.post-excerpt-image img {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.post-excerpt-text {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px;
+ padding: 20px 30px;
+ text-align: left;
+}
+
+.post-excerpt-text > * {
+ margin: 0 !important;
+}
+
+.post-excerpt-text > a:first-child {
+ width: 100%;
+ font-weight: var(--semi-bold);
+}
+
+.post-excerpt-text > div {
+ justify-content: flex-start;
+}
+
+.post-excerpt-text > p {
+ width: 100%;
+}
+
+@container (max-width: #{$wrap}) {
+ .post-excerpt {
+ flex-direction: column;
+ }
+
+ .post-excerpt-image {
+ width: unset;
+ height: $thumb-size;
+ }
+}
diff --git a/_styles/post-info.scss b/_styles/post-info.scss
new file mode 100644
index 0000000..65c86cb
--- /dev/null
+++ b/_styles/post-info.scss
@@ -0,0 +1,33 @@
+---
+---
+
+.post-info {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 20px;
+ margin: 20px 0;
+ color: var(--dark-gray);
+}
+
+.post-info .portrait {
+ margin: 0;
+}
+
+.post-info .icon {
+ margin-right: 0.5em;
+}
+
+.post-info a {
+ color: inherit;
+}
+
+.post-info a:hover {
+ color: var(--primary);
+}
+
+.post-info > span {
+ text-align: center;
+ white-space: nowrap;
+}
diff --git a/_styles/post-nav.scss b/_styles/post-nav.scss
new file mode 100644
index 0000000..40b5dd1
--- /dev/null
+++ b/_styles/post-nav.scss
@@ -0,0 +1,39 @@
+---
+---
+
+$wrap: 600px;
+
+.post-nav {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 10px;
+ color: var(--gray);
+}
+
+.post-nav > :first-child {
+ text-align: left;
+}
+
+.post-nav > :last-child {
+ text-align: right;
+}
+
+.post-nav > :first-child .icon {
+ margin-right: 0.5em;
+}
+
+.post-nav > :last-child .icon {
+ margin-left: 0.5em;
+}
+
+@media (max-width: $wrap) {
+ .post-nav {
+ align-items: center;
+ flex-direction: column;
+ }
+
+ .post-nav > * {
+ text-align: center !important;
+ }
+}
diff --git a/_styles/quote.scss b/_styles/quote.scss
new file mode 100644
index 0000000..68b2f3a
--- /dev/null
+++ b/_styles/quote.scss
@@ -0,0 +1,16 @@
+---
+---
+
+blockquote {
+ margin: 20px 0;
+ padding: 10px 20px;
+ border-left: solid 4px var(--light-gray);
+}
+
+blockquote > :first-child {
+ margin-top: 0;
+}
+
+blockquote > :last-child {
+ margin-bottom: 0;
+}
diff --git a/_styles/rule.scss b/_styles/rule.scss
new file mode 100644
index 0000000..abf797b
--- /dev/null
+++ b/_styles/rule.scss
@@ -0,0 +1,9 @@
+---
+---
+
+hr {
+ margin: 40px 0;
+ background: var(--light-gray);
+ border: none;
+ height: 1px;
+}
diff --git a/_styles/search-box.scss b/_styles/search-box.scss
new file mode 100644
index 0000000..5f20a78
--- /dev/null
+++ b/_styles/search-box.scss
@@ -0,0 +1,26 @@
+---
+---
+
+.search-box {
+ position: relative;
+ height: 40px;
+}
+
+.search-box .search-input {
+ width: 100%;
+ height: 100%;
+ padding-right: 40px;
+}
+
+.search-box button {
+ position: absolute;
+ inset: 0 0 0 auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+ aspect-ratio: 1 / 1;
+ background: none;
+ color: var(--black);
+ border: none;
+}
diff --git a/_styles/search-info.scss b/_styles/search-info.scss
new file mode 100644
index 0000000..63f9a17
--- /dev/null
+++ b/_styles/search-info.scss
@@ -0,0 +1,9 @@
+---
+---
+
+.search-info {
+ margin: 20px 0;
+ text-align: center;
+ font-style: italic;
+ line-height: var(--spacing);
+}
diff --git a/_styles/section.scss b/_styles/section.scss
new file mode 100644
index 0000000..332deb6
--- /dev/null
+++ b/_styles/section.scss
@@ -0,0 +1,39 @@
+---
+---
+
+$page: 1000px;
+$padding: 40px;
+
+section {
+ padding: $padding max($padding, calc((100% - $page) / 2));
+ transition: background var(--transition), color var(--transition);
+}
+
+section[data-size="wide"] {
+ padding: $padding;
+}
+
+section[data-size="full"] {
+ padding: 0;
+}
+
+section[data-size="full"] > * {
+ margin: 0;
+ border-radius: 0;
+}
+
+section[data-size="full"] img {
+ border-radius: 0;
+}
+
+main > section:last-of-type {
+ flex-grow: 1;
+}
+
+main > section:nth-of-type(odd) {
+ background: var(--background);
+}
+
+main > section:nth-of-type(even) {
+ background: var(--background-alt);
+}
diff --git a/_styles/table.scss b/_styles/table.scss
new file mode 100644
index 0000000..995c700
--- /dev/null
+++ b/_styles/table.scss
@@ -0,0 +1,18 @@
+---
+---
+
+table {
+ margin: 40px auto;
+ border-collapse: collapse;
+ overflow-wrap: anywhere;
+}
+
+th {
+ font-weight: var(--semi-bold);
+}
+
+th,
+td {
+ padding: 10px 15px;
+ border: solid 1px var(--light-gray);
+}
diff --git a/_styles/tags.scss b/_styles/tags.scss
new file mode 100644
index 0000000..4e98207
--- /dev/null
+++ b/_styles/tags.scss
@@ -0,0 +1,34 @@
+---
+---
+
+.tags {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 10px;
+ max-width: 100%;
+ margin: 20px 0;
+}
+
+.tag {
+ max-width: 100%;
+ margin: 0;
+ padding: 5px 10px;
+ border-radius: 999px;
+ background: var(--secondary);
+ color: var(--text);
+ text-decoration: none;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ transition: background var(--transition), color var(--transition);
+}
+
+.tag:hover {
+ background: var(--light-gray);
+}
+
+.tag[data-active] {
+ background: var(--light-gray);
+}
diff --git a/_styles/textbox.scss b/_styles/textbox.scss
new file mode 100644
index 0000000..6d33ced
--- /dev/null
+++ b/_styles/textbox.scss
@@ -0,0 +1,17 @@
+---
+---
+
+input[type="text"] {
+ width: 100%;
+ height: 40px;
+ margin: 0;
+ padding: 5px 10px;
+ border: solid 1px var(--light-gray);
+ border-radius: var(--rounded);
+ background: var(--background);
+ color: var(--text);
+ font-family: inherit;
+ font-size: inherit;
+ appearance: none;
+ box-shadow: var(--shadow);
+}
diff --git a/_styles/tooltip.scss b/_styles/tooltip.scss
new file mode 100644
index 0000000..0f21d2e
--- /dev/null
+++ b/_styles/tooltip.scss
@@ -0,0 +1,65 @@
+---
+---
+
+.tippy-box {
+ background: var(--background);
+ color: var(--text);
+ padding: 7.5px;
+ text-align: left;
+ box-shadow: var(--shadow);
+}
+
+.tippy-arrow {
+ width: 30px;
+ height: 30px;
+}
+
+.tippy-arrow:before {
+ width: 10px;
+ height: 10px;
+ background: var(--background);
+ box-shadow: var(--shadow);
+}
+
+// correct tippy arrow styles to support intuitive arrow styles above
+.tippy-arrow {
+ overflow: hidden;
+ pointer-events: none;
+}
+.tippy-box[data-placement="top"] .tippy-arrow {
+ inset: unset;
+ top: 100%;
+}
+.tippy-box[data-placement="bottom"] .tippy-arrow {
+ inset: unset;
+ bottom: 100%;
+}
+.tippy-box[data-placement="left"] .tippy-arrow {
+ inset: unset;
+ left: 100%;
+}
+.tippy-box[data-placement="right"] .tippy-arrow {
+ inset: unset;
+ right: 100%;
+}
+.tippy-arrow:before {
+ border: unset !important;
+ transform-origin: center !important;
+ transform: translate(-50%, -50%) rotate(45deg) !important;
+}
+.tippy-box[data-placement="top"] .tippy-arrow:before {
+ left: 50% !important;
+ top: 0 !important;
+}
+.tippy-box[data-placement="bottom"] .tippy-arrow:before {
+ left: 50% !important;
+ top: 100% !important;
+}
+.tippy-box[data-placement="left"] .tippy-arrow:before {
+ left: 0 !important;
+ top: 50% !important;
+}
+.tippy-box[data-placement="right"] .tippy-arrow:before {
+ left: 100% !important;
+ top: 50% !important;
+}
diff --git a/_styles/util.scss b/_styles/util.scss
new file mode 100644
index 0000000..308c3c1
--- /dev/null
+++ b/_styles/util.scss
@@ -0,0 +1,14 @@
+---
+---
+
+.left {
+ text-align: left;
+}
+
+.center {
+ text-align: center;
+}
+
+.right {
+ text-align: right;
+}
diff --git a/blog/index.md b/blog/index.md
new file mode 100644
index 0000000..b86f359
--- /dev/null
+++ b/blog/index.md
@@ -0,0 +1,21 @@
+---
+title: Blog
+nav:
+ order: 4
+ tooltip: Musings and miscellany
+---
+
+# {% include icon.html icon="fa-solid fa-feather-pointed" %}Blog
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+{% include section.html %}
+
+{% include search-box.html %}
+
+{% include tags.html tags=site.tags %}
+
+{% include search-info.html %}
+
+{% include list.html data="posts" component="post-excerpt" %}
diff --git a/contact/index.md b/contact/index.md
new file mode 100644
index 0000000..d9b7e4b
--- /dev/null
+++ b/contact/index.md
@@ -0,0 +1,77 @@
+---
+title: Contact
+nav:
+ order: 5
+ tooltip: Email, address, and location
+---
+
+# {% include icon.html icon="fa-regular fa-envelope" %}Contact
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
+incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
+nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+{%
+ include button.html
+ type="email"
+ text="jane@smith.com"
+ link="jane@smith.com"
+%}
+{%
+ include button.html
+ type="phone"
+ text="(555) 867-5309"
+ link="+1-555-867-5309"
+%}
+{%
+ include button.html
+ type="address"
+ tooltip="Our location on Google Maps for easy navigation"
+ link="https://www.google.com/maps"
+%}
+
+{% include section.html %}
+
+{% capture col1 %}
+
+{%
+ include figure.html
+ image="images/photo.jpg"
+ caption="Lorem ipsum"
+%}
+
+{% endcapture %}
+
+{% capture col2 %}
+
+{%
+ include figure.html
+ image="images/photo.jpg"
+ caption="Lorem ipsum"
+%}
+
+{% endcapture %}
+
+{% include cols.html col1=col1 col2=col2 %}
+
+{% include section.html dark=true %}
+
+{% capture col1 %}
+Lorem ipsum dolor sit amet
+consectetur adipiscing elit
+sed do eiusmod tempor
+{% endcapture %}
+
+{% capture col2 %}
+Lorem ipsum dolor sit amet
+consectetur adipiscing elit
+sed do eiusmod tempor
+{% endcapture %}
+
+{% capture col3 %}
+Lorem ipsum dolor sit amet
+consectetur adipiscing elit
+sed do eiusmod tempor
+{% endcapture %}
+
+{% include cols.html col1=col1 col2=col2 col3=col3 %}
diff --git a/images/background.jpg b/images/background.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..5b7c14613469d676414e3be069fd1130fa7e401c
GIT binary patch
literal 82867
zcmb5Wdsx$U8$Xomm3r5kVLNvB^4jYCtV2sdg#D
zsiXoti*j4r=O%o4x@#eHR_w{sZ5
QhJB>k~h8NIJv-8dh|Q%U>XYAfhxUYv32%xsjZDHlXcqEN)d?V
zRakIhskT*RZ|-HnKE!)_DXfbrHx27ZC%)DaIb`_>=ncFh=I@^E|iP
zP2fznmTA+~!Hh1?81e>JhYk0y1-o0$%MOvNGJCepM$$G@`a!jhE!Q(;Teu}ls5j5@
zkIuJKP6q?>1
z#S9weZ(ghiNwf2x4SA8
z4bofVX0_tmMSrn~<5c+{v7*UJepbA1$(?@0O%P#Afx^t1Qt>NLIl22;DJPXAWq@P|
zME-{i5212r7x5sc0ufC(#caG|!+#{oXzFyqm_%_FKs1mNN&+y9M945u#L<