├── .nvmrc ├── .husky ├── .gitignore └── pre-commit ├── static ├── CNAME ├── dunlap-logo.png ├── Numfocus_stamp.png ├── astropy_favicon.ico ├── learn-astropy-logo.png └── astropy_logo_notext.svg ├── .gitignore ├── .eslintignore ├── .prettierignore ├── .prettierrc.yaml ├── deployment ├── requirements.txt └── installtutorials.py ├── src ├── searchClient.js ├── styles │ ├── breakpoints.js │ └── globalStyles.js ├── components │ ├── instantsearch │ │ ├── refinementList.js │ │ ├── poweredBy.js │ │ ├── searchBox.js │ │ ├── virtualPrioritySort.js │ │ └── hits.js │ ├── callToAction.js │ ├── pageCover.js │ ├── searchLayout.js │ ├── layout.js │ ├── seo.js │ ├── header.js │ ├── footer.js │ └── resultCard.js ├── templates │ └── contribTemplate.js ├── pages │ ├── 404.js │ └── index.js └── contributing │ └── index.md ├── .github ├── dependabot.yml └── workflows │ ├── testdeploy.yaml │ ├── ci.yaml │ └── deploy.yaml ├── gatsby-config.js ├── gatsby-node.js ├── .pre-commit-config.yaml ├── LICENSE ├── README.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.16.0 2 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /static/CNAME: -------------------------------------------------------------------------------- 1 | learn.astropy.org 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .cache/ 3 | public 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | .github 4 | package.json 5 | package-lock.json 6 | public 7 | -------------------------------------------------------------------------------- /static/dunlap-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy-learn/learn-astropy/HEAD/static/dunlap-logo.png -------------------------------------------------------------------------------- /static/Numfocus_stamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy-learn/learn-astropy/HEAD/static/Numfocus_stamp.png -------------------------------------------------------------------------------- /static/astropy_favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy-learn/learn-astropy/HEAD/static/astropy_favicon.ico -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: es5 2 | singleQuote: true 3 | printWidth: 80 4 | endOfLine: auto 5 | proseWrap: never 6 | -------------------------------------------------------------------------------- /static/learn-astropy-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy-learn/learn-astropy/HEAD/static/learn-astropy-logo.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged && npx --no-install pretty-quick --staged 5 | -------------------------------------------------------------------------------- /deployment/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | uritemplate 3 | git+https://github.com/astropy-learn/learn-astropy-librarian.git#egg=astropy-librarian 4 | -------------------------------------------------------------------------------- /src/searchClient.js: -------------------------------------------------------------------------------- 1 | import algoliasearch from 'algoliasearch/lite'; 2 | 3 | // This is the Search-only API key 4 | const searchClient = algoliasearch( 5 | 'H6MWLDHTG5', 6 | '45fc58ab091520ba879eb8952b422c4e' 7 | ); 8 | 9 | export default searchClient; 10 | -------------------------------------------------------------------------------- /src/styles/breakpoints.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Breakpoints. 3 | * 4 | * We're using em units for media queries so that they scale with zooming. 5 | * Note that ems in media queries are always relative to the **default root 6 | * font size**, not the font-size set on :root. See Ch 5 of Scott Brown's 7 | * Flexible Typography. 8 | */ 9 | 10 | const bp = { 11 | phone: '24em', // maximum width of a phone (vertical) 12 | }; 13 | 14 | export default bp; 15 | -------------------------------------------------------------------------------- /src/components/instantsearch/refinementList.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Styled version of the Algolia InstantSearch RefinementList component. 3 | * 4 | * https://www.algolia.com/doc/api-reference/widgets/refinement-list/react/ 5 | */ 6 | 7 | import styled from 'styled-components'; 8 | import { RefinementList as BaseRefinementList } from 'react-instantsearch-dom'; 9 | 10 | const RefinementList = styled(BaseRefinementList)` 11 | .ais-RefinementList-labelText { 12 | margin-left: var(--astropy-size-s); 13 | } 14 | `; 15 | 16 | export default RefinementList; 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: ".github/workflows" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | groups: 13 | actions: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: 'Learn Astropy', 4 | description: 5 | 'Astropy is a Python library for use in astronomy. Learn Astropy provides a portal to all of the Astropy educational material.', 6 | author: 'Astropy Project', 7 | twitter: '@astropy', 8 | siteUrl: 'https://learn.astropy.org', 9 | }, 10 | plugins: [ 11 | 'gatsby-plugin-styled-components', 12 | 'gatsby-plugin-react-helmet', 13 | { 14 | resolve: `gatsby-source-filesystem`, 15 | options: { 16 | name: `markdown-docs`, 17 | path: `${__dirname}/src/contributing`, 18 | }, 19 | }, 20 | `gatsby-transformer-remark`, 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/instantsearch/poweredBy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Styled version of the Algolia InstantSearch PoweredBy component. 3 | * 4 | * https://www.algolia.com/doc/api-reference/widgets/powered-by/react/ 5 | */ 6 | 7 | import styled from 'styled-components'; 8 | import { PoweredBy as BasePoweredBy } from 'react-instantsearch-dom'; 9 | 10 | /* PoweredBy Algolia InstantSearch widget that's styled. 11 | * 12 | * https://www.algolia.com/doc/api-reference/widgets/powered-by/react/ 13 | */ 14 | const PoweredBy = styled(BasePoweredBy)` 15 | margin-left: var(--astropy-size-m); 16 | .ais-PoweredBy-text { 17 | color: var(--astropy-text-color); 18 | margin-right: var(--astropy-size-s); 19 | } 20 | .ais-PoweredBy-logo path:last-of-type { 21 | fill: var(--algolia-primary-color); 22 | } 23 | `; 24 | 25 | export default PoweredBy; 26 | -------------------------------------------------------------------------------- /src/components/instantsearch/searchBox.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Styled SearchBox that includes the PoweredBy widget. 3 | */ 4 | 5 | import React from 'react'; 6 | import styled from 'styled-components'; 7 | import { SearchBox as SearchBoxCore } from 'react-instantsearch-dom'; 8 | 9 | import PoweredBy from './poweredBy'; 10 | 11 | const SearchBoxContainer = styled.div` 12 | display: flex; /* Lay out box+powered by in line */ 13 | `; 14 | 15 | /* SearchBox Algolia InstantSearch widget that's styled. 16 | * https://www.algolia.com/doc/api-reference/widgets/search-box/react/ 17 | */ 18 | export const StyledSearchBoxCore = styled(SearchBoxCore)` 19 | flex: 1 1 0; 20 | `; 21 | 22 | const SearchBox = () => ( 23 | 24 | 25 | 26 | 27 | ); 28 | 29 | export default SearchBox; 30 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | exports.createPages = async ({ actions, graphql, reporter }) => { 2 | const { createPage } = actions; 3 | const contribTemplate = require.resolve(`./src/templates/contribTemplate.js`); 4 | const result = await graphql(` 5 | { 6 | allMarkdownRemark { 7 | edges { 8 | node { 9 | frontmatter { 10 | slug 11 | } 12 | } 13 | } 14 | } 15 | } 16 | `); 17 | // Handle errors 18 | if (result.errors) { 19 | reporter.panicOnBuild(`Error while running GraphQL query.`); 20 | return; 21 | } 22 | result.data.allMarkdownRemark.edges.forEach(({ node }) => { 23 | createPage({ 24 | path: node.frontmatter.slug, 25 | component: contribTemplate, 26 | context: { 27 | // additional data can be passed via context 28 | slug: node.frontmatter.slug, 29 | }, 30 | }); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/callToAction.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { Link } from 'gatsby'; 5 | 6 | const Button = styled.div` 7 | background-color: #fa743b; 8 | border-color: #fa743b; 9 | cursor: pointer; 10 | text-decoration: none; 11 | color: #ffffff; 12 | text-align: center; 13 | vertical-align: middle; 14 | padding: 0.375rem 0.75rem; 15 | font-size: 0.875rem; 16 | border-radius: 0.25rem; 17 | display: inline-block; 18 | `; 19 | 20 | /* 21 | * A call-to-action button that is a link to an internal page (using the 22 | * Gatsby Link API). 23 | */ 24 | export default function CallToActionLink({ children, to }) { 25 | return ( 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | CallToActionLink.propTypes = { 33 | children: PropTypes.node, 34 | to: PropTypes.string.isRequired, 35 | }; 36 | -------------------------------------------------------------------------------- /.github/workflows/testdeploy.yaml: -------------------------------------------------------------------------------- 1 | name: Test deployment scripts 2 | 3 | 'on': 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | 14 | - name: Set up Python 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: 3.12 18 | 19 | - name: Install Python dependencies 20 | run: | 21 | python -m pip install -U pip 22 | python -m pip install -r deployment/requirements.txt 23 | 24 | - name: Check Astropy Librarian installation 25 | run: | 26 | astropylibrarian --help 27 | 28 | - name: Install tutorials into site 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | run: | 32 | mkdir -p public/tutorials 33 | python deployment/installtutorials.py --dest public/tutorials 34 | 35 | - name: List tutorials 36 | run: | 37 | tree public/tutorials 38 | -------------------------------------------------------------------------------- /src/components/pageCover.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | const FullWidthContainer = styled.div` 6 | width: 100vw; 7 | position: relative; 8 | left: 50%; 9 | right: 50%; 10 | margin: 0 -50vw; 11 | background-color: var(--astropy-nav-header-color); 12 | color: var(--astropy-nav-header-text-color); 13 | 14 | @media screen and (max-width: 600px) { 15 | padding: 20px; 16 | } 17 | `; 18 | 19 | const StyledPageCoverContent = styled.header` 20 | margin: 0 auto; 21 | max-width: var(--astropy-content-width); 22 | padding: var(--astropy-size-m) var(--astropy-size-s); 23 | 24 | p { 25 | font-size: var(--astropy-font-size-ml); 26 | } 27 | 28 | @media screen and (max-width: 600px) { 29 | margin: 0 auto; 30 | } 31 | `; 32 | 33 | /* 34 | * The page cover is meant to contain the title and lead paragraph of a 35 | * content page. 36 | */ 37 | export default function PageCover({ children }) { 38 | return ( 39 | 40 | {children} 41 | 42 | ); 43 | } 44 | 45 | PageCover.propTypes = { 46 | children: PropTypes.node.isRequired, 47 | }; 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | 'on': 4 | # push: 5 | # branches: 6 | # - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | 16 | - name: Read .nvmrc 17 | id: node_version 18 | run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc) 19 | 20 | - name: Set up node 21 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 22 | with: 23 | node-version: ${{ steps.node_version.outputs.NODE_VERSION }} 24 | 25 | - name: Cache dependencies 26 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 27 | with: 28 | path: ~/.npm 29 | key: ${{ runner.os }}-node-${{ steps.node_version.outputs.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.os }}-node-${{ steps.node_version.outputs.NODE_VERSION }} 32 | 33 | - run: npm ci 34 | name: Install 35 | 36 | - run: npm run lint 37 | name: ESLint 38 | 39 | - run: npm run prettier 40 | name: Prettier 41 | 42 | - run: npm run build 43 | name: Build 44 | -------------------------------------------------------------------------------- /src/templates/contribTemplate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Template for contributing/documentation pages that are sourced from 3 | * Markdown. 4 | */ 5 | 6 | import React from 'react'; 7 | import PropTypes from 'prop-types'; 8 | import { graphql } from 'gatsby'; 9 | 10 | import Layout from '../components/layout'; 11 | import SEO from '../components/seo'; 12 | 13 | export const pageQuery = graphql` 14 | query ($slug: String!) { 15 | markdownRemark(frontmatter: { slug: { eq: $slug } }) { 16 | html 17 | frontmatter { 18 | slug 19 | title 20 | } 21 | } 22 | } 23 | `; 24 | 25 | export default function Template({ data, location }) { 26 | const { markdownRemark } = data; // data.markdownRemark holds your post data 27 | const { frontmatter, html } = markdownRemark; 28 | 29 | return ( 30 | 31 | 32 | 33 |

