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 100644
index 0000000..efe4047
--- /dev/null
+++ b/.docker/entrypoint.sh
@@ -0,0 +1,25 @@
+#! /bin/bash
+
+# print folder contents for debugging
+printf "\n\nContents:\n\n"
+ls
+
+# 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 100644
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..6194c83
--- /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: |
+ printf "\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..415e284
--- /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: printf "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."
+ printf "USER=${user}" >> $GITHUB_ENV
+ printf "DESCRIPTION=${description}" >> $GITHUB_ENV
+
+ - name: Personalize readme for user
+ run: |
+ printf "
+ # ${{ 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..35da88e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,165 @@
+# Changelog
+
+Reference: common-changelog.org
+
+## 1.2.2 - 2024-06-05
+
+### Added
+
+- Add `affiliation` member portrait field.
+
+### Changed
+
+- Change order and type of preferred ids from ORCID API.
+- Expand list of supported Manubot identifiers and thus keep ORCID API details less often.
+- Simplify portrait component under-the-hood.
+- Make tag component de-duplication consistent with search plugin de-duplication.
+
+## 1.2.1 - 2024-04-01
+
+### Changed
+
+- Minor bug fixes in cite process and sitemap generation.
+
+## 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..2ded51e
--- /dev/null
+++ b/CITATION.cff
@@ -0,0 +1,14 @@
+# citation metadata for the template itself
+
+title: "Lab Website Template"
+version: 1.2.2
+date-released: 2024-06-05
+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 @@
+
Lab Website Template
+
+
+
+
+Lab Website Template (LWT) is an easy-to-use, flexible website template for labs.
+Spend less time worrying about managing a website and citations, and more time running your lab.
+
+👇👇 **Get Started** 👇👇
+
+[**Documentation**](https://greene-lab.gitbook.io/lab-website-template-docs)
+
+## Key Features
+
+- 🤖 Based on Git, GitHub, and Jekyll.
+- 📜 Automatically generated citations from simple identifiers (DOI, PubMed, ORCID, and many more) using Manubot. E.g. `doi:1234/5678` -> `title`, `authors`, `publisher`, `date`, etc.
+- 🧱 A comprehensive and flexible suite of pre-made components (building blocks) for structuring and styling your website:
+ - Formatted tables, code blocks, figures, and other basic elements.
+ - Citations with thumbnails and other rich details.
+ - List large sets of data with flexible filters and components.
+ - ...many more
+- 👁️ Automatic pull request previews.
+- ⚙️ Easy and automated configuration.
+- 👥 Team member pages with bios, roles, and social media links.
+- 🖋️ Blog posts with tags and rich content.
+- 📱 Works and looks good on desktop and mobile.
+- 🤝 Great documentation and support (if we do say so ourselves).
+- ... and much more!
+
+![GitHub last commit](https://img.shields.io/github/last-commit/greenelab/lab-website-template)
diff --git a/_cite/cite.py b/_cite/cite.py
new file mode 100644
index 0000000..6cd5bef
--- /dev/null
+++ b/_cite/cite.py
@@ -0,0 +1,188 @@
+"""
+cite process to convert sources and metasources into full citations
+"""
+
+import traceback
+from importlib import import_module
+from pathlib import Path
+from dotenv import load_dotenv
+from util import *
+
+
+# load environment variables
+load_dotenv()
+
+
+# error flag
+error = False
+
+# output citations file
+output_file = "_data/citations.yaml"
+
+
+log()
+
+log("Compiling sources")
+
+# compiled list of sources
+sources = []
+
+# in-order list of plugins to run
+plugins = ["google-scholar", "pubmed", "orcid", "sources"]
+
+# loop through plugins
+for plugin in plugins:
+ # convert into path object
+ plugin = Path(f"plugins/{plugin}.py")
+
+ log(f"Running {plugin.stem} plugin")
+
+ # get all data files to process with current plugin
+ files = Path.cwd().glob(f"_data/{plugin.stem}*.*")
+ files = list(filter(lambda p: p.suffix in [".yaml", ".yml", ".json"], files))
+
+ log(f"Found {len(files)} {plugin.stem}* data file(s)", 1)
+
+ # loop through data files
+ for file in files:
+ log(f"Processing data file {file.name}", 1)
+
+ # load data from file
+ try:
+ data = load_data(file)
+ # check if file in correct format
+ if not list_of_dicts(data):
+ raise Exception("File not a list of dicts")
+ except Exception as e:
+ log(e, 2, "ERROR")
+ error = True
+ continue
+
+ # loop through data entries
+ for index, entry in enumerate(data):
+ log(f"Processing entry {index + 1} of {len(data)}, {label(entry)}", 2)
+
+ # run plugin on data entry to expand into multiple sources
+ try:
+ expanded = import_module(f"plugins.{plugin.stem}").main(entry)
+ # check that plugin returned correct format
+ if not list_of_dicts(expanded):
+ raise Exception("Plugin didn't return list of dicts")
+ # catch any plugin error
+ except Exception as e:
+ # log detailed pre-formatted/colored trace
+ print(traceback.format_exc())
+ # log high-level error
+ log(e, 3, "ERROR")
+ error = True
+ continue
+
+ # loop through sources
+ for source in expanded:
+ if plugin.stem != "sources":
+ log(label(source), 3)
+
+ # include meta info about source
+ source["plugin"] = plugin.name
+ source["file"] = file.name
+
+ # add source to compiled list
+ sources.append(source)
+
+ if plugin.stem != "sources":
+ log(f"{len(expanded)} source(s)", 3)
+
+
+log("Merging sources by id")
+
+# merge sources with matching (non-blank) ids
+for a in range(0, len(sources)):
+ a_id = get_safe(sources, f"{a}.id", "")
+ if not a_id:
+ continue
+ for b in range(a + 1, len(sources)):
+ b_id = get_safe(sources, f"{b}.id", "")
+ if b_id == a_id:
+ log(f"Found duplicate {b_id}", 2)
+ sources[a].update(sources[b])
+ sources[b] = {}
+sources = [entry for entry in sources if entry]
+
+
+log(f"{len(sources)} total source(s) to cite")
+
+
+log()
+
+log("Generating citations")
+
+# list of new citations
+citations = []
+
+
+# loop through compiled sources
+for index, source in enumerate(sources):
+ log(f"Processing source {index + 1} of {len(sources)}, {label(source)}")
+
+ # if explicitly flagged, remove/ignore entry
+ if get_safe(source, "remove", False) == True:
+ continue
+
+ # new citation data for source
+ citation = {}
+
+ # source id
+ _id = get_safe(source, "id", "").strip()
+
+ # Manubot doesn't work without an id
+ if _id:
+ log("Using Manubot to generate citation", 1)
+
+ try:
+ # run Manubot and set citation
+ citation = cite_with_manubot(_id)
+
+ # if Manubot cannot cite source
+ except Exception as e:
+ # if regular source (id entered by user), throw error
+ if get_safe(source, "plugin", "") == "sources.py":
+ log(e, 3, "ERROR")
+ error = True
+ # otherwise, if from metasource (id retrieved from some third-party API), just warn
+ else:
+ log(e, 3, "WARNING")
+ # discard source from citations
+ continue
+
+ # preserve fields from input source, overriding existing fields
+ citation.update(source)
+
+ # ensure date in proper format for correct date sorting
+ if get_safe(citation, "date", ""):
+ citation["date"] = format_date(get_safe(citation, "date", ""))
+
+ # add new citation to list
+ citations.append(citation)
+
+
+log()
+
+log("Saving updated citations")
+
+
+# save new citations
+try:
+ save_data(output_file, citations)
+except Exception as e:
+ log(e, level="ERROR")
+ error = True
+
+
+# exit at end, so user can see all errors in one run
+if error:
+ log("Error(s) occurred above", level="ERROR")
+ exit(1)
+else:
+ log("All done!", level="SUCCESS")
+
+log("\n")
diff --git a/_cite/plugins/google-scholar.py b/_cite/plugins/google-scholar.py
new file mode 100644
index 0000000..e3841b8
--- /dev/null
+++ b/_cite/plugins/google-scholar.py
@@ -0,0 +1,61 @@
+import os
+from serpapi import GoogleSearch
+from util import *
+
+
+def main(entry):
+ """
+ receives single list entry from google-scholar data file
+ returns list of sources to cite
+ """
+
+ # get api key (serp api key to access google scholar)
+ api_key = os.environ.get("GOOGLE_SCHOLAR_API_KEY", "")
+ if not api_key:
+ raise Exception('No "GOOGLE_SCHOLAR_API_KEY" env var')
+
+ # serp api properties
+ params = {
+ "engine": "google_scholar_author",
+ "api_key": api_key,
+ "num": 100, # max allowed
+ }
+
+ # get id from entry
+ _id = get_safe(entry, "gsid", "")
+ if not _id:
+ raise Exception('No "gsid" key')
+
+ # query api
+ @log_cache
+ @cache.memoize(name=__file__, expire=1 * (60 * 60 * 24))
+ def query(_id):
+ params["author_id"] = _id
+ return get_safe(GoogleSearch(params).get_dict(), "articles", [])
+
+ response = query(_id)
+
+ # list of sources to return
+ sources = []
+
+ # go through response and format sources
+ for work in response:
+ # create source
+ year = get_safe(work, "year", "")
+ source = {
+ "id": get_safe(work, "citation_id", ""),
+ # api does not provide Manubot-citeable id, so keep citation details
+ "title": get_safe(work, "title", ""),
+ "authors": list(map(str.strip, get_safe(work, "authors", "").split(","))),
+ "publisher": get_safe(work, "publication", ""),
+ "date": (year + "-01-01") if year else "",
+ "link": get_safe(work, "link", ""),
+ }
+
+ # copy fields from entry to source
+ source.update(entry)
+
+ # add source to list
+ sources.append(source)
+
+ return sources
diff --git a/_cite/plugins/orcid.py b/_cite/plugins/orcid.py
new file mode 100644
index 0000000..2f7d04d
--- /dev/null
+++ b/_cite/plugins/orcid.py
@@ -0,0 +1,109 @@
+import json
+from urllib.request import Request, urlopen
+from util import *
+from manubot.cite.handlers import prefix_to_handler as manubot_prefixes
+
+
+def main(entry):
+ """
+ receives single list entry from orcid data file
+ returns list of sources to cite
+ """
+
+ # orcid api
+ endpoint = "https://pub.orcid.org/v3.0/$ORCID/works"
+ headers = {"Accept": "application/json"}
+
+ # get id from entry
+ _id = get_safe(entry, "orcid", "")
+ if not _id:
+ raise Exception('No "orcid" key')
+
+ # query api
+ @log_cache
+ @cache.memoize(name=__file__, expire=1 * (60 * 60 * 24))
+ def query(_id):
+ url = endpoint.replace("$ORCID", _id)
+ request = Request(url=url, headers=headers)
+ response = json.loads(urlopen(request).read())
+ return get_safe(response, "group", [])
+
+ response = query(_id)
+
+ # list of sources to return
+ sources = []
+
+ # go through response structure and pull out ids e.g. doi:1234/56789
+ for work in response:
+ # get list of ids
+ ids = []
+ for summary in get_safe(work, "work-summary", []):
+ ids = ids + get_safe(summary, "external-ids.external-id", [])
+
+ # find first id of particular "relationship" type
+ _id = next(
+ (
+ id
+ for id in ids
+ if get_safe(id, "external-id-relationship", "")
+ in ["self", "version-of", "part-of"]
+ ),
+ ids[0] if len(ids) > 0 else None,
+ )
+
+ if _id == None:
+ continue
+
+ # get id and id-type from response
+ id_type = get_safe(_id, "external-id-type", "")
+ id_value = get_safe(_id, "external-id-value", "")
+
+ # create source
+ source = {"id": f"{id_type}:{id_value}"}
+
+ # if not an id type that Manubot can cite, keep citation details
+ if id_type not in manubot_prefixes:
+ # get summaries
+ summaries = get_safe(work, "work-summary", [])
+
+ # get first summary with defined sub-value
+ def first(get_func):
+ return next(
+ (value for value in map(get_func, summaries) if value), None
+ )
+
+ # get title
+ title = first(lambda s: get_safe(s, "title.title.value", ""))
+
+ # get publisher
+ publisher = first(lambda s: get_safe(s, "journal-title.value", ""))
+
+ # get date
+ date = (
+ get_safe(work, "last-modified-date.value")
+ or first(lambda s: get_safe(s, "last-modified-date.value"))
+ or get_safe(work, "created-date.value")
+ or first(lambda s: get_safe(s, "created-date.value"))
+ or 0
+ )
+
+ # get link
+ link = first(lambda s: get_safe(s, "url.value", ""))
+
+ # keep available details
+ if title:
+ source["title"] = title
+ if publisher:
+ source["publisher"] = publisher
+ if date:
+ source["date"] = format_date(date)
+ if link:
+ source["link"] = link
+
+ # copy fields from entry to source
+ source.update(entry)
+
+ # add source to list
+ sources.append(source)
+
+ return sources
diff --git a/_cite/plugins/pubmed.py b/_cite/plugins/pubmed.py
new file mode 100644
index 0000000..0f2ee60
--- /dev/null
+++ b/_cite/plugins/pubmed.py
@@ -0,0 +1,46 @@
+import json
+from urllib.request import Request, urlopen
+from urllib.parse import quote
+from util import *
+
+
+def main(entry):
+ """
+ receives single list entry from pubmed data file
+ returns list of sources to cite
+ """
+
+ # ncbi api
+ endpoint = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=$TERM&retmode=json&retmax=1000&usehistory=y"
+
+ # get id from entry
+ _id = get_safe(entry, "term", "")
+ if not _id:
+ raise Exception('No "term" key')
+
+ # query api
+ @log_cache
+ @cache.memoize(name=__file__, expire=1 * (60 * 60 * 24))
+ def query(_id):
+ url = endpoint.replace("$TERM", quote(_id))
+ request = Request(url=url)
+ response = json.loads(urlopen(request).read())
+ return get_safe(response, "esearchresult.idlist", [])
+
+ response = query(_id)
+
+ # list of sources to return
+ sources = []
+
+ # go through response and format sources
+ for _id in response:
+ # create source
+ source = {"id": f"pubmed:{_id}"}
+
+ # copy fields from entry to source
+ source.update(entry)
+
+ # add source to list
+ sources.append(source)
+
+ return sources
diff --git a/_cite/plugins/sources.py b/_cite/plugins/sources.py
new file mode 100644
index 0000000..9dde4fa
--- /dev/null
+++ b/_cite/plugins/sources.py
@@ -0,0 +1,6 @@
+def main(entry):
+ """
+ receives single list entry from sources data file
+ returns list of sources to cite
+ """
+ return [entry]
diff --git a/_cite/requirements.txt b/_cite/requirements.txt
new file mode 100644
index 0000000..9808e76
--- /dev/null
+++ b/_cite/requirements.txt
@@ -0,0 +1,7 @@
+manubot~=0.6
+PyYAML~=6.0
+diskcache~=5.6
+rich~=13.6
+python-dotenv~=0.21
+google-search-results~=2.4
+
diff --git a/_cite/util.py b/_cite/util.py
new file mode 100644
index 0000000..904ff33
--- /dev/null
+++ b/_cite/util.py
@@ -0,0 +1,236 @@
+"""
+utility functions for cite process and plugins
+"""
+
+import subprocess
+import json
+import yaml
+from yaml.loader import SafeLoader
+from pathlib import Path
+from datetime import date, datetime
+from rich import print
+from diskcache import Cache
+
+
+# cache for time-consuming network requests
+cache = Cache("./_cite/.cache")
+
+
+# clear expired items from cache
+cache.expire()
+
+
+def log_cache(func):
+ """
+ decorator to use around memoized function to log if cached or or not
+ """
+
+ def wrap(*args):
+ key = func.__cache_key__(*args)
+ if key in cache:
+ log(" (from cache)", level="INFO", newline=False)
+ return func(*args)
+
+ return wrap
+
+
+def log(message="\n--------------------\n", indent=0, level="", newline=True):
+ """
+ log to terminal, color determined by indent and level
+ """
+
+ palette = {
+ 0: "[orange1]",
+ 1: "[salmon1]",
+ 2: "[violet]",
+ 3: "[sky_blue1]",
+ "ERROR": "[white on #F43F5E]",
+ "WARNING": "[black on #EAB308]",
+ "SUCCESS": "[black on #10B981]",
+ "INFO": "[grey70]",
+ }
+ color = get_safe(palette, level, "") or get_safe(palette, indent, "") or "[white]"
+ if newline:
+ print()
+ print(indent * " " + color + str(message) + "[/]", end="", flush=True)
+
+
+def label(entry):
+ """
+ get "label" of dict entry (for logging purposes)
+ """
+
+ return str(list(entry.keys())[0]) + ": " + str(list(entry.values())[0])
+
+
+def get_safe(item, path, default=None):
+ """
+ safely access value in nested lists/dicts
+ """
+
+ for part in str(path).split("."):
+ try:
+ part = int(part)
+ except ValueError:
+ part = part
+ try:
+ item = item[part]
+ except (KeyError, IndexError, AttributeError, TypeError):
+ return default
+ return item
+
+
+def list_of_dicts(data):
+ """
+ check if data is list of dicts
+ """
+
+ return isinstance(data, list) and all(isinstance(entry, dict) for entry in data)
+
+
+def format_date(_date):
+ """
+ format date as YYYY-MM-DD, or no date if malformed
+ """
+
+ if isinstance(_date, int):
+ return datetime.fromtimestamp(_date // 1000.0).strftime("%Y-%m-%d")
+ if isinstance(_date, (date, datetime)):
+ return _date.strftime("%Y-%m-%d")
+ try:
+ return datetime.strptime(_date, "%Y-%m-%d").strftime("%Y-%m-%d")
+ except Exception:
+ return ""
+
+
+def load_data(path):
+ """
+ read data from yaml or json file
+ """
+
+ # convert to path object
+ path = Path(path)
+
+ # check if file exists
+ if not path.is_file():
+ raise Exception("Can't find file")
+
+ # try to open file
+ try:
+ file = open(path, encoding="utf8")
+ except Exception as e:
+ raise Exception(e or "Can't open file")
+
+ # try to parse as yaml
+ try:
+ with file:
+ data = yaml.load(file, Loader=SafeLoader)
+ except Exception:
+ raise Exception("Can't parse file. Make sure it's valid YAML.")
+
+ # if no errors, return data
+ return data
+
+
+def save_data(path, data):
+ """
+ write data to yaml file
+ """
+
+ # convert to path object
+ path = Path(path)
+
+ # try to open file
+ try:
+ file = open(path, mode="w")
+ except Exception:
+ raise Exception("Can't open file for writing")
+
+ # prevent yaml anchors/aliases (pointers)
+ yaml.Dumper.ignore_aliases = lambda *args: True
+
+ # try to save data as yaml
+ try:
+ with file:
+ yaml.dump(data, file, default_flow_style=False, sort_keys=False)
+ except Exception:
+ raise Exception("Can't save YAML to file")
+
+ # write warning note to top of file
+ note = "# DO NOT EDIT, GENERATED AUTOMATICALLY"
+ try:
+ with open(path, "r") as file:
+ data = file.read()
+ with open(path, "w") as file:
+ file.write(f"{note}\n\n{data}")
+ except Exception:
+ raise Exception("Can't write to file")
+
+
+@log_cache
+@cache.memoize(name="manubot", expire=90 * (60 * 60 * 24))
+def cite_with_manubot(_id):
+ """
+ generate citation data for source id with Manubot
+ """
+
+ # run Manubot
+ try:
+ commands = ["manubot", "cite", _id, "--log-level=WARNING"]
+ output = subprocess.Popen(commands, stdout=subprocess.PIPE).communicate()
+ except Exception as e:
+ log(e, 3)
+ raise Exception("Manubot could not generate citation")
+
+ # parse results as json
+ try:
+ manubot = json.loads(output[0])[0]
+ except Exception:
+ raise Exception("Couldn't parse Manubot response")
+
+ # new citation with only needed info
+ citation = {}
+
+ # original id
+ citation["id"] = _id
+
+ # title
+ citation["title"] = get_safe(manubot, "title", "").strip()
+
+ # authors
+ citation["authors"] = []
+ for author in get_safe(manubot, "author", {}):
+ given = get_safe(author, "given", "").strip()
+ family = get_safe(author, "family", "").strip()
+ if given or family:
+ citation["authors"].append(" ".join([given, family]))
+
+ # publisher
+ container = get_safe(manubot, "container-title", "").strip()
+ collection = get_safe(manubot, "collection-title", "").strip()
+ publisher = get_safe(manubot, "publisher", "").strip()
+ citation["publisher"] = container or publisher or collection or ""
+
+ # extract date part
+ def date_part(citation, index):
+ try:
+ return citation["issued"]["date-parts"][0][index]
+ except (KeyError, IndexError, TypeError):
+ return ""
+
+ # date
+ year = date_part(manubot, 0)
+ if year:
+ # fallbacks for month and day
+ month = date_part(manubot, 1) or "1"
+ day = date_part(manubot, 2) or "1"
+ citation["date"] = format_date(f"{year}-{month}-{day}")
+ else:
+ # if no year, consider date missing data
+ citation["date"] = ""
+
+ # link
+ citation["link"] = get_safe(manubot, "URL", "").strip()
+
+ # return citation data
+ return citation
diff --git a/_config.yaml b/_config.yaml
new file mode 100644
index 0000000..1c6b790
--- /dev/null
+++ b/_config.yaml
@@ -0,0 +1,73 @@
+# site properties and page defaults
+title: Lab Website Template
+subtitle: by the Greene Lab
+description: An easy-to-use, flexible website template for labs, with automatic citations, GitHub tag imports, pre-built components, and more.
+header: images/background.jpg
+footer: images/background.jpg
+proofer: false
+
+# site social media and other links
+links:
+ email: contact@your-lab.com
+ orcid: 0000-0001-8713-9213
+ google-scholar: ETJoidYAAAAJ
+ github: your-lab
+ twitter: YourLabHandle
+ youtube: YourLabChannel
+
+### jekyll settings
+
+# front matter defaults
+defaults:
+ # all markdown files
+ - scope:
+ path: ""
+ values:
+ layout: default
+ # markdown files in /_members
+ - scope:
+ type: "members"
+ values:
+ layout: member
+ # markdown files in /_posts
+ - scope:
+ type: "posts"
+ values:
+ layout: post
+
+collections:
+ # generate page for each member
+ members:
+ output: true
+ # generate page for each post
+ posts:
+ output: true
+
+# jekyll plugins
+plugins:
+ - jekyll-spaceship
+ - jekyll-sitemap
+ - jekyll-redirect-from
+ - jekyll-feed
+ - jekyll-last-modified-at
+
+# code block syntax highlighting
+highlighter: rouge
+
+# jekyll theme
+theme: null
+
+# sass settings
+sass:
+ sass_dir: _styles
+
+# force jekyll to include certain files/folders
+include:
+ - _styles
+ - _scripts
+
+# force jekyll to exclude certain files/folders
+exclude:
+ - README.md
+ - LICENSE.md
+ - CITATION.cff
diff --git a/_data/citations.yaml b/_data/citations.yaml
new file mode 100644
index 0000000..0e483e3
--- /dev/null
+++ b/_data/citations.yaml
@@ -0,0 +1,247 @@
+# DO NOT EDIT, GENERATED AUTOMATICALLY
+
+- id: doi:10.1093/nar/gkad1082
+ title: "The Monarch Initiative in 2024: an analytic platform integrating phenotypes,\
+ \ genes\_and diseases across species"
+ authors:
+ - Tim E Putman
+ - Kevin Schaper
+ - Nicolas Matentzoglu
+ - "Vincent\_P Rubinetti"
+ - "Faisal\_S Alquaddoomi"
+ - Corey Cox
+ - J Harry Caufield
+ - Glass Elsarboukh
+ - Sarah Gehrke
+ - Harshad Hegde
+ - "Justin\_T Reese"
+ - Ian Braun
+ - "Richard\_M Bruskiewich"
+ - Luca Cappelletti
+ - Seth Carbon
+ - "Anita\_R Caron"
+ - "Lauren\_E Chan"
+ - "Christopher\_G Chute"
+ - "Katherina\_G Cortes"
+ - "Vin\xEDcius De\_Souza"
+ - Tommaso Fontana
+ - "Nomi\_L Harris"
+ - "Emily\_L Hartley"
+ - Eric Hurwitz
+ - "Julius\_O B Jacobsen"
+ - Madan Krishnamurthy
+ - "Bryan\_J Laraway"
+ - "James\_A McLaughlin"
+ - "Julie\_A McMurry"
+ - "Sierra\_A T Moxon"
+ - "Kathleen\_R Mullen"
+ - "Shawn\_T O\u2019Neil"
+ - "Kent\_A Shefchek"
+ - Ray Stefancsik
+ - Sabrina Toro
+ - "Nicole\_A Vasilevsky"
+ - "Ramona\_L Walls"
+ - "Patricia\_L Whetzel"
+ - David Osumi-Sutherland
+ - Damian Smedley
+ - "Peter\_N Robinson"
+ - "Christopher\_J Mungall"
+ - "Melissa\_A Haendel"
+ - "Monica\_C Munoz-Torres"
+ publisher: Nucleic Acids Research
+ date: '2023-11-24'
+ link: https://doi.org/gs6kmr
+ orcid: 0000-0002-4655-3773
+ plugin: orcid.py
+ file: orcid.yaml
+- id: doi:10.1101/2023.10.11.560955
+ title: Integration of 168,000 samples reveals global patterns of the human gut microbiome
+ authors:
+ - Richard J. Abdill
+ - Samantha P. Graham
+ - Vincent Rubinetti
+ - Frank W. Albert
+ - Casey S. Greene
+ - Sean Davis
+ - Ran Blekhman
+ publisher: Cold Spring Harbor Laboratory
+ date: '2023-10-11'
+ link: https://doi.org/gsvf5z
+ orcid: 0000-0002-4655-3773
+ plugin: orcid.py
+ file: orcid.yaml
+- id: doi:10.1093/nar/gkad289
+ title: 'MyGeneset.info: an interactive and programmatic platform for community-curated
+ and user-created collections of genes'
+ authors:
+ - Ricardo Avila
+ - Vincent Rubinetti
+ - Xinghua Zhou
+ - Dongbo Hu
+ - Zhongchao Qian
+ - Marco Alvarado Cano
+ - Everaldo Rodolpho
+ - Ginger Tsueng
+ - Casey Greene
+ - Chunlei Wu
+ publisher: Nucleic Acids Research
+ date: '2023-04-18'
+ link: https://doi.org/gr5hb5
+ orcid: 0000-0002-4655-3773
+ plugin: orcid.py
+ file: orcid.yaml
+- id: doi:10.1101/2023.01.05.522941
+ title: Hetnet connectivity search provides rapid insights into how two biomedical
+ entities are related
+ authors:
+ - Daniel S. Himmelstein
+ - Michael Zietz
+ - Vincent Rubinetti
+ - Kyle Kloster
+ - Benjamin J. Heil
+ - Faisal Alquaddoomi
+ - Dongbo Hu
+ - David N. Nicholson
+ - Yun Hao
+ - Blair D. Sullivan
+ - Michael W. Nagle
+ - Casey S. Greene
+ publisher: Cold Spring Harbor Laboratory
+ date: '2023-01-07'
+ link: https://doi.org/grmcb9
+ orcid: 0000-0002-4655-3773
+ plugin: orcid.py
+ file: orcid.yaml
+- id: doi:10.1093/gigascience/giad047
+ title: Hetnet connectivity search provides rapid insights into how biomedical entities
+ are related
+ authors:
+ - Daniel S Himmelstein
+ - Michael Zietz
+ - Vincent Rubinetti
+ - Kyle Kloster
+ - Benjamin J Heil
+ - Faisal Alquaddoomi
+ - Dongbo Hu
+ - David N Nicholson
+ - Yun Hao
+ - Blair D Sullivan
+ - Michael W Nagle
+ - Casey S Greene
+ publisher: GigaScience
+ date: '2022-12-28'
+ link: https://doi.org/gsd85n
+ orcid: 0000-0002-4655-3773
+ plugin: orcid.py
+ file: orcid.yaml
+- id: doi:10.1101/2022.02.18.461833
+ title: 'MolEvolvR: A web-app for characterizing proteins using molecular evolution
+ and phylogeny'
+ authors:
+ - Jacob D Krol
+ - Joseph T Burke
+ - Samuel Z Chen
+ - Lo M Sosinski
+ - Faisal S Alquaddoomi
+ - Evan P Brenner
+ - Ethan P Wolfe
+ - Vincent P Rubinetti
+ - Shaddai Amolitos
+ - Kellen M Reason
+ - John B Johnston
+ - Janani Ravi
+ publisher: Cold Spring Harbor Laboratory
+ date: '2022-02-22'
+ link: https://doi.org/gstx7j
+ orcid: 0000-0002-4655-3773
+ plugin: orcid.py
+ file: orcid.yaml
+- id: doi:10.1186/s13059-020-02021-3
+ title: Compressing gene expression data using multiple latent space dimensionalities
+ learns complementary biological representations
+ authors:
+ - Gregory P. Way
+ - Michael Zietz
+ - Vincent Rubinetti
+ - Daniel S. Himmelstein
+ - Casey S. Greene
+ publisher: Genome Biology
+ date: '2020-05-11'
+ link: https://doi.org/gg2mjh
+ orcid: 0000-0002-4655-3773
+ plugin: orcid.py
+ file: orcid.yaml
+- id: doi:10.1371/journal.pcbi.1007128
+ title: Open collaborative writing with Manubot
+ authors:
+ - Daniel S. Himmelstein
+ - Vincent Rubinetti
+ - David R. Slochower
+ - Dongbo Hu
+ - Venkat S. Malladi
+ - Casey S. Greene
+ - Anthony Gitter
+ publisher: PLOS Computational Biology
+ date: '2020-12-04'
+ link: https://doi.org/c7np
+ orcid: 0000-0002-4655-3773
+ plugin: sources.py
+ file: sources.yaml
+ type: paper
+ description: Lorem ipsum _dolor_ **sit amet**, consectetur adipiscing elit, sed
+ do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ image: https://journals.plos.org/ploscompbiol/article/figure/image?size=inline&id=info:doi/10.1371/journal.pcbi.1007128.g001&rev=2
+ buttons:
+ - type: manubot
+ link: https://greenelab.github.io/meta-review/
+ - type: source
+ text: Manuscript Source
+ link: https://github.com/greenelab/meta-review
+ - type: website
+ link: http://manubot.org/
+ tags:
+ - open science
+ - collaboration
+ repo: greenelab/meta-review
+- id: doi:10.1101/573782
+ title: Sequential compression of gene expression across dimensionalities and methods
+ reveals no single best method or dimensionality
+ authors:
+ - Gregory P. Way
+ - Michael Zietz
+ - Vincent Rubinetti
+ - Daniel S. Himmelstein
+ - Casey S. Greene
+ publisher: Cold Spring Harbor Laboratory
+ date: '2019-03-11'
+ link: https://doi.org/gfxjxf
+ orcid: 0000-0002-4655-3773
+ plugin: orcid.py
+ file: orcid.yaml
+- id: doi:10.1016/j.csbj.2020.05.017
+ title: Constructing knowledge graphs and their biomedical applications
+ authors:
+ - David N. Nicholson
+ - Casey S. Greene
+ publisher: Computational and Structural Biotechnology Journal
+ date: '2020-01-01'
+ link: https://doi.org/gg7m48
+ image: https://ars.els-cdn.com/content/image/1-s2.0-S2001037020302804-gr1.jpg
+ plugin: sources.py
+ file: sources.yaml
+- id: doi:10.7554/eLife.32822
+ title: Sci-Hub provides access to nearly all scholarly literature
+ authors:
+ - Daniel S Himmelstein
+ - Ariel Rodriguez Romero
+ - Jacob G Levernier
+ - Thomas Anthony Munro
+ - Stephen Reid McLaughlin
+ - Bastian Greshake Tzovaras
+ - Casey S Greene
+ publisher: eLife
+ date: '2018-03-01'
+ link: https://doi.org/ckcj
+ image: https://iiif.elifesciences.org/lax:32822%2Felife-32822-fig8-v3.tif/full/863,/0/default.webp
+ plugin: sources.py
+ file: sources.yaml
diff --git a/_data/orcid.yaml b/_data/orcid.yaml
new file mode 100644
index 0000000..005d380
--- /dev/null
+++ b/_data/orcid.yaml
@@ -0,0 +1 @@
+- orcid: 0000-0002-4655-3773
diff --git a/_data/projects.yaml b/_data/projects.yaml
new file mode 100644
index 0000000..b3eb2a7
--- /dev/null
+++ b/_data/projects.yaml
@@ -0,0 +1,47 @@
+- title: Cool Dataset
+ subtitle: a subtitle
+ group: featured
+ image: images/photo.jpg
+ link: https://github.com/
+ description: Lorem ipsum _dolor sit amet_, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ repo: greenelab/lab-website-template
+ tags:
+ - resource
+
+- title: Cool Package
+ subtitle: a subtitle
+ group: featured
+ image: images/photo.jpg
+ link: https://github.com/
+ description: Lorem ipsum _dolor sit amet_, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ repo: greenelab/lab-website-template
+ tags:
+ - resource
+
+- title: Cool Tutorial
+ subtitle: a subtitle
+ image: images/photo.jpg
+ link: https://github.com/
+ description: Lorem ipsum _dolor sit amet_, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ repo: greenelab/lab-website-template
+ tags:
+ - resource
+ - publication
+
+- title: Cool Web App
+ subtitle: a subtitle
+ image: images/photo.jpg
+ link: https://github.com/
+ description: Lorem ipsum _dolor sit amet_, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ repo: greenelab/lab-website-template
+ tags:
+ - software
+
+- title: Cool Web Server
+ subtitle: a subtitle
+ image: images/photo.jpg
+ link: https://github.com/
+ description: Lorem ipsum _dolor sit amet_, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ repo: greenelab/lab-website-template
+ tags:
+ - software
diff --git a/_data/sources.yaml b/_data/sources.yaml
new file mode 100644
index 0000000..62cd922
--- /dev/null
+++ b/_data/sources.yaml
@@ -0,0 +1,23 @@
+- id: doi:10.1371/journal.pcbi.1007128
+ type: paper
+ description: Lorem ipsum _dolor_ **sit amet**, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ date: 2020-12-4
+ image: https://journals.plos.org/ploscompbiol/article/figure/image?size=inline&id=info:doi/10.1371/journal.pcbi.1007128.g001&rev=2
+ buttons:
+ - type: manubot
+ link: https://greenelab.github.io/meta-review/
+ - type: source
+ text: Manuscript Source
+ link: https://github.com/greenelab/meta-review
+ - type: website
+ link: http://manubot.org/
+ tags:
+ - open science
+ - collaboration
+ repo: greenelab/meta-review
+
+- id: doi:10.1016/j.csbj.2020.05.017
+ image: https://ars.els-cdn.com/content/image/1-s2.0-S2001037020302804-gr1.jpg
+
+- id: doi:10.7554/eLife.32822
+ image: https://iiif.elifesciences.org/lax:32822%2Felife-32822-fig8-v3.tif/full/863,/0/default.webp
diff --git a/_data/types.yaml b/_data/types.yaml
new file mode 100644
index 0000000..2909b0a
--- /dev/null
+++ b/_data/types.yaml
@@ -0,0 +1,216 @@
+# map general type to default/fallback icon, text, etc.
+
+# team member roles
+
+pi:
+ icon: fa-solid fa-microscope
+ description: Principal Investigator
+
+postdoc:
+ icon: fa-solid fa-glasses
+ description: Postdoctoral Researcher
+
+phd:
+ icon: fa-solid fa-graduation-cap
+ description: PhD Student
+
+undergrad:
+ icon: fa-solid fa-user-graduate
+ description: Undergraduate Student
+
+programmer:
+ icon: fa-solid fa-code
+ description: Programmer
+
+mascot:
+ icon: fa-solid fa-dog
+ description: Mascot
+
+# general
+
+link:
+ icon: fa-solid fa-globe
+ text: Link
+ tooltip: Link
+
+website:
+ icon: fa-solid fa-globe
+ text: Website
+ tooltip: Website
+
+external:
+ icon: fa-solid fa-up-right-from-square
+ text: External Link
+ tooltip: External link
+
+home-page:
+ icon: fa-solid fa-house-user
+ text: Home Page
+ tooltip: Home page
+
+email:
+ icon: fa-solid fa-envelope
+ text: Email
+ tooltip: Email
+ link: mailto:$VALUE
+
+phone:
+ icon: fa-solid fa-phone
+ text: Phone Number
+ tooltip: Phone number
+ link: tel:$VALUE
+
+address:
+ icon: fa-solid fa-map-location-dot
+ text: Address
+ tooltip: Address
+
+search:
+ icon: fa-solid fa-magnifying-glass
+ text: Search
+ tooltip: Search
+
+# social media
+
+orcid:
+ icon: fa-brands fa-orcid
+ text: ORCID
+ tooltip: ORCID
+ link: https://orcid.org/$VALUE
+
+google-scholar:
+ icon: fa-brands fa-google
+ text: Google Scholar
+ tooltip: Google Scholar
+ link: https://scholar.google.com/citations?user=$VALUE
+
+github:
+ icon: fa-brands fa-github
+ text: GitHub
+ tooltip: GitHub
+ link: https://github.com/$VALUE
+
+twitter:
+ icon: fa-brands fa-twitter
+ text: Twitter
+ tooltip: Twitter
+ link: https://twitter.com/$VALUE
+
+facebook:
+ icon: fa-brands fa-facebook
+ text: Facebook
+ tooltip: Facebook
+ link: https://facebook.com/$VALUE
+
+instagram:
+ icon: fa-brands fa-instagram
+ text: Instagram
+ tooltip: Instagram
+ link: https://instagram.com/$VALUE
+
+youtube:
+ icon: fa-brands fa-youtube
+ text: YouTube
+ tooltip: YouTube
+ link: https://youtube.com/$VALUE
+
+linkedin:
+ icon: fa-brands fa-linkedin
+ text: LinkedIn
+ tooltip: LinkedIn
+ link: https://www.linkedin.com/in/$VALUE
+
+# alerts
+
+tip:
+ icon: fa-solid fa-lightbulb
+ color: "#d946ef"
+
+help:
+ icon: fa-solid fa-circle-question
+ color: "#6366f1"
+
+info:
+ icon: fa-solid fa-circle-info
+ color: "#0ea5e9"
+
+success:
+ icon: fa-solid fa-circle-check
+ color: "#22c55e"
+
+warning:
+ icon: fa-solid fa-circle-exclamation
+ color: "#F59E0B"
+
+error:
+ icon: fa-solid fa-ban
+ color: "#ef4444"
+
+# publications
+
+paper:
+ icon: fa-solid fa-scroll
+ text: Paper
+ tooltip: Paper
+
+book:
+ icon: fa-solid fa-book
+ text: Book
+ tooltip: Book
+
+article:
+ icon: fa-solid fa-newspaper
+ text: Article
+ tooltip: Article
+
+journal:
+ icon: fa-regular fa-newspaper
+ text: Journal
+ tooltip: Journal
+
+preprint:
+ icon: fa-regular fa-eye
+ text: Preprint
+ tooltip: Preprint
+
+unpublished:
+ icon: fa-regular fa-eye
+ text: Unpublished
+ tooltip: Unpublished
+
+manubot:
+ icon: manubot.svg
+ text: Manubot
+ tooltip: Manubot
+
+# software
+
+docs:
+ icon: fa-solid fa-book
+ text: Documentation
+ tooltip: Documentation
+
+source:
+ icon: fa-solid fa-code
+ text: Source
+ tooltip: Source code
+
+server:
+ icon: fa-solid fa-server
+ text: Server
+ tooltip: Server
+
+app:
+ icon: fa-solid fa-hand-pointer
+ text: App
+ tooltip: App
+
+data:
+ icon: fa-solid fa-database
+ text: Data
+ tooltip: Data
+
+package:
+ icon: fa-solid fa-box
+ text: Package
+ tooltip: Package
diff --git a/_includes/alert.html b/_includes/alert.html
new file mode 100644
index 0000000..6fc9878
--- /dev/null
+++ b/_includes/alert.html
@@ -0,0 +1,10 @@
+{% assign color = site.data.types[include.type].color | default: "#808080" %}
+
+ {% assign icon = site.data.types[include.type].icon
+ | default: "fa-solid fa-circle-info"
+ %}
+ {% include icon.html icon=icon %}
+
+ {{ include.content | markdownify }}
+
+
diff --git a/_includes/analytics.html b/_includes/analytics.html
new file mode 100644
index 0000000..6af6788
--- /dev/null
+++ b/_includes/analytics.html
@@ -0,0 +1,3 @@
+
diff --git a/_includes/button.html b/_includes/button.html
new file mode 100644
index 0000000..8d1730d
--- /dev/null
+++ b/_includes/button.html
@@ -0,0 +1,24 @@
+{% assign button = include %}
+{% assign button = button | hash_default: site.data.types[include.type] %}
+
+{% if button.link or button.icon or button.text %}
+
+{% endif %}
diff --git a/_includes/card.html b/_includes/card.html
new file mode 100644
index 0000000..600cc97
--- /dev/null
+++ b/_includes/card.html
@@ -0,0 +1,47 @@
+{{ " " }}
+
+
+
+
+
+
+ {% if include.title %}
+
+ {{ include.title }}
+
+ {% endif %}
+
+ {% if include.subtitle %}
+
{{ include.subtitle }}
+ {% endif %}
+
+ {% if include.description %}
+
+ {{ include.description | markdownify | remove: "
" | remove: "
" }}
+
+ {% endif %}
+
+ {% if include.tags or include.repo %}
+ {% include tags.html tags=include.tags repo=include.repo %}
+ {% endif %}
+
+
diff --git a/_includes/citation.html b/_includes/citation.html
new file mode 100644
index 0000000..3eb8b2b
--- /dev/null
+++ b/_includes/citation.html
@@ -0,0 +1,109 @@
+{% if include.lookup %}
+ {% assign citation = site.data.citations
+ | where_exp: "citation",
+ "citation.id == include.lookup or citation.title contains include.lookup"
+ | first
+ %}
+{% else %}
+ {% assign citation = include %}
+{% endif %}
+
+
+
+ {% if include.style == "rich" %}
+
+
+
+ {% endif %}
+
+
+ {% assign type = site.data.types[citation.type] %}
+ {% include icon.html icon=type.icon %}
+
+
+ {{ citation.title | default: "[no title info]" }}
+
+
+
10 %}
+ data-tooltip="{{ citation.authors | join: ", " }} "
+ {% endif %}
+ tabindex="0"
+ >
+ {{
+ citation.authors
+ | join: ","
+ | split: ","
+ | array_carve: 5
+ | join: ", "
+ | markdownify
+ | remove: "
" | remove: "
"
+ | default: "[no author info]"
+ }}
+
+
+
+
+ {{- citation.publisher | default: "[no publisher info]" -}}
+
+ ·
+
+ {{- citation.date | default: "[no date info]" | date: "%d %b %Y" -}}
+
+ ·
+
+ {{- citation.id | default: "[no id info]" -}}
+
+
+
+ {% if include.style == "rich" %}
+ {% if citation.description %}
+
+ {{
+ citation.description
+ | markdownify
+ | remove: "
"
+ | remove: "
"
+ }}
+
+ {% endif %}
+
+ {% if citation.buttons.size > 0 %}
+
+ {% for button in citation.buttons %}
+ {%
+ include button.html
+ type=button.type
+ icon=button.icon
+ text=button.text
+ link=button.link
+ style="bare"
+ %}
+ {% endfor %}
+
+ {% endif %}
+
+ {% if citation.tags.size > 0 or citation.repo %}
+ {% include tags.html tags=citation.tags repo=citation.repo %}
+ {% endif %}
+ {% endif %}
+
+
+
diff --git a/_includes/cols.html b/_includes/cols.html
new file mode 100644
index 0000000..20dd865
--- /dev/null
+++ b/_includes/cols.html
@@ -0,0 +1,6 @@
+
+ {% for param in include %}
+ {% assign key = param[0] %}
+
{{ include[key] | markdownify }}
+ {% endfor %}
+
diff --git a/_includes/content.html b/_includes/content.html
new file mode 100644
index 0000000..f9d01c8
--- /dev/null
+++ b/_includes/content.html
@@ -0,0 +1,32 @@
+
+
+{% assign content = include.content %}
+
+{% assign sections = content | split: "" | array_filter %}
+
+{% for section in sections %}
+ {% assign dark = section | regex_scan: "dark: (.*);" | default: "" %}
+ {% assign background = section
+ | regex_scan: "background: (.*);"
+ | default: nil
+ %}
+ {% assign size = section | regex_scan: "size: (.*);" | default: "page" %}
+
+
+{% endfor %}
diff --git a/_includes/fallback.html b/_includes/fallback.html
new file mode 100644
index 0000000..5d60a80
--- /dev/null
+++ b/_includes/fallback.html
@@ -0,0 +1 @@
+onerror="this.src = '{{ "images/fallback.svg" | relative_url }}'; this.onerror = null;"
diff --git a/_includes/feature.html b/_includes/feature.html
new file mode 100644
index 0000000..27e33fa
--- /dev/null
+++ b/_includes/feature.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+ {% if include.title %}
+
{{ include.title }}
+ {% endif %}
+ {{ include.text | markdownify }}
+
+
diff --git a/_includes/figure.html b/_includes/figure.html
new file mode 100644
index 0000000..a426ff4
--- /dev/null
+++ b/_includes/figure.html
@@ -0,0 +1,25 @@
+
diff --git a/_includes/float.html b/_includes/float.html
new file mode 100644
index 0000000..a5d2955
--- /dev/null
+++ b/_includes/float.html
@@ -0,0 +1,11 @@
+
+ {{ include.content | markdownify }}
+
diff --git a/_includes/fonts.html b/_includes/fonts.html
new file mode 100644
index 0000000..153709b
--- /dev/null
+++ b/_includes/fonts.html
@@ -0,0 +1,17 @@
+
+
+{% assign googlefonts = "_styles/-theme.scss" | file_read | google_fonts %}
+
+
+
+
+{% assign fontawesome = "https://use.fontawesome.com/releases/v6.5.0/css/all.css" %}
+
+
diff --git a/_includes/footer.html b/_includes/footer.html
new file mode 100644
index 0000000..8230d53
--- /dev/null
+++ b/_includes/footer.html
@@ -0,0 +1,42 @@
+{% assign image = page.footer | default: site.footer | relative_url %}
+{% assign dark = page.footer-dark | is_nil: site.footer-dark | is_nil: true %}
+
+
diff --git a/_includes/grid.html b/_includes/grid.html
new file mode 100644
index 0000000..40ea2cf
--- /dev/null
+++ b/_includes/grid.html
@@ -0,0 +1,3 @@
+
+ {{ include.content | markdownify }}
+
diff --git a/_includes/head.html b/_includes/head.html
new file mode 100644
index 0000000..c21a44a
--- /dev/null
+++ b/_includes/head.html
@@ -0,0 +1,8 @@
+
+ {% include analytics.html %}
+ {% include verification.html %}
+ {% include meta.html %}
+ {% include fonts.html %}
+ {% include styles.html %}
+ {% include scripts.html %}
+
diff --git a/_includes/header.html b/_includes/header.html
new file mode 100644
index 0000000..4dbdac0
--- /dev/null
+++ b/_includes/header.html
@@ -0,0 +1,59 @@
+{% assign image = page.header | default: site.header | relative_url %}
+{% assign dark = page.header-dark | is_nil: site.header-dark | is_nil: true %}
+
+{% assign svg = "images/logo.svg" | file_exists %}
+{% assign png = "images/logo.png" | file_exists %}
+{% assign jpg = "images/logo.jpg" | file_exists %}
+{% assign logo = svg | default: png | default: jpg | default: nil %}
+
+
diff --git a/_includes/icon.html b/_includes/icon.html
new file mode 100644
index 0000000..0448e93
--- /dev/null
+++ b/_includes/icon.html
@@ -0,0 +1,10 @@
+{%- if include.icon contains ".svg" -%}
+ {%- capture inline -%}
+ {%- include {{ include.icon }} -%}
+ {%- endcapture -%}
+
+ {{- inline | strip_newlines -}}
+
+{%- elsif include.icon and include.icon != "" -%}
+
+{%- endif -%}
diff --git a/_includes/list.html b/_includes/list.html
new file mode 100644
index 0000000..cdb030c
--- /dev/null
+++ b/_includes/list.html
@@ -0,0 +1,59 @@
+{% assign emptyarray = "" | split: "," %}
+{% assign data = site.data[include.data]
+ | default: site[include.data]
+ | default: emptyarray
+ | data_filter: include.filters
+%}
+
+{% assign years = data
+ | group_by_exp: "d", "d.date | date: '%Y'"
+ | sort: "name"
+ | reverse
+%}
+
+{% for year in years %}
+ {% assign data = year.items %}
+
+ {% if years.size > 1 %}
+ {{--}}{{ 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" }}
+ affiliation=d.affiliation
+ 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 %}
+
+
+
+{{ fulltitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% if page.author %}
+
+
+
+
+
+
+{% else %}
+
+{% endif %}
+
+
+
+
diff --git a/_includes/portrait.html b/_includes/portrait.html
new file mode 100644
index 0000000..3986db0
--- /dev/null
+++ b/_includes/portrait.html
@@ -0,0 +1,51 @@
+{% if include.lookup %}
+ {% assign member = site.members
+ | where_exp: "member", "member.slug == include.lookup"
+ | first
+ %}
+{% else %}
+ {% assign member = include %}
+{% endif %}
+
+{% assign type = site.data.types[member.role] %}
+
+
diff --git a/_includes/post-excerpt.html b/_includes/post-excerpt.html
new file mode 100644
index 0000000..d01e111
--- /dev/null
+++ b/_includes/post-excerpt.html
@@ -0,0 +1,60 @@
+{% if include.lookup %}
+ {% assign post = site.posts
+ | where_exp: "post", "post.slug == include.lookup"
+ | first
+ | default: include
+ %}
+{% else %}
+ {% assign post = include %}
+{% endif %}
+
+
+
+ {% assign url = post.url %}
+ {% assign title = post.title %}
+ {% assign image = post.image %}
+
+ {% if image %}
+
+
+
+ {% endif %}
+
+
+
{{ title }}
+
+ {%
+ include post-info.html
+ author=post.author
+ published=post.date
+ updated=post.last_modified_at
+ tags=post.tags
+ %}
+
+ {% assign excerpt = post.content
+ | default: ""
+ | regex_scan: "(.*)", true
+ | default: post.excerpt
+ | default: ""
+ | strip_html
+ %}
+ {% assign search = post.content
+ | strip_html
+ | strip_newlines
+ | regex_strip
+ %}
+
+ {{ excerpt }}
+
+
+
+
diff --git a/_includes/post-info.html b/_includes/post-info.html
new file mode 100644
index 0000000..c0585d7
--- /dev/null
+++ b/_includes/post-info.html
@@ -0,0 +1,37 @@
+
+ {% if include.author %}
+ {% assign member = site.members
+ | where_exp: "member", "member.slug == include.author"
+ | first
+ %}
+ {% if member %}
+ {% include portrait.html lookup=include.author style="tiny" %}
+ {% else %}
+
+ {% include icon.html icon="fa-solid fa-feather-pointed" %}
+ {{ include.author }}
+
+ {% endif %}
+ {% endif %}
+
+ {% assign published = include.published | date: "%B %d, %Y" %}
+ {% assign updated = include.updated | date: "%B %d, %Y" %}
+
+ {% if published %}
+
+ {% include icon.html icon="fa-regular fa-calendar" %}
+ {{ published }}
+
+ {% endif %}
+
+ {% if updated and updated != "" and updated != published %}
+
+ {% include icon.html icon="fa-solid fa-clock-rotate-left" %}
+ {{ updated }}
+
+ {% endif %}
+
+
+{% if include.tags %}
+ {% include tags.html tags=include.tags link="blog" %}
+{% endif %}
diff --git a/_includes/post-nav.html b/_includes/post-nav.html
new file mode 100644
index 0000000..ac75bbf
--- /dev/null
+++ b/_includes/post-nav.html
@@ -0,0 +1,18 @@
+
+
+ {% if include.post.previous %}
+ {% include icon.html icon="fa-solid fa-angle-left" %} Previous post
+
+ {{ include.post.previous.title }}
+
+ {% endif %}
+
+
+ {% if include.post.next %}
+ Next post {% include icon.html icon="fa-solid fa-angle-right" %}
+
+ {{ include.post.next.title }}
+
+ {% endif %}
+
+
diff --git a/_includes/scripts.html b/_includes/scripts.html
new file mode 100644
index 0000000..30b7ac2
--- /dev/null
+++ b/_includes/scripts.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+{% assign scripts = site.static_files | where_exp: "file", "file.path contains '/_scripts'" %}
+{% for script in scripts %}
+
+{% endfor %}
diff --git a/_includes/search-box.html b/_includes/search-box.html
new file mode 100644
index 0000000..17d7a8b
--- /dev/null
+++ b/_includes/search-box.html
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/_includes/search-info.html b/_includes/search-info.html
new file mode 100644
index 0000000..165fdd4
--- /dev/null
+++ b/_includes/search-info.html
@@ -0,0 +1 @@
+
diff --git a/_includes/section.html b/_includes/section.html
new file mode 100644
index 0000000..d7255a8
--- /dev/null
+++ b/_includes/section.html
@@ -0,0 +1,10 @@
+{% comment %}
+ see content.html
+{% endcomment %}
+
+
+
diff --git a/_includes/site-search.html b/_includes/site-search.html
new file mode 100644
index 0000000..3926ff8
--- /dev/null
+++ b/_includes/site-search.html
@@ -0,0 +1,6 @@
+
diff --git a/_includes/styles.html b/_includes/styles.html
new file mode 100644
index 0000000..a506e87
--- /dev/null
+++ b/_includes/styles.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+{% assign styles = site.pages
+ | where_exp: "file", "file.url contains '/_styles'"
+%}
+{% for style in styles %}
+ {% unless style.url contains ".map" %}
+
+ {% endunless %}
+{% endfor %}
+
+
+{% assign styles = site.static_files
+ | where_exp: "file",
+ "file.path contains '/_styles' and file.path contains '.css'"
+%}
+{% for style in styles %}
+
+{% endfor %}
diff --git a/_includes/tags.html b/_includes/tags.html
new file mode 100644
index 0000000..4f92729
--- /dev/null
+++ b/_includes/tags.html
@@ -0,0 +1,33 @@
+{% assign tags = include.tags
+ | object_items
+ | join: ","
+ | downcase
+ | split: ","
+ | array_filter
+ | join: ","
+ | regex_replace: "\s+", "-"
+ | split: ","
+ | uniq
+%}
+{% assign link = include.link | default: page.dir | absolute_url %}
+{% if tags.size > 0 or include.repo %}
+
+{% endif %}
diff --git a/_includes/verification.html b/_includes/verification.html
new file mode 100644
index 0000000..e72f39b
--- /dev/null
+++ b/_includes/verification.html
@@ -0,0 +1,3 @@
+
diff --git a/_layouts/default.html b/_layouts/default.html
new file mode 100644
index 0000000..b5ac58d
--- /dev/null
+++ b/_layouts/default.html
@@ -0,0 +1,11 @@
+
+
+ {% include head.html %}
+
+ {% include header.html %}
+
+ {% include content.html content=content %}
+
+ {% include footer.html %}
+
+
diff --git a/_layouts/member.html b/_layouts/member.html
new file mode 100644
index 0000000..034b5d0
--- /dev/null
+++ b/_layouts/member.html
@@ -0,0 +1,51 @@
+---
+layout: default
+---
+
+{% capture floatcontent %}
+
+{% include portrait.html lookup=page.slug %}
+
+
+ {% for link in page.links %}
+ {% assign key = link[0] %}
+ {% assign value = link[1] %}
+ {% include button.html type=key link=value style="bare" %}
+ {% endfor %}
+
+
+{% endcapture %}
+
+{% include float.html content=floatcontent %}
+
+{{ content }}
+
+{% assign aliases = page.aliases
+ | default: page.name
+ | default: page.title
+ | join: ","
+ | split: ","
+ | array_filter
+%}
+
+{% capture search -%}
+ research/?search={% for alias in aliases %}"{{ alias }}" {% endfor %}
+{%- endcapture %}
+
+
+
+ Search for {{ page.name | default: page.title }}'s papers on the Research page
+
+
+
+{% capture search -%}
+ blog/?search={{ page.name }}
+{%- endcapture %}
+
+
diff --git a/_layouts/post.html b/_layouts/post.html
new file mode 100644
index 0000000..09fd57b
--- /dev/null
+++ b/_layouts/post.html
@@ -0,0 +1,24 @@
+---
+layout: default
+---
+
+{% include section.html background=page.image %}
+
+{{ 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..c60188e
--- /dev/null
+++ b/_members/jane-smith.md
@@ -0,0 +1,20 @@
+---
+name: Jane Smith
+image: images/photo.jpg
+role: pi
+affiliation: University of Colorado
+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..3d1da24
--- /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..d917857
--- /dev/null
+++ b/_styles/portrait.scss
@@ -0,0 +1,79 @@
+---
+---
+
+.portrait-wrapper {
+ display: contents;
+}
+
+.portrait {
+ position: relative;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ 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 .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"] .icon {
+ left: -2px;
+ top: -2px;
+}
+
+.portrait[data-style="tiny"] .icon {
+ display: none;
+}
+
+.portrait-image {
+ width: 100%;
+ margin-bottom: 20px;
+ aspect-ratio: 1 / 1;
+ border-radius: 999px;
+ object-fit: cover;
+ box-shadow: var(--shadow);
+}
+
+.portrait[data-style="tiny"] .portrait-image {
+ width: 50px;
+ margin: 0;
+}
+
+.portrait-name {
+ font-family: var(--heading);
+ font-weight: var(--semi-bold);
+}
+
+.portrait[data-style="tiny"] {
+ .portrait-description,
+ .portrait-affiliation {
+ display: none;
+ }
+}
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 0000000..5b7c146
Binary files /dev/null and b/images/background.jpg differ
diff --git a/images/fallback.svg b/images/fallback.svg
new file mode 100644
index 0000000..ac12be2
--- /dev/null
+++ b/images/fallback.svg
@@ -0,0 +1,10 @@
+
+
diff --git a/images/icon.png b/images/icon.png
new file mode 100644
index 0000000..774c447
Binary files /dev/null and b/images/icon.png differ
diff --git a/images/logo.svg b/images/logo.svg
new file mode 100644
index 0000000..1d66697
--- /dev/null
+++ b/images/logo.svg
@@ -0,0 +1,68 @@
+
diff --git a/images/photo.jpg b/images/photo.jpg
new file mode 100644
index 0000000..691a988
Binary files /dev/null and b/images/photo.jpg differ
diff --git a/images/share.jpg b/images/share.jpg
new file mode 100644
index 0000000..268a341
Binary files /dev/null and b/images/share.jpg differ
diff --git a/index.md b/index.md
new file mode 100644
index 0000000..bea6980
--- /dev/null
+++ b/index.md
@@ -0,0 +1,94 @@
+---
+---
+
+# Lab Website Template
+
+[Lab Website Template](https://github.com/greenelab/lab-website-template) is an easy-to-use, flexible website template for [labs](https://www.greenelab.com/).
+Spend less time worrying about managing a website and citations, and more time running your lab.
+
+{%
+ include button.html
+ type="docs"
+ link="https://greene-lab.gitbook.io/lab-website-template-docs"
+%}
+{%
+ include button.html
+ type="github"
+ text="On GitHub"
+ link="greenelab/lab-website-template"
+%}
+
+{% include section.html %}
+
+## Highlights
+
+{% capture text %}
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+{%
+ include button.html
+ link="research"
+ text="See our publications"
+ icon="fa-solid fa-arrow-right"
+ flip=true
+ style="bare"
+%}
+
+{% endcapture %}
+
+{%
+ include feature.html
+ image="images/photo.jpg"
+ link="research"
+ title="Our Research"
+ text=text
+%}
+
+{% capture text %}
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+{%
+ include button.html
+ link="projects"
+ text="Browse our projects"
+ icon="fa-solid fa-arrow-right"
+ flip=true
+ style="bare"
+%}
+
+{% endcapture %}
+
+{%
+ include feature.html
+ image="images/photo.jpg"
+ link="projects"
+ title="Our Projects"
+ flip=true
+ style="bare"
+ text=text
+%}
+
+{% capture text %}
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+{%
+ include button.html
+ link="team"
+ text="Meet our team"
+ icon="fa-solid fa-arrow-right"
+ flip=true
+ style="bare"
+%}
+
+{% endcapture %}
+
+{%
+ include feature.html
+ image="images/photo.jpg"
+ link="team"
+ title="Our Team"
+ text=text
+%}
diff --git a/projects/index.md b/projects/index.md
new file mode 100644
index 0000000..1d29a19
--- /dev/null
+++ b/projects/index.md
@@ -0,0 +1,27 @@
+---
+title: Projects
+nav:
+ order: 2
+ tooltip: Software, datasets, and more
+---
+
+# {% include icon.html icon="fa-solid fa-wrench" %}Projects
+
+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 tags.html tags="publication, resource, website" %}
+
+{% include search-info.html %}
+
+{% include section.html %}
+
+## Featured
+
+{% include list.html component="card" data="projects" filters="group: featured" %}
+
+{% include section.html %}
+
+## More
+
+{% include list.html component="card" data="projects" filters="group: " style="small" %}
diff --git a/research/index.md b/research/index.md
new file mode 100644
index 0000000..0f700f4
--- /dev/null
+++ b/research/index.md
@@ -0,0 +1,27 @@
+---
+title: Research
+nav:
+ order: 1
+ tooltip: Published works
+---
+
+# {% include icon.html icon="fa-solid fa-microscope" %}Research
+
+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 %}
+
+## Highlighted
+
+{% include citation.html lookup="Open collaborative writing with Manubot" style="rich" %}
+
+{% include section.html %}
+
+## All
+
+{% include search-box.html %}
+
+{% include search-info.html %}
+
+{% include list.html data="citations" component="citation" style="rich" %}
diff --git a/team/index.md b/team/index.md
new file mode 100644
index 0000000..08318c9
--- /dev/null
+++ b/team/index.md
@@ -0,0 +1,35 @@
+---
+title: Team
+nav:
+ order: 3
+ tooltip: About our team
+---
+
+# {% include icon.html icon="fa-solid fa-users" %}Team
+
+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 list.html data="members" component="portrait" filters="role: pi" %}
+{% include list.html data="members" component="portrait" filters="role: ^(?!pi$)" %}
+
+{% include section.html background="images/background.jpg" dark=true %}
+
+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 %}
+
+{% capture content %}
+
+{% include figure.html image="images/photo.jpg" %}
+{% include figure.html image="images/photo.jpg" %}
+{% include figure.html image="images/photo.jpg" %}
+
+{% endcapture %}
+
+{% include grid.html style="square" content=content %}
diff --git a/testbed.md b/testbed.md
new file mode 100644
index 0000000..6b343a5
--- /dev/null
+++ b/testbed.md
@@ -0,0 +1,437 @@
+---
+title: Testbed
+header: https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1024px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg
+footer: https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1024px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg
+header-dark: false
+footer-dark: false
+---
+
+# Testbed
+
+{% include section.html %}
+
+# Basic formatting
+
+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.
+Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+[External link](https://some-website.org/)
+
+[Internal link](team)
+
+_italic text_
+
+**bold text**
+
+~~strike-through text~~
+
+
+
+Text with extra blank lines above and below
+
+
+
+- list item a
+- list item b
+- list item c
+
+1. ordered list item 1
+1. ordered list item 2
+1. ordered list item 3
+
+
+
+1. top level list item
+ - nested list item
+ 1. even deeper nested list item
+
+Plain image:
+
+![plain image](/images/photo.jpg)
+
+# Heading 1
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+## Heading 2
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+### Heading 3
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+#### Heading 4
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+##### Heading 5
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+###### Heading 6
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+---
+
+| TABLE | Game 1 | Game 2 | Game 3 | Total |
+| :---- | :----: | :----: | :----: | ----: |
+| Anna | 144 | 123 | 218 | 485 |
+| Bill | 90 | 175 | 120 | 385 |
+| Cara | 102 | 214 | 233 | 549 |
+
+> It was the best of times it was the worst of times.
+> It was the age of wisdom, it was the age of foolishness.
+> It was the spring of hope, it was the winter of despair.
+
+```javascript
+// some code with syntax highlighting
+const popup = document.querySelector("#popup");
+popup.style.width = "100%";
+popup.innerText =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
+```
+
+This sentence has `inline code`, useful for making references to variables, packages, versions, etc. within a sentence.
+
+Lorem ipsum dolor sit amet.
+{:.left}
+Consectetur adipiscing elit.
+{:.center}
+Sed do eiusmod tempor incididunt.
+{:.right}
+
+{% include section.html %}
+
+# Jekyll Spaceship
+
+| Stage | Direct Products | ATP Yields |
+| -----------------: | --------------: | ---------: |
+| Glycolysis | 2 ATP | |
+| ^^ | 2 NADH | 3--5 ATP |
+| Pyruvaye oxidation | 2 NADH | 5 ATP |
+| Citric acid cycle | 2 ATP | |
+| ^^ | 6 NADH | 15 ATP |
+| ^^ | 2 FADH | 3 ATP |
+| 30--32 ATP | | |
+
+$ a \* b = c ^ b $
+
+$ 2^{\frac{n-1}{3}} $
+
+$ \int_a^b f(x)\,dx. $
+
+```mermaid!
+pie title Pets adopted by volunteers
+ "Dogs" : 386
+ "Cats" : 85
+ "Rats" : 35
+```
+
+{% include section.html %}
+
+# Components
+
+## Section
+
+{% include section.html background="images/background.jpg" %}
+
+Section, `background`
+
+{% include section.html dark=true %}
+
+Section, `dark=true`
+
+{% include section.html background="images/background.jpg" dark=true %}
+
+Section, `background` `dark=true`
+
+{% include section.html size="wide" %}
+
+Section, `size=wide`
+
+{% include section.html size="full" %}
+
+Section, `size=full` w/ figure
+
+{% include figure.html image="https://images.rawpixel.com/image_1000/cHJpdmF0ZS9sci9pbWFnZXMvd2Vic2l0ZS8yMDIyLTA1L2ZsMjYyODgwODcyMjYtaW1hZ2VfMS1rb3k1Zzkxay5qcGc.jpg" link="team" width="100%" %}
+
+{% include section.html %}
+
+## Figure
+
+{% include figure.html image="images/icon.png" %}
+{% include figure.html image="images/icon.png" caption="_Lorem_ **ipsum**." %}
+{% include figure.html image="images/icon.png" caption="_Lorem_ **ipsum**. `px` width" width="400px" %}
+{% include figure.html image="images/icon.png" caption="_Lorem_ **ipsum**. `%` width" link="team" width="50%" %}
+{% include figure.html image="images/icon.png" caption="_Lorem_ **ipsum**. `px` height" link="team" height="200px" %}
+{% include figure.html image="images/fallback.svg" caption="_Lorem_ **ipsum**. `px` width, svg" link="team" width="400px" %}
+{% include figure.html image="images/fallback.svg" caption="_Lorem_ **ipsum**. `%` width, svg" link="team" width="50%" %}
+{% include figure.html image="images/fallback.svg" caption="_Lorem_ **ipsum**. `px` height, svg" link="team" height="200px" %}
+
+{% include section.html %}
+
+## Button
+
+{% include button.html type="github" %}
+{% include button.html type="github" style="bare" %}
+{% include button.html type="github" icon="fa-brands fa-youtube" text="Override Text" tooltip="Override tooltip" %}
+{% include button.html type="github" text="" style="bare" %}
+{% include button.html type="github" text="" link="github-handle" %}
+
+{% include section.html %}
+
+## Icon
+
+{% include icon.html icon="fa-solid fa-bacteria" %}
+{% include icon.html icon="fa-solid fa-virus" %}
+{% include icon.html icon="fa-solid fa-flask" %}
+{% include icon.html icon="manubot.svg" %}
+
+{% include icon.html icon="fa-brands fa-github" %} Lorem
+{% include icon.html icon="fa-solid fa-microscope" %} Ipsum
+{% include icon.html icon="manubot.svg" %} Dolor
+
+{% include section.html %}
+
+## Feature
+
+{% capture text %}
+_Lorem_ **ipsum** dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+{% endcapture%}
+{% include feature.html image="images/icon.png" link="team" title="Title" text=text %}
+{% include feature.html image="images/icon.png" title="Title" text=text flip=true %}
+{% include feature.html link="team" %}
+
+{% include section.html %}
+
+## List
+
+### List citations
+
+{% include list.html data="citations" component="citation" %}
+
+---
+
+### List projects
+
+{% include list.html data="projects" component="card" %}
+
+---
+
+### List team members
+
+{% include list.html data="members" component="portrait" %}
+
+---
+
+### List blog posts
+
+{% include list.html data="posts" component="post-excerpt" %}
+
+{% include section.html %}
+
+## Citation
+
+{% include citation.html lookup="doi:10.1016/j.csbj.2020.05.017" %}
+{% include citation.html lookup="Open collaborative writing" style="rich" %}
+{% include citation.html title="Manual title" authors="Manual authors" %}
+
+{% include section.html %}
+
+## Card
+
+{% include card.html image="images/icon.png" link="https://nasa.gov/" title="A Large Card" subtitle="A cool card" description="A cool description" tooltip="A cool tooltip" tags="manual tag" repo="greenelab/lab-website-template" %}
+{% include card.html image="images/icon.png" title="A Small Card" subtitle="A cool card" description="_Lorem_ **ipsum**" tooltip="A cool tooltip" tags="manual tag" repo="greenelab/lab-website-template" style="small" %}
+
+{% include section.html %}
+
+## Portrait
+
+{% include portrait.html lookup="jane-smith" %}
+{% include portrait.html lookup="john-doe" style="small" %}
+{% include portrait.html name="Manual name" style="small" %}
+{% include portrait.html style="small" %}
+
+{% include section.html %}
+
+## Post Excerpt
+
+{% include post-excerpt.html lookup="example-post-1" %}
+{% include post-excerpt.html title="Manual title" author="Manual author" date="2020-02-20" last_modified_at="" %}
+
+{% include section.html %}
+
+## Alert
+
+{% capture lorem %}
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+{% endcapture %}
+{% capture content %}**Tip** {{ lorem }}{% endcapture %}
+{% include alert.html type="tip" content=content %}
+{% capture content %}**Help** {{ lorem }}{% endcapture %}
+{% include alert.html type="help" content=content %}
+{% capture content %}**Info** {{ lorem }}{% endcapture %}
+{% include alert.html type="info" content=content %}
+{% capture content %}**Success** {{ lorem }}{% endcapture %}
+{% include alert.html type="success" content=content %}
+{% capture content %}**Warning** {{ lorem }}{% endcapture %}
+{% include alert.html type="warning" content=content %}
+{% capture content %}**Error** {{ lorem }}{% endcapture %}
+{% include alert.html type="error" content=content %}
+
+{% include section.html %}
+
+## Tags
+
+{% include tags.html tags="ovarian cancer, dataset, gene expression" repo="greenelab/lab-website-template" link="blog" %}
+
+{% include section.html %}
+
+## Float
+
+### Figures
+
+{% capture content %}
+{% include figure.html image="images/icon.png" caption="Caption" width="200px" %}
+{% endcapture %}
+{% include float.html content=content %}
+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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+{% include float.html clear=true %}
+
+### Code
+
+{% capture content %}
+
+```javascript
+const test = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
+```
+
+{% endcapture %}
+{% include float.html content=content flip=true %}
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nulla facilisi etiam dignissim diam quis. Id aliquet lectus proin nibh nisl condimentum id venenatis a. Tristique magna sit amet purus gravida quis blandit turpis cursus. Ultrices eros in cursus turpis massa tincidunt dui ut ornare. A cras semper auctor neque vitae tempus quam pellentesque nec. At tellus at urna condimentum mattis pellentesque. Ipsum consequat nisl vel pretium. Ultrices mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Integer vitae justo eget magna fermentum iaculis eu non diam. Mus mauris vitae ultricies leo integer malesuada nunc vel. Leo integer malesuada nunc vel risus. Ornare arcu odio ut sem nulla pharetra. Purus semper eget duis at tellus at urna condimentum. Enim neque volutpat ac tincidunt vitae semper quis lectus.
+
+{% include section.html %}
+
+## Grid
+
+### Regular
+
+With Markdown images
+
+{% capture content %}
+![image](https://journals.plos.org/ploscompbiol/article/figure/image?size=inline&id=info:doi/10.1371/journal.pcbi.1007128.g001&rev=2)
+
+![image](https://ars.els-cdn.com/content/image/1-s2.0-S2001037020302804-gr1.jpg)
+
+![image](https://iiif.elifesciences.org/lax:32822%2Felife-32822-fig8-v3.tif/full/863,/0/default.webp)
+
+![image]({{ "/images/icon.png" | relative_url }})
+
+![image]({{ "/images/icon.png" | relative_url }})
+
+![image]({{ "/images/icon.png" | relative_url }})
+{% endcapture %}
+{% include grid.html content=content %}
+
+### Square
+
+With figure components
+
+{% capture content %}
+{% include figure.html image="https://journals.plos.org/ploscompbiol/article/figure/image?size=inline&id=info:doi/10.1371/journal.pcbi.1007128.g001&rev=2" %}
+{% include figure.html image="https://ars.els-cdn.com/content/image/1-s2.0-S2001037020302804-gr1.jpg" %}
+{% include figure.html image="https://iiif.elifesciences.org/lax:32822%2Felife-32822-fig8-v3.tif/full/863,/0/default.webp" %}
+{% include figure.html image="images/icon.png" %}
+{% include figure.html image="images/icon.png" %}
+{% include figure.html image="images/icon.png" %}
+{% endcapture %}
+{% include grid.html style="square" content=content %}
+
+### Grid of citations
+
+{% capture content %}
+{% include list.html data="citations" component="citation" style="rich" %}
+{% endcapture %}
+{% include grid.html content=content %}
+
+### Grid of blog posts
+
+{% capture content %}
+{% include list.html data="posts" component="post-excerpt" %}
+{% endcapture %}
+{% include grid.html content=content %}
+
+{% include section.html %}
+
+## Cols
+
+### Text
+
+{% capture col1 %}
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+{% endcapture %}
+{% capture col2 %}
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nulla facilisi etiam dignissim diam quis. Id aliquet lectus proin nibh nisl condimentum id venenatis a.
+{% endcapture %}
+{% capture col3 %}
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nulla facilisi etiam dignissim diam quis. Id aliquet lectus proin nibh nisl condimentum id venenatis a. Tristique magna sit amet purus gravida quis blandit turpis cursus. Ultrices eros in cursus turpis massa tincidunt dui ut ornare. A cras semper auctor neque vitae tempus quam pellentesque nec. At tellus at urna condimentum mattis pellentesque. Ipsum consequat nisl vel pretium. Ultrices mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Integer vitae justo eget magna fermentum iaculis eu non diam. Mus mauris vitae ultricies leo integer malesuada nunc vel. Leo integer malesuada nunc vel risus. Ornare arcu odio ut sem nulla pharetra. Purus semper eget duis at tellus at urna condimentum. Enim neque volutpat ac tincidunt vitae semper quis lectus.
+{% endcapture %}
+{% include cols.html col1=col1 col2=col2 col3=col3 %}
+
+### Images
+
+{% capture col1 %}
+{% include figure.html image="images/icon.png" caption="Fig. 1a" %}
+Lorem _ipsum_ dolor **sit** amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+{% endcapture %}
+{% capture col2 %}
+{% include figure.html image="images/icon.png" caption="Fig. 1b" %}
+Lorem _ipsum_ dolor **sit** amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+{% endcapture %}
+{% capture col3 %}
+{% include figure.html image="images/icon.png" caption="Fig. 1c" %}
+Lorem _ipsum_ dolor **sit** amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+{% endcapture %}
+{% include cols.html col1=col1 col2=col2 col3=col3 %}
+
+### Code
+
+{% capture col1 %}
+
+```javascript
+const test = "Lorem ipsum dolor sit amet";
+```
+
+{% endcapture %}
+{% capture col2 %}
+
+```javascript
+const test = "Lorem ipsum dolor sit amet";
+```
+
+{% endcapture %}
+{% capture col3 %}
+
+```javascript
+const test = "Lorem ipsum dolor sit amet";
+```
+
+{% endcapture %}
+{% include cols.html col1=col1 col2=col2 col3=col3 %}
+
+{% include section.html %}
+
+## Search
+
+{% include search-box.html %}
+{% include search-info.html %}
+
+{% include section.html %}
+
+## Site Search
+
+{% include site-search.html %}