├── .docker ├── Dockerfile ├── entrypoint.sh └── run.sh ├── .github ├── DISCUSSION_TEMPLATE │ ├── general.yaml │ └── q-a.yaml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── issue.yaml ├── pull_request_template.md ├── user_pull_request_template.md └── workflows │ ├── build-preview.yaml │ ├── build-site.yaml │ ├── first-time-setup.yaml │ ├── on-pages.yaml │ ├── on-pull-request.yaml │ ├── on-push.yaml │ ├── on-schedule.yaml │ ├── update-citations.yaml │ ├── update-url.yaml │ └── versioning.yaml ├── .gitignore ├── 404.md ├── CHANGELOG.md ├── CITATION.cff ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── _cite ├── .cache │ └── cache.db ├── cite.py ├── plugins │ ├── google-scholar.py │ ├── orcid.py │ ├── pubmed.py │ └── sources.py ├── requirements.txt └── util.py ├── _config.yaml ├── _data ├── citations.yaml ├── orcid.yaml ├── projects.yaml ├── sources.yaml └── types.yaml ├── _includes ├── alert.html ├── analytics.html ├── button.html ├── card.html ├── citation.html ├── cols.html ├── content.html ├── fallback.html ├── feature.html ├── figure.html ├── float.html ├── fonts.html ├── footer.html ├── grid.html ├── head.html ├── header.html ├── icon.html ├── list.html ├── manubot.svg ├── meta.html ├── portrait.html ├── post-excerpt.html ├── post-info.html ├── post-nav.html ├── scripts.html ├── search-box.html ├── search-info.html ├── section.html ├── site-search.html ├── styles.html ├── tags.html └── verification.html ├── _layouts ├── default.html ├── member.html └── post.html ├── _members ├── jane-smith.md ├── john-doe.md └── sarah-johnson.md ├── _plugins ├── array.rb ├── file.rb ├── hash.rb ├── misc.rb └── regex.rb ├── _posts ├── 2019-01-07-example-post-1.md ├── 2021-09-30-example-post-2.md └── 2023-02-23-example-post-3.md ├── _scripts ├── anchors.js ├── dark-mode.js ├── fetch-tags.js ├── search.js ├── site-search.js ├── table-wrap.js └── tooltip.js ├── _styles ├── -theme.scss ├── alert.scss ├── all.scss ├── anchor.scss ├── background.scss ├── body.scss ├── bold.scss ├── button.scss ├── card.scss ├── checkbox.scss ├── citation.scss ├── code.scss ├── cols.scss ├── dark-toggle.scss ├── details.scss ├── feature.scss ├── figure.scss ├── float.scss ├── font.scss ├── footer.scss ├── form.scss ├── grid.scss ├── header.scss ├── heading.scss ├── highlight.scss ├── icon.scss ├── image.scss ├── link.scss ├── list.scss ├── main.scss ├── paragraph.scss ├── portrait.scss ├── post-excerpt.scss ├── post-info.scss ├── post-nav.scss ├── quote.scss ├── rule.scss ├── search-box.scss ├── search-info.scss ├── section.scss ├── table.scss ├── tags.scss ├── textbox.scss ├── tooltip.scss └── util.scss ├── blog └── index.md ├── contact └── index.md ├── images ├── background.jpg ├── fallback.svg ├── icon.png ├── logo.svg ├── photo.jpg └── share.jpg ├── index.md ├── projects └── index.md ├── research └── index.md ├── team └── index.md └── testbed.md /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # start with official ruby docker image as base 2 | FROM ruby:3.1.2 3 | 4 | # set working directory within container 5 | WORKDIR /usr/src/app 6 | 7 | # pull in ruby (jekyll) and python (cite process) package info 8 | COPY Gemfile Gemfile.lock _cite/requirements.txt ./ 9 | 10 | # install ruby packages 11 | RUN VERSION=$(grep -A 1 'BUNDLED WITH' Gemfile.lock | tail -n 1 | xargs); \ 12 | gem install bundler --version ${VERSION} && \ 13 | bundle _${VERSION}_ install 14 | 15 | # install python 16 | RUN apt update && apt install -y python3 python3-pip 17 | 18 | # install python packages 19 | RUN python3 -m pip install --no-cache-dir --upgrade --requirement requirements.txt 20 | 21 | # install python package for listening for file changes 22 | RUN pip install "watchdog[watchmedo]==3.0.0" 23 | 24 | # ports used by jekyll 25 | EXPOSE 4000 26 | EXPOSE 35729 27 | 28 | # run jekyll and cite process 29 | COPY .docker/entrypoint.sh /var 30 | CMD [ "/var/entrypoint.sh" ] 31 | -------------------------------------------------------------------------------- /.docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # print folder contents for debugging 4 | printf "\n\nContents:\n\n" 5 | ls 6 | 7 | # run cite process 8 | python3 _cite/cite.py 9 | 10 | # run jekyll serve in hot-reload mode 11 | # rerun whenever _config.yaml changes (jekyll hot-reload doesn't work with this file) 12 | watchmedo auto-restart \ 13 | --debug-force-polling \ 14 | --patterns="_config.yaml" \ 15 | --signal SIGTERM \ 16 | -- bundle exec jekyll serve --open-url --force_polling --livereload --trace --host=0.0.0.0 \ 17 | | sed "s/LiveReload address.*//g;s/0.0.0.0/localhost/g" & 18 | 19 | # rerun cite process whenever _data files change 20 | watchmedo shell-command \ 21 | --debug-force-polling \ 22 | --recursive \ 23 | --wait \ 24 | --command="python3 _cite/cite.py" \ 25 | --patterns="_data/sources*;_data/orcid*;_data/pubmed*;_data/google-scholar*" \ 26 | -------------------------------------------------------------------------------- /.docker/run.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # name of image 4 | IMAGE=lab-website-renderer:latest 5 | 6 | # name of running container 7 | CONTAINER=lab-website-renderer 8 | 9 | # choose platform flag 10 | PLATFORM="" 11 | 12 | # default vars 13 | DOCKER_RUN="docker run" 14 | WORKING_DIR=$(pwd) 15 | 16 | # fix windows faux linux shells/tools 17 | if [[ $OSTYPE == msys* ]] || [[ $OSTYPE == cygwin* ]]; then 18 | DOCKER_RUN="winpty docker run" 19 | WORKING_DIR=$(cmd //c cd) 20 | fi 21 | 22 | # build docker image 23 | docker build ${PLATFORM} \ 24 | --tag ${IMAGE} \ 25 | --file ./.docker/Dockerfile . && \ 26 | 27 | # run built docker image 28 | ${DOCKER_RUN} ${PLATFORM} \ 29 | --name ${CONTAINER} \ 30 | --init \ 31 | --rm \ 32 | --interactive \ 33 | --tty \ 34 | --publish 4000:4000 \ 35 | --publish 35729:35729 \ 36 | --volume "${WORKING_DIR}:/usr/src/app" \ 37 | ${IMAGE} "$@" 38 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/general.yaml: -------------------------------------------------------------------------------- 1 | body: 2 | - type: checkboxes 3 | attributes: 4 | label: Checks 5 | options: 6 | - 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. 7 | required: true 8 | 9 | - type: input 10 | id: repo 11 | attributes: 12 | label: Link to your website repo 13 | description: "In almost all cases, **we cannot help you if you don't provide this**." 14 | placeholder: ex. https://github.com/greenelab/greenelab.com 15 | validations: 16 | required: true 17 | 18 | - type: input 19 | id: version 20 | attributes: 21 | label: Version of Lab Website Template you are using 22 | description: See your `CITATION.cff` file. 23 | placeholder: ex. 1.0.0 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | id: description 29 | attributes: 30 | label: Description 31 | description: | 32 | 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? 33 | placeholder: Description 34 | validations: 35 | required: true 36 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/q-a.yaml: -------------------------------------------------------------------------------- 1 | body: 2 | - type: checkboxes 3 | attributes: 4 | label: Checks 5 | options: 6 | - 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. 7 | required: true 8 | 9 | - type: input 10 | id: repo 11 | attributes: 12 | label: Link to your website repo 13 | description: "In almost all cases, **we cannot help you if you don't provide this**." 14 | placeholder: ex. https://github.com/greenelab/greenelab.com 15 | validations: 16 | required: true 17 | 18 | - type: input 19 | id: version 20 | attributes: 21 | label: Version of Lab Website Template you are using 22 | description: See your `CITATION.cff` file. 23 | placeholder: ex. 1.0.0 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | id: description 29 | attributes: 30 | label: Description 31 | description: | 32 | 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? 33 | placeholder: Description 34 | validations: 35 | required: true 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: 💬 Start a discussion 4 | url: https://github.com/greenelab/lab-website-template/discussions 5 | about: I need help, I have a question, or other discussion. 6 | - name: 📚 Docs issue 7 | url: https://github.com/greenelab/lab-website-template-docs/issues 8 | about: I have a question or issue related to the template documentation. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.yaml: -------------------------------------------------------------------------------- 1 | name: 🐞 Create an issue 2 | description: I think I've discovered a bug, I want to request a feature/change, or other issue. 3 | 4 | body: 5 | - type: checkboxes 6 | attributes: 7 | label: Checks 8 | options: 9 | - 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. 10 | required: true 11 | 12 | - type: input 13 | id: repo 14 | attributes: 15 | label: Link to your website repo 16 | description: "In almost all cases, **we cannot help you if you don't provide this**." 17 | placeholder: ex. https://github.com/greenelab/greenelab.com 18 | validations: 19 | required: true 20 | 21 | - type: input 22 | id: version 23 | attributes: 24 | label: Version of Lab Website Template you are using 25 | description: See your `CITATION.cff` file. 26 | placeholder: ex. 1.0.0 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | id: description 32 | attributes: 33 | label: Description 34 | description: | 35 | 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? 36 | placeholder: Description 37 | validations: 38 | required: true 39 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | STOP!!! 2 | 3 | You are about to open this pull request against THE TEMPLATE ITSELF. You probably meant to open it against your own website repo. 4 | 5 | --- 6 | 7 | FOR THE TEMPLATE MAINTAINER(S) 8 | 9 | New template version checklist: 10 | 11 | - [ ] I have updated CITATION and CHANGELOG as appropriate. 12 | - [ ] I have updated lab-website-template-docs as appropriate. 13 | - [ ] I have checked the testbed as appropriate. 14 | -------------------------------------------------------------------------------- /.github/user_pull_request_template.md: -------------------------------------------------------------------------------- 1 | This website is based on the Lab Website Template. 2 | See its documentation for working with this site: 3 | 4 | https://greene-lab.gitbook.io/lab-website-template-docs 5 | -------------------------------------------------------------------------------- /.github/workflows/build-preview.yaml: -------------------------------------------------------------------------------- 1 | name: build-preview 2 | run-name: build pull request preview 3 | 4 | on: 5 | # run when called from another workflow 6 | workflow_call: 7 | 8 | # run if user manually requests it 9 | workflow_dispatch: 10 | 11 | # variables 12 | env: 13 | PREVIEWS_FOLDER: preview 14 | 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | 19 | jobs: 20 | build-preview: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Debug dump 25 | uses: crazy-max/ghaction-dump-context@v2 26 | 27 | - name: Checkout branch contents 28 | uses: actions/checkout@v4 29 | with: 30 | repository: ${{ github.event.pull_request.head.repo.full_name }} 31 | ref: ${{ github.head_ref }} 32 | 33 | - name: Install Ruby packages 34 | if: github.event.action != 'closed' 35 | uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: "3.1" 38 | bundler-cache: true 39 | 40 | - name: Get Pages url 41 | if: github.event.action != 'closed' 42 | id: pages 43 | uses: actions/configure-pages@v4 44 | 45 | - name: SSH debug 46 | if: runner.debug == '1' 47 | uses: mxschmitt/action-tmate@v3 48 | 49 | - name: Build preview version of site 50 | if: github.event.action != 'closed' 51 | run: | 52 | JEKYLL_ENV=production bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path || '' }}/${{ env.PREVIEWS_FOLDER }}/pr-${{ github.event.number }}" 53 | 54 | - name: Commit preview to Pages branch 55 | uses: rossjrw/pr-preview-action@v1.4.7 56 | with: 57 | source-dir: _site 58 | umbrella-dir: ${{ env.PREVIEWS_FOLDER }} 59 | -------------------------------------------------------------------------------- /.github/workflows/build-site.yaml: -------------------------------------------------------------------------------- 1 | name: build-site 2 | run-name: build live site 3 | 4 | on: 5 | # run when called from another workflow 6 | workflow_call: 7 | 8 | # run if user manually requests it 9 | workflow_dispatch: 10 | 11 | # variables 12 | env: 13 | PREVIEWS_FOLDER: preview 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | build-site: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Debug dump 24 | uses: crazy-max/ghaction-dump-context@v2 25 | 26 | - name: Checkout branch contents 27 | uses: actions/checkout@v4 28 | 29 | - name: Install Ruby packages 30 | uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: "3.1" 33 | bundler-cache: true 34 | 35 | - name: Get Pages url 36 | id: pages 37 | uses: actions/configure-pages@v4 38 | 39 | - name: SSH debug 40 | if: runner.debug == '1' 41 | uses: mxschmitt/action-tmate@v3 42 | 43 | - name: Set root url 44 | run: | 45 | printf "\n\nurl: ${{ steps.pages.outputs.origin }}" >> _config.yaml 46 | 47 | - name: Build live version of site 48 | run: | 49 | JEKYLL_ENV=production bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path || '' }}" 50 | 51 | - name: Commit live site to Pages branch 52 | uses: JamesIves/github-pages-deploy-action@v4.5.0 53 | with: 54 | folder: _site 55 | clean-exclude: ${{ env.PREVIEWS_FOLDER }} 56 | force: false 57 | -------------------------------------------------------------------------------- /.github/workflows/first-time-setup.yaml: -------------------------------------------------------------------------------- 1 | name: first-time-setup 2 | run-name: first time setup of repo 3 | 4 | on: 5 | # run if user manually requests it 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | first-time-setup: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Debug dump 17 | uses: crazy-max/ghaction-dump-context@v2 18 | 19 | - name: Create Pages branch 20 | uses: peterjgrainger/action-create-branch@v3.0.0 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | branch: "gh-pages" 25 | 26 | - name: Checkout Pages branch 27 | uses: actions/checkout@v4 28 | with: 29 | ref: gh-pages 30 | 31 | - name: SSH debug 32 | if: runner.debug == '1' 33 | uses: mxschmitt/action-tmate@v3 34 | 35 | # clean slate, as if starting from orphan branch 36 | - name: Clear Pages branch 37 | run: rm -rf ./* .github .docker .gitignore 38 | 39 | # prevent GitHub from running Jekyll a second time after build 40 | - name: Make .nojekyll file 41 | run: touch .nojekyll 42 | 43 | - name: Make placeholder homepage 44 | run: printf "Placeholder homepage" > index.html 45 | 46 | - name: Commit changes to Pages branch 47 | uses: stefanzweifel/git-auto-commit-action@v5 48 | with: 49 | branch: gh-pages 50 | commit_message: "Clear branch" 51 | 52 | - name: Checkout main branch 53 | uses: actions/checkout@v4 54 | 55 | - name: Remove files user doesn't need 56 | run: | 57 | rm -rf \ 58 | CHANGELOG.md \ 59 | testbed.md \ 60 | .github/ISSUE_TEMPLATE \ 61 | .github/DISCUSSION_TEMPLATE \ 62 | .github/workflows/versioning.yaml \ 63 | .github/pull_request_template.md \ 64 | 65 | - name: Rename files 66 | run: | 67 | mv -f .github/user_pull_request_template.md .github/pull_request_template.md 68 | 69 | - name: Set vars for personalization 70 | run: | 71 | user="${{ github.repository_owner }}" 72 | description="An engaging 1-3 sentence description of your lab." 73 | printf "\nUSER=%s" "$user" >> "$GITHUB_ENV" 74 | printf "\nDESCRIPTION=%s" "$description" >> "$GITHUB_ENV" 75 | 76 | - name: Personalize readme for user 77 | run: | 78 | printf " 79 | ![on-push](../../actions/workflows/on-push.yaml/badge.svg) 80 | ![on-pull-request](../../actions/workflows/on-pull-request.yaml/badge.svg) 81 | ![on-schedule](../../actions/workflows/on-schedule.yaml/badge.svg) 82 | 83 | # %s's Website 84 | 85 | Visit **[website url](#)** 🚀 86 | 87 | _Built with [Lab Website Template](https://greene-lab.gitbook.io/lab-website-template-docs)_ 88 | " "${{ env.USER }}" > README.md 89 | 90 | - name: Personalize Jekyll config for user 91 | uses: actions/github-script@v7 92 | with: 93 | script: | 94 | const { readFileSync, writeFileSync } = require("fs"); 95 | const file = "_config.yaml"; 96 | const contents = readFileSync(file) 97 | .toString() 98 | .replace(/(^title: ).*$/m, "$1${{ env.USER }}") 99 | .replace(/(^subtitle: ).*$/m, "$1") 100 | .replace(/(^description: ).*$/m, "$1${{ env.DESCRIPTION }}") 101 | .replace(/(^ email: ).*$/m, "$1contact@${{ env.USER }}.com") 102 | .replace(/(^ github: ).*$/m, "$1${{ env.USER }}") 103 | .replace(/(^ twitter: ).*$/m, "$1${{ env.USER }}") 104 | .replace(/(^ youtube: ).*$/m, "$1${{ env.USER }}"); 105 | writeFileSync(file, contents); 106 | 107 | - name: Personalize homepage for user 108 | uses: actions/github-script@v7 109 | with: 110 | script: | 111 | const { readFileSync, writeFileSync } = require("fs"); 112 | const file = "index.md"; 113 | let contents = readFileSync(file).toString(); 114 | const find = /\# Lab Website Template[\s\S]+({% include section\.html)/; 115 | const replace = `# ${{ env.USER }}'s Website\n\n${{ env.DESCRIPTION }}\n\n$1`; 116 | contents = contents.replace(find, replace); 117 | writeFileSync(file, contents); 118 | 119 | - name: Commit changed files 120 | uses: stefanzweifel/git-auto-commit-action@v5 121 | with: 122 | commit_message: "Set up repo" 123 | -------------------------------------------------------------------------------- /.github/workflows/on-pages.yaml: -------------------------------------------------------------------------------- 1 | name: on-pages 2 | run-name: on pages deploy 3 | 4 | on: 5 | workflow_run: 6 | workflows: [pages-build-deployment] 7 | types: 8 | - completed 9 | branches: 10 | - gh-pages 11 | 12 | # run if user manually requests it 13 | workflow_dispatch: 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | update-url: 20 | # only run on user instance of template, not template itself 21 | if: github.repository != 'greenelab/lab-website-template' 22 | uses: ./.github/workflows/update-url.yaml 23 | 24 | build-site: 25 | needs: update-url 26 | if: needs.update-url.outputs.changed == 'true' 27 | uses: ./.github/workflows/build-site.yaml 28 | -------------------------------------------------------------------------------- /.github/workflows/on-pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: on-pull-request 2 | run-name: on pull request activity 3 | 4 | on: 5 | pull_request_target: 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | - closed 11 | 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | 16 | jobs: 17 | update-citations: 18 | uses: ./.github/workflows/update-citations.yaml 19 | 20 | build-preview: 21 | needs: update-citations 22 | uses: ./.github/workflows/build-preview.yaml 23 | -------------------------------------------------------------------------------- /.github/workflows/on-push.yaml: -------------------------------------------------------------------------------- 1 | name: on-push 2 | run-name: on push to main 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | # run if user manually requests it 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | 16 | jobs: 17 | update-citations: 18 | # skip first run because nothing enabled or setup yet 19 | if: github.run_number != 1 20 | uses: ./.github/workflows/update-citations.yaml 21 | 22 | build-site: 23 | needs: update-citations 24 | uses: ./.github/workflows/build-site.yaml 25 | -------------------------------------------------------------------------------- /.github/workflows/on-schedule.yaml: -------------------------------------------------------------------------------- 1 | name: on-schedule 2 | run-name: on schedule 3 | 4 | on: 5 | schedule: 6 | # weekly 7 | - cron: "0 0 * * 1" 8 | 9 | # run if user manually requests it 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | 16 | jobs: 17 | update-citations: 18 | # only run on user instance of template, not template itself 19 | if: github.repository != 'greenelab/lab-website-template' 20 | uses: ./.github/workflows/update-citations.yaml 21 | with: 22 | open-pr: true 23 | -------------------------------------------------------------------------------- /.github/workflows/update-citations.yaml: -------------------------------------------------------------------------------- 1 | name: update-citations 2 | run-name: update citations 3 | 4 | on: 5 | # run when called from another workflow 6 | workflow_call: 7 | inputs: 8 | open-pr: 9 | type: boolean 10 | outputs: 11 | changed: 12 | value: ${{ jobs.update-citations.outputs.changed }} 13 | 14 | # run if user manually requests it 15 | workflow_dispatch: 16 | 17 | permissions: 18 | contents: write 19 | pull-requests: write 20 | 21 | env: 22 | FORCE_COLOR: true 23 | GOOGLE_SCHOLAR_API_KEY: ${{ secrets.GOOGLE_SCHOLAR_API_KEY }} 24 | 25 | jobs: 26 | update-citations: 27 | runs-on: ubuntu-latest 28 | timeout-minutes: 15 29 | 30 | steps: 31 | - name: Debug dump 32 | uses: crazy-max/ghaction-dump-context@v2 33 | 34 | - name: Checkout branch contents 35 | if: github.event.action != 'closed' 36 | uses: actions/checkout@v4 37 | with: 38 | repository: ${{ github.event.pull_request.head.repo.full_name }} 39 | ref: ${{ github.head_ref }} 40 | 41 | - name: Set up Python 42 | if: github.event.action != 'closed' 43 | uses: actions/setup-python@v5 44 | with: 45 | python-version: "3.11" 46 | cache: "pip" 47 | cache-dependency-path: "**/requirements.txt" 48 | 49 | - name: Install Python packages 50 | if: github.event.action != 'closed' 51 | run: | 52 | python -m pip install --upgrade --requirement ./_cite/requirements.txt 53 | 54 | - name: SSH debug 55 | if: runner.debug == '1' 56 | uses: mxschmitt/action-tmate@v3 57 | 58 | - name: Build updated citations 59 | if: github.event.action != 'closed' 60 | run: python _cite/cite.py 61 | timeout-minutes: 15 62 | 63 | - name: Commit cache 64 | if: failure() 65 | uses: stefanzweifel/git-auto-commit-action@v5 66 | with: 67 | file_pattern: "**/.cache/**" 68 | commit_message: "Commit cache" 69 | 70 | - name: Check if citations changed 71 | if: github.event.action != 'closed' 72 | id: changed 73 | uses: tj-actions/verify-changed-files@v18 74 | with: 75 | files: | 76 | _data/citations.yaml 77 | 78 | - name: Commit updated citations to branch 79 | if: | 80 | github.event.action != 'closed' && 81 | steps.changed.outputs.files_changed == 'true' && 82 | inputs.open-pr != true 83 | uses: stefanzweifel/git-auto-commit-action@v5 84 | with: 85 | commit_message: "Update citations" 86 | 87 | - name: Open pull request with updated citations 88 | if: | 89 | github.event.action != 'closed' && 90 | steps.changed.outputs.files_changed == 'true' && 91 | inputs.open-pr == true 92 | uses: peter-evans/create-pull-request@v6 93 | with: 94 | branch: citation-update 95 | title: Periodic citation update 96 | body: | 97 | To see a live preview of this PR, close (don't merge) and reopen it. 98 | 99 | outputs: 100 | changed: ${{ steps.changed.outputs.files_changed || false }} 101 | -------------------------------------------------------------------------------- /.github/workflows/update-url.yaml: -------------------------------------------------------------------------------- 1 | name: update-url 2 | run-name: update site after url change 3 | 4 | on: 5 | # run when called from another workflow 6 | workflow_call: 7 | outputs: 8 | changed: 9 | value: ${{ jobs.update-url.outputs.changed }} 10 | 11 | # run if user manually requests it 12 | workflow_dispatch: 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | update-url: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Debug dump 23 | uses: crazy-max/ghaction-dump-context@v2 24 | 25 | - name: Get Pages url 26 | id: pages 27 | uses: actions/configure-pages@v4 28 | 29 | - name: Checkout branch contents 30 | uses: actions/checkout@v4 31 | 32 | - name: SSH debug 33 | if: runner.debug == '1' 34 | uses: mxschmitt/action-tmate@v3 35 | 36 | # update link to site in readme 37 | - name: Update readme 38 | uses: actions/github-script@v7 39 | with: 40 | script: | 41 | const { readFileSync, writeFileSync } = require("fs"); 42 | const file = "README.md"; 43 | let contents = readFileSync(file).toString(); 44 | const find = /\*\*\[.*\]\(.*\)\*\*/; 45 | const host = "${{ steps.pages.outputs.host }}"; 46 | const path = "${{ steps.pages.outputs.base_path }}"; 47 | const url = "${{ steps.pages.outputs.base_url }}"; 48 | const replace = `**[${host}${path}](${url})**`; 49 | if (contents.match(find)) 50 | contents = contents.replace(find, replace); 51 | else 52 | contents = `Visit ${replace} 🚀\n\n` + contents; 53 | writeFileSync(file, contents); 54 | 55 | - name: Check if readme changed 56 | id: changed 57 | uses: tj-actions/verify-changed-files@v18 58 | with: 59 | files: | 60 | README.md 61 | 62 | - name: Commit changed files 63 | if: steps.changed.outputs.files_changed == 'true' 64 | uses: stefanzweifel/git-auto-commit-action@v5 65 | with: 66 | commit_message: "Update url" 67 | 68 | outputs: 69 | changed: ${{ steps.changed.outputs.files_changed }} 70 | -------------------------------------------------------------------------------- /.github/workflows/versioning.yaml: -------------------------------------------------------------------------------- 1 | name: versioning 2 | run-name: versioning tasks 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | push: 9 | branches: 10 | - main 11 | 12 | permissions: 13 | contents: write 14 | 15 | jobs: 16 | pull-request: 17 | # only run on template itself, not user instance of template 18 | if: | 19 | github.repository == 'greenelab/lab-website-template' && 20 | github.event_name == 'pull_request' 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Debug dump 24 | uses: crazy-max/ghaction-dump-context@v2 25 | 26 | - if: runner.debug == '1' 27 | uses: mxschmitt/action-tmate@v3 28 | 29 | - name: Checkout base branch contents 30 | uses: actions/checkout@v4 31 | with: 32 | ref: main 33 | path: base 34 | 35 | - name: Checkout pr branch contents 36 | uses: actions/checkout@v4 37 | with: 38 | path: pr 39 | 40 | - name: Lint actions 41 | run: | 42 | bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 43 | ./actionlint -color 44 | shell: bash 45 | working-directory: pr 46 | 47 | - name: Install packages 48 | run: npm install yaml semver 49 | 50 | - name: Check version, date, changelog 51 | uses: actions/github-script@v7 52 | with: 53 | script: | 54 | const { readFileSync } = require("fs"); 55 | const { lte, valid } = require("semver"); 56 | const { parse } = require("yaml"); 57 | 58 | // load and parse file contents 59 | const { version: oldVersion, "date-released": oldDate } = parse( 60 | readFileSync("base/CITATION.cff").toString() 61 | ); 62 | const { version: newVersion, "date-released": newDate } = parse( 63 | readFileSync("pr/CITATION.cff").toString() 64 | ); 65 | const changelog = readFileSync("pr/CHANGELOG.md") 66 | .toString() 67 | .split(/^## /m) 68 | .map((section) => { 69 | const [heading, ...body] = section.split("\n"); 70 | return [heading.trim(), body.join("\n").trim()]; 71 | }); 72 | 73 | // check version 74 | if (!valid(newVersion)) throw Error("Version not valid"); 75 | if (lte(newVersion, oldVersion)) throw Error("Version not updated"); 76 | 77 | // check date 78 | if (new Date(newDate).toISOString().split("T")[0] !== newDate) 79 | throw Error("Date not valid"); 80 | if (new Date(newDate) <= new Date(oldDate)) throw Error("Date not updated"); 81 | 82 | // check changelog 83 | const newSection = changelog.find( 84 | ([heading, body]) => 85 | heading.includes(newVersion) && heading.includes(newDate) && body 86 | ); 87 | if (!newSection) throw Error("Changelog not updated or not valid"); 88 | 89 | push: 90 | # only run on template itself, not user instance of template 91 | if: | 92 | github.repository == 'greenelab/lab-website-template' && 93 | github.event_name == 'push' 94 | runs-on: ubuntu-latest 95 | steps: 96 | - name: Debug dump 97 | uses: crazy-max/ghaction-dump-context@v2 98 | 99 | - name: Checkout branch contents 100 | uses: actions/checkout@v4 101 | 102 | - name: Install packages 103 | run: npm install yaml semver 104 | 105 | - name: SSH debug 106 | if: runner.debug == '1' 107 | uses: mxschmitt/action-tmate@v3 108 | 109 | - name: Get version and body 110 | id: version 111 | uses: actions/github-script@v7 112 | with: 113 | script: | 114 | const { readFileSync } = require("fs"); 115 | const { parse } = require("yaml"); 116 | 117 | // load and parse file contents 118 | const { version, "date-released": date } = parse( 119 | readFileSync("CITATION.cff").toString() 120 | ); 121 | const changelog = readFileSync("CHANGELOG.md") 122 | .toString() 123 | .split(/^## /m) 124 | .map((section) => { 125 | const [heading, ...body] = section.split("\n"); 126 | return [heading.trim(), body.join("\n").trim()]; 127 | }); 128 | 129 | // find changelog body for version 130 | const [, body = ""] = 131 | changelog.find( 132 | ([heading]) => heading.includes(version) && heading.includes(date) 133 | ) || []; 134 | 135 | return { version, body }; 136 | 137 | - name: Create GitHub release 138 | uses: ncipollo/release-action@v1.14.0 139 | with: 140 | commit: ${{ github.ref }} 141 | tag: v${{ fromJson(steps.version.outputs.result).version }} 142 | name: v${{ fromJson(steps.version.outputs.result).version }} 143 | body: ${{ fromJson(steps.version.outputs.result).body }} 144 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | debug.log 7 | __pycache__ 8 | .DS_STORE 9 | .env* 10 | package.json 11 | package-lock.json 12 | yarn.lock 13 | node_modules 14 | -------------------------------------------------------------------------------- /404.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 404 3 | permalink: /404.html 4 | --- 5 | 6 | ## {% include icon.html icon="fa-solid fa-heart-crack" %} Page Not Found 7 | 8 | Try searching the whole site for the content you want: 9 | {:.center} 10 | 11 | {% include site-search.html %} 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Reference: common-changelog.org 4 | 5 | ## 1.3.5 - 2025-05-12 6 | 7 | ### 8 | 9 | - Fix workflow bug where PR previews on GitHub Actions have broken styles/links/etc. 10 | - Fix tags component relative link bug 11 | - Make Actions workflows a bit more robust 12 | 13 | ## 1.3.4 - 2025-02-03 14 | 15 | ### Changed 16 | 17 | - Fix section component parsing bug. 18 | 19 | ## 1.3.3 - 2025-01-25 20 | 21 | ### Changed 22 | 23 | - Citation process logging enhancements. 24 | 25 | ### Added 26 | 27 | - Add support for multiple authors in blog posts. 28 | - Add GitHub Actions workflow status badges to readme. 29 | 30 | ## 1.3.2 - 2025-01-06 31 | 32 | ### Changed 33 | 34 | - Misc enhancements and bug fixes. 35 | 36 | ## 1.3.1 - 2024-11-11 37 | 38 | ### Changed 39 | 40 | - Update and improve workflows for building site and citations. 41 | - Escape user inputs better in rare edge cases. 42 | 43 | ## 1.3.0 - 2024-08-16 44 | 45 | ### Changed 46 | 47 | - List component `filters` parameter changed to `filter` and now takes any Ruby expression instead of the existing custom syntax. 48 | Example: `filters="publisher: bioRxiv, date: ^2020"` becomes `filter="publisher == 'bioRxiv' and date =~ /^2020/"`. 49 | See docs for more info. 50 | - Fix rare bug where data (e.g. a paper title) containing certain characters (e.g. a double quote) can mess up HTML rendering. 51 | - Fix "first time setup" workflow bug. 52 | - Tweak GitHub Actions debugging/logging. 53 | 54 | ### Added 55 | 56 | - Styling for `
` HTML element. 57 | 58 | ## 1.2.2 - 2024-06-05 59 | 60 | ### Added 61 | 62 | - Add `affiliation` member portrait field. 63 | 64 | ### Changed 65 | 66 | - Change order and type of preferred ids from ORCID API. 67 | - Expand list of supported Manubot identifiers and thus keep ORCID API details less often. 68 | - Simplify portrait component under-the-hood. 69 | - Make tag component de-duplication consistent with search plugin de-duplication. 70 | 71 | ## 1.2.1 - 2024-04-01 72 | 73 | ### Changed 74 | 75 | - Minor bug fixes in cite process and sitemap generation. 76 | 77 | ## 1.2.0 - 2024-03-08 78 | 79 | ### Changed 80 | 81 | - Update all GitHub Actions to fix "Node v16 deprecated" warnings. 82 | - Sources that Manubot doesn't know how to cite (e.g. wosuid:12345) are now ignored by default if they're from metasources. 83 | - Fix bug where passing tags to tags component manually doesn't work. 84 | - Fix bug in citation (and other) components when `lookup` is blank. 85 | - Fix nested tables bug. 86 | - Dark mode tweaks. 87 | - Various CSS tweaks and fixes. 88 | 89 | ### Added 90 | 91 | - Add `image` param to support blog post thumbnails. 92 | - Add `html-proofer` plugin that checks for broken images/links/etc. 93 | - Add `remove` flag to remove a source from a metasource. 94 | 95 | ## 1.1.6 - 2023-10-06 96 | 97 | ### Changed 98 | 99 | - Use latest minor versions of Python packages in auto-cite script. 100 | 101 | ## 1.1.5 - 2023-05-19 102 | 103 | ### Changed 104 | 105 | - Fix ORCID plugin bug and other cite process tweaks. 106 | 107 | ## 1.1.4 - 2023-04-28 108 | 109 | ### Changed 110 | 111 | - Fix ORCID plugin and other cite process bugs. 112 | 113 | ## 1.1.3 - 2023-04-20 114 | 115 | ### Changed 116 | 117 | - Fix first-time-setup mv bug. 118 | - Fix citation, float, and portrait component CSS. 119 | - Filter and trim citation info fields. 120 | 121 | ## 1.1.2 - 2023-04-11 122 | 123 | ### Changed 124 | 125 | - Fix first-time-setup rm bug. 126 | 127 | ## 1.1.1 - 2023-04-06 128 | 129 | ### Changed 130 | 131 | - Change member profile page from col layout to float. 132 | - Fix first time setup. Preserve config formatting and comments. 133 | - Improve Docker cite process behavior. 134 | - Fix post excerpt component start/end markers and special search attr chars. 135 | - Fix misc CSS. 136 | 137 | ### Added 138 | 139 | - Add show-title and show-subtitle site config options. 140 | - Include site subtitle in description meta tag. 141 | - Add user pull request template. 142 | - Add title and link fallbacks to citation component. 143 | 144 | ## 1.1.0 - 2023-03-17 145 | 146 | Add alert component, Docker support, accessibility fixes. 147 | 148 | ### Changed 149 | 150 | - Fix Lighthouse accessibility issues. 151 | - De-href components when link isn't provided (no hand cursor icon on hover or nav on click). 152 | - In search script, limit highlights by total count instead of char length. 153 | - Grid and link style tweaks. 154 | - Take ORCID icon from Font Awesome. 155 | - Misc bug fixes in tags script, float component. 156 | 157 | ### Added 158 | 159 | - Add Docker configuration and scripts for local previewing. 160 | - Add alert component and types. 161 | - Role icon in portrait component hoisted to top left. 162 | 163 | ## 1.0.0 - 2023-02-28 164 | 165 | First official release. 166 | 167 | High-level comparison with pre-releases: 168 | 169 | - Simpler configuration. 170 | - More automation, less setup. 171 | - More customization and flexibility. 172 | - Redesigned components. 173 | - New docs. 174 | - Complete rewrite. 175 | - Culmination of years of feedback. 176 | 177 | ### Changed 178 | 179 | - Template is no longer limited to GitHub Pages white-listed Jekyll plugins. Any plugins possible. 180 | - Pull request previews happen right within GitHub instead of needing Netlify. 181 | - Better versioning. `CITATION.cff` file now source of truth for version, and tags/releases enforced. 182 | - 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`. 183 | - Folder renames for clarity and for better separation of template and user content: `/auto-cite` → `/_cite`, `/css` → `/_styles`, `/js` → `/_scripts`. 184 | - Rename "Tools" page to "Projects" to be more clear and general purpose. 185 | - Rename `extra-links` to `buttons` in `sources.yaml` files. 186 | - Rename `theme.scss` to `-theme.scss`. 187 | - Rename/repurpose components: link → button, two-col → cols, gallery → grid. 188 | - Combine "link" and "role" data lists into single `types.yaml` map. 189 | - Redesign components, change parameters and behavior. 190 | - Update Font Awesome icon names from v5 to v6. 191 | - Change placeholder text, images, and other images. 192 | - Use CSS variables instead of Sass variables. 193 | - Simplify caching method in cite process. 194 | - Simplify Liquid code by including custom Ruby plugins. 195 | - Simplify styles and scripts. 196 | 197 | ### Added 198 | 199 | - New docs at greene-lab.gitbook.io/lab-website-template-docs. 200 | - Add automations for first time setup and URL change. 201 | - Write PubMed and Google Scholar automatic citation plugins. 202 | - Automatic citations through GitHub Actions should now work from (most) forks. 203 | - Add optional description and type params for citations. 204 | - Add periodic cite process run that opens a pull request. 205 | - List component filters can now accept arbitrary regex. 206 | - Add light/dark mode toggle. 207 | - Pre-install selection of useful Jekyll plugins, namely Jekyll Spaceship. 208 | - Add author portrait and updated date for blog posts. 209 | - Add richer metadata for SEO. 210 | - Google Fonts link determined automatically from theme file. 211 | 212 | ### Removed 213 | 214 | - Remove options from `_config.yaml` to simplify configuration: `baseurl`, `auto-cite`, `logo`. 215 | - Remove `/favicons` folder, hardcode files for logo, icon, and share in `/images`. 216 | - Remove `palettes.scss` and `mixins.scss`. 217 | - Remove banner component (same thing can be achieved with full width section and figure components). 218 | - Remove role component. Combine with portrait component. 219 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # citation metadata for the template itself 2 | 3 | title: "Lab Website Template" 4 | version: 1.3.5 5 | date-released: 2025-05-12 6 | url: "https://github.com/greenelab/lab-website-template" 7 | authors: 8 | - family-names: "Rubinetti" 9 | given-names: "Vincent" 10 | orcid: "https://orcid.org/0000-0002-4655-3773" 11 | - family-names: "Greene" 12 | given-names: "Casey" 13 | orcid: "https://orcid.org/0000-0001-8713-9213" 14 | cff-version: 1.2.0 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # jekyll 4 | gem "jekyll", "~> 4.3" 5 | gem "webrick", "~> 1.7" 6 | 7 | gem "html-proofer", "~> 5.0" 8 | 9 | # plugins 10 | group :jekyll_plugins do 11 | gem "jekyll-spaceship" 12 | gem "jekyll-sitemap" 13 | gem "jekyll-redirect-from" 14 | gem "jekyll-feed" 15 | gem "jekyll-last-modified-at" 16 | end 17 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | Ascii85 (1.1.0) 5 | addressable (2.8.1) 6 | public_suffix (>= 2.0.2, < 6.0) 7 | afm (0.2.2) 8 | async (2.8.1) 9 | console (~> 1.10) 10 | fiber-annotation 11 | io-event (~> 1.1) 12 | timers (~> 4.1) 13 | colorator (1.1.0) 14 | concurrent-ruby (1.2.2) 15 | console (1.23.4) 16 | fiber-annotation 17 | fiber-local 18 | json 19 | em-websocket (0.5.3) 20 | eventmachine (>= 0.12.9) 21 | http_parser.rb (~> 0) 22 | ethon (0.16.0) 23 | ffi (>= 1.15.0) 24 | eventmachine (1.2.7) 25 | ffi (1.15.5) 26 | ffi (1.15.5-x64-mingw-ucrt) 27 | fiber-annotation (0.2.0) 28 | fiber-local (1.0.0) 29 | forwardable-extended (2.6.0) 30 | gemoji (3.0.1) 31 | google-protobuf (3.22.0) 32 | google-protobuf (3.22.0-arm64-darwin) 33 | google-protobuf (3.22.0-x64-mingw-ucrt) 34 | hashery (2.1.2) 35 | html-proofer (5.0.8) 36 | addressable (~> 2.3) 37 | async (~> 2.1) 38 | nokogiri (~> 1.13) 39 | pdf-reader (~> 2.11) 40 | rainbow (~> 3.0) 41 | typhoeus (~> 1.3) 42 | yell (~> 2.0) 43 | zeitwerk (~> 2.5) 44 | http_parser.rb (0.8.0) 45 | i18n (1.12.0) 46 | concurrent-ruby (~> 1.0) 47 | io-event (1.4.4) 48 | jekyll (4.3.2) 49 | addressable (~> 2.4) 50 | colorator (~> 1.0) 51 | em-websocket (~> 0.5) 52 | i18n (~> 1.0) 53 | jekyll-sass-converter (>= 2.0, < 4.0) 54 | jekyll-watch (~> 2.0) 55 | kramdown (~> 2.3, >= 2.3.1) 56 | kramdown-parser-gfm (~> 1.0) 57 | liquid (~> 4.0) 58 | mercenary (>= 0.3.6, < 0.5) 59 | pathutil (~> 0.9) 60 | rouge (>= 3.0, < 5.0) 61 | safe_yaml (~> 1.0) 62 | terminal-table (>= 1.8, < 4.0) 63 | webrick (~> 1.7) 64 | jekyll-feed (0.17.0) 65 | jekyll (>= 3.7, < 5.0) 66 | jekyll-last-modified-at (1.3.0) 67 | jekyll (>= 3.7, < 5.0) 68 | posix-spawn (~> 0.3.9) 69 | jekyll-redirect-from (0.16.0) 70 | jekyll (>= 3.3, < 5.0) 71 | jekyll-sass-converter (3.0.0) 72 | sass-embedded (~> 1.54) 73 | jekyll-sitemap (1.4.0) 74 | jekyll (>= 3.7, < 5.0) 75 | jekyll-spaceship (0.10.2) 76 | gemoji (~> 3.0) 77 | jekyll (>= 3.6, < 5.0) 78 | nokogiri (~> 1.6) 79 | rainbow (~> 3.0) 80 | jekyll-watch (2.2.1) 81 | listen (~> 3.0) 82 | json (2.7.1) 83 | kramdown (2.4.0) 84 | rexml 85 | kramdown-parser-gfm (1.1.0) 86 | kramdown (~> 2.0) 87 | liquid (4.0.4) 88 | listen (3.8.0) 89 | rb-fsevent (~> 0.10, >= 0.10.3) 90 | rb-inotify (~> 0.9, >= 0.9.10) 91 | mercenary (0.4.0) 92 | mini_portile2 (2.8.1) 93 | nokogiri (1.13.10) 94 | mini_portile2 (~> 2.8.0) 95 | racc (~> 1.4) 96 | nokogiri (1.13.10-arm64-darwin) 97 | racc (~> 1.4) 98 | pathutil (0.16.2) 99 | forwardable-extended (~> 2.6) 100 | pdf-reader (2.12.0) 101 | Ascii85 (~> 1.0) 102 | afm (~> 0.2.1) 103 | hashery (~> 2.0) 104 | ruby-rc4 105 | ttfunk 106 | posix-spawn (0.3.15) 107 | public_suffix (5.0.1) 108 | racc (1.6.2) 109 | rainbow (3.1.1) 110 | rake (13.1.0) 111 | rb-fsevent (0.11.2) 112 | rb-inotify (0.10.1) 113 | ffi (~> 1.0) 114 | rexml (3.2.5) 115 | rouge (3.30.0) 116 | ruby-rc4 (0.1.5) 117 | safe_yaml (1.0.5) 118 | sass-embedded (1.58.3) 119 | google-protobuf (~> 3.21) 120 | rake (>= 10.0.0) 121 | sass-embedded (1.58.3-arm64-darwin) 122 | google-protobuf (~> 3.21) 123 | sass-embedded (1.58.3-x64-mingw-ucrt) 124 | google-protobuf (~> 3.21) 125 | terminal-table (3.0.2) 126 | unicode-display_width (>= 1.1.1, < 3) 127 | timers (4.3.5) 128 | ttfunk (1.7.0) 129 | typhoeus (1.4.1) 130 | ethon (>= 0.9.0) 131 | unicode-display_width (2.4.2) 132 | webrick (1.8.1) 133 | yell (2.2.2) 134 | zeitwerk (2.6.13) 135 | 136 | PLATFORMS 137 | aarch64-linux 138 | linux 139 | universal-darwin-21 140 | universal-darwin-22 141 | x64-mingw-ucrt 142 | x64-mingw32 143 | x64-unknown 144 | x86_64-linux 145 | 146 | DEPENDENCIES 147 | html-proofer (~> 5.0) 148 | jekyll (~> 4.3) 149 | jekyll-feed 150 | jekyll-last-modified-at 151 | jekyll-redirect-from 152 | jekyll-sitemap 153 | jekyll-spaceship 154 | webrick (~> 1.7) 155 | 156 | BUNDLED WITH 157 | 2.5.6 158 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Greene Laboratory 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Lab Website Template