{frontmatter.title}

34 | {/* eslint-disable react/no-danger */} 35 |
36 | {/* eslint-enable react/no-danger */} 37 | 38 | ); 39 | } 40 | 41 | Template.propTypes = { 42 | data: PropTypes.object.isRequired, 43 | location: PropTypes.shape({ 44 | pathname: PropTypes.string.isRequired, 45 | }), 46 | }; 47 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | 4 | ci: 5 | autoupdate_commit_msg: 'chore: update pre-commit hooks' 6 | autofix_commit_msg: 'style: pre-commit fixes' 7 | autofix_prs: true 8 | 9 | repos: 10 | - repo: https://github.com/astral-sh/ruff-pre-commit 11 | rev: 'v0.12.1' 12 | hooks: 13 | - id: ruff 14 | types_or: [python, pyi, jupyter] 15 | args: [--fix, --show-fixes] 16 | - id: ruff-format 17 | types_or: [python, pyi, jupyter] 18 | 19 | - repo: https://github.com/pre-commit/pre-commit-hooks 20 | rev: v5.0.0 21 | hooks: 22 | - id: trailing-whitespace 23 | exclude: '.*\.fits$' 24 | - id: end-of-file-fixer 25 | exclude_types: [csv] 26 | exclude: '.*\.fits$' 27 | - id: check-yaml 28 | # prevent addition of (presumably data) files >= 10 MB 29 | # (for such files, see https://learn.astropy.org/contributing/how-to-contribute) 30 | - id: check-added-large-files 31 | args: ['--maxkb=10000'] 32 | 33 | - repo: https://github.com/kynan/nbstripout 34 | rev: 0.8.1 35 | hooks: 36 | - id: nbstripout 37 | args: 38 | [ 39 | '--extra-keys=metadata.kernelspec metadata.language_info.version metadata.toc', 40 | ] 41 | -------------------------------------------------------------------------------- /src/components/searchLayout.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import bp from '../styles/breakpoints'; 4 | 5 | export const SearchLayout = styled.div` 6 | grid-template-columns: 16rem 1fr; 7 | grid-template-rows: auto 1fr; 8 | grid-column-gap: 2rem; 9 | grid-row-gap: 2rem; 10 | margin-top: 4rem; 11 | 12 | /* 13 | * Use grid layout on bigger screens. 14 | */ 15 | @media only screen and (min-width: ${bp.phone}) { 16 | display: grid; 17 | } 18 | 19 | .search-box-area { 20 | grid-column: 2 / 3; 21 | grid-row: 1 / 2; 22 | 23 | @media screen and (max-width: 750px) { 24 | grid-column: 1 / 3; 25 | } 26 | } 27 | 28 | .search-refinements-area { 29 | grid-column: 1 / 2; 30 | grid-row: 1 / 3; 31 | 32 | margin-top: 1rem; 33 | @media only screen and (min-width: ${bp.phone}) { 34 | margin-top: 0; 35 | } 36 | @media screen and (max-width: 750px) { 37 | display: none; 38 | } 39 | } 40 | 41 | .search-results-area { 42 | grid-column: 2 / 3; 43 | grid-row: 2 / 3; 44 | 45 | @media screen and (max-width: 750px) { 46 | grid-column: 1 / 3; 47 | } 48 | } 49 | `; 50 | 51 | /* Styled component div around a refinement widget. 52 | * 53 | * This styling controls spacing and the heading styling 54 | */ 55 | export const SearchRefinementsSection = styled.div` 56 | margin-bottom: var(--astropy-size-l); 57 | 58 | h2 { 59 | margin-top: 0; 60 | font-size: var(--astropy-font-size-ml); 61 | } 62 | `; 63 | -------------------------------------------------------------------------------- /src/components/instantsearch/virtualPrioritySort.js: -------------------------------------------------------------------------------- 1 | /* 2 | * An extension of the SortBy component that toggles between priority and 3 | * relevance-based sorting whenever the user enters a query string. 4 | * 5 | * The component is "virtual" since there is not visual interaction; the 6 | * toggling happens automatically based on the search state. 7 | */ 8 | 9 | import React from 'react'; 10 | 11 | import PropTypes from 'prop-types'; 12 | 13 | import { connectStateResults, connectSortBy } from 'react-instantsearch-dom'; 14 | 15 | /* 16 | * The VirtualSortBy is a "virtual" SortBy component, meaning that it takes 17 | * the props of SortBy and can control sorting; but it does not render in the 18 | * UI. 19 | */ 20 | const VirtualSortBy = connectSortBy(() => null); 21 | 22 | const PrioritySortCore = ({ 23 | searchState, 24 | priorityRefinement, 25 | relevanceRefinement, 26 | }) => { 27 | const refinement = searchState.query 28 | ? relevanceRefinement 29 | : priorityRefinement; 30 | 31 | const items = [ 32 | { value: priorityRefinement, label: priorityRefinement }, 33 | { value: relevanceRefinement, label: relevanceRefinement }, 34 | ]; 35 | 36 | return ; 37 | }; 38 | 39 | PrioritySortCore.propTypes = { 40 | searchState: PropTypes.object, 41 | priorityRefinement: PropTypes.string, 42 | relevanceRefinement: PropTypes.string, 43 | }; 44 | 45 | const PrioritySort = connectStateResults(PrioritySortCore); 46 | 47 | export default PrioritySort; 48 | -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'gatsby'; 4 | import SEO from '../components/seo'; 5 | 6 | // styles 7 | const pageStyles = { 8 | color: '#232129', 9 | padding: '96px', 10 | fontFamily: '-apple-system, Roboto, sans-serif, serif', 11 | }; 12 | const headingStyles = { 13 | marginTop: 0, 14 | marginBottom: 64, 15 | maxWidth: 320, 16 | }; 17 | 18 | const paragraphStyles = { 19 | marginBottom: 48, 20 | }; 21 | const codeStyles = { 22 | color: '#8A6534', 23 | padding: 4, 24 | backgroundColor: '#FFF4DB', 25 | fontSize: '1.25rem', 26 | borderRadius: 4, 27 | }; 28 | 29 | // markup 30 | const NotFoundPage = ({ location }) => ( 31 |
32 | 33 |

Page not found

34 |

35 | Sorry{' '} 36 | 37 | 😔 38 | {' '} 39 | we couldn’t find what you were looking for. 40 |
41 | {process.env.NODE_ENV === 'development' ? ( 42 | <> 43 |
44 | Try creating a page in src/pages/. 45 |
46 | 47 | ) : null} 48 |
49 | Go home. 50 |

51 |
52 | ); 53 | 54 | export default NotFoundPage; 55 | 56 | NotFoundPage.propTypes = { 57 | location: PropTypes.shape({ 58 | pathname: PropTypes.string.isRequired, 59 | }), 60 | }; 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Astropy Developers 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 | -------------------------------------------------------------------------------- /src/components/instantsearch/hits.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extension to the Algolia Hits widget that passes props through to individual 3 | * Hit components. 4 | */ 5 | 6 | import React from 'react'; 7 | import PropTypes from 'prop-types'; 8 | import styled from 'styled-components'; 9 | import { connectHits } from 'react-instantsearch-dom'; 10 | 11 | /** 12 | * Custom Hits component that passes props to individual Hit components. 13 | */ 14 | const Hits = ({ hits, hitComponent, className = '' }) => { 15 | const HitComponent = hitComponent; 16 | 17 | return ( 18 |
    19 | {hits.map((hit) => ( 20 |
  1. 21 | 22 |
  2. 23 | ))} 24 |
25 | ); 26 | }; 27 | 28 | const HitPropTypes = PropTypes.shape({ 29 | objectID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 30 | .isRequired, 31 | }); 32 | 33 | Hits.propTypes = { 34 | hits: PropTypes.arrayOf(HitPropTypes.isRequired).isRequired, 35 | hitComponent: PropTypes.func.isRequired, 36 | className: PropTypes.string, 37 | }; 38 | 39 | /** 40 | * The Hits component, connected to Algolia instantsearch. 41 | */ 42 | const ConnectedHits = connectHits(Hits); 43 | 44 | /** 45 | * Styled components wrapper for ConnectedHits. 46 | */ 47 | export const StyledHits = styled(ConnectedHits)` 48 | display: block; 49 | margin: 0; 50 | padding: 0; 51 | list-style: none; 52 | 53 | .hits-item { 54 | width: 100%; 55 | margin-bottom: var(--astropy-size-m); 56 | padding: var(--astropy-size-s); 57 | border: 1px solid #ddd; 58 | border-radius: var(--astropy-border-radius-m); 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /src/components/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | // Fonts from fontsource 6 | // https://github.com/fontsource/fontsource 7 | import '@fontsource/source-sans-pro/400.css'; 8 | import '@fontsource/source-sans-pro/400-italic.css'; 9 | import '@fontsource/source-sans-pro/700.css'; 10 | 11 | // Full Algolia instantsearch theme includes its reset 12 | import 'instantsearch.css/themes/satellite.css'; 13 | 14 | import GlobalStyles from '../styles/globalStyles'; 15 | import Header from './header'; 16 | import Footer from './footer'; 17 | 18 | /* 19 | * Layout wrapper div. 20 | * 21 | * Its main job is to provide a "sticky footer" so that the Footer component 22 | * stays at the bottom of the page and the Header/MainContent components 23 | * take up any excess space. See 24 | * https://css-tricks.com/couple-takes-sticky-footer/ 25 | */ 26 | const StyledLayout = styled.div` 27 | /* Flexbox for the sticky footer */ 28 | display: flex; 29 | flex-direction: column; 30 | min-height: 100vh; 31 | 32 | .upper-container { 33 | flex: 1 0 auto; 34 | } 35 | .sticky-footer-container { 36 | flex-shrink: 0; 37 | } 38 | `; 39 | 40 | const StyledMain = styled.main` 41 | margin: 0 auto; 42 | max-width: var(--astropy-content-width); 43 | padding: 0 var(--astropy-size-s); 44 | `; 45 | 46 | /* 47 | * The Layout component wraps the contents of every page. 48 | */ 49 | export default function Layout({ children }) { 50 | return ( 51 | <> 52 | 53 | 54 |
55 |
56 | {children} 57 |
58 |
59 |
60 |
61 |
62 | 63 | ); 64 | } 65 | 66 | Layout.propTypes = { 67 | children: PropTypes.node.isRequired, 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/seo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Helmet from 'react-helmet'; 4 | import { useStaticQuery, graphql } from 'gatsby'; 5 | 6 | /* 7 | * SEO component that adds tags to the page's header using react-helmet. 8 | */ 9 | export default function SEO({ children, location, title, description, image }) { 10 | const { site } = useStaticQuery( 11 | graphql` 12 | query { 13 | site { 14 | siteMetadata { 15 | title 16 | description 17 | author 18 | siteUrl 19 | twitter 20 | } 21 | } 22 | } 23 | ` 24 | ); 25 | 26 | // The description can be overidden for individual pages via description 27 | // prop. 28 | const desc = description || site.siteMetadata.description; 29 | 30 | // The page's canonical URL 31 | const canonicalUrl = site.siteMetadata.siteUrl + location.pathname; 32 | 33 | return ( 34 | 35 | 36 | {title} 37 | {/* Favicon */} 38 | 39 | 40 | {/* General meta tags */} 41 | 42 | 43 | 44 | 45 | {/* Open Graph */} 46 | 47 | 52 | 53 | 58 | 59 | {/* Twitter card */} 60 | 65 | {children} 66 | 67 | ); 68 | } 69 | 70 | SEO.propTypes = { 71 | children: PropTypes.node, 72 | title: PropTypes.string.isRequired, 73 | description: PropTypes.string, 74 | location: PropTypes.shape({ 75 | pathname: PropTypes.string.isRequired, 76 | }), 77 | image: PropTypes.string, 78 | }; 79 | -------------------------------------------------------------------------------- /src/styles/globalStyles.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | import { normalize } from 'polished'; 4 | 5 | const GlobalStyles = createGlobalStyle` 6 | /* 7 | * CSS reset via normalize. 8 | */ 9 | ${normalize()} 10 | 11 | html { 12 | box-sizing: border-box; 13 | } 14 | 15 | /* 16 | * Inherit border-box sizing from html 17 | * https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ 18 | */ 19 | *, 20 | *:before, 21 | *:after { 22 | box-sizing: inherit; 23 | } 24 | 25 | :root { 26 | /* 27 | * Reinforce that we're respecting the user's ability to set a default 28 | * font size. The rem unit now becomes relative to this. 29 | * Flexible Typesetting, Tim Brown, ch 2 and 4 30 | */ 31 | font-size: 1.1rem; 32 | 33 | /* 34 | * Design tokens: Color palette 35 | */ 36 | --astropy-primary-color: #fa743b; 37 | --astropy-neutral-100: #111111; 38 | --astropy-neutral-900: #ffffff; 39 | --algolia-primary-color: #182359; 40 | 41 | /* 42 | * Design tokens: Sizes 43 | */ 44 | --astropy-size-xxs: 0.125rem; 45 | --astropy-size-xs: 0.25rem; 46 | --astropy-size-s: 0.5rem; 47 | --astropy-size-m: 1rem; 48 | --astropy-size-ml: 1.2rem; 49 | --astropy-size-l: 2rem; 50 | --astropy-size-xl: 4rem; 51 | 52 | /* 53 | * Design tokens: font sizes 54 | */ 55 | --astropy-font-size-s: 0.8rem; 56 | --astropy-font-size-m: 1rem; 57 | --astropy-font-size-ml: 1.2rem; 58 | 59 | /* 60 | * Design tokens: border radii 61 | */ 62 | --astropy-border-radius-s: 0.125rem; 63 | --astropy-border-radius-m: 0.25rem; 64 | --astropy-border-radius-l: 0.5rem; 65 | 66 | /* 67 | * Applied colors 68 | */ 69 | --astropy-text-color: var(--astropy-neutral-100); 70 | --astropy-page-background-color: var(--astropy-neutral-900); 71 | --astropy-nav-header-color: var(--astropy-neutral-100); 72 | --astropy-nav-header-text-color: var(--astropy-neutral-900); 73 | 74 | /* 75 | * Applied sizes 76 | */ 77 | --astropy-content-width: 60em; 78 | } 79 | 80 | html, body { 81 | padding: 0; 82 | margin: 0; 83 | font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI, 84 | Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, 85 | sans-serif; 86 | line-height: 1.45; 87 | color: var(--astropy-text-color); 88 | background-color: var(--astropy-page-background-color); 89 | } 90 | 91 | a { 92 | color: var(--astropy-primary-color); 93 | font-weight: 700; 94 | text-decoration: none; 95 | } 96 | 97 | a:hover { 98 | text-decoration: solid underline var(--astropy-primary-color) 2px; 99 | } 100 | `; 101 | 102 | export default GlobalStyles; 103 | -------------------------------------------------------------------------------- /src/components/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link } from 'gatsby'; 4 | 5 | import logo from '../../static/learn-astropy-logo.png'; 6 | 7 | const HeaderContainer = styled.header` 8 | width: 100%; 9 | padding: var(--astropy-size-s) var(--astropy-size-m); 10 | margin: 0; 11 | background-color: var(--astropy-nav-header-color); 12 | color: var(--astropy-nav-header-text-color); 13 | 14 | display: flex; 15 | flex-direction: row; 16 | flex-wrap: nowrap; 17 | justify-content: flex-start; 18 | align-items: center; 19 | 20 | .learn-astropy-logo { 21 | width: 12rem; 22 | } 23 | 24 | .main-nav { 25 | display: flex; 26 | margin-left: 2rem; 27 | flex-direction: row 28 | flex-wrap: nowrap; 29 | justify-content: flex-start; 30 | align-items: flex-start; 31 | 32 | @media screen and (max-width: 600px) { 33 | margin-left: -1rem; 34 | } 35 | } 36 | 37 | .astropy-link { 38 | margin-left: auto; 39 | } 40 | 41 | a { 42 | color: var(--astropy-neutral-900); 43 | } 44 | 45 | a:hover { 46 | text-decoration: none; 47 | } 48 | 49 | @media screen and (max-width: 600px) { 50 | display: flex; 51 | flex-direction: column; 52 | flex-wrap: nowrap; 53 | justify-content: flex-start; 54 | align-items: flex-start; 55 | } 56 | `; 57 | 58 | const NavItem = styled.div` 59 | transition: all 0.2s ease-in-out; 60 | margin: 0 1em; 61 | border-bottom: 2px solid transparent; 62 | 63 | &:hover { 64 | border-bottom: 2px solid var(--astropy-primary-color); 65 | color: var(--astropy-primary-color); 66 | } 67 | 68 | // .astropy-link { 69 | // margin-left: auto; 70 | // } 71 | 72 | @media screen and (max-width: 600px) { 73 | width: 100vw; 74 | display: flex; 75 | flex-direction: column; 76 | align-items: flex-start; 77 | padding-top: 10px; 78 | padding-left: 0.5rem; 79 | } 80 | `; 81 | 82 | /* 83 | * Header component that includes the logo, search bar, and navigation tabs. 84 | */ 85 | export default function NavHeader() { 86 | return ( 87 | <> 88 | 89 | 90 | Learn Astropy Homepage 95 | 96 | 114 | 115 | 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astropy Learn 2 | 3 | This repository hosts the homepage of the Astropy Learn project, https://learn.astropy.org, and serves the tutorial content from the [astropy-learn](https://github.com/astropy-learn) organization. The site itself is built with [Gatsby](https://www.gatsbyjs.com/) and the [Algolia](https://www.algolia.com) search service. Records for the Algolia database are curated and formatted by the [learn-astropy-librarian](https://github.com/astropy-learn/learn-astropy-librarian) app. 4 | 5 | ## Developer guide 6 | 7 | ### Initial set up 8 | 9 | Create a fork on https://github.com/astropy-learn/learn. 10 | 11 | ```bash 12 | npm install 13 | ``` 14 | 15 | ### Run a development server 16 | 17 | You can run a development server that will serve the site and reload as you develop the app: 18 | 19 | ```bash 20 | npm run develop 21 | ``` 22 | 23 | By default the app is hosted at http://localhost:8000. You can also interact with the GraphQL data layer by browsing 24 | 25 | ### Build for production 26 | 27 | ```bash 28 | npm run build 29 | ``` 30 | 31 | Preview the built site by running: 32 | 33 | ```bash 34 | npm run serve 35 | ``` 36 | 37 | ### Linting and autoformatting 38 | 39 | This app uses ESLint to lint JavaScript, which in turn runs Prettier to format JavaScript. The configuration is based on [wesbos/eslint-config-wesbos](https://github.com/wesbos/eslint-config-wesbos). 40 | 41 | A Git pre-commit hooks runs both ESLint and Prettier and automatically lints and reformats code before every commit. These hooks are run by [husky](https://typicode.github.io/husky/#/) and should already be installed when you ran `npm install`. 42 | 43 | To manually lint the code base: 44 | 45 | ```bash 46 | npm run lint 47 | ``` 48 | 49 | To also fix issues and format the code base: 50 | 51 | ```bash 52 | npm run lint:fix 53 | ``` 54 | 55 | Ideally your editor will also apply eslint/prettier on save, though these commands are handy as a fallback. 56 | 57 | ### About the node version 58 | 59 | This project is intended to be built with a Node.js version that's encoded in the [`.nvmrc`](./.nvmrc) file. To adopt this Node version, we recommend installing and using the [node version manager](https://github.com/nvm-sh/nvm). 60 | 61 | Then you can use the preferred node version by running `nvm` from the project root: 62 | 63 | ```sh 64 | nvm use 65 | ``` 66 | 67 | ### Additional resources for developers 68 | 69 | Learn more about Gatsby: 70 | 71 | - [Documentation](https://www.gatsbyjs.com/docs/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 72 | - [Tutorials](https://www.gatsbyjs.com/tutorial/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 73 | - [Guides](https://www.gatsbyjs.com/tutorial/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 74 | - [API Reference](https://www.gatsbyjs.com/docs/api-reference/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 75 | - [Plugin Library](https://www.gatsbyjs.com/plugins?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 76 | - [Cheat Sheet](https://www.gatsbyjs.com/docs/cheat-sheet/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 77 | 78 | Learn more about Algolia: 79 | 80 | - [Documentation](https://www.algolia.com/doc/) 81 | - [React instantsearch](https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/react/) 82 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { InstantSearch, Configure } from 'react-instantsearch-dom'; 5 | 6 | import Layout from '../components/layout'; 7 | import { 8 | SearchLayout, 9 | SearchRefinementsSection, 10 | } from '../components/searchLayout'; 11 | import SEO from '../components/seo'; 12 | import PageCover from '../components/pageCover'; 13 | import searchClient from '../searchClient'; 14 | import { StyledHits } from '../components/instantsearch/hits'; 15 | import RefinementList from '../components/instantsearch/refinementList'; 16 | import SearchBox from '../components/instantsearch/searchBox'; 17 | import PrioritySort from '../components/instantsearch/virtualPrioritySort'; 18 | import ResultCard from '../components/resultCard'; 19 | 20 | export default function IndexPage({ location }) { 21 | return ( 22 | 23 | 24 | 25 |

Learn Astropy

26 |

27 | Learn how to use Python for research in astronomy with tutorials and 28 | guides covering Astropy and the broader astronomy Python ecosystem. 29 |

30 |
31 | 32 | 33 | 34 | 38 | 39 |
40 | 41 |
42 |
43 | 44 |

Format

45 | 46 |
47 | 48 |

Astropy packages

49 | 56 |
57 | 58 |

Python packages

59 | 66 |
67 | 68 |

Tasks

69 | 76 |
77 | 78 |

Science domains

79 | 86 |
87 |
88 |
89 | 90 |
91 |
92 |
93 |
94 | ); 95 | } 96 | 97 | IndexPage.propTypes = { 98 | location: PropTypes.shape({ 99 | pathname: PropTypes.string.isRequired, 100 | }), 101 | }; 102 | -------------------------------------------------------------------------------- /src/components/footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'gatsby'; 3 | import styled from 'styled-components'; 4 | 5 | import numfocusStamp from '../../static/Numfocus_stamp.png'; 6 | import dunlapLogo from '../../static/dunlap-logo.png'; 7 | 8 | const FullWidthContainer = styled.div` 9 | width: 100vw; 10 | position: relative; 11 | left: 50%; 12 | right: 50%; 13 | margin: var(--astropy-size-xl) -50vw 0; 14 | background-color: rgb(250, 250, 250); 15 | `; 16 | 17 | const StyledFooter = styled.footer` 18 | margin: 0 auto; 19 | max-width: var(--astropy-content-width); 20 | padding: var(--astropy-size-m) var(--astropy-size-s); 21 | 22 | h2 { 23 | font-size: var(--astropy-font-size-ml); 24 | font-weight: 400; 25 | } 26 | 27 | nav ul { 28 | list-style: none; 29 | padding-left: 0; 30 | } 31 | 32 | nav a { 33 | font-weight: 500; 34 | } 35 | 36 | nav ul li:first-child a { 37 | font-weight: 700; 38 | } 39 | 40 | .footer-content-layer { 41 | display: flex; 42 | flex-flow: row nowrap; 43 | justify-content: space-between; 44 | align-items: flex-start; 45 | margin: var(--astropy-size-m) 0; 46 | } 47 | 48 | .sponsors, 49 | .code-of-conduct { 50 | width: 24rem; 51 | } 52 | 53 | .code-of-conduct p { 54 | margin-bottom: 0; 55 | } 56 | 57 | .numfocusStamp { 58 | margin-top: var(--astropy-size-l); 59 | } 60 | 61 | .sponsors .numfocusStamp__image { 62 | width: 16rem; 63 | } 64 | 65 | .sponsors .dunlapLogo__image { 66 | margin-top: var(--astropy-size-m); 67 | width: 20rem; 68 | } 69 | 70 | .copyright { 71 | margin-top: var(--astropy-size-xl); 72 | } 73 | `; 74 | 75 | /* 76 | * Footer component (contained within a Layout component). 77 | */ 78 | export default function Footer() { 79 | return ( 80 | 81 | 82 | 89 |
90 |
91 |

Code of Conduct

92 |

93 | The Astropy project is committed to fostering an inclusive 94 | community. The community of participants in open source Astronomy 95 | projects is made up of members from around the globe with a 96 | diverse set of skills, personalities, and experiences. It is 97 | through these differences that our community experiences success 98 | and continued growth.{' '} 99 | 100 | Learn more. 101 | 102 |

103 |
104 | 125 |
126 |

127 | Copyright {new Date().getFullYear()} The Astropy Developers 128 |

129 |
130 |
131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /src/components/resultCard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The ResultCard renders an algoliasearch hit. 3 | * 4 | * See https://www.algolia.com/doc/api-reference/widgets/hits/react/ 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import styled from 'styled-components'; 10 | import { Snippet } from 'react-instantsearch-dom'; 11 | 12 | const ResultCardContainer = styled.div` 13 | .result-title { 14 | display: flex; 15 | flex: 1 1 0; 16 | align-items: center; 17 | margin-bottom: 1rem; 18 | } 19 | 20 | h2 { 21 | line-height: 1.1; 22 | margin: 0; 23 | font-size: var(--astropy-font-size-m); 24 | } 25 | 26 | a { 27 | text-decoration: none; 28 | 29 | &: hover { 30 | text-decoration: underline; 31 | } 32 | } 33 | 34 | .content-type-tag { 35 | background-color: var(--astropy-primary-color); 36 | border-radius: var(--astropy-border-radius-m); 37 | color: white; 38 | font-size: 0.6rem; 39 | font-weight: 700; 40 | text-transform: uppercase; 41 | letter-spacing: 0.05em; 42 | margin-right: var(--astropy-size-s); 43 | padding: 0.125rem var(--astropy-size-xs); 44 | } 45 | 46 | .root-title { 47 | font-size: var(--astropy-font-size-s); 48 | margin: -0.5rem 0 1rem; 49 | } 50 | 51 | .sidebyside { 52 | display: flex; 53 | } 54 | 55 | .sidebyside__image { 56 | margin-right: 1rem; 57 | flex: 0 0 8rem; 58 | } 59 | 60 | .sidebyside__image img { 61 | width: 100%; 62 | } 63 | 64 | .sidebyside__content { 65 | flex: 1 1 auto; 66 | 67 | font-size: var(--astropy-font-size-s); 68 | } 69 | 70 | .sidebyside__content *:first-child { 71 | margin-top: 0; 72 | } 73 | `; 74 | 75 | const StyledSnippetBlock = styled.blockquote` 76 | padding: 0.5rem 1rem; 77 | margin-left: 0; 78 | margin-right: 0; 79 | border-left: 4px solid #ddd; 80 | background: #eee; 81 | border-radius: var(--astropy-border-radius-s); 82 | 83 | &::before { 84 | content: '[…] '; 85 | opacity: 0.5; 86 | } 87 | 88 | &::after { 89 | content: '[…]'; 90 | opacity: 0.5; 91 | } 92 | `; 93 | 94 | const StyledSnippet = styled(Snippet)` 95 | span, 96 | ${({ tagName }) => tagName} { 97 | // more specific than Algolia theme 98 | font-size: var(--astropy-font-size-s); 99 | } 100 | 101 | ${({ tagName }) => tagName} { 102 | background: yellow; 103 | } 104 | `; 105 | 106 | const ResultCard = ({ hit }) => { 107 | let linkUrl; 108 | let title; 109 | 110 | if (hit.content_type === 'guide' && hit.importance > 1) { 111 | linkUrl = hit.url; 112 | title = hit.h1; 113 | } else { 114 | linkUrl = hit.root_url; 115 | title = hit.root_title; 116 | } 117 | 118 | return ( 119 | 120 |
121 | {hit.content_type} 122 | 123 |

{title}

124 |
125 |
126 | {hit.content_type === 'guide' && hit.importance > 1 && ( 127 |

128 | Inside{' '} 129 | 130 | {hit.root_title} 131 | 132 |

133 | )} 134 | 135 |
136 | {hit.thumbnail_url && ( 137 |
138 | 139 | 140 | 141 |
142 | )} 143 |
144 |

{hit.root_summary}

145 | {hit._snippetResult.content.matchLevel !== 'none' && ( 146 | 147 | {' '} 153 | 154 | )} 155 |
156 |
157 |
158 | ); 159 | }; 160 | 161 | ResultCard.propTypes = { 162 | hit: PropTypes.object.isRequired, 163 | }; 164 | 165 | export default ResultCard; 166 | -------------------------------------------------------------------------------- /deployment/installtutorials.py: -------------------------------------------------------------------------------- 1 | """Install the built tutorials HTML from astropy-learn/astropy-tutorials into the 2 | built Gatsby site. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import argparse 8 | import os 9 | from pathlib import Path 10 | import tempfile 11 | from subprocess import CalledProcessError, check_call 12 | import glob 13 | import shutil 14 | import requests 15 | 16 | 17 | def parse_args() -> argparse.Namespace: 18 | parser = argparse.ArgumentParser( 19 | description=( 20 | "Install the tutorials HTML files from " 21 | "each tutorial repo into the build Gatsby site." 22 | ), 23 | formatter_class=argparse.RawDescriptionHelpFormatter, 24 | ) 25 | parser.add_argument( 26 | "--dest", 27 | required=True, 28 | help="Directory where the tutorials are installed. This should be " 29 | "inside the Gatsby 'public' directory.", 30 | ) 31 | 32 | return parser.parse_args() 33 | 34 | 35 | def process_repo(repo, destination_directory): 36 | """Process a tutorial repository to copy its rendered tutorial(s) into `destination_directory`.""" 37 | os.makedirs(destination_directory, exist_ok=True) 38 | 39 | repo_name = repo["full_name"] 40 | if not repo_name.split("/")[1].startswith("tutorial--"): 41 | return 42 | if repo_name.split("/")[1] == "tutorial--template": 43 | return 44 | 45 | print(f"\nProcessing {repo_name}") 46 | 47 | with tempfile.TemporaryDirectory() as tmp: 48 | branch_name = "converted" 49 | try: 50 | check_call( 51 | f"git clone --depth 1 --branch {branch_name} https://github.com/{repo_name}.git {tmp}".split() 52 | ) 53 | except CalledProcessError: 54 | print(f"Failed to clone {repo_name}") 55 | return 56 | 57 | repo = Path(tmp) 58 | # os.system(f'tree {repo}') 59 | tutorials = glob.glob(f"{repo}/_sources/*.ipynb") 60 | for t in tutorials: 61 | tname = os.path.splitext(os.path.basename(t))[0] 62 | print(f"Copying tutorial {tname}") 63 | shutil.copy(t, destination_directory) 64 | shutil.copy( 65 | f"{repo}/{tname}.html", 66 | destination_directory, 67 | ) 68 | # if len(tutorials) > 1: 69 | # index_files = glob.glob(f"{repo}/index-*.html") 70 | # if index_files: 71 | # index = index_files[0] 72 | # print(f"More than 1 tutorial found; also copying index file {index}") 73 | # shutil.copy(index, destination_directory) 74 | # else: 75 | # raise FileNotFoundError(f"No index-*.html file found for {repo_name}.") 76 | 77 | # copy files for in-notebook search bar 78 | # shutil.copy( 79 | # f"{repo}/search.html", 80 | # destination_directory, 81 | # ) 82 | # shutil.copy( 83 | # f"{repo}/searchindex.js", 84 | # destination_directory, 85 | # ) 86 | 87 | # copy _static files (CSS, JS) for page rendering 88 | shutil.copytree( 89 | f"{repo}/_static", 90 | f"{destination_directory}/_static", 91 | dirs_exist_ok=True, 92 | ) 93 | 94 | # copy images (plots) in notebook for faster page loading 95 | images = glob.glob(f"{repo}/_images/*.png") 96 | if len(images) > 0: 97 | print("Copying notebook cell output images") 98 | image_dir = f"{destination_directory}/_images" 99 | os.makedirs(image_dir, exist_ok=True) 100 | for i in images: 101 | shutil.copy(i, image_dir) 102 | else: 103 | print("No notebook cell output images found to copy") 104 | 105 | 106 | if __name__ == "__main__": 107 | args = parse_args() 108 | dest_dir = args.dest 109 | url = "https://api.github.com/orgs/astropy-learn/repos" 110 | with requests.Session() as s: 111 | while True: 112 | response = s.get(url) 113 | response.raise_for_status() 114 | data = response.json() 115 | list(map(process_repo, data, [dest_dir] * len(data))) 116 | url = response.links.get("next", {}).get("url") 117 | if not url: 118 | break 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-astropy", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Learn Astropy", 6 | "author": "Jonathan Sick", 7 | "keywords": [ 8 | "gatsby" 9 | ], 10 | "scripts": { 11 | "develop": "gatsby develop", 12 | "start": "gatsby develop", 13 | "build": "gatsby build", 14 | "serve": "gatsby serve", 15 | "clean": "gatsby clean", 16 | "lint": "eslint .", 17 | "lint:fix": "eslint . --fix", 18 | "prettier": "npx prettier . --check", 19 | "prettier:fix": "npm run prettier -- --write", 20 | "prepare": "husky install" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "airbnb", 25 | "prettier" 26 | ], 27 | "parser": "babel-eslint", 28 | "plugins": [ 29 | "html", 30 | "react-hooks" 31 | ], 32 | "rules": { 33 | "no-debugger": 0, 34 | "no-alert": 0, 35 | "no-await-in-loop": 0, 36 | "no-return-assign": [ 37 | "error", 38 | "except-parens" 39 | ], 40 | "no-restricted-syntax": [ 41 | 2, 42 | "ForInStatement", 43 | "LabeledStatement", 44 | "WithStatement" 45 | ], 46 | "no-unused-vars": [ 47 | 1, 48 | { 49 | "ignoreRestSiblings": true, 50 | "argsIgnorePattern": "res|next|^err" 51 | } 52 | ], 53 | "prefer-const": [ 54 | "error", 55 | { 56 | "destructuring": "all" 57 | } 58 | ], 59 | "arrow-body-style": [ 60 | 2, 61 | "as-needed" 62 | ], 63 | "no-unused-expressions": [ 64 | 2, 65 | { 66 | "allowTaggedTemplates": true 67 | } 68 | ], 69 | "no-param-reassign": [ 70 | 2, 71 | { 72 | "props": false 73 | } 74 | ], 75 | "no-console": 0, 76 | "import/prefer-default-export": 0, 77 | "import": 0, 78 | "func-names": 0, 79 | "space-before-function-paren": 0, 80 | "comma-dangle": 0, 81 | "max-len": 0, 82 | "import/extensions": 0, 83 | "no-underscore-dangle": 0, 84 | "consistent-return": 0, 85 | "react/display-name": 1, 86 | "react/no-array-index-key": 0, 87 | "react/react-in-jsx-scope": 0, 88 | "react/prefer-stateless-function": 0, 89 | "react/forbid-prop-types": 0, 90 | "react/no-unescaped-entities": 0, 91 | "jsx-a11y/accessible-emoji": 0, 92 | "jsx-a11y/label-has-associated-control": [ 93 | "error", 94 | { 95 | "assert": "either" 96 | } 97 | ], 98 | "react/require-default-props": 0, 99 | "react/jsx-filename-extension": [ 100 | 1, 101 | { 102 | "extensions": [ 103 | ".js", 104 | ".jsx" 105 | ] 106 | } 107 | ], 108 | "radix": 0, 109 | "no-shadow": [ 110 | 2, 111 | { 112 | "hoist": "all", 113 | "allow": [ 114 | "resolve", 115 | "reject", 116 | "done", 117 | "next", 118 | "err", 119 | "error" 120 | ] 121 | } 122 | ], 123 | "quotes": [ 124 | 2, 125 | "single", 126 | { 127 | "avoidEscape": true, 128 | "allowTemplateLiterals": true 129 | } 130 | ], 131 | "jsx-a11y/href-no-hash": "off", 132 | "jsx-a11y/anchor-is-valid": [ 133 | "warn", 134 | { 135 | "aspects": [ 136 | "invalidHref" 137 | ] 138 | } 139 | ], 140 | "react-hooks/rules-of-hooks": "error", 141 | "react-hooks/exhaustive-deps": "warn" 142 | } 143 | }, 144 | "lint-staged": { 145 | "*.js": "eslint" 146 | }, 147 | "dependencies": { 148 | "@fontsource/source-sans-pro": "^4.5.0", 149 | "algoliasearch": "^4.10.3", 150 | "babel-plugin-styled-components": "^1.13.2", 151 | "gatsby": "^3.15.0", 152 | "gatsby-image": "^3.10.0", 153 | "gatsby-plugin-react-helmet": "^4.10.0", 154 | "gatsby-plugin-styled-components": "^4.12.0", 155 | "gatsby-source-filesystem": "^3.10.0", 156 | "gatsby-transformer-remark": "^4.7.0", 157 | "instantsearch.css": "^7.4.5", 158 | "polished": "^4.1.3", 159 | "prop-types": "^15.7.2", 160 | "react": "^17.0.2", 161 | "react-dom": "^17.0.2", 162 | "react-helmet": "^6.1.0", 163 | "react-instantsearch-dom": "^6.12.1", 164 | "styled-components": "^5.3.1" 165 | }, 166 | "devDependencies": { 167 | "babel-eslint": "^10.1.0", 168 | "eslint": "^7.32.0", 169 | "eslint-config-airbnb": "^18.2.1", 170 | "eslint-config-prettier": "^8.3.0", 171 | "eslint-plugin-html": "^6.1.2", 172 | "eslint-plugin-import": "^2.23.4", 173 | "eslint-plugin-jsx-a11y": "^6.4.1", 174 | "eslint-plugin-react": "^7.24.0", 175 | "eslint-plugin-react-hooks": "^4.2.0", 176 | "husky": "^7.0.1", 177 | "lint-staged": "^11.1.1", 178 | "prettier": "^2.3.2", 179 | "pretty-quick": "^3.1.1" 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | 'on': 4 | push: 5 | branches: 6 | - main 7 | schedule: 8 | # weekly on Sunday 9 | - cron: "20 10 * * 0" 10 | # allow manual triggering of workflow 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | 20 | - name: Read .nvmrc 21 | id: node_version 22 | run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc) 23 | 24 | - name: Set up node 25 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 26 | with: 27 | node-version: ${{ steps.node_version.outputs.NODE_VERSION }} 28 | 29 | - name: Cache dependencies 30 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 31 | with: 32 | path: ~/.npm 33 | key: ${{ runner.os }}-node-${{ steps.node_version.outputs.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }} 34 | restore-keys: | 35 | ${{ runner.os }}-node-${{ steps.node_version.outputs.NODE_VERSION }} 36 | 37 | - run: npm ci 38 | name: Install 39 | 40 | - run: npm run build 41 | name: Build 42 | 43 | - name: Upload gatsby artifact 44 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 45 | with: 46 | name: gatsby-build 47 | path: ./public 48 | 49 | deploy: 50 | runs-on: ubuntu-latest 51 | needs: build 52 | 53 | steps: 54 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 55 | 56 | - name: Set up Python 57 | uses: actions/setup-python@v5 58 | with: 59 | python-version: 3.12 60 | 61 | - name: Install Python dependencies 62 | run: | 63 | python -m pip install -U pip 64 | python -m pip install -r deployment/requirements.txt 65 | 66 | - name: Download gatsby artifact 67 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 68 | with: 69 | name: gatsby-build 70 | path: ./public 71 | 72 | - name: Install tutorials into site 73 | run: | 74 | python deployment/installtutorials.py --dest public/tutorials 75 | 76 | - name: List website content 77 | run: | 78 | tree public 79 | 80 | - name: Deploy to gh-pages 81 | uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 82 | with: 83 | github_token: ${{ secrets.GITHUB_TOKEN }} 84 | publish_dir: ./public 85 | 86 | - name: Upload site for indexing 87 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 88 | with: 89 | name: site-for-indexing 90 | path: ./public 91 | 92 | index: 93 | # this job effectively tests the indexing before the next job that 94 | # clears it (so that the site doesn't end up empty if the indexing fails) 95 | runs-on: ubuntu-latest 96 | needs: deploy 97 | 98 | steps: 99 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 100 | 101 | - name: Download site for indexing 102 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 103 | with: 104 | name: site-for-indexing 105 | path: ./public 106 | 107 | - name: Set up Python 108 | uses: actions/setup-python@v5 109 | with: 110 | python-version: 3.12 111 | 112 | - name: Install Python dependencies 113 | run: | 114 | python -m pip install -U pip 115 | python -m pip install -r deployment/requirements.txt 116 | 117 | - name: Pre-index tutorials 118 | id: preindex 119 | env: 120 | ALGOLIA_ID: ${{ secrets.ALGOLIA_ID }} 121 | ALGOLIA_KEY: ${{ secrets.ALGOLIA_KEY }} 122 | ALGOLIA_INDEX: ${{ secrets.ALGOLIA_INDEX }} 123 | run: | 124 | astropylibrarian index tutorial-site \ 125 | public/tutorials \ 126 | https://learn.astropy.org/tutorials 127 | 128 | - name: sleep 129 | run: sleep 5 130 | 131 | - name: Clear Algolia index 132 | id: clearindex 133 | if: steps.preindex.outcome == 'success' 134 | env: 135 | ALGOLIA_ID: ${{ secrets.ALGOLIA_ID }} 136 | ALGOLIA_KEY: ${{ secrets.ALGOLIA_KEY }} 137 | ALGOLIA_INDEX: ${{ secrets.ALGOLIA_INDEX }} 138 | run: | 139 | astropylibrarian clear-index \ 140 | --algolia-id "$ALGOLIA_ID" \ 141 | --algolia-key "$ALGOLIA_KEY" \ 142 | --index "$ALGOLIA_INDEX" 143 | 144 | - name: sleep 145 | run: sleep 5 146 | 147 | - name: Index tutorials 148 | if: steps.clearindex.outcome == 'success' 149 | env: 150 | ALGOLIA_ID: ${{ secrets.ALGOLIA_ID }} 151 | ALGOLIA_KEY: ${{ secrets.ALGOLIA_KEY }} 152 | ALGOLIA_INDEX: ${{ secrets.ALGOLIA_INDEX }} 153 | run: | 154 | astropylibrarian index tutorial-site \ 155 | public/tutorials \ 156 | https://learn.astropy.org/tutorials 157 | 158 | - name: Index guides 159 | # continue on error because guides can be externally hosted; avoid a change to 160 | # them from preventing the tutorials updating on the site 161 | continue-on-error: true 162 | env: 163 | ALGOLIA_ID: ${{ secrets.ALGOLIA_ID }} 164 | ALGOLIA_KEY: ${{ secrets.ALGOLIA_KEY }} 165 | ALGOLIA_INDEX: ${{ secrets.ALGOLIA_INDEX }} 166 | run: | 167 | astropylibrarian index guide \ 168 | http://www.astropy.org/ccd-reduction-and-photometry-guide/ \ 169 | --algolia-id "$ALGOLIA_ID" \ 170 | --algolia-key "$ALGOLIA_KEY" \ 171 | --index "$ALGOLIA_INDEX" \ 172 | --priority 2 173 | -------------------------------------------------------------------------------- /src/contributing/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Contributing to Learn Astropy' 3 | slug: '/contributing/' 4 | --- 5 | 6 | We are always interested in incorporating new tutorials into Learn Astropy and the Astropy Tutorials series. We welcome tutorials covering astro-relevant topics; they do not need to use the Astropy package in order to be hosted or indexed here. If you have astronomy tutorials that you would like to contribute, or if you have a separate tutorial series that you would like indexed by the Learn Astropy website, see below. 7 | 8 | ## Content Guidelines 9 | 10 | ### Overview 11 | 12 | - Each tutorial should have 3–5 explicit [Learning Goals](http://tll.mit.edu/help/intended-learning-outcomes), demonstrate ~2–3 pieces of functionality relevant to astronomy, and contain 2–3 demonstrations of generic but commonly used functionality (e.g., `numpy`, `matplotlib`). 13 | - Each tutorial should roughly follow this progression: 14 | - _Input/Output_: read in some data (use [astroquery](https://astroquery.readthedocs.io/en/latest/) where possible to query real astronomical datasets) 15 | - _Analysis_: do something insightful / useful with the data 16 | - _Visualization_: make a pretty figure (use [astropy.visualization](http://docs.astropy.org/en/stable/visualization/) where possible) 17 | - The tutorials must be compatible with the versions supported by the latest major release of the Astropy core package 18 | 19 | ### Introduction cell template 20 | 21 | The first cell in every tutorial notebook is a markdown cell used for the title, author list, keywords, and summary. All of this information should be contained in a single cell and should adhere to the following format: 22 | 23 | ``` 24 | # Title name 25 | 26 | ## Authors 27 | Jane Smith (@GITHUB-ID, ORCID-ID), Jose Jones (@GITHUB-ID, ORCID-ID) 28 | 29 | ## Learning Goals 30 | * Query the ... dataset 31 | * Calculate ... 32 | * Display ... 33 | 34 | ## Keywords 35 | Example, example, example 36 | 37 | ## Companion Content 38 | Carroll & Ostlie 10.3, Binney & Tremaine 1.5 39 | 40 | ## Summary 41 | In this tutorial, we will download a data file, do something to it, and then 42 | visualize it. 43 | ``` 44 | 45 | Please draw keywords from [this list](https://github.com/astropy-learn/astropy-tutorials/blob/main/resources/keywords.md). 46 | 47 | ### Code 48 | 49 | - Demonstrate good commenting practice 50 | - Add comments to sections of code that use concepts not included in the Learning Goals 51 | - Demonstrate best practices of variable names 52 | - Variables should be all lower case with words separated by underscores 53 | - Variable names should be descriptive, e.g., `galaxy_mass`, `u_mag` 54 | - Use the print function explicitly to display information about variables 55 | - As much as possible, comply with [PEP8](https://www.python.org/dev/peps/pep-0008/). 56 | - As much as possible, comply with Jupyter notebook style guides - [STScI style guide](https://github.com/spacetelescope/style-guides/blob/master/guides/jupyter-notebooks.md) and [Official Coding Style](https://docs.jupyter.org/en/stable/contributing/ipython-dev-guide/coding_style.html). 57 | - Imports 58 | - Do not use `from package import *`; import packages, classes, and functions explicitly 59 | - Follow recommended package name abbreviations: 60 | - `import numpy as np` 61 | - `import matplotlib as mpl` 62 | - `import matplotlib.pyplot as plt` 63 | - `import astropy.units as u` 64 | - `import astropy.coordinates as coord` 65 | - `from astropy.io import fits` 66 | - Display figures inline using matplotlib's inline backend: 67 | - `%matplotlib inline # make plots display in notebooks` 68 | 69 | ### Narrative 70 | 71 | - Please read through the other tutorials to get a sense of the desired tone and length. 72 | - Use [Markdown formatting](http://jupyter-notebook.readthedocs.io/en/latest/examples/Notebook/Working%20With%20Markdown%20Cells.html) in text cells for formatting, links, latex, and code snippets. 73 | - Titles should be short yet descriptive and emphasize the learning goals of the tutorial. Try to make the title appeal to a broad audience and avoid referencing a specific instrument, catalog, or anything wavelength dependent. 74 | - List all authors' full names (comma separated) and link to GitHub profiles and/or [ORCID iD](https://orcid.org/) when relevant. 75 | - Include [Learning Goals](http://tll.mit.edu/help/intended-learning-outcomes) at the top as a bulleted list. 76 | - Include Keywords as a comma separated list of topics, packages, and functions demonstrated. 77 | - The first paragraph should give a brief overview of the entire tutorial including relevant astronomy concepts. 78 | - Use the first-person inclusive plural ("we"). For example, "We are going to make a plot which...", or "Above, we did it the hard way, but here is the easier way..." 79 | - Section headings should be in the imperative mood. For example, "Download the data." 80 | - Avoid extraneous words such as "obviously", "just", "simply", or "easily." For example, avoid phrases like "we just have to do this one thing." 81 | - Use `
Note
` for Notes and `
Warning
` for Warnings (Markdown supports raw HTML) 82 | 83 | ## Procedure for contributing a notebook or set of notebooks 84 | 85 | To contribute tutorial content, open an issue in the [astropy-tutorials repository](https://github.com/astropy-learn/astropy-tutorials/issues). When you click 'New issue', select the 'Tutorial submission' option, completing all fields of that form. You have the option to have your tutorial made citable via an upload (by the Astropy Learn maintainers) to Zenodo. If you have any data files needed by the notebook to run, see the 'Data files' section below. 86 | 87 | Maintainers will review your notebook and may ask questions and/or suggest edits (e.g., to conform to the above content guidelines). When the review is complete and the tutorial is ready to be incorporated, maintainers will create a new repository for the tutorial, add the notebook(s), and upload your tutorial(s) to this website. 88 | 89 | ### Data files 90 | 91 | If your tutorial includes large data files (where large means >~ 1 MB), including them in the tutorial's repository would drastically slow down cloning of the repository. Instead, for files < 10 MB, we encourage use of the `astropy.utils.download_files` function, and we will host data files on the http://data.astropy.org server (or you can do this directly by opening a PR at the https://github.com/astropy/astropy-data repository). Alternatively, if the file size is > 10 MB, the data should be hosted on Zenodo. To do the former, use the following procedure: 92 | 93 | - Assuming you have a data file named `mydatafile.fits`, you can access the file in the notebook with something like this at the top of the notebook: 94 | 95 | ``` 96 | from astropy.utils.data import download_file 97 | 98 | tutorialpath = '' 99 | mydatafilename1 = download_file(tutorialpath + 'mydatafile1.fits', cache=True) 100 | mydatafilename2 = download_file(tutorialpath + 'mydatafile2.dat', cache=True) 101 | ``` 102 | 103 | And then use them like this: 104 | 105 | ``` 106 | fits.open(mydatafilename1) 107 | ... 108 | with open(mydatafilename2) as f: 109 | ... 110 | ``` 111 | 112 | If you do this, the only change necessary in your submission of the notebook to [astropy-tutorials](https://github.com/astropy-learn/astropy-tutorials/issues) will be to set `tutorialpath` to `'http://data.astropy.org/tutorials/My-tutorial-name/'`. 113 | 114 | For data files that are larger than 10 MB in size, we recommend hosting with Zenodo. To use this approach, follow these steps: 115 | 116 | - Sign up for an account at https://zenodo.org/ if you do not have one already. 117 | 118 | - Log in to Zenodo and perform a new upload. Follow the Zenodo instructions and complete all the required fields in order to have the data file(s) uploaded to their records. Once this is done you will have a link to share the data. 119 | 120 | - With the link to the data file record, which has the format `https://zenodo.org/api/records/:id`, an example HTTP GET request needed to retrieve the data using the Python package `requests` is shown below: 121 | 122 | ``` 123 | import requests 124 | r = requests.get("https://zenodo.org/api/records/1234) 125 | ``` 126 | 127 | To use the output as a locally stored file, you would first need to write the file contents to a file, for example: 128 | 129 | ``` 130 | with open('./some-data-file.fits', 'wb') as f: 131 | f.write(r.content) 132 | ``` 133 | -------------------------------------------------------------------------------- /static/astropy_logo_notext.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 24 | 28 | 32 | 33 | 36 | 40 | 41 | 43 | 47 | 51 | 52 | 56 | 62 | 67 | 72 | 76 | 77 | 87 | 88 | 94 | 99 | 104 | 108 | 109 | 119 | 120 | 130 | 131 | 155 | 157 | 158 | 160 | image/svg+xml 161 | 163 | 164 | 165 | 166 | 167 | 172 | 184 | 197 | 207 | 213 | 216 | 221 | 226 | 231 | 232 | 238 | 251 | 252 | 253 | --------------------------------------------------------------------------------