2 |

3 | Lab Website Template 4 |

5 | 6 | Lab Website Template (LWT) is an easy-to-use, flexible website template for labs. 7 | Spend less time worrying about managing a website and citations, and more time running your lab. 8 | 9 | 👇👇 **Get Started** 👇👇 10 | 11 | [**Documentation**](https://greene-lab.gitbook.io/lab-website-template-docs) 12 | 13 | ## Key Features 14 | 15 | - 🤖 Based on Git, GitHub, and Jekyll. 16 | - 📜 Automatically generated citations from simple identifiers (DOI, PubMed, ORCID, and many more) using Manubot. E.g. `doi:1234/5678` -> `title`, `authors`, `publisher`, `date`, etc. 17 | - 🧱 A comprehensive and flexible suite of pre-made components (building blocks) for structuring and styling your website: 18 | - Formatted tables, code blocks, figures, and other basic elements. 19 | - Citations with thumbnails and other rich details. 20 | - List large sets of data with flexible filters and components. 21 | - ...many more 22 | - 👁️ Automatic pull request previews. 23 | - ⚙️ Easy and automated configuration. 24 | - 👥 Team member pages with bios, roles, and social media links. 25 | - 🖋️ Blog posts with tags and rich content. 26 | - 📱 Works and looks good on desktop and mobile. 27 | - 🤝 Great documentation and support (if we do say so ourselves). 28 | - ... and much more! 29 | 30 | ![GitHub last commit](https://img.shields.io/github/last-commit/greenelab/lab-website-template) 31 | -------------------------------------------------------------------------------- /_cite/.cache/cache.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenelab/lab-website-template/ebeb5e200ae5d61df5265f5594f59b5a45d2d4fa/_cite/.cache/cache.db -------------------------------------------------------------------------------- /_cite/cite.py: -------------------------------------------------------------------------------- 1 | """ 2 | cite process to convert sources and metasources into full citations 3 | """ 4 | 5 | import traceback 6 | from importlib import import_module 7 | from pathlib import Path 8 | from dotenv import load_dotenv 9 | from util import * 10 | 11 | 12 | # load environment variables 13 | load_dotenv() 14 | 15 | 16 | # save errors/warnings for reporting at end 17 | errors = [] 18 | warnings = [] 19 | 20 | # output citations file 21 | output_file = "_data/citations.yaml" 22 | 23 | 24 | log() 25 | 26 | log("Compiling sources") 27 | 28 | # compiled list of sources 29 | sources = [] 30 | 31 | # in-order list of plugins to run 32 | plugins = ["google-scholar", "pubmed", "orcid", "sources"] 33 | 34 | # loop through plugins 35 | for plugin in plugins: 36 | # convert into path object 37 | plugin = Path(f"plugins/{plugin}.py") 38 | 39 | log(f"Running {plugin.stem} plugin") 40 | 41 | # get all data files to process with current plugin 42 | files = Path.cwd().glob(f"_data/{plugin.stem}*.*") 43 | files = list(filter(lambda p: p.suffix in [".yaml", ".yml", ".json"], files)) 44 | 45 | log(f"Found {len(files)} {plugin.stem}* data file(s)", indent=1) 46 | 47 | # loop through data files 48 | for file in files: 49 | log(f"Processing data file {file.name}", indent=1) 50 | 51 | # load data from file 52 | try: 53 | data = load_data(file) 54 | # check if file in correct format 55 | if not list_of_dicts(data): 56 | raise Exception(f"{file.name} data file not a list of dicts") 57 | except Exception as e: 58 | log(e, indent=2, level="ERROR") 59 | errors.append(e) 60 | continue 61 | 62 | # loop through data entries 63 | for index, entry in enumerate(data): 64 | log(f"Processing entry {index + 1} of {len(data)}, {label(entry)}", level=2) 65 | 66 | # run plugin on data entry to expand into multiple sources 67 | try: 68 | expanded = import_module(f"plugins.{plugin.stem}").main(entry) 69 | # check that plugin returned correct format 70 | if not list_of_dicts(expanded): 71 | raise Exception(f"{plugin.stem} plugin didn't return list of dicts") 72 | # catch any plugin error 73 | except Exception as e: 74 | # log detailed pre-formatted/colored trace 75 | print(traceback.format_exc()) 76 | # log high-level error 77 | log(e, indent=3, level="ERROR") 78 | errors.append(e) 79 | continue 80 | 81 | # loop through sources 82 | for source in expanded: 83 | if plugin.stem != "sources": 84 | log(label(source), level=3) 85 | 86 | # include meta info about source 87 | source["plugin"] = plugin.name 88 | source["file"] = file.name 89 | 90 | # add source to compiled list 91 | sources.append(source) 92 | 93 | if plugin.stem != "sources": 94 | log(f"{len(expanded)} source(s)", indent=3) 95 | 96 | 97 | log("Merging sources by id") 98 | 99 | # merge sources with matching (non-blank) ids 100 | for a in range(0, len(sources)): 101 | a_id = get_safe(sources, f"{a}.id", "") 102 | if not a_id: 103 | continue 104 | for b in range(a + 1, len(sources)): 105 | b_id = get_safe(sources, f"{b}.id", "") 106 | if b_id == a_id: 107 | log(f"Found duplicate {b_id}", indent=2) 108 | sources[a].update(sources[b]) 109 | sources[b] = {} 110 | sources = [entry for entry in sources if entry] 111 | 112 | 113 | log(f"{len(sources)} total source(s) to cite") 114 | 115 | 116 | log() 117 | 118 | log("Generating citations") 119 | 120 | # list of new citations 121 | citations = [] 122 | 123 | 124 | # loop through compiled sources 125 | for index, source in enumerate(sources): 126 | log(f"Processing source {index + 1} of {len(sources)}, {label(source)}") 127 | 128 | # if explicitly flagged, remove/ignore entry 129 | if get_safe(source, "remove", False) == True: 130 | continue 131 | 132 | # new citation data for source 133 | citation = {} 134 | 135 | # source id 136 | _id = get_safe(source, "id", "").strip() 137 | 138 | # Manubot doesn't work without an id 139 | if _id: 140 | log("Using Manubot to generate citation", indent=1) 141 | 142 | try: 143 | # run Manubot and set citation 144 | citation = cite_with_manubot(_id) 145 | 146 | # if Manubot cannot cite source 147 | except Exception as e: 148 | plugin = get_safe(source, "plugin", "") 149 | file = get_safe(source, "file", "") 150 | # if regular source (id entered by user), throw error 151 | if plugin == "sources.py": 152 | log(e, indent=3, level="ERROR") 153 | errors.append(f"Manubot could not generate citation for source {_id}") 154 | # otherwise, if from metasource (id retrieved from some third-party API), just warn 155 | else: 156 | log(e, indent=3, level="WARNING") 157 | warnings.append( 158 | f"Manubot could not generate citation for source {_id} (from {file} with {plugin})" 159 | ) 160 | # discard source from citations 161 | continue 162 | 163 | # preserve fields from input source, overriding existing fields 164 | citation.update(source) 165 | 166 | # ensure date in proper format for correct date sorting 167 | if get_safe(citation, "date", ""): 168 | citation["date"] = format_date(get_safe(citation, "date", "")) 169 | 170 | # add new citation to list 171 | citations.append(citation) 172 | 173 | 174 | log() 175 | 176 | log("Saving updated citations") 177 | 178 | 179 | # save new citations 180 | try: 181 | save_data(output_file, citations) 182 | except Exception as e: 183 | log(e, level="ERROR") 184 | errors.append(e) 185 | 186 | 187 | log() 188 | 189 | 190 | # exit at end, so user can see all errors/warnings in one run 191 | if len(warnings): 192 | log(f"{len(warnings)} warning(s) occurred above", level="WARNING") 193 | for warning in warnings: 194 | log(warning, indent=1, level="WARNING") 195 | 196 | if len(errors): 197 | log(f"{len(errors)} error(s) occurred above", level="ERROR") 198 | for error in errors: 199 | log(error, indent=1, level="ERROR") 200 | log() 201 | exit(1) 202 | 203 | else: 204 | log("All done!", level="SUCCESS") 205 | 206 | log() 207 | -------------------------------------------------------------------------------- /_cite/plugins/google-scholar.py: -------------------------------------------------------------------------------- 1 | import os 2 | from serpapi import GoogleSearch 3 | from util import * 4 | 5 | 6 | def main(entry): 7 | """ 8 | receives single list entry from google-scholar data file 9 | returns list of sources to cite 10 | """ 11 | 12 | # get api key (serp api key to access google scholar) 13 | api_key = os.environ.get("GOOGLE_SCHOLAR_API_KEY", "") 14 | if not api_key: 15 | raise Exception('No "GOOGLE_SCHOLAR_API_KEY" env var') 16 | 17 | # serp api properties 18 | params = { 19 | "engine": "google_scholar_author", 20 | "api_key": api_key, 21 | "num": 100, # max allowed 22 | } 23 | 24 | # get id from entry 25 | _id = get_safe(entry, "gsid", "") 26 | if not _id: 27 | raise Exception('No "gsid" key') 28 | 29 | # query api 30 | @log_cache 31 | @cache.memoize(name=__file__, expire=1 * (60 * 60 * 24)) 32 | def query(_id): 33 | params["author_id"] = _id 34 | return get_safe(GoogleSearch(params).get_dict(), "articles", []) 35 | 36 | response = query(_id) 37 | 38 | # list of sources to return 39 | sources = [] 40 | 41 | # go through response and format sources 42 | for work in response: 43 | # create source 44 | year = get_safe(work, "year", "") 45 | source = { 46 | "id": get_safe(work, "citation_id", ""), 47 | # api does not provide Manubot-citeable id, so keep citation details 48 | "title": get_safe(work, "title", ""), 49 | "authors": list(map(str.strip, get_safe(work, "authors", "").split(","))), 50 | "publisher": get_safe(work, "publication", ""), 51 | "date": (year + "-01-01") if year else "", 52 | "link": get_safe(work, "link", ""), 53 | } 54 | 55 | # copy fields from entry to source 56 | source.update(entry) 57 | 58 | # add source to list 59 | sources.append(source) 60 | 61 | return sources 62 | -------------------------------------------------------------------------------- /_cite/plugins/orcid.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib.request import Request, urlopen 3 | from util import * 4 | from manubot.cite.handlers import prefix_to_handler as manubot_prefixes 5 | 6 | 7 | def main(entry): 8 | """ 9 | receives single list entry from orcid data file 10 | returns list of sources to cite 11 | """ 12 | 13 | # orcid api 14 | endpoint = "https://pub.orcid.org/v3.0/$ORCID/works" 15 | headers = {"Accept": "application/json"} 16 | 17 | # get id from entry 18 | _id = get_safe(entry, "orcid", "") 19 | if not _id: 20 | raise Exception('No "orcid" key') 21 | 22 | # query api 23 | @log_cache 24 | @cache.memoize(name=__file__, expire=1 * (60 * 60 * 24)) 25 | def query(_id): 26 | url = endpoint.replace("$ORCID", _id) 27 | request = Request(url=url, headers=headers) 28 | response = json.loads(urlopen(request).read()) 29 | return get_safe(response, "group", []) 30 | 31 | response = query(_id) 32 | 33 | # list of sources to return 34 | sources = [] 35 | 36 | # go through response structure and pull out ids e.g. doi:1234/56789 37 | for work in response: 38 | # get list of ids 39 | ids = [] 40 | for summary in get_safe(work, "work-summary", []): 41 | ids = ids + get_safe(summary, "external-ids.external-id", []) 42 | 43 | # find first id of particular "relationship" type 44 | _id = next( 45 | ( 46 | id 47 | for id in ids 48 | if get_safe(id, "external-id-relationship", "") 49 | in ["self", "version-of", "part-of"] 50 | ), 51 | ids[0] if len(ids) > 0 else None, 52 | ) 53 | 54 | if _id == None: 55 | continue 56 | 57 | # get id and id-type from response 58 | id_type = get_safe(_id, "external-id-type", "") 59 | id_value = get_safe(_id, "external-id-value", "") 60 | 61 | # create source 62 | source = {"id": f"{id_type}:{id_value}"} 63 | 64 | # if not an id type that Manubot can cite, keep citation details 65 | if id_type not in manubot_prefixes: 66 | # get summaries 67 | summaries = get_safe(work, "work-summary", []) 68 | 69 | # get first summary with defined sub-value 70 | def first(get_func): 71 | return next( 72 | (value for value in map(get_func, summaries) if value), None 73 | ) 74 | 75 | # get title 76 | title = first(lambda s: get_safe(s, "title.title.value", "")) 77 | 78 | # get publisher 79 | publisher = first(lambda s: get_safe(s, "journal-title.value", "")) 80 | 81 | # get date 82 | date = ( 83 | get_safe(work, "last-modified-date.value") 84 | or first(lambda s: get_safe(s, "last-modified-date.value")) 85 | or get_safe(work, "created-date.value") 86 | or first(lambda s: get_safe(s, "created-date.value")) 87 | or 0 88 | ) 89 | 90 | # get link 91 | link = first(lambda s: get_safe(s, "url.value", "")) 92 | 93 | # keep available details 94 | if title: 95 | source["title"] = title 96 | if publisher: 97 | source["publisher"] = publisher 98 | if date: 99 | source["date"] = format_date(date) 100 | if link: 101 | source["link"] = link 102 | 103 | # copy fields from entry to source 104 | source.update(entry) 105 | 106 | # add source to list 107 | sources.append(source) 108 | 109 | return sources 110 | -------------------------------------------------------------------------------- /_cite/plugins/pubmed.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib.request import Request, urlopen 3 | from urllib.parse import quote 4 | from util import * 5 | 6 | 7 | def main(entry): 8 | """ 9 | receives single list entry from pubmed data file 10 | returns list of sources to cite 11 | """ 12 | 13 | # ncbi api 14 | endpoint = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=$TERM&retmode=json&retmax=1000&usehistory=y" 15 | 16 | # get id from entry 17 | _id = get_safe(entry, "term", "") 18 | if not _id: 19 | raise Exception('No "term" key') 20 | 21 | # query api 22 | @log_cache 23 | @cache.memoize(name=__file__, expire=1 * (60 * 60 * 24)) 24 | def query(_id): 25 | url = endpoint.replace("$TERM", quote(_id)) 26 | request = Request(url=url) 27 | response = json.loads(urlopen(request).read()) 28 | return get_safe(response, "esearchresult.idlist", []) 29 | 30 | response = query(_id) 31 | 32 | # list of sources to return 33 | sources = [] 34 | 35 | # go through response and format sources 36 | for _id in response: 37 | # create source 38 | source = {"id": f"pubmed:{_id}"} 39 | 40 | # copy fields from entry to source 41 | source.update(entry) 42 | 43 | # add source to list 44 | sources.append(source) 45 | 46 | return sources 47 | -------------------------------------------------------------------------------- /_cite/plugins/sources.py: -------------------------------------------------------------------------------- 1 | def main(entry): 2 | """ 3 | receives single list entry from sources data file 4 | returns list of sources to cite 5 | """ 6 | return [entry] 7 | -------------------------------------------------------------------------------- /_cite/requirements.txt: -------------------------------------------------------------------------------- 1 | manubot~=0.6 2 | PyYAML~=6.0 3 | diskcache~=5.6 4 | rich~=13.6 5 | python-dotenv~=0.21 6 | google-search-results~=2.4 7 | 8 | -------------------------------------------------------------------------------- /_cite/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | utility functions for cite process and plugins 3 | """ 4 | 5 | import subprocess 6 | import json 7 | import yaml 8 | from yaml.loader import SafeLoader 9 | from pathlib import Path 10 | from datetime import date, datetime 11 | from rich import print 12 | from diskcache import Cache 13 | 14 | 15 | # cache for time-consuming network requests 16 | cache = Cache("./_cite/.cache") 17 | 18 | 19 | # clear expired items from cache 20 | cache.expire() 21 | 22 | 23 | def log_cache(func): 24 | """ 25 | decorator to use around memoized function to log if cached or or not 26 | """ 27 | 28 | def wrap(*args): 29 | key = func.__cache_key__(*args) 30 | if key in cache: 31 | log(" (from cache)", level="INFO", newline=False) 32 | return func(*args) 33 | 34 | return wrap 35 | 36 | 37 | def log(message="\n--------------------\n", indent=0, level="", newline=True): 38 | """ 39 | log to terminal, color determined by indent and level 40 | """ 41 | 42 | colors = { 43 | 0: "[orange1]", 44 | 1: "[salmon1]", 45 | 2: "[violet]", 46 | 3: "[sky_blue1]", 47 | "ERROR": "[#F43F5E]", 48 | "WARNING": "[#EAB308]", 49 | "SUCCESS": "[#10B981]", 50 | "INFO": "[grey70]", 51 | } 52 | prefixes = { 53 | "ERROR": "🚫 ERROR: ", 54 | "WARNING": "⚠️ WARNING: ", 55 | } 56 | color = get_safe(colors, level, "") or get_safe(colors, indent, "") or "[white]" 57 | prefix = get_safe(prefixes, level, "") 58 | if newline: 59 | print() 60 | print(indent * " " + color + prefix + str(message) + "[/]", end="", flush=True) 61 | 62 | 63 | def label(entry): 64 | """ 65 | get "label" of dict entry (for logging purposes) 66 | """ 67 | 68 | return str(list(entry.keys())[0]) + ": " + str(list(entry.values())[0]) 69 | 70 | 71 | def get_safe(item, path, default=None): 72 | """ 73 | safely access value in nested lists/dicts 74 | """ 75 | 76 | for part in str(path).split("."): 77 | try: 78 | part = int(part) 79 | except ValueError: 80 | part = part 81 | try: 82 | item = item[part] 83 | except (KeyError, IndexError, AttributeError, TypeError): 84 | return default 85 | return item 86 | 87 | 88 | def list_of_dicts(data): 89 | """ 90 | check if data is list of dicts 91 | """ 92 | 93 | return isinstance(data, list) and all(isinstance(entry, dict) for entry in data) 94 | 95 | 96 | def format_date(_date): 97 | """ 98 | format date as YYYY-MM-DD, or no date if malformed 99 | """ 100 | 101 | if isinstance(_date, int): 102 | return datetime.fromtimestamp(_date // 1000.0).strftime("%Y-%m-%d") 103 | if isinstance(_date, (date, datetime)): 104 | return _date.strftime("%Y-%m-%d") 105 | try: 106 | return datetime.strptime(_date, "%Y-%m-%d").strftime("%Y-%m-%d") 107 | except Exception: 108 | return "" 109 | 110 | 111 | def load_data(path): 112 | """ 113 | read data from yaml or json file 114 | """ 115 | 116 | # convert to path object 117 | path = Path(path) 118 | 119 | # check if file exists 120 | if not path.is_file(): 121 | raise Exception("Can't find file") 122 | 123 | # try to open file 124 | try: 125 | file = open(path, encoding="utf8") 126 | except Exception as e: 127 | raise Exception(e or "Can't open file") 128 | 129 | # try to parse as yaml 130 | try: 131 | with file: 132 | data = yaml.load(file, Loader=SafeLoader) 133 | except Exception: 134 | raise Exception("Can't parse file. Make sure it's valid YAML.") 135 | 136 | # if no errors, return data 137 | return data 138 | 139 | 140 | def save_data(path, data): 141 | """ 142 | write data to yaml file 143 | """ 144 | 145 | # convert to path object 146 | path = Path(path) 147 | 148 | # try to open file 149 | try: 150 | file = open(path, mode="w") 151 | except Exception: 152 | raise Exception("Can't open file for writing") 153 | 154 | # prevent yaml anchors/aliases (pointers) 155 | yaml.Dumper.ignore_aliases = lambda *args: True 156 | 157 | # try to save data as yaml 158 | try: 159 | with file: 160 | yaml.dump(data, file, default_flow_style=False, sort_keys=False) 161 | except Exception: 162 | raise Exception("Can't save YAML to file") 163 | 164 | # write warning note to top of file 165 | note = "# DO NOT EDIT, GENERATED AUTOMATICALLY" 166 | try: 167 | with open(path, "r") as file: 168 | data = file.read() 169 | with open(path, "w") as file: 170 | file.write(f"{note}\n\n{data}") 171 | except Exception: 172 | raise Exception("Can't write to file") 173 | 174 | 175 | @log_cache 176 | @cache.memoize(name="manubot", expire=90 * (60 * 60 * 24)) 177 | def cite_with_manubot(_id): 178 | """ 179 | generate citation data for source id with Manubot 180 | """ 181 | 182 | # run Manubot 183 | try: 184 | commands = ["manubot", "cite", _id, "--log-level=WARNING"] 185 | output = subprocess.Popen(commands, stdout=subprocess.PIPE).communicate() 186 | except Exception as e: 187 | log(e, indent=3) 188 | raise Exception("Manubot could not generate citation") 189 | 190 | # parse results as json 191 | try: 192 | manubot = json.loads(output[0])[0] 193 | except Exception: 194 | raise Exception("Couldn't parse Manubot response") 195 | 196 | # new citation with only needed info 197 | citation = {} 198 | 199 | # original id 200 | citation["id"] = _id 201 | 202 | # title 203 | citation["title"] = get_safe(manubot, "title", "").strip() 204 | 205 | # authors 206 | citation["authors"] = [] 207 | for author in get_safe(manubot, "author", {}): 208 | given = get_safe(author, "given", "").strip() 209 | family = get_safe(author, "family", "").strip() 210 | if given or family: 211 | citation["authors"].append(" ".join([given, family])) 212 | 213 | # publisher 214 | container = get_safe(manubot, "container-title", "").strip() 215 | collection = get_safe(manubot, "collection-title", "").strip() 216 | publisher = get_safe(manubot, "publisher", "").strip() 217 | citation["publisher"] = container or publisher or collection or "" 218 | 219 | # extract date part 220 | def date_part(citation, index): 221 | try: 222 | return citation["issued"]["date-parts"][0][index] 223 | except (KeyError, IndexError, TypeError): 224 | return "" 225 | 226 | # date 227 | year = date_part(manubot, 0) 228 | if year: 229 | # fallbacks for month and day 230 | month = date_part(manubot, 1) or "1" 231 | day = date_part(manubot, 2) or "1" 232 | citation["date"] = format_date(f"{year}-{month}-{day}") 233 | else: 234 | # if no year, consider date missing data 235 | citation["date"] = "" 236 | 237 | # link 238 | citation["link"] = get_safe(manubot, "URL", "").strip() 239 | 240 | # return citation data 241 | return citation 242 | -------------------------------------------------------------------------------- /_config.yaml: -------------------------------------------------------------------------------- 1 | # site properties and page defaults 2 | title: Lab Website Template 3 | subtitle: by the Greene Lab 4 | description: An easy-to-use, flexible website template for labs, with automatic citations, GitHub tag imports, pre-built components, and more. 5 | header: images/background.jpg 6 | footer: images/background.jpg 7 | proofer: false 8 | 9 | # site social media and other links 10 | links: 11 | email: contact@your-lab.com 12 | orcid: 0000-0001-8713-9213 13 | google-scholar: ETJoidYAAAAJ 14 | github: your-lab 15 | twitter: YourLabHandle 16 | youtube: YourLabChannel 17 | 18 | ### jekyll settings 19 | 20 | # front matter defaults 21 | defaults: 22 | # all markdown files 23 | - scope: 24 | path: "" 25 | values: 26 | layout: default 27 | # markdown files in /_members 28 | - scope: 29 | type: "members" 30 | values: 31 | layout: member 32 | # markdown files in /_posts 33 | - scope: 34 | type: "posts" 35 | values: 36 | layout: post 37 | 38 | collections: 39 | # generate page for each member 40 | members: 41 | output: true 42 | # generate page for each post 43 | posts: 44 | output: true 45 | 46 | # jekyll plugins 47 | plugins: 48 | - jekyll-spaceship 49 | - jekyll-sitemap 50 | - jekyll-redirect-from 51 | - jekyll-feed 52 | - jekyll-last-modified-at 53 | 54 | # code block syntax highlighting 55 | highlighter: rouge 56 | 57 | # jekyll theme 58 | theme: null 59 | 60 | # sass settings 61 | sass: 62 | sass_dir: _styles 63 | 64 | # force jekyll to include certain files/folders 65 | include: 66 | - _styles 67 | - _scripts 68 | 69 | # force jekyll to exclude certain files/folders 70 | exclude: 71 | - README.md 72 | - LICENSE.md 73 | - CITATION.cff 74 | -------------------------------------------------------------------------------- /_data/citations.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT, GENERATED AUTOMATICALLY 2 | 3 | - id: doi:10.1093/nar/gkad1082 4 | title: "The Monarch Initiative in 2024: an analytic platform integrating phenotypes,\ 5 | \ genes\_and diseases across species" 6 | authors: 7 | - Tim E Putman 8 | - Kevin Schaper 9 | - Nicolas Matentzoglu 10 | - "Vincent\_P Rubinetti" 11 | - "Faisal\_S Alquaddoomi" 12 | - Corey Cox 13 | - J Harry Caufield 14 | - Glass Elsarboukh 15 | - Sarah Gehrke 16 | - Harshad Hegde 17 | - "Justin\_T Reese" 18 | - Ian Braun 19 | - "Richard\_M Bruskiewich" 20 | - Luca Cappelletti 21 | - Seth Carbon 22 | - "Anita\_R Caron" 23 | - "Lauren\_E Chan" 24 | - "Christopher\_G Chute" 25 | - "Katherina\_G Cortes" 26 | - "Vin\xEDcius De\_Souza" 27 | - Tommaso Fontana 28 | - "Nomi\_L Harris" 29 | - "Emily\_L Hartley" 30 | - Eric Hurwitz 31 | - "Julius\_O B Jacobsen" 32 | - Madan Krishnamurthy 33 | - "Bryan\_J Laraway" 34 | - "James\_A McLaughlin" 35 | - "Julie\_A McMurry" 36 | - "Sierra\_A T Moxon" 37 | - "Kathleen\_R Mullen" 38 | - "Shawn\_T O\u2019Neil" 39 | - "Kent\_A Shefchek" 40 | - Ray Stefancsik 41 | - Sabrina Toro 42 | - "Nicole\_A Vasilevsky" 43 | - "Ramona\_L Walls" 44 | - "Patricia\_L Whetzel" 45 | - David Osumi-Sutherland 46 | - Damian Smedley 47 | - "Peter\_N Robinson" 48 | - "Christopher\_J Mungall" 49 | - "Melissa\_A Haendel" 50 | - "Monica\_C Munoz-Torres" 51 | publisher: Nucleic Acids Research 52 | date: '2023-11-24' 53 | link: https://doi.org/gs6kmr 54 | orcid: 0000-0002-4655-3773 55 | plugin: orcid.py 56 | file: orcid.yaml 57 | - id: doi:10.1101/2023.10.11.560955 58 | title: Integration of 168,000 samples reveals global patterns of the human gut microbiome 59 | authors: 60 | - Richard J. Abdill 61 | - Samantha P. Graham 62 | - Vincent Rubinetti 63 | - Frank W. Albert 64 | - Casey S. Greene 65 | - Sean Davis 66 | - Ran Blekhman 67 | publisher: Cold Spring Harbor Laboratory 68 | date: '2023-10-11' 69 | link: https://doi.org/gsvf5z 70 | orcid: 0000-0002-4655-3773 71 | plugin: orcid.py 72 | file: orcid.yaml 73 | - id: doi:10.1093/nar/gkad289 74 | title: 'MyGeneset.info: an interactive and programmatic platform for community-curated 75 | and user-created collections of genes' 76 | authors: 77 | - Ricardo Avila 78 | - Vincent Rubinetti 79 | - Xinghua Zhou 80 | - Dongbo Hu 81 | - Zhongchao Qian 82 | - Marco Alvarado Cano 83 | - Everaldo Rodolpho 84 | - Ginger Tsueng 85 | - Casey Greene 86 | - Chunlei Wu 87 | publisher: Nucleic Acids Research 88 | date: '2023-04-18' 89 | link: https://doi.org/gr5hb5 90 | orcid: 0000-0002-4655-3773 91 | plugin: orcid.py 92 | file: orcid.yaml 93 | - id: doi:10.1101/2023.01.05.522941 94 | title: Hetnet connectivity search provides rapid insights into how two biomedical 95 | entities are related 96 | authors: 97 | - Daniel S. Himmelstein 98 | - Michael Zietz 99 | - Vincent Rubinetti 100 | - Kyle Kloster 101 | - Benjamin J. Heil 102 | - Faisal Alquaddoomi 103 | - Dongbo Hu 104 | - David N. Nicholson 105 | - Yun Hao 106 | - Blair D. Sullivan 107 | - Michael W. Nagle 108 | - Casey S. Greene 109 | publisher: Cold Spring Harbor Laboratory 110 | date: '2023-01-07' 111 | link: https://doi.org/grmcb9 112 | orcid: 0000-0002-4655-3773 113 | plugin: orcid.py 114 | file: orcid.yaml 115 | - id: doi:10.1093/gigascience/giad047 116 | title: Hetnet connectivity search provides rapid insights into how biomedical entities 117 | are related 118 | authors: 119 | - Daniel S Himmelstein 120 | - Michael Zietz 121 | - Vincent Rubinetti 122 | - Kyle Kloster 123 | - Benjamin J Heil 124 | - Faisal Alquaddoomi 125 | - Dongbo Hu 126 | - David N Nicholson 127 | - Yun Hao 128 | - Blair D Sullivan 129 | - Michael W Nagle 130 | - Casey S Greene 131 | publisher: GigaScience 132 | date: '2022-12-28' 133 | link: https://doi.org/gsd85n 134 | orcid: 0000-0002-4655-3773 135 | plugin: orcid.py 136 | file: orcid.yaml 137 | - id: doi:10.1101/2022.02.18.461833 138 | title: 'MolEvolvR: A web-app for characterizing proteins using molecular evolution 139 | and phylogeny' 140 | authors: 141 | - Jacob D Krol 142 | - Joseph T Burke 143 | - Samuel Z Chen 144 | - Lo M Sosinski 145 | - Faisal S Alquaddoomi 146 | - Evan P Brenner 147 | - Ethan P Wolfe 148 | - Vincent P Rubinetti 149 | - Shaddai Amolitos 150 | - Kellen M Reason 151 | - John B Johnston 152 | - Janani Ravi 153 | publisher: Cold Spring Harbor Laboratory 154 | date: '2022-02-22' 155 | link: https://doi.org/gstx7j 156 | orcid: 0000-0002-4655-3773 157 | plugin: orcid.py 158 | file: orcid.yaml 159 | - id: doi:10.1186/s13059-020-02021-3 160 | title: Compressing gene expression data using multiple latent space dimensionalities 161 | learns complementary biological representations 162 | authors: 163 | - Gregory P. Way 164 | - Michael Zietz 165 | - Vincent Rubinetti 166 | - Daniel S. Himmelstein 167 | - Casey S. Greene 168 | publisher: Genome Biology 169 | date: '2020-05-11' 170 | link: https://doi.org/gg2mjh 171 | orcid: 0000-0002-4655-3773 172 | plugin: orcid.py 173 | file: orcid.yaml 174 | - id: doi:10.1371/journal.pcbi.1007128 175 | title: Open collaborative writing with Manubot 176 | authors: 177 | - Daniel S. Himmelstein 178 | - Vincent Rubinetti 179 | - David R. Slochower 180 | - Dongbo Hu 181 | - Venkat S. Malladi 182 | - Casey S. Greene 183 | - Anthony Gitter 184 | publisher: PLOS Computational Biology 185 | date: '2020-12-04' 186 | link: https://doi.org/c7np 187 | orcid: 0000-0002-4655-3773 188 | plugin: sources.py 189 | file: sources.yaml 190 | type: paper 191 | description: Lorem ipsum _dolor_ **sit amet**, consectetur adipiscing elit, sed 192 | do eiusmod tempor incididunt ut labore et dolore magna aliqua. 193 | image: https://journals.plos.org/ploscompbiol/article/figure/image?size=inline&id=info:doi/10.1371/journal.pcbi.1007128.g001&rev=2 194 | buttons: 195 | - type: manubot 196 | link: https://greenelab.github.io/meta-review/ 197 | - type: source 198 | text: Manuscript Source 199 | link: https://github.com/greenelab/meta-review 200 | - type: website 201 | link: http://manubot.org/ 202 | tags: 203 | - open science 204 | - collaboration 205 | repo: greenelab/meta-review 206 | - id: doi:10.1101/573782 207 | title: Sequential compression of gene expression across dimensionalities and methods 208 | reveals no single best method or dimensionality 209 | authors: 210 | - Gregory P. Way 211 | - Michael Zietz 212 | - Vincent Rubinetti 213 | - Daniel S. Himmelstein 214 | - Casey S. Greene 215 | publisher: Cold Spring Harbor Laboratory 216 | date: '2019-03-11' 217 | link: https://doi.org/gfxjxf 218 | orcid: 0000-0002-4655-3773 219 | plugin: orcid.py 220 | file: orcid.yaml 221 | - id: doi:10.1016/j.csbj.2020.05.017 222 | title: Constructing knowledge graphs and their biomedical applications 223 | authors: 224 | - David N. Nicholson 225 | - Casey S. Greene 226 | publisher: Computational and Structural Biotechnology Journal 227 | date: '2020-01-01' 228 | link: https://doi.org/gg7m48 229 | image: https://ars.els-cdn.com/content/image/1-s2.0-S2001037020302804-gr1.jpg 230 | plugin: sources.py 231 | file: sources.yaml 232 | - id: doi:10.7554/eLife.32822 233 | title: Sci-Hub provides access to nearly all scholarly literature 234 | authors: 235 | - Daniel S Himmelstein 236 | - Ariel Rodriguez Romero 237 | - Jacob G Levernier 238 | - Thomas Anthony Munro 239 | - Stephen Reid McLaughlin 240 | - Bastian Greshake Tzovaras 241 | - Casey S Greene 242 | publisher: eLife 243 | date: '2018-03-01' 244 | link: https://doi.org/ckcj 245 | image: https://iiif.elifesciences.org/lax:32822%2Felife-32822-fig8-v3.tif/full/863,/0/default.webp 246 | plugin: sources.py 247 | file: sources.yaml 248 | -------------------------------------------------------------------------------- /_data/orcid.yaml: -------------------------------------------------------------------------------- 1 | - orcid: 0000-0002-4655-3773 2 | -------------------------------------------------------------------------------- /_data/projects.yaml: -------------------------------------------------------------------------------- 1 | - title: Cool Dataset 2 | subtitle: a subtitle 3 | group: featured 4 | image: images/photo.jpg 5 | link: https://github.com/ 6 | description: Lorem ipsum _dolor sit amet_, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 7 | repo: greenelab/lab-website-template 8 | tags: 9 | - resource 10 | 11 | - title: Cool Package 12 | subtitle: a subtitle 13 | group: featured 14 | image: images/photo.jpg 15 | link: https://github.com/ 16 | description: Lorem ipsum _dolor sit amet_, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 17 | repo: greenelab/lab-website-template 18 | tags: 19 | - resource 20 | 21 | - title: Cool Tutorial 22 | subtitle: a subtitle 23 | image: images/photo.jpg 24 | link: https://github.com/ 25 | description: Lorem ipsum _dolor sit amet_, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 26 | repo: greenelab/lab-website-template 27 | tags: 28 | - resource 29 | - publication 30 | 31 | - title: Cool Web App 32 | subtitle: a subtitle 33 | image: images/photo.jpg 34 | link: https://github.com/ 35 | description: Lorem ipsum _dolor sit amet_, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 36 | repo: greenelab/lab-website-template 37 | tags: 38 | - software 39 | 40 | - title: Cool Web Server 41 | subtitle: a subtitle 42 | image: images/photo.jpg 43 | link: https://github.com/ 44 | description: Lorem ipsum _dolor sit amet_, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 45 | repo: greenelab/lab-website-template 46 | tags: 47 | - software 48 | -------------------------------------------------------------------------------- /_data/sources.yaml: -------------------------------------------------------------------------------- 1 | - id: doi:10.1371/journal.pcbi.1007128 2 | type: paper 3 | description: Lorem ipsum _dolor_ **sit amet**, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 4 | date: 2020-12-4 5 | image: https://journals.plos.org/ploscompbiol/article/figure/image?size=inline&id=info:doi/10.1371/journal.pcbi.1007128.g001&rev=2 6 | buttons: 7 | - type: manubot 8 | link: https://greenelab.github.io/meta-review/ 9 | - type: source 10 | text: Manuscript Source 11 | link: https://github.com/greenelab/meta-review 12 | - type: website 13 | link: http://manubot.org/ 14 | tags: 15 | - open science 16 | - collaboration 17 | repo: greenelab/meta-review 18 | 19 | - id: doi:10.1016/j.csbj.2020.05.017 20 | image: https://ars.els-cdn.com/content/image/1-s2.0-S2001037020302804-gr1.jpg 21 | 22 | - id: doi:10.7554/eLife.32822 23 | image: https://iiif.elifesciences.org/lax:32822%2Felife-32822-fig8-v3.tif/full/863,/0/default.webp 24 | -------------------------------------------------------------------------------- /_data/types.yaml: -------------------------------------------------------------------------------- 1 | # map general type to default/fallback icon, text, etc. 2 | 3 | # team member roles 4 | 5 | principal-investigator: 6 | icon: fa-solid fa-microscope 7 | description: Principal Investigator 8 | 9 | postdoc: 10 | icon: fa-solid fa-glasses 11 | description: Postdoctoral Researcher 12 | 13 | phd: 14 | icon: fa-solid fa-graduation-cap 15 | description: PhD Student 16 | 17 | undergrad: 18 | icon: fa-solid fa-user-graduate 19 | description: Undergraduate Student 20 | 21 | programmer: 22 | icon: fa-solid fa-code 23 | description: Programmer 24 | 25 | mascot: 26 | icon: fa-solid fa-dog 27 | description: Mascot 28 | 29 | # general 30 | 31 | link: 32 | icon: fa-solid fa-globe 33 | text: Link 34 | tooltip: Link 35 | 36 | website: 37 | icon: fa-solid fa-globe 38 | text: Website 39 | tooltip: Website 40 | 41 | external: 42 | icon: fa-solid fa-up-right-from-square 43 | text: External Link 44 | tooltip: External link 45 | 46 | home-page: 47 | icon: fa-solid fa-house-user 48 | text: Home Page 49 | tooltip: Home page 50 | 51 | email: 52 | icon: fa-solid fa-envelope 53 | text: Email 54 | tooltip: Email 55 | link: mailto:$VALUE 56 | 57 | phone: 58 | icon: fa-solid fa-phone 59 | text: Phone Number 60 | tooltip: Phone number 61 | link: tel:$VALUE 62 | 63 | address: 64 | icon: fa-solid fa-map-location-dot 65 | text: Address 66 | tooltip: Address 67 | 68 | search: 69 | icon: fa-solid fa-magnifying-glass 70 | text: Search 71 | tooltip: Search 72 | 73 | # social media 74 | 75 | orcid: 76 | icon: fa-brands fa-orcid 77 | text: ORCID 78 | tooltip: ORCID 79 | link: https://orcid.org/$VALUE 80 | 81 | google-scholar: 82 | icon: fa-brands fa-google 83 | text: Google Scholar 84 | tooltip: Google Scholar 85 | link: https://scholar.google.com/citations?user=$VALUE 86 | 87 | github: 88 | icon: fa-brands fa-github 89 | text: GitHub 90 | tooltip: GitHub 91 | link: https://github.com/$VALUE 92 | 93 | twitter: 94 | icon: fa-brands fa-twitter 95 | text: Twitter 96 | tooltip: Twitter 97 | link: https://twitter.com/$VALUE 98 | 99 | bluesky: 100 | icon: fa-brands fa-bluesky 101 | text: Bluesky 102 | tooltip: Bluesky 103 | link: https://bsky.app/profile/$VALUE 104 | 105 | facebook: 106 | icon: fa-brands fa-facebook 107 | text: Facebook 108 | tooltip: Facebook 109 | link: https://facebook.com/$VALUE 110 | 111 | instagram: 112 | icon: fa-brands fa-instagram 113 | text: Instagram 114 | tooltip: Instagram 115 | link: https://instagram.com/$VALUE 116 | 117 | youtube: 118 | icon: fa-brands fa-youtube 119 | text: YouTube 120 | tooltip: YouTube 121 | link: https://youtube.com/$VALUE 122 | 123 | linkedin: 124 | icon: fa-brands fa-linkedin 125 | text: LinkedIn 126 | tooltip: LinkedIn 127 | link: https://www.linkedin.com/in/$VALUE 128 | 129 | # alerts 130 | 131 | tip: 132 | icon: fa-solid fa-lightbulb 133 | color: "#d946ef" 134 | 135 | help: 136 | icon: fa-solid fa-circle-question 137 | color: "#6366f1" 138 | 139 | info: 140 | icon: fa-solid fa-circle-info 141 | color: "#0ea5e9" 142 | 143 | success: 144 | icon: fa-solid fa-circle-check 145 | color: "#22c55e" 146 | 147 | warning: 148 | icon: fa-solid fa-circle-exclamation 149 | color: "#F59E0B" 150 | 151 | error: 152 | icon: fa-solid fa-ban 153 | color: "#ef4444" 154 | 155 | # publications 156 | 157 | paper: 158 | icon: fa-solid fa-scroll 159 | text: Paper 160 | tooltip: Paper 161 | 162 | book: 163 | icon: fa-solid fa-book 164 | text: Book 165 | tooltip: Book 166 | 167 | article: 168 | icon: fa-solid fa-newspaper 169 | text: Article 170 | tooltip: Article 171 | 172 | journal: 173 | icon: fa-regular fa-newspaper 174 | text: Journal 175 | tooltip: Journal 176 | 177 | preprint: 178 | icon: fa-regular fa-eye 179 | text: Preprint 180 | tooltip: Preprint 181 | 182 | unpublished: 183 | icon: fa-regular fa-eye 184 | text: Unpublished 185 | tooltip: Unpublished 186 | 187 | manubot: 188 | icon: manubot.svg 189 | text: Manubot 190 | tooltip: Manubot 191 | 192 | # software 193 | 194 | docs: 195 | icon: fa-solid fa-book 196 | text: Documentation 197 | tooltip: Documentation 198 | 199 | source: 200 | icon: fa-solid fa-code 201 | text: Source 202 | tooltip: Source code 203 | 204 | server: 205 | icon: fa-solid fa-server 206 | text: Server 207 | tooltip: Server 208 | 209 | app: 210 | icon: fa-solid fa-hand-pointer 211 | text: App 212 | tooltip: App 213 | 214 | data: 215 | icon: fa-solid fa-database 216 | text: Data 217 | tooltip: Data 218 | 219 | package: 220 | icon: fa-solid fa-box 221 | text: Package 222 | tooltip: Package 223 | -------------------------------------------------------------------------------- /_includes/alert.html: -------------------------------------------------------------------------------- 1 | {% assign color = site.data.types[include.type].color | default: "#808080" %} 2 |
3 | {% assign icon = site.data.types[include.type].icon 4 | | default: "fa-solid fa-circle-info" 5 | %} 6 | {% include icon.html icon=icon %} 7 |
8 | {{ include.content | markdownify }} 9 |
10 |
11 | -------------------------------------------------------------------------------- /_includes/analytics.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /_includes/button.html: -------------------------------------------------------------------------------- 1 | {% assign button = include %} 2 | {% assign button = button | hash_default: site.data.types[include.type] %} 3 | 4 | {% if button.link or button.icon or button.text %} 5 |
6 | 18 | {% include icon.html icon=button.icon %} 19 | {% if button.text and button.text != "" %} 20 | {{ button.text }} 21 | {% endif %} 22 | 23 |
24 | {% endif %} 25 | -------------------------------------------------------------------------------- /_includes/card.html: -------------------------------------------------------------------------------- 1 | {{ " " }} 2 |
3 | 10 | {{ include.title | default: 16 | 17 | 18 |
19 | {% if include.title %} 20 | 29 | {{ include.title }} 30 | 31 | {% endif %} 32 | 33 | {% if include.subtitle %} 34 | {{ include.subtitle }} 35 | {% endif %} 36 | 37 | {% if include.description %} 38 |

39 | {{ include.description | markdownify | remove: "

" | remove: "

" }} 40 |

41 | {% endif %} 42 | 43 | {% if include.tags or include.repo %} 44 | {% include tags.html tags=include.tags repo=include.repo %} 45 | {% endif %} 46 |
47 |
48 | -------------------------------------------------------------------------------- /_includes/citation.html: -------------------------------------------------------------------------------- 1 | {% if include.lookup %} 2 | {% assign citation = site.data.citations 3 | | where_exp: "citation", 4 | "citation.id == include.lookup or citation.title contains include.lookup" 5 | | first 6 | %} 7 | {% else %} 8 | {% assign citation = include %} 9 | {% endif %} 10 | 11 |
12 |
13 | {% if include.style == "rich" %} 14 | 21 | {{ citation.title | default: 27 | 28 | {% endif %} 29 | 30 |
31 | {% assign type = site.data.types[citation.type] %} 32 | {% include icon.html icon=type.icon %} 33 | 34 | 40 | {{ citation.title | default: "[no title info]" }} 41 | 42 | 43 |
10 %} 46 | data-tooltip="{{ citation.authors | join: ", " | xml_escape }}" 47 | {% endif %} 48 | tabindex="0" 49 | > 50 | {{ 51 | citation.authors 52 | | join: "," 53 | | split: "," 54 | | array_carve: 5 55 | | join: ", " 56 | | markdownify 57 | | remove: "

" | remove: "

" 58 | | default: "[no author info]" 59 | }} 60 |
61 | 62 |
63 | 64 | {{- citation.publisher | default: "[no publisher info]" -}} 65 | 66 |  ·  67 | 68 | {{- citation.date | default: "[no date info]" | date: "%d %b %Y" -}} 69 | 70 |  ·  71 | 72 | {{- citation.id | default: "[no id info]" -}} 73 | 74 |
75 | 76 | {% if include.style == "rich" %} 77 | {% if citation.description %} 78 |
79 | {{ 80 | citation.description 81 | | markdownify 82 | | remove: "

" 83 | | remove: "

" 84 | }} 85 |
86 | {% endif %} 87 | 88 | {% if citation.buttons.size > 0 %} 89 |
90 | {% for button in citation.buttons %} 91 | {% 92 | include button.html 93 | type=button.type 94 | icon=button.icon 95 | text=button.text 96 | link=button.link 97 | style="bare" 98 | %} 99 | {% endfor %} 100 |
101 | {% endif %} 102 | 103 | {% if citation.tags.size > 0 or citation.repo %} 104 | {% include tags.html tags=citation.tags repo=citation.repo %} 105 | {% endif %} 106 | {% endif %} 107 |
108 |
109 |
110 | -------------------------------------------------------------------------------- /_includes/cols.html: -------------------------------------------------------------------------------- 1 |
2 | {% for param in include %} 3 | {% assign key = param[0] %} 4 |
{{ include[key] | markdownify }}
5 | {% endfor %} 6 |
7 | -------------------------------------------------------------------------------- /_includes/content.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | {% assign content = include.content %} 9 | 10 | {% assign sections = content | split: "" | array_filter %} 11 | 12 | {% for section in sections %} 13 | {% assign dark = section 14 | | regex_scan: "(.*?)" 15 | | default: "" 16 | %} 17 | {% assign image = section 18 | | regex_scan: "(.*?)" 19 | | default: nil 20 | %} 21 | {% assign size = section 22 | | regex_scan: "(.*?)" 23 | | default: "page" 24 | %} 25 | 26 |
36 | {{ section }} 37 |
38 | {% endfor %} 39 | -------------------------------------------------------------------------------- /_includes/fallback.html: -------------------------------------------------------------------------------- 1 | onerror="this.src = '{{ "images/fallback.svg" | relative_url }}'; this.onerror = null;" 2 | -------------------------------------------------------------------------------- /_includes/feature.html: -------------------------------------------------------------------------------- 1 |
7 | 14 | {{ include.title | default: 20 | 21 |
22 | {% if include.title %} 23 |

{{ include.title }}

24 | {% endif %} 25 | {{ include.text | markdownify }} 26 |
27 |
28 | -------------------------------------------------------------------------------- /_includes/figure.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | {{ include.caption | default: 19 | 20 | {% if include.caption %} 21 |
22 | {{ include.caption | markdownify | remove: "

" | remove: "

" }} 23 |
24 | {% endif %} 25 |
26 | -------------------------------------------------------------------------------- /_includes/float.html: -------------------------------------------------------------------------------- 1 |
10 | {{ include.content | markdownify }} 11 |
12 | -------------------------------------------------------------------------------- /_includes/fonts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% assign googlefonts = "_styles/-theme.scss" | file_read | google_fonts %} 4 | 5 | 6 | 7 | 8 | {% assign fontawesome = "https://use.fontawesome.com/releases/v6.7.0/css/all.css" %} 9 | 15 | 18 | -------------------------------------------------------------------------------- /_includes/footer.html: -------------------------------------------------------------------------------- 1 | {% assign image = page.footer | default: site.footer %} 2 | {% assign dark = page.footer-dark | is_nil: site.footer-dark | is_nil: true %} 3 | 4 | 43 | -------------------------------------------------------------------------------- /_includes/grid.html: -------------------------------------------------------------------------------- 1 |
2 | {{ include.content | markdownify }} 3 |
4 | -------------------------------------------------------------------------------- /_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | {% include analytics.html %} 3 | {% include verification.html %} 4 | {% include meta.html %} 5 | {% include fonts.html %} 6 | {% include styles.html %} 7 | {% include scripts.html %} 8 | 9 | -------------------------------------------------------------------------------- /_includes/header.html: -------------------------------------------------------------------------------- 1 | {% assign image = page.header | default: site.header %} 2 | {% assign dark = page.header-dark | is_nil: site.header-dark | is_nil: true %} 3 | 4 | {% assign svg = "images/logo.svg" | file_exists %} 5 | {% assign png = "images/logo.png" | file_exists %} 6 | {% assign jpg = "images/logo.jpg" | file_exists %} 7 | {% assign logo = svg | default: png | default: jpg | default: nil %} 8 | 9 |
19 | 20 | {% if logo %} 21 | 28 | {% endif %} 29 | {% if site.logo-text != false %} 30 | 31 | {% if site.title and site.show-title != false %} 32 | {{ site.title }} 33 | {% endif %} 34 | {% if site.subtitle and site.show-subtitle != false %} 35 | {{ site.subtitle }} 36 | {% endif %} 37 | 38 | {% endif %} 39 | 40 | 41 | 42 | 43 | 59 |
60 | -------------------------------------------------------------------------------- /_includes/icon.html: -------------------------------------------------------------------------------- 1 | {%- if include.icon contains ".svg" -%} 2 | {%- capture inline -%} 3 | {%- include {{ include.icon }} -%} 4 | {%- endcapture -%} 5 | 6 | {{- inline | strip_newlines -}} 7 | 8 | {%- elsif include.icon and include.icon != "" -%} 9 | 10 | {%- endif -%} 11 | -------------------------------------------------------------------------------- /_includes/list.html: -------------------------------------------------------------------------------- 1 | {% assign emptyarray = "" | split: "," %} 2 | {% assign data = site.data[include.data] 3 | | default: site[include.data] 4 | | default: emptyarray 5 | | data_filter: include.filter 6 | %} 7 | 8 | {% assign years = data 9 | | group_by_exp: "d", "d.date | date: '%Y'" 10 | | sort: "name" 11 | | reverse 12 | %} 13 | 14 | {% for year in years %} 15 | {% assign data = year.items %} 16 | 17 | {% if years.size > 1 %} 18 | {{--}}

{{ year.name }}

19 | {% assign data = data | sort: "date" | reverse %} 20 | {% endif %} 21 | 22 | {% for d in data %} 23 | {% assign style = d.style | default: include.style %} 24 | 25 | {% 26 | include {{ include.component | append: ".html" }} 27 | affiliation=d.affiliation 28 | author=d.author 29 | authors=d.authors 30 | buttons=d.buttons 31 | caption=d.caption 32 | content=d.content 33 | date=d.date 34 | description=d.description 35 | excerpt=d.excerpt 36 | height=d.height 37 | icon=d.icon 38 | id=d.id 39 | image=d.image 40 | last_modified_at=d.last_modified_at 41 | link=d.link 42 | lookup=d.lookup 43 | name=d.name 44 | publisher=d.publisher 45 | repo=d.repo 46 | role=d.role 47 | slug=d.slug 48 | style=style 49 | subtitle=d.subtitle 50 | tags=d.tags 51 | text=d.text 52 | title=d.title 53 | tooltip=d.tooltip 54 | type=d.type 55 | url=d.url 56 | width=d.width 57 | %} 58 | {% endfor %} 59 | {% endfor %} 60 | -------------------------------------------------------------------------------- /_includes/manubot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 29 | 41 | 51 | 61 | 69 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /_includes/meta.html: -------------------------------------------------------------------------------- 1 | {% assign filename = page.path | split: "/" | last %} 2 | {% if page.name and page.name != filename %} 3 | {% assign title = page.name %} 4 | {% elsif page.title %} 5 | {% assign title = page.title %} 6 | {% else %} 7 | {% assign title = nil %} 8 | {% endif %} 9 | 10 | {% assign title = title | xml_escape %} 11 | 12 | {% assign fulltitle = "" | split: "," %} 13 | {% if title and title != "" %} 14 | {% assign fulltitle = fulltitle | push: title %} 15 | {% endif %} 16 | {% if site.title and site.title != "" %} 17 | {% assign fulltitle = fulltitle | push: site.title %} 18 | {% endif %} 19 | {% assign fulltitle = fulltitle | join: " | " %} 20 | 21 | {% assign subtitle = site.subtitle %} 22 | 23 | {% assign description = page.description | default: site.description %} 24 | {% if site.subtitle %} 25 | {% capture description -%} 26 | {{ site.subtitle }}. {{ description }} 27 | {%- endcapture %} 28 | {% endif %} 29 | {% capture url -%} 30 | {{ site.url }}{{ site.baseurl }} 31 | {%- endcapture %} 32 | 33 | {% assign description = description | xml_escape %} 34 | 35 | {% assign png = "images/icon.png" | file_exists %} 36 | {% assign jpg = "images/icon.jpg" | file_exists %} 37 | {% assign icon = png | default: jpg | relative_url %} 38 | 39 | {% assign jpg = "images/share.jpg" | file_exists %} 40 | {% assign png = "images/share.png" | file_exists %} 41 | {% assign share = jpg | default: png | relative_url %} 42 | 43 | {% assign published = page.date | date_to_xmlschema %} 44 | {% assign updated = page.last_modified_at | date_to_xmlschema %} 45 | 46 | {% assign feed = "feed.xml" | absolute_url %} 47 | 48 | 49 | 50 | {{ fulltitle }} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | {% if page.author %} 71 | 72 | 73 | 74 | 75 | 76 | 77 | {% else %} 78 | 79 | {% endif %} 80 | 81 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /_includes/portrait.html: -------------------------------------------------------------------------------- 1 | {% if include.lookup %} 2 | {% assign member = site.members 3 | | where_exp: "member", "member.slug == include.lookup" 4 | | first 5 | %} 6 | {% else %} 7 | {% assign member = include %} 8 | {% endif %} 9 | 10 | {% assign type = site.data.types[member.role] %} 11 | 12 |
13 | 21 | {% if type %} 22 | {% include icon.html icon=type.icon %} 23 | {% endif %} 24 | 25 | member portrait 32 | 33 | {% if member.name %} 34 | 35 | {{ member.name }} 36 | 37 | {% endif %} 38 | 39 | {% if member.description or type %} 40 | 41 | {{ member.description | default: type.description }} 42 | 43 | {% endif %} 44 | 45 | {% if member.affiliation %} 46 | 47 | {{ member.affiliation }} 48 | 49 | {% endif %} 50 | 51 |
52 | -------------------------------------------------------------------------------- /_includes/post-excerpt.html: -------------------------------------------------------------------------------- 1 | {% if include.lookup %} 2 | {% assign post = site.posts 3 | | where_exp: "post", "post.slug == include.lookup" 4 | | first 5 | | default: include 6 | %} 7 | {% else %} 8 | {% assign post = include %} 9 | {% endif %} 10 | 11 |
12 |
13 | {% assign url = post.url | relative_url | uri_escape %} 14 | {% assign title = post.title %} 15 | {% assign image = post.image | relative_url | uri_escape %} 16 | 17 | {% if image %} 18 | 23 | {{ title | default: 29 | 30 | {% endif %} 31 | 32 |
33 | {{ title }} 34 | 35 | {% 36 | include post-info.html 37 | author=post.author 38 | published=post.date 39 | updated=post.last_modified_at 40 | tags=post.tags 41 | %} 42 | 43 | {% assign excerpt = post.content 44 | | default: "" 45 | | regex_scan: "(.*)", true 46 | | default: post.excerpt 47 | | default: "" 48 | | strip_html 49 | %} 50 | {% assign search = post.content 51 | | strip_html 52 | | strip_newlines 53 | | regex_strip 54 | %} 55 |

56 | {{ excerpt }} 57 |

58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /_includes/post-info.html: -------------------------------------------------------------------------------- 1 |
2 | {% if include.author %} 3 | {% assign authors = include.author 4 | | join: "," 5 | | downcase 6 | | split: "," 7 | | array_filter 8 | %} 9 | {% for author in authors %} 10 | {% assign member = site.members 11 | | where_exp: "member", "member.slug == author" 12 | | first 13 | %} 14 | {% if member %} 15 | {% include portrait.html lookup=author style="tiny" %} 16 | {% else %} 17 | 18 | {% include icon.html icon="fa-solid fa-feather-pointed" %} 19 | {{ author }} 20 | 21 | {% endif %} 22 | {% endfor %} 23 | {% endif %} 24 | 25 | {% assign published = include.published | date: "%B %d, %Y" %} 26 | {% assign updated = include.updated | date: "%B %d, %Y" %} 27 | 28 | {% if published %} 29 | 30 | {% include icon.html icon="fa-regular fa-calendar" %} 31 | {{ published }} 32 | 33 | {% endif %} 34 | 35 | {% if updated and updated != "" and updated != published %} 36 | 37 | {% include icon.html icon="fa-solid fa-clock-rotate-left" %} 38 | {{ updated }} 39 | 40 | {% endif %} 41 |
42 | 43 | {% if include.tags %} 44 | {% include tags.html tags=include.tags link="blog" %} 45 | {% endif %} 46 | -------------------------------------------------------------------------------- /_includes/post-nav.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {% if include.post.previous %} 4 | {% include icon.html icon="fa-solid fa-angle-left" %} Previous post
5 | 6 | {{ include.post.previous.title }} 7 | 8 | {% endif %} 9 |
10 | 11 | {% if include.post.next %} 12 | Next post {% include icon.html icon="fa-solid fa-angle-right" %}
13 | 14 | {{ include.post.next.title }} 15 | 16 | {% endif %} 17 |
18 |
19 | -------------------------------------------------------------------------------- /_includes/scripts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% assign scripts = site.static_files | where_exp: "file", "file.path contains '/_scripts'" %} 8 | {% for script in scripts %} 9 | 10 | {% endfor %} 11 | -------------------------------------------------------------------------------- /_includes/search-box.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /_includes/search-info.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /_includes/section.html: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | see content.html 3 | {% endcomment %} 4 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /_includes/site-search.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 |
7 | -------------------------------------------------------------------------------- /_includes/styles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | {% assign styles = site.pages 10 | | where_exp: "file", "file.url contains '/_styles'" 11 | %} 12 | {% for style in styles %} 13 | {% unless style.url contains ".map" %} 14 | 18 | {% endunless %} 19 | {% endfor %} 20 | 21 | 22 | {% assign styles = site.static_files 23 | | where_exp: "file", 24 | "file.path contains '/_styles' and file.path contains '.css'" 25 | %} 26 | {% for style in styles %} 27 | 28 | {% endfor %} 29 | -------------------------------------------------------------------------------- /_includes/tags.html: -------------------------------------------------------------------------------- 1 | {% assign tags = include.tags 2 | | object_items 3 | | join: "," 4 | | downcase 5 | | split: "," 6 | | array_filter 7 | | join: "," 8 | | regex_replace: "\s+", "-" 9 | | split: "," 10 | | uniq 11 | %} 12 | {% assign link = include.link | default: page.dir %} 13 | {% if tags.size > 0 or include.repo %} 14 |
20 | {% for tag in tags %} 21 | 26 | {{ tag }} 27 | 28 | {% endfor %} 29 |
30 | {% endif %} 31 | -------------------------------------------------------------------------------- /_includes/verification.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 | {% include header.html %} 6 |
7 | {% include content.html content=content %} 8 |
9 | {% include footer.html %} 10 | 11 | 12 | -------------------------------------------------------------------------------- /_layouts/member.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | {% capture floatcontent %} 6 | 7 | {% include portrait.html lookup=page.slug %} 8 | 9 |
10 | {% for link in page.links %} 11 | {% assign key = link[0] %} 12 | {% assign value = link[1] %} 13 | {% include button.html type=key link=value style="bare" %}
14 | {% endfor %} 15 |
16 | 17 | {% endcapture %} 18 | 19 | {% include float.html content=floatcontent %} 20 | 21 | {{ content }} 22 | 23 | {% assign aliases = page.aliases 24 | | default: page.name 25 | | default: page.title 26 | | join: "," 27 | | split: "," 28 | | array_filter 29 | %} 30 | 31 | {% capture search -%} 32 | research/?search={% for alias in aliases %}"{{ alias }}" {% endfor %} 33 | {%- endcapture %} 34 | 35 |

36 | 37 | Search for {{ page.name | default: page.title }}'s papers on the Research page 38 | 39 |

40 | 41 | {% capture search -%} 42 | blog/?search={{ page.name }} 43 | {%- endcapture %} 44 | 45 | 52 | -------------------------------------------------------------------------------- /_layouts/post.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | {% include section.html background=page.image %} 6 | 7 |

{{ page.title }}

8 | 9 | {% 10 | include post-info.html 11 | author=page.author 12 | member=page.member 13 | published=page.date 14 | updated=page.last_modified_at 15 | tags=page.tags 16 | %} 17 | 18 | {% include section.html %} 19 | 20 | {{ content }} 21 | 22 | {% include section.html %} 23 | 24 | {% include post-nav.html post=page %} 25 | -------------------------------------------------------------------------------- /_members/jane-smith.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Jane Smith 3 | image: images/photo.jpg 4 | role: principal-investigator 5 | affiliation: University of Colorado 6 | aliases: 7 | - J. Smith 8 | - J Smith 9 | links: 10 | home-page: https://janesmith.com 11 | orcid: 0000-0001-8713-9213 12 | --- 13 | 14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 15 | Faucibus purus in massa tempor nec feugiat nisl pretium fusce. 16 | Elit at imperdiet dui accumsan. 17 | Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. 18 | Vitae elementum curabitur vitae nunc sed velit dignissim sodales. 19 | Lacinia at quis risus sed vulputate odio ut. 20 | Magna eget est lorem ipsum. 21 | -------------------------------------------------------------------------------- /_members/john-doe.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: John Doe 3 | image: images/photo.jpg 4 | role: phd 5 | group: alum 6 | links: 7 | github: john-doe 8 | --- 9 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 11 | -------------------------------------------------------------------------------- /_members/sarah-johnson.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Sarah Johnson 3 | image: images/photo.jpg 4 | description: Lead Programmer 5 | role: programmer 6 | links: 7 | email: sarah.johnson@gmail.com 8 | twitter: sarahjohnson 9 | --- 10 | 11 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 12 | -------------------------------------------------------------------------------- /_plugins/array.rb: -------------------------------------------------------------------------------- 1 | require 'liquid' 2 | 3 | module Jekyll 4 | module ArrayFilters 5 | # filter out empty and trim entries in array 6 | def array_filter(array) 7 | return array 8 | .map{|x| x.is_a?(String) ? x.strip() : x} 9 | .select{|x| x and x != ""} 10 | end 11 | 12 | # omit middle items of array with ellipsis, leave N items on either side 13 | def array_carve(array, length = 3) 14 | if array.length <= length * 2 15 | return array 16 | else 17 | left = array.slice(0, length) || [] 18 | right = array.slice(-length, length) || [] 19 | return [left, "...", right].flatten() 20 | end 21 | end 22 | end 23 | end 24 | 25 | Liquid::Template.register_filter(Jekyll::ArrayFilters) 26 | -------------------------------------------------------------------------------- /_plugins/file.rb: -------------------------------------------------------------------------------- 1 | require 'liquid' 2 | 3 | module Jekyll 4 | module FileFilters 5 | # check if file exists 6 | def file_exists(file) 7 | path = File.join(Dir.getwd, file) 8 | # pass back filename if exists 9 | return File.file?(path) ? file : nil 10 | end 11 | 12 | # read text contents of file 13 | def file_read(file) 14 | path = File.join(Dir.getwd, file) 15 | return File.file?(path) ? File.read(path) : nil 16 | end 17 | end 18 | end 19 | 20 | Liquid::Template.register_filter(Jekyll::FileFilters) 21 | -------------------------------------------------------------------------------- /_plugins/hash.rb: -------------------------------------------------------------------------------- 1 | require 'liquid' 2 | 3 | module Jekyll 4 | module HashFilters 5 | # merge main hash with another hash of defaults 6 | def hash_default(hash, defaults) 7 | if not defaults.is_a?(Hash) 8 | return hash 9 | end 10 | defaults.each do |key, value| 11 | # substitute main string into default string and set main item 12 | if value.is_a?(String) and value.include?"$VALUE" 13 | if hash[key].is_a?(String) 14 | hash[key] = value.sub"$VALUE", hash[key] 15 | end 16 | # set main item to default item if not defined 17 | else 18 | if hash[key] == nil or !hash.key?(key) 19 | hash[key] = value 20 | end 21 | end 22 | end 23 | return hash 24 | end 25 | end 26 | end 27 | 28 | Liquid::Template.register_filter(Jekyll::HashFilters) 29 | -------------------------------------------------------------------------------- /_plugins/misc.rb: -------------------------------------------------------------------------------- 1 | require 'liquid' 2 | require 'html-proofer' 3 | 4 | module Jekyll 5 | module MiscFilters 6 | # fallback if value unspecified 7 | def is_nil(value, fallback) 8 | return value == nil ? fallback : value 9 | end 10 | 11 | # get list of hash keys or array entries 12 | def object_items(object) 13 | if object.is_a?(Hash) 14 | return object.keys 15 | elsif object.is_a?(Array) 16 | return object 17 | end 18 | return object 19 | end 20 | 21 | 22 | def empty_binding 23 | binding 24 | end 25 | 26 | # make arbitrary string into valid ruby variable name 27 | def safe_var_name(name) 28 | return name.to_s.gsub(/[^a-z]+/i, "_").gsub(/^_|_$/, "") 29 | end 30 | 31 | # filter a list of hashes 32 | def data_filter(data, filter) 33 | if not filter.is_a?(String) 34 | return data 35 | end 36 | 37 | # filter data 38 | return data.clone.select{ 39 | |item| 40 | # if jekyll doc collection, get hash of doc data 41 | if item.is_a? Jekyll::Document 42 | item = item.data 43 | end 44 | # start with empty context of local variables 45 | b = empty_binding 46 | # add item as local variable 47 | b.local_variable_set("item", item) 48 | # also set each item field as local variable when evaluating filter 49 | item.each do |var, val| 50 | b.local_variable_set(safe_var_name(var), val) 51 | end 52 | # whether to keep item 53 | keep = true 54 | while true 55 | begin 56 | # evaluate expression as true/false 57 | keep = !!eval(filter, b) 58 | break 59 | # if a var in expression isn't a field on item 60 | rescue NameError => e 61 | # define it and re-evaluate 62 | b.local_variable_set(safe_var_name(e.name), nil) 63 | end 64 | end 65 | # keep/discard item 66 | keep 67 | } 68 | end 69 | 70 | # from css text, find font family definitions and construct google font url 71 | def google_fonts(css) 72 | names = regex_scan(css, '--\S*:\s*"(.*)",?.*;', false, true).sort.uniq 73 | weights = regex_scan(css, '--\S*:\s(\d{3});', false, true).sort.uniq 74 | url = "https://fonts.googleapis.com/css2?display=swap&" 75 | for name in names do 76 | name.sub!" ", "+" 77 | url += "&family=#{name}:ital,wght@" 78 | for ital in [0, 1] do 79 | for weight in weights do 80 | url += "#{ital},#{weight};" 81 | end 82 | end 83 | url.delete_suffix!(";") 84 | end 85 | return url 86 | end 87 | end 88 | 89 | # based on https://github.com/episource/jekyll-html-proofer 90 | module HtmlProofer 91 | priority = Jekyll::Hooks::PRIORITY_MAP[:high] + 1000 92 | 93 | Jekyll::Hooks.register(:site, :post_write, priority: priority) do |site| 94 | if not site.config["proofer"] == false 95 | options = { 96 | allow_missing_href: true, 97 | enforce_https: false, 98 | ignore_files: [/.*testbed.html/], 99 | ignore_urls: [ 100 | /fonts\.gstatic\.com/, 101 | /localhost:/, 102 | /0\.0\.0\.0:/, 103 | ], 104 | } 105 | 106 | begin 107 | HTMLProofer.check_directory(site.dest, options).run 108 | rescue Exception => error 109 | STDERR.puts error 110 | # raise error 111 | end 112 | end 113 | end 114 | end 115 | end 116 | 117 | Liquid::Template.register_filter(Jekyll::MiscFilters) 118 | -------------------------------------------------------------------------------- /_plugins/regex.rb: -------------------------------------------------------------------------------- 1 | require 'liquid' 2 | 3 | module Jekyll 4 | module RegexFilters 5 | # search string for regex capture group, return first or all matches 6 | def regex_scan(string, search, multi = false, all = false) 7 | regex = multi ? /#{search}/m : /#{search}/ 8 | matches = string.scan(regex).flatten 9 | if matches.length 10 | return all ? matches : matches[0] 11 | else 12 | return "" 13 | end 14 | end 15 | 16 | # find regex capture group in string and replace 17 | def regex_replace(string, search, replace) 18 | return string.gsub(/#{search}/m, replace) 19 | end 20 | 21 | # strip all non-letter and non-number characters from string 22 | def regex_strip(string) 23 | return string.gsub(/[^\p{L}\p{N}.,;:-]/u, " ").gsub(/\s+/, " ").strip 24 | end 25 | end 26 | end 27 | 28 | Liquid::Template.register_filter(Jekyll::RegexFilters) 29 | -------------------------------------------------------------------------------- /_posts/2019-01-07-example-post-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example post 1 3 | author: sarah-johnson 4 | tags: 5 | - biology 6 | - medicine 7 | - big data 8 | --- 9 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 11 | -------------------------------------------------------------------------------- /_posts/2021-09-30-example-post-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example post 2 3 | author: jane-smith 4 | --- 5 | 6 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 7 | -------------------------------------------------------------------------------- /_posts/2023-02-23-example-post-3.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example post 3 3 | image: images/photo.jpg 4 | author: john-doe 5 | tags: biology, medicine 6 | --- 7 | 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 9 | -------------------------------------------------------------------------------- /_scripts/anchors.js: -------------------------------------------------------------------------------- 1 | /* 2 | creates link next to each heading that links to that section. 3 | */ 4 | 5 | { 6 | const onLoad = () => { 7 | // for each heading 8 | const headings = document.querySelectorAll( 9 | "h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]" 10 | ); 11 | for (const heading of headings) { 12 | // create anchor link 13 | const link = document.createElement("a"); 14 | link.classList.add("icon", "fa-solid", "fa-link", "anchor"); 15 | link.href = "#" + heading.id; 16 | link.setAttribute("aria-label", "link to this section"); 17 | heading.append(link); 18 | 19 | // if first heading in the section, move id to parent section 20 | if (heading.matches("section > :first-child")) { 21 | heading.parentElement.id = heading.id; 22 | heading.removeAttribute("id"); 23 | } 24 | } 25 | }; 26 | 27 | // scroll to target of url hash 28 | const scrollToTarget = () => { 29 | const id = window.location.hash.replace("#", ""); 30 | const target = document.getElementById(id); 31 | 32 | if (!target) return; 33 | const offset = document.querySelector("header").clientHeight || 0; 34 | window.scrollTo({ 35 | top: target.getBoundingClientRect().top + window.scrollY - offset, 36 | behavior: "smooth", 37 | }); 38 | }; 39 | 40 | // after page loads 41 | window.addEventListener("load", onLoad); 42 | window.addEventListener("load", scrollToTarget); 43 | window.addEventListener("tagsfetched", scrollToTarget); 44 | 45 | // when hash nav happens 46 | window.addEventListener("hashchange", scrollToTarget); 47 | } 48 | -------------------------------------------------------------------------------- /_scripts/dark-mode.js: -------------------------------------------------------------------------------- 1 | /* 2 | manages light/dark mode. 3 | */ 4 | 5 | { 6 | // immediately load saved (or default) mode before page renders 7 | document.documentElement.dataset.dark = 8 | window.localStorage.getItem("dark-mode") ?? "false"; 9 | 10 | const onLoad = () => { 11 | // update toggle button to match loaded mode 12 | document.querySelector(".dark-toggle").checked = 13 | document.documentElement.dataset.dark === "true"; 14 | }; 15 | 16 | // after page loads 17 | window.addEventListener("load", onLoad); 18 | 19 | // when user toggles mode button 20 | window.onDarkToggleChange = (event) => { 21 | const value = event.target.checked; 22 | document.documentElement.dataset.dark = value; 23 | window.localStorage.setItem("dark-mode", value); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /_scripts/fetch-tags.js: -------------------------------------------------------------------------------- 1 | /* 2 | fetches tags (aka "topics") from a given GitHub repo and adds them to row of 3 | tag buttons. specify repo in data-repo attribute on row. 4 | */ 5 | 6 | { 7 | const onLoad = async () => { 8 | // get tag rows with specified repos 9 | const rows = document.querySelectorAll("[data-repo]"); 10 | 11 | // for each repo 12 | for (const row of rows) { 13 | // get props from tag row 14 | const repo = row.dataset.repo.trim(); 15 | const link = row.dataset.link.trim(); 16 | 17 | // get tags from github 18 | if (!repo) continue; 19 | let tags = await fetchTags(repo); 20 | 21 | // filter out tags already present in row 22 | let existing = [...row.querySelectorAll(".tag")].map((tag) => 23 | window.normalizeTag(tag.innerText) 24 | ); 25 | tags = tags.filter((tag) => !existing.includes(normalizeTag(tag))); 26 | 27 | // add tags to row 28 | for (const tag of tags) { 29 | const a = document.createElement("a"); 30 | a.classList.add("tag"); 31 | a.innerHTML = tag; 32 | a.href = `${link}?search="tag: ${tag}"`; 33 | a.dataset.tooltip = `Show items with the tag "${tag}"`; 34 | row.append(a); 35 | } 36 | 37 | // delete tags container if empty 38 | if (!row.innerText.trim()) row.remove(); 39 | } 40 | 41 | // emit "tags done" event for other scripts to listen for 42 | window.dispatchEvent(new Event("tagsfetched")); 43 | }; 44 | 45 | // after page loads 46 | window.addEventListener("load", onLoad); 47 | 48 | // GitHub topics endpoint 49 | const api = "https://api.github.com/repos/REPO/topics"; 50 | const headers = new Headers(); 51 | headers.set("Accept", "application/vnd.github+json"); 52 | 53 | // get tags from GitHub based on repo name 54 | const fetchTags = async (repo) => { 55 | const url = api.replace("REPO", repo); 56 | try { 57 | const response = await (await fetch(url)).json(); 58 | if (response.names) return response.names; 59 | else throw new Error(JSON.stringify(response)); 60 | } catch (error) { 61 | console.groupCollapsed("GitHub fetch tags error"); 62 | console.log(error); 63 | console.groupEnd(); 64 | return []; 65 | } 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /_scripts/search.js: -------------------------------------------------------------------------------- 1 | /* 2 | filters elements on page based on url or search box. 3 | syntax: term1 term2 "full phrase 1" "full phrase 2" "tag: tag 1" 4 | match if: all terms AND at least one phrase AND at least one tag 5 | */ 6 | { 7 | // elements to filter 8 | const elementSelector = ".card, .citation, .post-excerpt"; 9 | // search box element 10 | const searchBoxSelector = ".search-box"; 11 | // results info box element 12 | const infoBoxSelector = ".search-info"; 13 | // tags element 14 | const tagSelector = ".tag"; 15 | 16 | // split search query into terms, phrases, and tags 17 | const splitQuery = (query) => { 18 | // split into parts, preserve quotes 19 | const parts = query.match(/"[^"]*"|\S+/g) || []; 20 | 21 | // bins 22 | const terms = []; 23 | const phrases = []; 24 | const tags = []; 25 | 26 | // put parts into bins 27 | for (let part of parts) { 28 | if (part.startsWith('"')) { 29 | part = part.replaceAll('"', "").trim(); 30 | if (part.startsWith("tag:")) 31 | tags.push(normalizeTag(part.replace(/tag:\s*/, ""))); 32 | else phrases.push(part.toLowerCase()); 33 | } else terms.push(part.toLowerCase()); 34 | } 35 | 36 | return { terms, phrases, tags }; 37 | }; 38 | 39 | // normalize tag string for comparison 40 | window.normalizeTag = (tag) => 41 | tag.trim().toLowerCase().replaceAll(/\s+/g, "-"); 42 | 43 | // get data attribute contents of element and children 44 | const getAttr = (element, attr) => 45 | [element, ...element.querySelectorAll(`[data-${attr}]`)] 46 | .map((element) => element.dataset[attr]) 47 | .join(" "); 48 | 49 | // determine if element should show up in results based on query 50 | const elementMatches = (element, { terms, phrases, tags }) => { 51 | // tag elements within element 52 | const tagElements = [...element.querySelectorAll(".tag")]; 53 | 54 | // check if text content exists in element 55 | const hasText = (string) => 56 | ( 57 | element.innerText + 58 | getAttr(element, "tooltip") + 59 | getAttr(element, "search") 60 | ) 61 | .toLowerCase() 62 | .includes(string); 63 | // check if text matches a tag in element 64 | const hasTag = (string) => 65 | tagElements.some((tag) => normalizeTag(tag.innerText) === string); 66 | 67 | // match logic 68 | return ( 69 | (terms.every(hasText) || !terms.length) && 70 | (phrases.some(hasText) || !phrases.length) && 71 | (tags.some(hasTag) || !tags.length) 72 | ); 73 | }; 74 | 75 | // loop through elements, hide/show based on query, and return results info 76 | const filterElements = (parts) => { 77 | let elements = document.querySelectorAll(elementSelector); 78 | 79 | // results info 80 | let x = 0; 81 | let n = elements.length; 82 | let tags = parts.tags; 83 | 84 | // filter elements 85 | for (const element of elements) { 86 | if (elementMatches(element, parts)) { 87 | element.style.display = ""; 88 | x++; 89 | } else element.style.display = "none"; 90 | } 91 | 92 | return [x, n, tags]; 93 | }; 94 | 95 | // highlight search terms 96 | const highlightMatches = async ({ terms, phrases }) => { 97 | // make sure Mark library available 98 | if (typeof Mark === "undefined") return; 99 | 100 | // reset 101 | new Mark(document.body).unmark(); 102 | 103 | // limit number of highlights to avoid slowdown 104 | let counter = 0; 105 | const filter = () => counter++ < 100; 106 | 107 | // highlight terms and phrases 108 | new Mark(elementSelector) 109 | .mark(terms, { separateWordSearch: true, filter }) 110 | .mark(phrases, { separateWordSearch: false, filter }); 111 | }; 112 | 113 | // update search box based on query 114 | const updateSearchBox = (query = "") => { 115 | const boxes = document.querySelectorAll(searchBoxSelector); 116 | 117 | for (const box of boxes) { 118 | const input = box.querySelector("input"); 119 | const button = box.querySelector("button"); 120 | const icon = box.querySelector("button i"); 121 | input.value = query; 122 | icon.className = input.value.length 123 | ? "icon fa-solid fa-xmark" 124 | : "icon fa-solid fa-magnifying-glass"; 125 | button.disabled = input.value.length ? false : true; 126 | } 127 | }; 128 | 129 | // update info box based on query and results 130 | const updateInfoBox = (query, x, n) => { 131 | const boxes = document.querySelectorAll(infoBoxSelector); 132 | 133 | if (query.trim()) { 134 | // show all info boxes 135 | boxes.forEach((info) => (info.style.display = "")); 136 | 137 | // info template 138 | let info = ""; 139 | info += `Showing ${x.toLocaleString()} of ${n.toLocaleString()} results
`; 140 | info += "Clear search"; 141 | 142 | // set info HTML string 143 | boxes.forEach((el) => (el.innerHTML = info)); 144 | } 145 | // if nothing searched 146 | else { 147 | // hide all info boxes 148 | boxes.forEach((info) => (info.style.display = "none")); 149 | } 150 | }; 151 | 152 | // update tags based on query 153 | const updateTags = (query) => { 154 | const { tags } = splitQuery(query); 155 | document.querySelectorAll(tagSelector).forEach((tag) => { 156 | // set active if tag is in query 157 | if (tags.includes(normalizeTag(tag.innerText))) 158 | tag.setAttribute("data-active", ""); 159 | else tag.removeAttribute("data-active"); 160 | }); 161 | }; 162 | 163 | // run search with query 164 | const runSearch = (query = "") => { 165 | const parts = splitQuery(query); 166 | const [x, n] = filterElements(parts); 167 | updateSearchBox(query); 168 | updateInfoBox(query, x, n); 169 | updateTags(query); 170 | highlightMatches(parts); 171 | }; 172 | 173 | // update url based on query 174 | const updateUrl = (query = "") => { 175 | const url = new URL(window.location); 176 | let params = new URLSearchParams(url.search); 177 | params.set("search", query); 178 | url.search = params.toString(); 179 | window.history.replaceState(null, null, url); 180 | }; 181 | 182 | // search based on url param 183 | const searchFromUrl = () => { 184 | const query = 185 | new URLSearchParams(window.location.search).get("search") || ""; 186 | runSearch(query); 187 | }; 188 | 189 | // return func that runs after delay 190 | const debounce = (callback, delay = 250) => { 191 | let timeout; 192 | return (...args) => { 193 | window.clearTimeout(timeout); 194 | timeout = window.setTimeout(() => callback(...args), delay); 195 | }; 196 | }; 197 | 198 | // when user types into search box 199 | const debouncedRunSearch = debounce(runSearch, 1000); 200 | window.onSearchInput = (target) => { 201 | debouncedRunSearch(target.value); 202 | updateUrl(target.value); 203 | }; 204 | 205 | // when user clears search box with button 206 | window.onSearchClear = () => { 207 | runSearch(); 208 | updateUrl(); 209 | }; 210 | 211 | // after page loads 212 | window.addEventListener("load", searchFromUrl); 213 | // after tags load 214 | window.addEventListener("tagsfetched", searchFromUrl); 215 | } 216 | -------------------------------------------------------------------------------- /_scripts/site-search.js: -------------------------------------------------------------------------------- 1 | /* 2 | for site search component. searches site/domain via google. 3 | */ 4 | 5 | { 6 | // when user submits site search form/box 7 | window.onSiteSearchSubmit = (event) => { 8 | event.preventDefault(); 9 | const google = "https://www.google.com/search?q=site:"; 10 | const site = window.location.origin; 11 | const query = event.target.elements.query.value; 12 | window.location = google + site + " " + query; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /_scripts/table-wrap.js: -------------------------------------------------------------------------------- 1 | /* 2 | put a wrapper around each table to allow scrolling. 3 | */ 4 | 5 | { 6 | const onLoad = () => { 7 | // for each top-level table 8 | const tables = document.querySelectorAll("table:not(table table)"); 9 | for (const table of tables) { 10 | // create wrapper with scroll 11 | const wrapper = document.createElement("div"); 12 | wrapper.style.overflowX = "auto"; 13 | 14 | // undo css force-text-wrap 15 | table.style.overflowWrap = "normal"; 16 | 17 | // add wrapper around table 18 | table.parentNode.insertBefore(wrapper, table); 19 | wrapper.appendChild(table); 20 | } 21 | }; 22 | 23 | // after page loads 24 | window.addEventListener("load", onLoad); 25 | } 26 | -------------------------------------------------------------------------------- /_scripts/tooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | shows a popup of text on hover/focus of any element with the data-tooltip 3 | attribute. 4 | */ 5 | 6 | { 7 | const onLoad = () => { 8 | // make sure Tippy library available 9 | if (typeof tippy === "undefined") return; 10 | 11 | // get elements with non-empty tooltips 12 | const elements = [...document.querySelectorAll("[data-tooltip]")].filter( 13 | (element) => element.dataset.tooltip.trim() && !element._tippy 14 | ); 15 | 16 | // add tooltip to elements 17 | tippy(elements, { 18 | content: (element) => element.dataset.tooltip.trim(), 19 | delay: [200, 0], 20 | offset: [0, 20], 21 | allowHTML: true, 22 | interactive: true, 23 | appendTo: () => document.body, 24 | aria: { 25 | content: "describedby", 26 | expanded: null, 27 | }, 28 | onShow: ({ reference, popper }) => { 29 | const dark = reference.closest("[data-dark]")?.dataset.dark; 30 | if (dark === "false") popper.dataset.dark = true; 31 | if (dark === "true") popper.dataset.dark = false; 32 | }, 33 | // onHide: () => false, // debug 34 | }); 35 | }; 36 | 37 | // after page loads 38 | window.addEventListener("load", onLoad); 39 | // after tags load 40 | window.addEventListener("tagsfetched", onLoad); 41 | } 42 | -------------------------------------------------------------------------------- /_styles/-theme.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | // colors 5 | [data-dark="false"] { 6 | --primary: #0795d9; 7 | --secondary: #7dd3fc; 8 | --text: #000000; 9 | --background: #ffffff; 10 | --background-alt: #fafafa; 11 | --light-gray: #e0e0e0; 12 | --gray: #808080; 13 | --dark-gray: #404040; 14 | --overlay: #00000020; 15 | } 16 | [data-dark="true"] { 17 | --primary: #0795d9; 18 | --secondary: #075985; 19 | --text: #ffffff; 20 | --background: #181818; 21 | --background-alt: #1c1c1c; 22 | --light-gray: #404040; 23 | --gray: #808080; 24 | --dark-gray: #b0b0b0; 25 | --overlay: #ffffff10; 26 | } 27 | 28 | :root { 29 | // font families 30 | --title: "Barlow", sans-serif; 31 | --heading: "Barlow", sans-serif; 32 | --body: "Barlow", sans-serif; 33 | --code: "Roboto Mono", monospace; 34 | 35 | // font sizes 36 | --large: 1.2rem; 37 | --xl: 1.4rem; 38 | --xxl: 1.6rem; 39 | 40 | // font weights 41 | --thin: 200; 42 | --regular: 400; 43 | --semi-bold: 500; 44 | --bold: 600; 45 | 46 | // text line spacing 47 | --spacing: 2; 48 | --compact: 1.5; 49 | 50 | // effects 51 | --rounded: 3px; 52 | --shadow: 0 0 10px 0 var(--overlay); 53 | --transition: 0.2s ease; 54 | } 55 | -------------------------------------------------------------------------------- /_styles/alert.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .alert { 5 | position: relative; 6 | display: flex; 7 | gap: 20px; 8 | align-items: center; 9 | margin: 20px 0; 10 | padding: 20px; 11 | border-radius: var(--rounded); 12 | overflow: hidden; 13 | text-align: left; 14 | line-height: var(--spacing); 15 | } 16 | 17 | .alert:before { 18 | content: ""; 19 | position: absolute; 20 | inset: 0; 21 | opacity: 0.1; 22 | background: var(--color); 23 | z-index: -1; 24 | } 25 | 26 | .alert > .icon { 27 | color: var(--color); 28 | font-size: var(--large); 29 | } 30 | 31 | .alert-content > :first-child { 32 | margin-top: 0; 33 | } 34 | 35 | .alert-content > :last-child { 36 | margin-bottom: 0; 37 | } 38 | -------------------------------------------------------------------------------- /_styles/all.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | *, 5 | ::before, 6 | ::after { 7 | box-sizing: border-box; 8 | -moz-text-size-adjust: none; 9 | -webkit-text-size-adjust: none; 10 | text-size-adjust: none; 11 | } 12 | -------------------------------------------------------------------------------- /_styles/anchor.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .anchor { 5 | display: inline-block; 6 | position: relative; 7 | top: -0.15em; 8 | left: 0.5em; 9 | width: 0; 10 | margin: 0 !important; 11 | color: var(--primary) !important; 12 | opacity: 0; 13 | font-size: 0.75em !important; 14 | text-decoration: none; 15 | transition: opacity var(--transition), color var(--transition); 16 | } 17 | 18 | :hover > .anchor, 19 | .anchor:focus { 20 | opacity: 1; 21 | } 22 | 23 | .anchor:hover { 24 | color: var(--text) !important; 25 | } 26 | -------------------------------------------------------------------------------- /_styles/background.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .background { 5 | position: relative; 6 | background: var(--background); 7 | color: var(--text); 8 | z-index: 1; 9 | } 10 | 11 | .background:before { 12 | content: ""; 13 | position: absolute; 14 | inset: 0; 15 | background-image: var(--image); 16 | background-size: cover; 17 | background-repeat: no-repeat; 18 | background-position: 50% 50%; 19 | opacity: 0.25; 20 | z-index: -1; 21 | } 22 | -------------------------------------------------------------------------------- /_styles/body.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | body { 5 | display: flex; 6 | flex-direction: column; 7 | margin: 0; 8 | padding: 0; 9 | min-height: 100vh; 10 | background: var(--background); 11 | color: var(--text); 12 | font-family: var(--body); 13 | text-align: center; 14 | line-height: var(--compact); 15 | } 16 | -------------------------------------------------------------------------------- /_styles/bold.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | b, 5 | strong { 6 | font-weight: var(--bold); 7 | } 8 | -------------------------------------------------------------------------------- /_styles/button.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | button { 5 | cursor: pointer; 6 | } 7 | 8 | .button-wrapper { 9 | display: contents; 10 | } 11 | 12 | .button { 13 | display: inline-flex; 14 | justify-content: center; 15 | align-items: center; 16 | gap: 10px; 17 | max-width: calc(100% - 5px - 5px); 18 | margin: 5px; 19 | padding: 8px 15px; 20 | border: none; 21 | border-radius: var(--rounded); 22 | background: var(--primary); 23 | color: var(--background); 24 | text-align: center; 25 | font: inherit; 26 | font-family: var(--heading); 27 | font-weight: var(--semi-bold); 28 | text-decoration: none; 29 | vertical-align: middle; 30 | appearance: none; 31 | transition: background var(--transition), color var(--transition); 32 | } 33 | 34 | .button:hover { 35 | background: var(--text); 36 | color: var(--background); 37 | } 38 | 39 | .button[data-style="bare"] { 40 | padding: 5px; 41 | background: none; 42 | color: var(--primary); 43 | 44 | &:hover { 45 | color: var(--text); 46 | } 47 | } 48 | 49 | .button[data-flip] { 50 | flex-direction: row-reverse; 51 | } 52 | -------------------------------------------------------------------------------- /_styles/card.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .card { 5 | display: inline-flex; 6 | justify-content: stretch; 7 | align-items: center; 8 | flex-direction: column; 9 | width: 350px; 10 | max-width: calc(100% - 20px - 20px); 11 | margin: 20px; 12 | background: var(--background); 13 | border-radius: var(--rounded); 14 | overflow: hidden; 15 | box-shadow: var(--shadow); 16 | vertical-align: top; 17 | } 18 | 19 | .card[data-style="small"] { 20 | width: 250px; 21 | } 22 | 23 | .card-image img { 24 | aspect-ratio: 3 / 2; 25 | object-fit: cover; 26 | width: 100%; 27 | // box-shadow: var(--shadow); 28 | } 29 | 30 | .card-text { 31 | display: inline-flex; 32 | justify-content: flex-start; 33 | align-items: center; 34 | flex-direction: column; 35 | gap: 20px; 36 | max-width: 100%; 37 | padding: 20px; 38 | } 39 | 40 | .card-text > * { 41 | margin: 0 !important; 42 | } 43 | 44 | .card-title { 45 | font-family: var(--heading); 46 | font-weight: var(--semi-bold); 47 | } 48 | 49 | .card-subtitle { 50 | margin-top: -10px !important; 51 | font-style: italic; 52 | } 53 | -------------------------------------------------------------------------------- /_styles/checkbox.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | input[type="checkbox"] { 5 | cursor: pointer; 6 | } 7 | -------------------------------------------------------------------------------- /_styles/citation.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | $thumb-size: 180px; 5 | $wrap: 800px; 6 | 7 | .citation-container { 8 | container-type: inline-size; 9 | } 10 | 11 | .citation { 12 | display: flex; 13 | margin: 20px 0; 14 | border-radius: var(--rounded); 15 | background: var(--background); 16 | overflow: hidden; 17 | box-shadow: var(--shadow); 18 | } 19 | 20 | .citation-image { 21 | position: relative; 22 | width: $thumb-size; 23 | flex-shrink: 0; 24 | // box-shadow: var(--shadow); 25 | } 26 | 27 | .citation-image img { 28 | position: absolute; 29 | inset: 0; 30 | width: 100%; 31 | height: 100%; 32 | object-fit: contain; 33 | } 34 | 35 | .citation-text { 36 | position: relative; 37 | display: inline-flex; 38 | flex-wrap: wrap; 39 | gap: 10px; 40 | max-width: 100%; 41 | height: min-content; 42 | padding: 20px; 43 | padding-left: 30px; 44 | text-align: left; 45 | overflow-wrap: break-word; 46 | z-index: 0; 47 | } 48 | 49 | .citation-title, 50 | .citation-authors, 51 | .citation-details, 52 | .citation-description { 53 | width: 100%; 54 | } 55 | 56 | .citation-title { 57 | font-weight: var(--semi-bold); 58 | } 59 | 60 | .citation-text > .icon { 61 | position: absolute; 62 | top: 20px; 63 | right: 20px; 64 | color: var(--light-gray); 65 | opacity: 0.5; 66 | font-size: 30px; 67 | z-index: -1; 68 | } 69 | 70 | .citation-publisher { 71 | text-transform: capitalize; 72 | } 73 | 74 | .citation-description { 75 | color: var(--gray); 76 | } 77 | 78 | .citation-buttons { 79 | display: flex; 80 | flex-wrap: wrap; 81 | gap: 10px; 82 | } 83 | 84 | .citation-buttons .button { 85 | margin: 0; 86 | } 87 | 88 | .citation-text > .tags { 89 | display: inline-flex; 90 | justify-content: flex-start; 91 | margin: 0; 92 | } 93 | 94 | @container (max-width: #{$wrap}) { 95 | .citation { 96 | flex-direction: column; 97 | } 98 | 99 | .citation-image { 100 | width: unset; 101 | height: $thumb-size; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /_styles/code.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | pre, 5 | code, 6 | pre *, 7 | code * { 8 | font-family: var(--code); 9 | } 10 | 11 | // inline code 12 | code.highlighter-rouge { 13 | padding: 2px 6px; 14 | background: var(--light-gray); 15 | border-radius: var(--rounded); 16 | } 17 | 18 | // code block 19 | div.highlighter-rouge { 20 | width: 100%; 21 | margin: 40px 0; 22 | border-radius: var(--rounded); 23 | overflow-x: auto; 24 | overflow-y: auto; 25 | text-align: left; 26 | 27 | div.highlight { 28 | display: contents; 29 | 30 | pre.highlight { 31 | width: fit-content; 32 | min-width: 100%; 33 | margin: 0; 34 | padding: 20px; 35 | color: var(--white); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /_styles/cols.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | $two: 750px; 5 | $one: 500px; 6 | 7 | .cols { 8 | display: grid; 9 | --repeat: min(3, var(--cols)); 10 | grid-template-columns: repeat(var(--repeat), 1fr); 11 | align-items: flex-start; 12 | gap: 40px; 13 | margin: 40px 0; 14 | } 15 | 16 | .cols > * { 17 | min-width: 0; 18 | min-height: 0; 19 | } 20 | 21 | .cols > div > :first-child { 22 | margin-top: 0 !important; 23 | } 24 | 25 | .cols > div > :last-child { 26 | margin-bottom: 0 !important; 27 | } 28 | 29 | @media (max-width: $two) { 30 | .cols { 31 | --repeat: min(2, var(--cols)); 32 | } 33 | } 34 | 35 | @media (max-width: $one) { 36 | .cols { 37 | --repeat: min(1, var(--cols)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /_styles/dark-toggle.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .dark-toggle { 5 | position: relative; 6 | width: 40px; 7 | height: 25px; 8 | margin: 0; 9 | border-radius: 999px; 10 | background: var(--primary); 11 | appearance: none; 12 | transition: background var(--transition); 13 | } 14 | 15 | .dark-toggle:after { 16 | content: "\f185"; 17 | position: absolute; 18 | left: 12px; 19 | top: 50%; 20 | color: var(--text); 21 | font-size: 15px; 22 | font-family: "Font Awesome 6 Free"; 23 | font-weight: 900; 24 | transform: translate(-50%, -50%); 25 | transition: left var(--transition); 26 | } 27 | 28 | .dark-toggle:checked:after { 29 | content: "\f186"; 30 | left: calc(100% - 12px); 31 | } 32 | -------------------------------------------------------------------------------- /_styles/details.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | details { 5 | margin: 20px 0; 6 | padding: 0 20px; 7 | border-radius: var(--rounded); 8 | background: var(--theme-light); 9 | border: solid 1px var(--light-gray); 10 | text-align: left; 11 | overflow: hidden; 12 | } 13 | 14 | summary { 15 | list-style: none; 16 | margin: 0 -20px; 17 | padding: 10px; 18 | line-height: var(--compact); 19 | transition: background var(--transition); 20 | cursor: pointer; 21 | } 22 | 23 | summary:hover { 24 | background: var(--light-gray); 25 | } 26 | 27 | details[open] summary { 28 | background: var(--light-gray); 29 | } 30 | 31 | summary::before { 32 | content: "❯"; 33 | display: inline-flex; 34 | justify-content: center; 35 | align-items: center; 36 | margin-right: 5px; 37 | width: 1em; 38 | height: 1em; 39 | } 40 | 41 | details[open] summary::before { 42 | transform: rotate(90deg); 43 | } 44 | -------------------------------------------------------------------------------- /_styles/feature.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | $wrap: 800px; 5 | 6 | .feature { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | gap: 40px; 11 | margin: 40px 0; 12 | } 13 | 14 | .feature-image { 15 | flex-shrink: 0; 16 | width: 40%; 17 | aspect-ratio: 3 / 2; 18 | border-radius: var(--rounded); 19 | overflow: hidden; 20 | box-shadow: var(--shadow); 21 | } 22 | 23 | .feature-image img { 24 | width: 100%; 25 | height: 100%; 26 | object-fit: cover; 27 | } 28 | 29 | .feature-text { 30 | flex-grow: 1; 31 | } 32 | 33 | .feature-title { 34 | font-size: var(--large); 35 | text-align: center; 36 | font-family: var(--heading); 37 | font-weight: var(--semi-bold); 38 | } 39 | 40 | .feature[data-flip] { 41 | flex-direction: row-reverse; 42 | } 43 | 44 | @media (max-width: $wrap) { 45 | .feature { 46 | flex-direction: column !important; 47 | } 48 | 49 | .feature-image { 50 | width: 100%; 51 | max-width: calc($wrap / 2); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /_styles/figure.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .figure { 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | flex-direction: column; 9 | gap: 10px; 10 | margin: 40px 0; 11 | } 12 | 13 | .figure-image { 14 | display: contents; 15 | } 16 | 17 | .figure-image img { 18 | border-radius: var(--rounded); 19 | overflow: hidden; 20 | box-shadow: var(--shadow); 21 | } 22 | 23 | .figure-caption { 24 | font-style: italic; 25 | text-align: center; 26 | } 27 | -------------------------------------------------------------------------------- /_styles/float.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | $wrap: 600px; 5 | 6 | .float { 7 | margin-bottom: 20px; 8 | max-width: 50%; 9 | } 10 | 11 | .float > * { 12 | margin: 0 !important; 13 | } 14 | 15 | .float:not([data-flip]) { 16 | float: left; 17 | margin-right: 40px; 18 | } 19 | 20 | .float[data-flip] { 21 | float: right; 22 | margin-left: 40px; 23 | } 24 | 25 | .float[data-clear] { 26 | float: unset; 27 | clear: both; 28 | margin: 0; 29 | } 30 | 31 | @media (max-width: $wrap) { 32 | .float { 33 | float: unset !important; 34 | clear: both !important; 35 | margin: auto !important; 36 | max-width: unset; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /_styles/font.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @font-face { 5 | } 6 | -------------------------------------------------------------------------------- /_styles/footer.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | footer { 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | flex-direction: column; 9 | gap: 20px; 10 | padding: 40px; 11 | line-height: var(--spacing); 12 | box-shadow: var(--shadow); 13 | } 14 | 15 | footer a { 16 | color: var(--text) !important; 17 | } 18 | 19 | footer a:hover { 20 | color: var(--primary) !important; 21 | } 22 | 23 | footer .icon { 24 | font-size: var(--xl); 25 | } 26 | -------------------------------------------------------------------------------- /_styles/form.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | form { 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | gap: 10px; 9 | } 10 | -------------------------------------------------------------------------------- /_styles/grid.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | $two: 750px; 5 | $one: 500px; 6 | 7 | .grid { 8 | display: grid; 9 | --repeat: 3; 10 | grid-template-columns: repeat(var(--repeat), 1fr); 11 | justify-content: center; 12 | align-items: flex-start; 13 | gap: 40px; 14 | margin: 40px 0; 15 | } 16 | 17 | .grid > * { 18 | min-width: 0; 19 | min-height: 0; 20 | width: 100%; 21 | // max-height: 50vh; 22 | margin: 0 !important; 23 | } 24 | 25 | @media (max-width: $two) { 26 | .grid { 27 | --repeat: 2; 28 | } 29 | } 30 | 31 | @media (max-width: $one) { 32 | .grid { 33 | --repeat: 1; 34 | } 35 | } 36 | 37 | .grid[data-style="square"] { 38 | align-items: center; 39 | 40 | & > * { 41 | aspect-ratio: 1 / 1; 42 | } 43 | 44 | & img { 45 | aspect-ratio: 1 / 1; 46 | object-fit: cover; 47 | max-width: unset; 48 | max-height: unset; 49 | } 50 | } 51 | 52 | .grid > :where(h1, h2, h3, h4, h5, h6) { 53 | display: none; 54 | } 55 | -------------------------------------------------------------------------------- /_styles/header.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | $logo-big: 80px; 5 | $logo: 40px; 6 | $big-padding: 100px; 7 | $collapse: 700px; 8 | $sticky: true; 9 | 10 | header { 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | flex-wrap: wrap; 15 | gap: 20px; 16 | padding: 20px; 17 | box-shadow: var(--shadow); 18 | 19 | @if $sticky { 20 | position: sticky !important; 21 | top: 0; 22 | z-index: 10 !important; 23 | } 24 | } 25 | 26 | header a { 27 | color: var(--text); 28 | text-decoration: none; 29 | } 30 | 31 | .home { 32 | display: flex; 33 | justify-content: flex-start; 34 | align-items: center; 35 | gap: 10px; 36 | flex-basis: 0; 37 | flex-grow: 1; 38 | max-width: 100%; 39 | } 40 | 41 | .logo { 42 | height: $logo; 43 | } 44 | 45 | .logo > * { 46 | width: unset; 47 | height: 100%; 48 | } 49 | 50 | .title-text { 51 | display: flex; 52 | justify-content: flex-start; 53 | align-items: baseline; 54 | flex-wrap: wrap; 55 | gap: 5px; 56 | min-width: 0; 57 | font-family: var(--title); 58 | text-align: left; 59 | } 60 | 61 | .title { 62 | font-size: var(--large); 63 | } 64 | 65 | .subtitle { 66 | opacity: 0.65; 67 | font-weight: var(--thin); 68 | } 69 | 70 | .nav-toggle { 71 | display: none; 72 | position: relative; 73 | width: 30px; 74 | height: 30px; 75 | margin: 0; 76 | color: var(--text); 77 | appearance: none; 78 | transition: background var(--transition); 79 | } 80 | 81 | .nav-toggle:after { 82 | content: "\f0c9"; 83 | position: absolute; 84 | left: 50%; 85 | top: 50%; 86 | color: var(--text); 87 | font-size: 15px; 88 | font-family: "Font Awesome 6 Free"; 89 | font-weight: 900; 90 | transform: translate(-50%, -50%); 91 | } 92 | 93 | .nav-toggle:checked:after { 94 | content: "\f00d"; 95 | } 96 | 97 | nav { 98 | display: flex; 99 | justify-content: center; 100 | align-items: center; 101 | flex-wrap: wrap; 102 | gap: 10px; 103 | font-family: var(--heading); 104 | text-transform: uppercase; 105 | } 106 | 107 | nav > a { 108 | padding: 5px; 109 | } 110 | 111 | nav > a:hover { 112 | color: var(--primary); 113 | } 114 | 115 | header:not([data-big]) { 116 | @media (max-width: $collapse) { 117 | justify-content: flex-end; 118 | 119 | .nav-toggle { 120 | display: flex; 121 | } 122 | 123 | .nav-toggle:not(:checked) + nav { 124 | display: none; 125 | } 126 | 127 | nav { 128 | align-items: flex-end; 129 | flex-direction: column; 130 | width: 100%; 131 | } 132 | } 133 | } 134 | 135 | header[data-big] { 136 | justify-content: center; 137 | align-items: center; 138 | flex-direction: column; 139 | padding: $big-padding 20px; 140 | 141 | @if $sticky { 142 | top: unset; 143 | } 144 | 145 | .home { 146 | flex-direction: column; 147 | flex-grow: 0; 148 | } 149 | 150 | .logo { 151 | height: $logo-big; 152 | flex-shrink: 0; 153 | } 154 | 155 | .title-text { 156 | flex-direction: column; 157 | align-items: center; 158 | text-align: center; 159 | } 160 | 161 | .title { 162 | font-size: var(--xxl); 163 | } 164 | 165 | .subtitle { 166 | font-size: var(--large); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /_styles/heading.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | h1, 5 | h2, 6 | h3, 7 | h4, 8 | h5, 9 | h6 { 10 | margin: 40px 0 20px 0; 11 | font-family: var(--heading); 12 | font-weight: var(--semi-bold); 13 | text-align: left; 14 | letter-spacing: 1px; 15 | } 16 | 17 | h1 { 18 | margin: 40px 0; 19 | font-size: 1.6rem; 20 | font-weight: var(--regular); 21 | text-transform: uppercase; 22 | text-align: center; 23 | } 24 | 25 | h2 { 26 | font-size: 1.6rem; 27 | padding-bottom: 5px; 28 | border-bottom: solid 1px var(--light-gray); 29 | font-weight: var(--regular); 30 | } 31 | 32 | h3 { 33 | font-size: 1.5rem; 34 | } 35 | 36 | h4 { 37 | font-size: 1.3rem; 38 | } 39 | 40 | h5 { 41 | font-size: 1.15rem; 42 | } 43 | 44 | h6 { 45 | font-size: 1rem; 46 | } 47 | 48 | :where(h1, h2, h3, h4, h5, h6) > .icon { 49 | margin-right: 1em; 50 | color: var(--light-gray); 51 | } 52 | -------------------------------------------------------------------------------- /_styles/highlight.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | mark { 5 | background: #fef08a; 6 | color: #000000; 7 | } 8 | -------------------------------------------------------------------------------- /_styles/icon.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .icon { 5 | font-size: 1em; 6 | } 7 | 8 | span.icon { 9 | line-height: 1; 10 | } 11 | 12 | span.icon > svg { 13 | position: relative; 14 | top: 0.1em; 15 | height: 1em; 16 | } 17 | -------------------------------------------------------------------------------- /_styles/image.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | img { 5 | max-width: 100%; 6 | max-height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /_styles/link.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | a { 5 | color: var(--primary); 6 | transition: color var(--transition); 7 | overflow-wrap: break-word; 8 | } 9 | 10 | a:hover { 11 | color: var(--text); 12 | } 13 | 14 | a:not([href]) { 15 | color: var(--text); 16 | } 17 | -------------------------------------------------------------------------------- /_styles/list.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | ul, 5 | ol { 6 | margin: 20px 0; 7 | padding-left: 40px; 8 | } 9 | 10 | ul { 11 | list-style-type: square; 12 | } 13 | 14 | li { 15 | margin: 5px 0; 16 | padding-left: 10px; 17 | text-align: justify; 18 | line-height: var(--spacing); 19 | 20 | ul, 21 | ol { 22 | margin: 0; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /_styles/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | main { 5 | display: flex; 6 | flex-direction: column; 7 | flex-grow: 1; 8 | } 9 | -------------------------------------------------------------------------------- /_styles/paragraph.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | p { 5 | margin: 20px 0; 6 | text-align: justify; 7 | line-height: var(--spacing); 8 | } 9 | -------------------------------------------------------------------------------- /_styles/portrait.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .portrait-wrapper { 5 | display: contents; 6 | } 7 | 8 | .portrait { 9 | position: relative; 10 | display: inline-flex; 11 | justify-content: center; 12 | align-items: center; 13 | flex-direction: column; 14 | margin: 20px; 15 | width: 175px; 16 | max-width: calc(100% - 20px - 20px); 17 | text-decoration: none; 18 | } 19 | 20 | .portrait[data-style="small"] { 21 | width: 100px; 22 | } 23 | 24 | .portrait[data-style="tiny"] { 25 | flex-direction: row; 26 | gap: 15px; 27 | width: unset; 28 | text-align: left; 29 | } 30 | 31 | .portrait .icon { 32 | position: absolute; 33 | left: 0; 34 | top: 0; 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | width: calc(20px + 10%); 39 | aspect-ratio: 1 / 1; 40 | border-radius: 999px; 41 | background: var(--background); 42 | box-shadow: var(--shadow); 43 | transform: translate(14%, 14%); 44 | } 45 | 46 | .portrait[data-style="small"] .icon { 47 | left: -2px; 48 | top: -2px; 49 | } 50 | 51 | .portrait[data-style="tiny"] .icon { 52 | display: none; 53 | } 54 | 55 | .portrait-image { 56 | width: 100%; 57 | margin-bottom: 20px; 58 | aspect-ratio: 1 / 1; 59 | border-radius: 999px; 60 | object-fit: cover; 61 | box-shadow: var(--shadow); 62 | } 63 | 64 | .portrait[data-style="tiny"] .portrait-image { 65 | width: 50px; 66 | margin: 0; 67 | } 68 | 69 | .portrait-name { 70 | font-family: var(--heading); 71 | font-weight: var(--semi-bold); 72 | } 73 | 74 | .portrait[data-style="tiny"] { 75 | .portrait-description, 76 | .portrait-affiliation { 77 | display: none; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /_styles/post-excerpt.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | $thumb-size: 200px; 5 | $wrap: 800px; 6 | 7 | .post-excerpt-container { 8 | container-type: inline-size; 9 | } 10 | 11 | .post-excerpt { 12 | display: flex; 13 | margin: 20px 0; 14 | border-radius: var(--rounded); 15 | background: var(--background); 16 | overflow: hidden; 17 | box-shadow: var(--shadow); 18 | } 19 | 20 | .post-excerpt-image { 21 | position: relative; 22 | width: $thumb-size; 23 | flex-shrink: 0; 24 | // box-shadow: var(--shadow); 25 | } 26 | 27 | .post-excerpt-image img { 28 | position: absolute; 29 | inset: 0; 30 | width: 100%; 31 | height: 100%; 32 | object-fit: cover; 33 | } 34 | 35 | .post-excerpt-text { 36 | display: flex; 37 | flex-wrap: wrap; 38 | gap: 20px; 39 | padding: 20px 30px; 40 | text-align: left; 41 | } 42 | 43 | .post-excerpt-text > * { 44 | margin: 0 !important; 45 | } 46 | 47 | .post-excerpt-text > a:first-child { 48 | width: 100%; 49 | font-weight: var(--semi-bold); 50 | } 51 | 52 | .post-excerpt-text > div { 53 | justify-content: flex-start; 54 | } 55 | 56 | .post-excerpt-text > p { 57 | width: 100%; 58 | } 59 | 60 | @container (max-width: #{$wrap}) { 61 | .post-excerpt { 62 | flex-direction: column; 63 | } 64 | 65 | .post-excerpt-image { 66 | width: unset; 67 | height: $thumb-size; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /_styles/post-info.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .post-info { 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | flex-wrap: wrap; 9 | gap: 20px; 10 | margin: 20px 0; 11 | color: var(--dark-gray); 12 | } 13 | 14 | .post-info .portrait { 15 | margin: 0; 16 | } 17 | 18 | .post-info .icon { 19 | margin-right: 0.5em; 20 | } 21 | 22 | .post-info a { 23 | color: inherit; 24 | } 25 | 26 | .post-info a:hover { 27 | color: var(--primary); 28 | } 29 | 30 | .post-info > span { 31 | text-align: center; 32 | white-space: nowrap; 33 | } 34 | -------------------------------------------------------------------------------- /_styles/post-nav.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | $wrap: 600px; 5 | 6 | .post-nav { 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: flex-start; 10 | gap: 10px; 11 | color: var(--gray); 12 | } 13 | 14 | .post-nav > :first-child { 15 | text-align: left; 16 | } 17 | 18 | .post-nav > :last-child { 19 | text-align: right; 20 | } 21 | 22 | .post-nav > :first-child .icon { 23 | margin-right: 0.5em; 24 | } 25 | 26 | .post-nav > :last-child .icon { 27 | margin-left: 0.5em; 28 | } 29 | 30 | @media (max-width: $wrap) { 31 | .post-nav { 32 | align-items: center; 33 | flex-direction: column; 34 | } 35 | 36 | .post-nav > * { 37 | text-align: center !important; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /_styles/quote.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | blockquote { 5 | margin: 20px 0; 6 | padding: 10px 20px; 7 | border-left: solid 4px var(--light-gray); 8 | } 9 | 10 | blockquote > :first-child { 11 | margin-top: 0; 12 | } 13 | 14 | blockquote > :last-child { 15 | margin-bottom: 0; 16 | } 17 | -------------------------------------------------------------------------------- /_styles/rule.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | hr { 5 | margin: 40px 0; 6 | background: var(--light-gray); 7 | border: none; 8 | height: 1px; 9 | } 10 | -------------------------------------------------------------------------------- /_styles/search-box.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .search-box { 5 | position: relative; 6 | height: 40px; 7 | } 8 | 9 | .search-box .search-input { 10 | width: 100%; 11 | height: 100%; 12 | padding-right: 40px; 13 | } 14 | 15 | .search-box button { 16 | position: absolute; 17 | inset: 0 0 0 auto; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | padding: 0; 22 | aspect-ratio: 1 / 1; 23 | background: none; 24 | color: var(--black); 25 | border: none; 26 | } 27 | -------------------------------------------------------------------------------- /_styles/search-info.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .search-info { 5 | margin: 20px 0; 6 | text-align: center; 7 | font-style: italic; 8 | line-height: var(--spacing); 9 | } 10 | -------------------------------------------------------------------------------- /_styles/section.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | $page: 1000px; 5 | $padding: 40px; 6 | 7 | section { 8 | padding: $padding max($padding, calc((100% - $page) / 2)); 9 | transition: background var(--transition), color var(--transition); 10 | } 11 | 12 | section[data-size="wide"] { 13 | padding: $padding; 14 | } 15 | 16 | section[data-size="full"] { 17 | padding: 0; 18 | } 19 | 20 | section[data-size="full"] > * { 21 | margin: 0; 22 | border-radius: 0; 23 | } 24 | 25 | section[data-size="full"] img { 26 | border-radius: 0; 27 | } 28 | 29 | main > section:last-of-type { 30 | flex-grow: 1; 31 | } 32 | 33 | main > section:nth-of-type(odd) { 34 | background: var(--background); 35 | } 36 | 37 | main > section:nth-of-type(even) { 38 | background: var(--background-alt); 39 | } 40 | -------------------------------------------------------------------------------- /_styles/table.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | table { 5 | margin: 40px auto; 6 | border-collapse: collapse; 7 | overflow-wrap: anywhere; 8 | } 9 | 10 | th { 11 | font-weight: var(--semi-bold); 12 | } 13 | 14 | th, 15 | td { 16 | padding: 10px 15px; 17 | border: solid 1px var(--light-gray); 18 | } 19 | -------------------------------------------------------------------------------- /_styles/tags.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .tags { 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | flex-wrap: wrap; 9 | gap: 10px; 10 | max-width: 100%; 11 | margin: 20px 0; 12 | } 13 | 14 | .tag { 15 | max-width: 100%; 16 | margin: 0; 17 | padding: 5px 10px; 18 | border-radius: 999px; 19 | background: var(--secondary); 20 | color: var(--text); 21 | text-decoration: none; 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | white-space: nowrap; 25 | transition: background var(--transition), color var(--transition); 26 | } 27 | 28 | .tag:hover { 29 | background: var(--light-gray); 30 | } 31 | 32 | .tag[data-active] { 33 | background: var(--light-gray); 34 | } 35 | -------------------------------------------------------------------------------- /_styles/textbox.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | input[type="text"] { 5 | width: 100%; 6 | height: 40px; 7 | margin: 0; 8 | padding: 5px 10px; 9 | border: solid 1px var(--light-gray); 10 | border-radius: var(--rounded); 11 | background: var(--background); 12 | color: var(--text); 13 | font-family: inherit; 14 | font-size: inherit; 15 | appearance: none; 16 | box-shadow: var(--shadow); 17 | } 18 | -------------------------------------------------------------------------------- /_styles/tooltip.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .tippy-box { 5 | background: var(--background); 6 | color: var(--text); 7 | padding: 7.5px; 8 | text-align: left; 9 | box-shadow: var(--shadow); 10 | } 11 | 12 | .tippy-arrow { 13 | width: 30px; 14 | height: 30px; 15 | } 16 | 17 | .tippy-arrow:before { 18 | width: 10px; 19 | height: 10px; 20 | background: var(--background); 21 | box-shadow: var(--shadow); 22 | } 23 | 24 | // correct tippy arrow styles to support intuitive arrow styles above 25 | .tippy-arrow { 26 | overflow: hidden; 27 | pointer-events: none; 28 | } 29 | .tippy-box[data-placement="top"] .tippy-arrow { 30 | inset: unset; 31 | top: 100%; 32 | } 33 | .tippy-box[data-placement="bottom"] .tippy-arrow { 34 | inset: unset; 35 | bottom: 100%; 36 | } 37 | .tippy-box[data-placement="left"] .tippy-arrow { 38 | inset: unset; 39 | left: 100%; 40 | } 41 | .tippy-box[data-placement="right"] .tippy-arrow { 42 | inset: unset; 43 | right: 100%; 44 | } 45 | .tippy-arrow:before { 46 | border: unset !important; 47 | transform-origin: center !important; 48 | transform: translate(-50%, -50%) rotate(45deg) !important; 49 | } 50 | .tippy-box[data-placement="top"] .tippy-arrow:before { 51 | left: 50% !important; 52 | top: 0 !important; 53 | } 54 | .tippy-box[data-placement="bottom"] .tippy-arrow:before { 55 | left: 50% !important; 56 | top: 100% !important; 57 | } 58 | .tippy-box[data-placement="left"] .tippy-arrow:before { 59 | left: 0 !important; 60 | top: 50% !important; 61 | } 62 | .tippy-box[data-placement="right"] .tippy-arrow:before { 63 | left: 100% !important; 64 | top: 50% !important; 65 | } 66 | -------------------------------------------------------------------------------- /_styles/util.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | .left { 5 | text-align: left; 6 | } 7 | 8 | .center { 9 | text-align: center; 10 | } 11 | 12 | .right { 13 | text-align: right; 14 | } 15 | -------------------------------------------------------------------------------- /blog/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Blog 3 | nav: 4 | order: 4 5 | tooltip: Musings and miscellany 6 | --- 7 | 8 | # {% include icon.html icon="fa-solid fa-feather-pointed" %}Blog 9 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 11 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 12 | 13 | {% include section.html %} 14 | 15 | {% include search-box.html %} 16 | 17 | {% include tags.html tags=site.tags %} 18 | 19 | {% include search-info.html %} 20 | 21 | {% include list.html data="posts" component="post-excerpt" %} 22 | -------------------------------------------------------------------------------- /contact/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contact 3 | nav: 4 | order: 5 5 | tooltip: Email, address, and location 6 | --- 7 | 8 | # {% include icon.html icon="fa-regular fa-envelope" %}Contact 9 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 11 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 12 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 13 | 14 | {% 15 | include button.html 16 | type="email" 17 | text="jane@smith.com" 18 | link="jane@smith.com" 19 | %} 20 | {% 21 | include button.html 22 | type="phone" 23 | text="(555) 867-5309" 24 | link="+1-555-867-5309" 25 | %} 26 | {% 27 | include button.html 28 | type="address" 29 | tooltip="Our location on Google Maps for easy navigation" 30 | link="https://www.google.com/maps" 31 | %} 32 | 33 | {% include section.html %} 34 | 35 | {% capture col1 %} 36 | 37 | {% 38 | include figure.html 39 | image="images/photo.jpg" 40 | caption="Lorem ipsum" 41 | %} 42 | 43 | {% endcapture %} 44 | 45 | {% capture col2 %} 46 | 47 | {% 48 | include figure.html 49 | image="images/photo.jpg" 50 | caption="Lorem ipsum" 51 | %} 52 | 53 | {% endcapture %} 54 | 55 | {% include cols.html col1=col1 col2=col2 %} 56 | 57 | {% include section.html dark=true %} 58 | 59 | {% capture col1 %} 60 | Lorem ipsum dolor sit amet 61 | consectetur adipiscing elit 62 | sed do eiusmod tempor 63 | {% endcapture %} 64 | 65 | {% capture col2 %} 66 | Lorem ipsum dolor sit amet 67 | consectetur adipiscing elit 68 | sed do eiusmod tempor 69 | {% endcapture %} 70 | 71 | {% capture col3 %} 72 | Lorem ipsum dolor sit amet 73 | consectetur adipiscing elit 74 | sed do eiusmod tempor 75 | {% endcapture %} 76 | 77 | {% include cols.html col1=col1 col2=col2 col3=col3 %} 78 | -------------------------------------------------------------------------------- /images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenelab/lab-website-template/ebeb5e200ae5d61df5265f5594f59b5a45d2d4fa/images/background.jpg -------------------------------------------------------------------------------- /images/fallback.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenelab/lab-website-template/ebeb5e200ae5d61df5265f5594f59b5a45d2d4fa/images/icon.png -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | 50 | 68 | 69 | -------------------------------------------------------------------------------- /images/photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenelab/lab-website-template/ebeb5e200ae5d61df5265f5594f59b5a45d2d4fa/images/photo.jpg -------------------------------------------------------------------------------- /images/share.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenelab/lab-website-template/ebeb5e200ae5d61df5265f5594f59b5a45d2d4fa/images/share.jpg -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | # Lab Website Template 5 | 6 | [Lab Website Template](https://github.com/greenelab/lab-website-template) is an easy-to-use, flexible website template for [labs](https://www.greenelab.com/). 7 | Spend less time worrying about managing a website and citations, and more time running your lab. 8 | 9 | {% 10 | include button.html 11 | type="docs" 12 | link="https://greene-lab.gitbook.io/lab-website-template-docs" 13 | %} 14 | {% 15 | include button.html 16 | type="github" 17 | text="On GitHub" 18 | link="greenelab/lab-website-template" 19 | %} 20 | 21 | {% include section.html %} 22 | 23 | ## Highlights 24 | 25 | {% capture text %} 26 | 27 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 28 | 29 | {% 30 | include button.html 31 | link="research" 32 | text="See our publications" 33 | icon="fa-solid fa-arrow-right" 34 | flip=true 35 | style="bare" 36 | %} 37 | 38 | {% endcapture %} 39 | 40 | {% 41 | include feature.html 42 | image="images/photo.jpg" 43 | link="research" 44 | title="Our Research" 45 | text=text 46 | %} 47 | 48 | {% capture text %} 49 | 50 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 51 | 52 | {% 53 | include button.html 54 | link="projects" 55 | text="Browse our projects" 56 | icon="fa-solid fa-arrow-right" 57 | flip=true 58 | style="bare" 59 | %} 60 | 61 | {% endcapture %} 62 | 63 | {% 64 | include feature.html 65 | image="images/photo.jpg" 66 | link="projects" 67 | title="Our Projects" 68 | flip=true 69 | style="bare" 70 | text=text 71 | %} 72 | 73 | {% capture text %} 74 | 75 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 76 | 77 | {% 78 | include button.html 79 | link="team" 80 | text="Meet our team" 81 | icon="fa-solid fa-arrow-right" 82 | flip=true 83 | style="bare" 84 | %} 85 | 86 | {% endcapture %} 87 | 88 | {% 89 | include feature.html 90 | image="images/photo.jpg" 91 | link="team" 92 | title="Our Team" 93 | text=text 94 | %} 95 | -------------------------------------------------------------------------------- /projects/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Projects 3 | nav: 4 | order: 2 5 | tooltip: Software, datasets, and more 6 | --- 7 | 8 | # {% include icon.html icon="fa-solid fa-wrench" %}Projects 9 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 11 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 12 | 13 | {% include tags.html tags="publication, resource, website" %} 14 | 15 | {% include search-info.html %} 16 | 17 | {% include section.html %} 18 | 19 | ## Featured 20 | 21 | {% include list.html component="card" data="projects" filter="group == 'featured'" %} 22 | 23 | {% include section.html %} 24 | 25 | ## More 26 | 27 | {% include list.html component="card" data="projects" filter="!group" style="small" %} 28 | -------------------------------------------------------------------------------- /research/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Research 3 | nav: 4 | order: 1 5 | tooltip: Published works 6 | --- 7 | 8 | # {% include icon.html icon="fa-solid fa-microscope" %}Research 9 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 11 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 12 | 13 | {% include section.html %} 14 | 15 | ## Highlighted 16 | 17 | {% include citation.html lookup="Open collaborative writing with Manubot" style="rich" %} 18 | 19 | {% include section.html %} 20 | 21 | ## All 22 | 23 | {% include search-box.html %} 24 | 25 | {% include search-info.html %} 26 | 27 | {% include list.html data="citations" component="citation" style="rich" %} 28 | -------------------------------------------------------------------------------- /team/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Team 3 | nav: 4 | order: 3 5 | tooltip: About our team 6 | --- 7 | 8 | # {% include icon.html icon="fa-solid fa-users" %}Team 9 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 11 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 12 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 13 | 14 | {% include section.html %} 15 | 16 | {% include list.html data="members" component="portrait" filter="role == 'pi'" %} 17 | {% include list.html data="members" component="portrait" filter="role != 'pi'" %} 18 | 19 | {% include section.html background="images/background.jpg" dark=true %} 20 | 21 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 22 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 23 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 24 | 25 | {% include section.html %} 26 | 27 | {% capture content %} 28 | 29 | {% include figure.html image="images/photo.jpg" %} 30 | {% include figure.html image="images/photo.jpg" %} 31 | {% include figure.html image="images/photo.jpg" %} 32 | 33 | {% endcapture %} 34 | 35 | {% include grid.html style="square" content=content %} 36 | -------------------------------------------------------------------------------- /testbed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Testbed 3 | 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 4 | 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 5 | header-dark: false 6 | footer-dark: false 7 | --- 8 | 9 | # Testbed 10 | 11 | {% include section.html %} 12 | 13 | # Basic formatting 14 | 15 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 16 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 17 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 18 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 19 | 20 | [External link](https://some-website.org/) 21 | 22 | [Internal link](team) 23 | 24 | _italic text_ 25 | 26 | **bold text** 27 | 28 | ~~strike-through text~~ 29 | 30 |
31 |
32 | Text with extra blank lines above and below 33 |
34 |
35 | 36 | - list item a 37 | - list item b 38 | - list item c 39 | 40 | 1. ordered list item 1 41 | 1. ordered list item 2 42 | 1. ordered list item 3 43 | 44 | 45 | 46 | 1. top level list item 47 | - nested list item 48 | 1. even deeper nested list item 49 | 50 | Plain image: 51 | 52 | ![plain image](/images/photo.jpg) 53 | 54 | # Heading 1 55 | 56 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 57 | 58 | ## Heading 2 59 | 60 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 61 | 62 | ### Heading 3 63 | 64 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 65 | 66 | #### Heading 4 67 | 68 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 69 | 70 | ##### Heading 5 71 | 72 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 73 | 74 | ###### Heading 6 75 | 76 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 77 | 78 | --- 79 | 80 | | TABLE | Game 1 | Game 2 | Game 3 | Total | 81 | | :---- | :----: | :----: | :----: | ----: | 82 | | Anna | 144 | 123 | 218 | 485 | 83 | | Bill | 90 | 175 | 120 | 385 | 84 | | Cara | 102 | 214 | 233 | 549 | 85 | 86 | > It was the best of times it was the worst of times. 87 | > It was the age of wisdom, it was the age of foolishness. 88 | > It was the spring of hope, it was the winter of despair. 89 | 90 | ```javascript 91 | // some code with syntax highlighting 92 | const popup = document.querySelector("#popup"); 93 | popup.style.width = "100%"; 94 | popup.innerText = 95 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; 96 | ``` 97 | 98 | This sentence has `inline code`, useful for making references to variables, packages, versions, etc. within a sentence. 99 | 100 | Lorem ipsum dolor sit amet. 101 | {:.left} 102 | Consectetur adipiscing elit. 103 | {:.center} 104 | Sed do eiusmod tempor incididunt. 105 | {:.right} 106 | 107 |
108 | Hidden content 109 | 110 | **Lorem** _ipsum_ [dolor sit amet](), consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 111 | 112 |
113 | 114 | {% include section.html %} 115 | 116 | # Jekyll Spaceship 117 | 118 | | Stage | Direct Products | ATP Yields | 119 | | -----------------: | --------------: | ---------: | 120 | | Glycolysis | 2 ATP | | 121 | | ^^ | 2 NADH | 3--5 ATP | 122 | | Pyruvaye oxidation | 2 NADH | 5 ATP | 123 | | Citric acid cycle | 2 ATP | | 124 | | ^^ | 6 NADH | 15 ATP | 125 | | ^^ | 2 FADH | 3 ATP | 126 | | 30--32 ATP | | | 127 | 128 | $ a \* b = c ^ b $ 129 | 130 | $ 2^{\frac{n-1}{3}} $ 131 | 132 | $ \int_a^b f(x)\,dx. $ 133 | 134 | ```mermaid! 135 | pie title Pets adopted by volunteers 136 | "Dogs" : 386 137 | "Cats" : 85 138 | "Rats" : 35 139 | ``` 140 | 141 | {% include section.html %} 142 | 143 | # Components 144 | 145 | ## Section 146 | 147 | {% include section.html background="images/background.jpg" %} 148 | 149 | Section, `background` 150 | 151 | {% include section.html dark=true %} 152 | 153 | Section, `dark=true` 154 | 155 | {% include section.html background="images/background.jpg" dark=true %} 156 | 157 | Section, `background` `dark=true` 158 | 159 | {% include section.html size="wide" %} 160 | 161 | Section, `size=wide` 162 | 163 | {% include section.html size="full" %} 164 | 165 | Section, `size=full` w/ figure 166 | 167 | {% include figure.html image="https://images.rawpixel.com/image_1000/cHJpdmF0ZS9sci9pbWFnZXMvd2Vic2l0ZS8yMDIyLTA1L2ZsMjYyODgwODcyMjYtaW1hZ2VfMS1rb3k1Zzkxay5qcGc.jpg" link="team" width="100%" %} 168 | 169 | {% include section.html %} 170 | 171 | ## Figure 172 | 173 | {% include figure.html image="images/icon.png" %} 174 | {% include figure.html image="images/icon.png" caption="_Lorem_ **ipsum**." %} 175 | {% include figure.html image="images/icon.png" caption="_Lorem_ **ipsum**. `px` width" width="400px" %} 176 | {% include figure.html image="images/icon.png" caption="_Lorem_ **ipsum**. `%` width" link="team" width="50%" %} 177 | {% include figure.html image="images/icon.png" caption="_Lorem_ **ipsum**. `px` height" link="team" height="200px" %} 178 | {% include figure.html image="images/fallback.svg" caption="_Lorem_ **ipsum**. `px` width, svg" link="team" width="400px" %} 179 | {% include figure.html image="images/fallback.svg" caption="_Lorem_ **ipsum**. `%` width, svg" link="team" width="50%" %} 180 | {% include figure.html image="images/fallback.svg" caption="_Lorem_ **ipsum**. `px` height, svg" link="team" height="200px" %} 181 | 182 | {% include section.html %} 183 | 184 | ## Button 185 | 186 | {% include button.html type="github" %} 187 | {% include button.html type="github" style="bare" %} 188 | {% include button.html type="github" icon="fa-brands fa-youtube" text="Override Text" tooltip="Override tooltip" %} 189 | {% include button.html type="github" text="" style="bare" %} 190 | {% include button.html type="github" text="" link="github-handle" %} 191 | 192 | {% include section.html %} 193 | 194 | ## Icon 195 | 196 | {% include icon.html icon="fa-solid fa-bacteria" %} 197 | {% include icon.html icon="fa-solid fa-virus" %} 198 | {% include icon.html icon="fa-solid fa-flask" %} 199 | {% include icon.html icon="manubot.svg" %} 200 | 201 | {% include icon.html icon="fa-brands fa-github" %} Lorem 202 | {% include icon.html icon="fa-solid fa-microscope" %} Ipsum 203 | {% include icon.html icon="manubot.svg" %} Dolor 204 | 205 | {% include section.html %} 206 | 207 | ## Feature 208 | 209 | {% capture text %} 210 | _Lorem_ **ipsum** dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 211 | {% endcapture%} 212 | {% include feature.html image="images/icon.png" link="team" title="Title" text=text %} 213 | {% include feature.html image="images/icon.png" title="Title" text=text flip=true %} 214 | {% include feature.html link="team" %} 215 | 216 | {% include section.html %} 217 | 218 | ## List 219 | 220 | ### List citations 221 | 222 | {% include list.html data="citations" component="citation" %} 223 | 224 | --- 225 | 226 | ### List projects 227 | 228 | {% include list.html data="projects" component="card" %} 229 | 230 | --- 231 | 232 | ### List team members 233 | 234 | {% include list.html data="members" component="portrait" %} 235 | 236 | --- 237 | 238 | ### List blog posts 239 | 240 | {% include list.html data="posts" component="post-excerpt" %} 241 | 242 | {% include section.html %} 243 | 244 | ## Citation 245 | 246 | {% include citation.html lookup="doi:10.1016/j.csbj.2020.05.017" %} 247 | {% include citation.html lookup="Open collaborative writing" style="rich" %} 248 | {% include citation.html title="Manual title" authors="Manual authors" %} 249 | 250 | {% include section.html %} 251 | 252 | ## Card 253 | 254 | {% 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" %} 255 | {% 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" %} 256 | 257 | {% include section.html %} 258 | 259 | ## Portrait 260 | 261 | {% include portrait.html lookup="jane-smith" %} 262 | {% include portrait.html lookup="john-doe" style="small" %} 263 | {% include portrait.html name="Manual name" style="small" %} 264 | {% include portrait.html style="small" %} 265 | 266 | {% include section.html %} 267 | 268 | ## Post Excerpt 269 | 270 | {% include post-excerpt.html lookup="example-post-1" %} 271 | {% include post-excerpt.html title="Manual title" author="Manual author" date="2020-02-20" last_modified_at="" %} 272 | 273 | {% include section.html %} 274 | 275 | ## Alert 276 | 277 | {% capture lorem %} 278 | 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. 279 | {% endcapture %} 280 | {% capture content %}**Tip** {{ lorem }}{% endcapture %} 281 | {% include alert.html type="tip" content=content %} 282 | {% capture content %}**Help** {{ lorem }}{% endcapture %} 283 | {% include alert.html type="help" content=content %} 284 | {% capture content %}**Info** {{ lorem }}{% endcapture %} 285 | {% include alert.html type="info" content=content %} 286 | {% capture content %}**Success** {{ lorem }}{% endcapture %} 287 | {% include alert.html type="success" content=content %} 288 | {% capture content %}**Warning** {{ lorem }}{% endcapture %} 289 | {% include alert.html type="warning" content=content %} 290 | {% capture content %}**Error** {{ lorem }}{% endcapture %} 291 | {% include alert.html type="error" content=content %} 292 | 293 | {% include section.html %} 294 | 295 | ## Tags 296 | 297 | {% include tags.html tags="ovarian cancer, dataset, gene expression" repo="greenelab/lab-website-template" link="blog" %} 298 | 299 | {% include section.html %} 300 | 301 | ## Float 302 | 303 | ### Figures 304 | 305 | {% capture content %} 306 | {% include figure.html image="images/icon.png" caption="Caption" width="200px" %} 307 | {% endcapture %} 308 | {% include float.html content=content %} 309 | 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. 310 | {% include float.html clear=true %} 311 | 312 | ### Code 313 | 314 | {% capture content %} 315 | 316 | ```javascript 317 | const test = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; 318 | ``` 319 | 320 | {% endcapture %} 321 | {% include float.html content=content flip=true %} 322 | 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. 323 | 324 | {% include section.html %} 325 | 326 | ## Grid 327 | 328 | ### Regular 329 | 330 | With Markdown images 331 | 332 | {% capture content %} 333 | ![image](https://journals.plos.org/ploscompbiol/article/figure/image?size=inline&id=info:doi/10.1371/journal.pcbi.1007128.g001&rev=2) 334 | 335 | ![image](https://ars.els-cdn.com/content/image/1-s2.0-S2001037020302804-gr1.jpg) 336 | 337 | ![image](https://iiif.elifesciences.org/lax:32822%2Felife-32822-fig8-v3.tif/full/863,/0/default.webp) 338 | 339 | ![image]({{ "/images/icon.png" | relative_url }}) 340 | 341 | ![image]({{ "/images/icon.png" | relative_url }}) 342 | 343 | ![image]({{ "/images/icon.png" | relative_url }}) 344 | {% endcapture %} 345 | {% include grid.html content=content %} 346 | 347 | ### Square 348 | 349 | With figure components 350 | 351 | {% capture content %} 352 | {% 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" %} 353 | {% include figure.html image="https://ars.els-cdn.com/content/image/1-s2.0-S2001037020302804-gr1.jpg" %} 354 | {% include figure.html image="https://iiif.elifesciences.org/lax:32822%2Felife-32822-fig8-v3.tif/full/863,/0/default.webp" %} 355 | {% include figure.html image="images/icon.png" %} 356 | {% include figure.html image="images/icon.png" %} 357 | {% include figure.html image="images/icon.png" %} 358 | {% endcapture %} 359 | {% include grid.html style="square" content=content %} 360 | 361 | ### Grid of citations 362 | 363 | {% capture content %} 364 | {% include list.html data="citations" component="citation" style="rich" %} 365 | {% endcapture %} 366 | {% include grid.html content=content %} 367 | 368 | ### Grid of blog posts 369 | 370 | {% capture content %} 371 | {% include list.html data="posts" component="post-excerpt" %} 372 | {% endcapture %} 373 | {% include grid.html content=content %} 374 | 375 | {% include section.html %} 376 | 377 | ## Cols 378 | 379 | ### Text 380 | 381 | {% capture col1 %} 382 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 383 | {% endcapture %} 384 | {% capture col2 %} 385 | 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. 386 | {% endcapture %} 387 | {% capture col3 %} 388 | 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. 389 | {% endcapture %} 390 | {% include cols.html col1=col1 col2=col2 col3=col3 %} 391 | 392 | ### Images 393 | 394 | {% capture col1 %} 395 | {% include figure.html image="images/icon.png" caption="Fig. 1a" %} 396 | Lorem _ipsum_ dolor **sit** amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 397 | {% endcapture %} 398 | {% capture col2 %} 399 | {% include figure.html image="images/icon.png" caption="Fig. 1b" %} 400 | Lorem _ipsum_ dolor **sit** amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 401 | {% endcapture %} 402 | {% capture col3 %} 403 | {% include figure.html image="images/icon.png" caption="Fig. 1c" %} 404 | Lorem _ipsum_ dolor **sit** amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 405 | {% endcapture %} 406 | {% include cols.html col1=col1 col2=col2 col3=col3 %} 407 | 408 | ### Code 409 | 410 | {% capture col1 %} 411 | 412 | ```javascript 413 | const test = "Lorem ipsum dolor sit amet"; 414 | ``` 415 | 416 | {% endcapture %} 417 | {% capture col2 %} 418 | 419 | ```javascript 420 | const test = "Lorem ipsum dolor sit amet"; 421 | ``` 422 | 423 | {% endcapture %} 424 | {% capture col3 %} 425 | 426 | ```javascript 427 | const test = "Lorem ipsum dolor sit amet"; 428 | ``` 429 | 430 | {% endcapture %} 431 | {% include cols.html col1=col1 col2=col2 col3=col3 %} 432 | 433 | {% include section.html %} 434 | 435 | ## Search 436 | 437 | {% include search-box.html %} 438 | {% include search-info.html %} 439 | 440 | {% include section.html %} 441 | 442 | ## Site Search 443 | 444 | {% include site-search.html %} 445 | --------------------------------------------------------------------------------