├── requirements.txt ├── MANIFEST.in ├── sphinx_lesson ├── _version.py ├── _static │ ├── term_role_formatting.css │ └── sphinx_lesson.css ├── term_role_formatting.py ├── __init__.py ├── md_transforms.py ├── directives.py └── exerciselist.py ├── content ├── favicon.ico ├── img │ └── sample-image.png ├── changelog.md ├── cheatsheet.md ├── executing-code.md ├── building.md ├── substitutions-replacements.md ├── gh-action.md ├── figures.md ├── toctree.md ├── presentation-mode.md ├── md-transforms.rst ├── getting-started.md ├── installation.md ├── jupyter.ipynb ├── convert-old-lessons.md ├── indexing.md ├── index.md ├── intersphinx.md ├── conf.py ├── exercise-list.md ├── contributing-to-a-lesson.md ├── directives.md ├── md-and-rst.md ├── sample-episode-myst.md └── sample-episode-rst.rst ├── .gitignore ├── Makefile ├── LICENSE ├── pyproject.toml ├── .github └── workflows │ ├── test.yml │ ├── release.yml │ └── sphinx.yml ├── rst-to-myst-config.yaml └── README.rst /requirements.txt: -------------------------------------------------------------------------------- 1 | . 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | -------------------------------------------------------------------------------- /sphinx_lesson/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.8.19' 2 | -------------------------------------------------------------------------------- /content/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderefinery/sphinx-lesson/HEAD/content/favicon.ico -------------------------------------------------------------------------------- /content/img/sample-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderefinery/sphinx-lesson/HEAD/content/img/sample-image.png -------------------------------------------------------------------------------- /sphinx_lesson/_static/term_role_formatting.css: -------------------------------------------------------------------------------- 1 | /* Make terms bold */ 2 | a.reference span.std-term { 3 | font-weight: bold; 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | .ipynb_checkpoints 3 | /venv* 4 | .jupyter_cache 5 | jupyter_execute 6 | 7 | /*.egg-info/ 8 | /build 9 | /dist 10 | /.idea 11 | -------------------------------------------------------------------------------- /content/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This isn't very up to date right now. There haven't been significant 4 | backwards-incompatible changes, so this hasn't been kept in earnest. 5 | 6 | ## 0.8.0 7 | 8 | - First PyPI release 9 | -------------------------------------------------------------------------------- /content/cheatsheet.md: -------------------------------------------------------------------------------- 1 | # Cheatsheet 2 | 3 | Nothing here yet. See the sample episodes: 4 | 5 | * [MyST markdown](https://github.com/coderefinery/sphinx-lesson/blob/master/content/sample-episode-myst.md?plain=1) 6 | * [ReST](https://github.com/coderefinery/sphinx-lesson/blob/master/content/sample-episode-rst.rst?plain=1) 7 | -------------------------------------------------------------------------------- /sphinx_lesson/term_role_formatting.py: -------------------------------------------------------------------------------- 1 | """Make term definition links bold 2 | """ 3 | 4 | from . import __version__ 5 | 6 | def setup(app): 7 | "Sphinx extension setup" 8 | app.add_css_file("term_role_formatting.css") 9 | 10 | return { 11 | 'version': __version__, 12 | 'parallel_read_safe': True, 13 | 'parallel_write_safe': True, 14 | } 15 | -------------------------------------------------------------------------------- /content/executing-code.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | kernelspec: 6 | display_name: Python 3 7 | name: python3 8 | --- 9 | 10 | 11 | # Executing code cells in MyST markdown 12 | 13 | This is a `.md` file that has a special header and a `{code-cell}` 14 | directive, that lets it 15 | 16 | ```{code-cell} 17 | !hostname 18 | ``` 19 | 20 | You can see how this works [on the myst-nb 21 | docs](https://myst-nb.readthedocs.io/en/latest/use/markdown.html). 22 | -------------------------------------------------------------------------------- /content/building.md: -------------------------------------------------------------------------------- 1 | # Building the lesson 2 | 3 | ```{highlight} console 4 | ``` 5 | 6 | It is built the normal Sphinx ways. Using Sphinx directly, one would 7 | run: 8 | 9 | ``` 10 | $ sphinx-build -M html content/ _build 11 | ``` 12 | 13 | If you have `make` installed, you can: 14 | 15 | ``` 16 | $ make html 17 | ## or 18 | $ make dirhtml 19 | ## full build 20 | $ make clean html 21 | ``` 22 | 23 | However, there are different ways to set up a Sphinx project. 24 | CodeRefinery lessons puts the results in `_build/`. 25 | -------------------------------------------------------------------------------- /content/substitutions-replacements.md: -------------------------------------------------------------------------------- 1 | # Substitutions and replacement variables 2 | 3 | Like most languages, ReST and MyST both have direct ways to substitute 4 | variables to locally customize lessons. While this works for simple 5 | cases, this can quickly become difficult to manage with the "main 6 | copy" of possibly important content being separated from the main 7 | body, and keeping the substitutions up to date. 8 | 9 | [sphinx_ext_substitution](https://github.com/NordicHPC/sphinx_ext_substitution) tries to 10 | solve these problems by keeping the default values within the document 11 | and providing tools to manage the substitution as the context changes 12 | over time. It is tested with ReST and should work with MyST as well. 13 | -------------------------------------------------------------------------------- /sphinx_lesson/__init__.py: -------------------------------------------------------------------------------- 1 | """Sphinx extension for CodeRefinery lessons""" 2 | from ._version import __version__ 3 | 4 | def setup(app): 5 | "Sphinx extension setup" 6 | app.setup_extension('myst_nb') 7 | app.setup_extension('sphinx_copybutton') 8 | app.setup_extension('sphinx_minipres') 9 | app.setup_extension('sphinx_tabs.tabs') 10 | app.setup_extension('sphinx_togglebutton') 11 | app.setup_extension(__name__+'.directives') 12 | app.setup_extension(__name__+'.md_transforms') 13 | app.setup_extension(__name__+'.exerciselist') 14 | app.setup_extension(__name__+'.term_role_formatting') 15 | 16 | return { 17 | 'version': __version__, 18 | 'parallel_read_safe': True, 19 | 'parallel_write_safe': True, 20 | } 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = content 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | # Live reload site documents for local development 23 | livehtml: 24 | sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 25 | -------------------------------------------------------------------------------- /content/gh-action.md: -------------------------------------------------------------------------------- 1 | # Deployment with Github Actions 2 | 3 | In sphinx-lesson-template (and this lesson...), there is a `.github/workflows/sphinx.yml` file 4 | that contains a Github Action that: 5 | 6 | - Installs dependencies 7 | 8 | - Builds the project with Sphinx 9 | 10 | - Deploys it 11 | 12 | - If branch = `main`, deploy to github pages normally 13 | - For other branches, deploy to github-pages but put the result in 14 | the `branch/{branch-name}` subdirectory. If the branch name has 15 | a `/` in it, replace it with `--`. 16 | - Keep all previous deployments, but remove branch subdirectories 17 | for branches that no longer exist. 18 | 19 | This allows you to view builds from pull requests or other branches. 20 | 21 | ## Usage 22 | 23 | It is recommended to copy from 24 | [sphinx-lesson-template](https://github.com/coderefinery/sphinx-lesson-template/), 25 | since that is the primary copy that is updated with all of the latest developments. 26 | 27 | Direct link: 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 CodeRefinery team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /content/figures.md: -------------------------------------------------------------------------------- 1 | # Figures 2 | 3 | The `figure` directive inserts an image and also provides a caption 4 | and other material. 5 | 6 | - The path is the relative or absolute path *within the sphinx source 7 | directory*. 8 | - You can give optional CSS classes, `with-border` gives it a black 9 | border. Remove this if you don't want it - the examples below 10 | include it. 11 | 12 | :::{figure} img/sample-image.png 13 | :class: with-border 14 | 15 | This is the caption. 16 | ::: 17 | 18 | In ReST, this is: 19 | 20 | ``` 21 | .. figure:: img/sample-image.png 22 | :class: with-border 23 | 24 | This is the caption. 25 | ``` 26 | 27 | In MyST Markdown, this is: 28 | 29 | ```` 30 | ```{figure} img/sample-image.png 31 | --- 32 | class: with-border 33 | --- 34 | 35 | This is the figure caption. 36 | ``` 37 | ```` 38 | 39 | When adding figures, optimize for narrow columns. First off, many 40 | themes keep it in a small column but even if not, learners will 41 | usually need to keep the text in a small column anyway, to share the 42 | screen with their workspace. If you are `690` or less, then 43 | sphinx_rtd_theme has no image scaling. `600` or less may be best. 44 | If larger, then they may be scaled down (in some themes) or scroll 45 | left (others). 46 | -------------------------------------------------------------------------------- /content/toctree.md: -------------------------------------------------------------------------------- 1 | # Table of Contents tree 2 | 3 | Pages are not found by globbing a pattern: you can explicitly define a 4 | table of contents list. There must be one master toctree in the index 5 | document, but then they can be nested down. For the purposes of 6 | sphinx-lesson, we probably don't need such features 7 | 8 | MyST: 9 | 10 | ```` 11 | ```{toctree} 12 | --- 13 | caption: Episodes 14 | maxdepth: 1 15 | --- 16 | 17 | basics 18 | creating-using-web 19 | creating-using-desktop 20 | contributing 21 | doi 22 | websites 23 | ``` 24 | ```` 25 | 26 | 27 | ReST: 28 | 29 | ``` 30 | .. toctree:: 31 | caption: Episodes 32 | maxdepth: 1 33 | 34 | basics 35 | creating-using-web 36 | creating-using-desktop 37 | contributing 38 | doi 39 | websites 40 | ``` 41 | 42 | The pages are added by filename (no extension needed). The name by 43 | default comes from the document title, but you can override it if you 44 | want. 45 | 46 | You can have multiple toctrees: check sphinx-test-lesson, we have one 47 | for the episodes, and one for extra material (like quick ref and 48 | instructor guide). 49 | 50 | ## See also 51 | 52 | - Read more about the [Sphinx toctree directive](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#table-of-contents). 53 | -------------------------------------------------------------------------------- /content/presentation-mode.md: -------------------------------------------------------------------------------- 1 | # Presentation mode 2 | 3 | :::{note} 4 | This is a technical demo, included by default to make it easy to 5 | use. It may be removed in a future release. 6 | ::: 7 | 8 | Using [minipres](https://github.com/coderefinery/sphinx-minipres), 9 | any web page can be turned into a presentation. As usual, there is 10 | nothing very specific to sphinx-lesson about this, but currently 11 | minipres is only tested on `sphinx_rtd_theme`, but theoretically can 12 | work on others. 13 | 14 | Using minipres, you only have to write one page: the material your 15 | students read. Then, you hide the unnecessary elements (table of 16 | contents), focus on one section, and provide a quick way to jump 17 | between sections. Then you can get the focused attention of a 18 | presentation and the readability of a single page. 19 | 20 | How it works: 21 | 22 | - Add `?minipres` to the URL of any page, and it goes into 23 | presentation mode. 24 | - Add `?plain` to the URL of any page to go to plain mode. 25 | 26 | In presentation mode: 27 | 28 | - The sidebars are removed (this is the only thing that happens in 29 | `plain` mode). 30 | - Extra space is added between each section (HTML headings), so that 31 | you focus on one section at a time 32 | - The **left/right arrow keys** scroll between sections. 33 | 34 | Examples: 35 | 36 | ```{eval-rst} 37 | - `View this page as an example of minipres <../sample-episode-rst/?minipres>`__. 38 | - `View this page in "plain" mode <../sample-episode-rst/?plain>`__. 39 | ``` 40 | -------------------------------------------------------------------------------- /content/md-transforms.rst: -------------------------------------------------------------------------------- 1 | Markdown transforms 2 | =================== 3 | 4 | **This is mostly obsolete and unmaintained now that almost everything 5 | is transitioned to MyST-markdown. If this is important for you, get 6 | in touch and we can revive it.** 7 | 8 | To ease the transition from other Markdown dialects (like the one used 9 | in software-carpentry), we implement some transformations in Sphinx 10 | which happen as source preprocessing. 11 | These are implemented in the ``sphinx_lessons.md_transforms`` module 12 | and are implemented using regular expressions, so they are a 13 | bit fragile. 14 | 15 | Code fences 16 | ----------- 17 | 18 | Code fence syntax is translated to CommonMark. Input:: 19 | 20 | ``` 21 | blah 22 | ``` 23 | {: output} 24 | 25 | Output:: 26 | 27 | ```{output} 28 | blah 29 | ``` 30 | 31 | Block quotes 32 | ------------ 33 | 34 | Transform CSS styles into MyST directives (implemented as code 35 | fences. Input:: 36 | 37 | > ## some-heading 38 | > text 39 | > text 40 | {: .block-class} 41 | 42 | Output:: 43 | 44 | ```{block-class} some-heading 45 | text 46 | text 47 | ``` 48 | 49 | The ``block-class`` is the directive name (we maintain compatibility 50 | with old jekyll-common) 51 | 52 | Raw HTML images 53 | --------------- 54 | 55 | Raw HTML isn't a good idea in portable formats. Plus, in the old 56 | jekyll formats, bad relative path handling caused absolute paths to be 57 | embedded a lot. Transform this:: 58 | 59 | 60 | 61 | into this:: 62 | 63 | ```{figure} /path/to/img.png 64 | ``` 65 | 66 | Exclude any possible ``{{ ... }}`` template variables used to 67 | semi-hard code absolute paths. 68 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [tool.flit.module] 6 | name = "sphinx_lesson" 7 | 8 | [project] 9 | name = "sphinx-lesson" 10 | authors = [{name = "Richard Darst"}] # FIXME 11 | readme = "README.rst" 12 | license = {file = "LICENSE"} 13 | # https://pypi.org/classifiers/ 14 | classifiers = [ 15 | "License :: OSI Approved :: MIT License", 16 | "Programming Language :: Python :: 3", 17 | "Development Status :: 4 - Beta", 18 | "Framework :: Sphinx", 19 | "Framework :: Sphinx :: Extension", 20 | "Operating System :: OS Independent", 21 | ] 22 | keywords = ["sphinx-extension"] 23 | requires-python = ">=3.3" 24 | dynamic = ["version", "description"] 25 | dependencies = [ 26 | # FIXME 27 | "sphinx<9", # https://github.com/coderefinery/train-the-trainer/pull/104 28 | "sphinx_rtd_theme", 29 | "sphinx_copybutton", 30 | "sphinx_minipres", 31 | "sphinx_tabs", 32 | "sphinx_togglebutton>=0.2.0", 33 | "sphinx-autobuild", 34 | #"myst_parser[sphinx]", 35 | "myst_nb>0.8.3", 36 | "sphinx_rtd_theme_ext_color_contrast", 37 | ] 38 | 39 | #[project.optional-dependencies] 40 | #test = [ 41 | # # FIXME 42 | # "pytest", 43 | #] 44 | 45 | [project.urls] 46 | Repository = "https://github.com/coderefinery/sphinx-lesson" 47 | Documentation = "https://coderefinery.github.io/sphinx-lesson/" 48 | 49 | 50 | # https://flit.pypa.io/en/latest/upload.html 51 | # flit build 52 | # You need to configure a .pypirc file for test upload, or use environment variables: 53 | # https://flit.pypa.io/en/latest/upload.html#using-pypirc 54 | # flit publish --repository testpypi 55 | # or: FLIT_INDEX_URL=https://test.pypi.org/legacy/ FLIT_USERNAME=xxx and FLIT_PASSWORD=xxx flit publish 56 | # flit publish 57 | -------------------------------------------------------------------------------- /content/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ## From a template repository 4 | 5 | You can get started by making a Sphinx project and configuring the 6 | extension. We recommend you use the sphinx-lesson-template repository 7 | (). 8 | 9 | This template repository is updated with new copies of base files as 10 | the lesson develops - you might want to check back for them, later. 11 | 12 | ## Convert an existing jekyll lesson 13 | 14 | See {doc}`convert-old-lessons`. This hasn't been used in years so may 15 | be out of date. 16 | 17 | ## From scratch 18 | 19 | See the next page, {doc}`installation`, for raw Python packages to 20 | install and how to configure a arbitrary Sphinx project. 21 | 22 | ## Github Pages initial commit 23 | 24 | The included Github Actions file will automatically push to Github 25 | Pages, but due to some quirk/bugs in gh-pages *the very first 26 | non-human gh-pages push won't enable Github Pages*. So, you have to 27 | do one push yourself (or go to settings and disable-enable gh-pages 28 | the first time). 29 | 30 | You can make an empty commit to gh-pages this way, which will trigger 31 | the gh-pages deployment (and everything will be automatic after that): 32 | 33 | ``` 34 | git checkout -b gh-pages origin/gh-pages 35 | git commit -m 'empty commit to trigger gh-pages' --allow-empty 36 | git push 37 | ``` 38 | 39 | ## Demo lessons 40 | 41 | This guide can't currently stand alone. It is probably good to look 42 | at and copy from existing lessons for some things: 43 | 44 | - Python for Scientific Computing uses many of the features: 45 | . 46 | - Github without the command line is a complete lesson using the 47 | theme: . 48 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: test 4 | on: [push, pull_request] 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | sphinx_version: ["", "~=8.0.0"] 12 | python_version: ["3.10", '3.x'] 13 | #exclude: 14 | # - {sphinx_version: "~=6.0.0", python_version: '3.8'} 15 | # - {sphinx_version: "~=4.0.0", python_version: '3.10'} 16 | # - {sphinx_version: "~=3.0.0", python_version: '3.x'} 17 | include: 18 | #- {sphinx_version: "~=3.0.0", python_version: '3.6'} 19 | - {sphinx_version: "~=4.0.0", python_version: '3.8'} 20 | - {sphinx_version: "~=5.0.0", python_version: '3.10'} 21 | - {sphinx_version: "~=7.0.0", python_version: '3.12'} 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v4 27 | - name: Install Python 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: ${{ matrix.python_version }} 31 | - name: Install dependencies 32 | run: | 33 | pip install sphinx${{matrix.sphinx_version}} -r requirements.txt . 34 | # jinja2 broke on 2022-03-24 35 | # https://github.com/sphinx-doc/sphinx/issues/10291 36 | # https://github.com/sphinx-doc/sphinx/issues/10289 37 | # Unsure of long-term plan here. 38 | if [ -n "{{matrix.sphinx_version}}" ] ; then 39 | pip install "jinja2<3.1" 40 | fi 41 | - name: List dependency versions 42 | run: | 43 | python -V 44 | pip list 45 | 46 | # Runs a set of commands using the runners shell 47 | - name: Build and fail on errors 48 | run: | 49 | make html SPHINXOPTS="-W --keep-going -T -v" 50 | -------------------------------------------------------------------------------- /content/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Sphinx Python package 4 | 5 | This is distributed as a normal Sphinx extension, so it is easy to 6 | use. To use it, install `sphinx_lesson` via PyPI. 7 | 8 | Then, enable the extension in your Sphinx `conf.py`. This will both 9 | define our special directives, and load the other required extensions 10 | (`myst_nb`). The `myst_nb` extension can be configured normally: 11 | 12 | ``` 13 | extensions = [ 14 | 'sphinx_lesson', 15 | ] 16 | ``` 17 | 18 | ## HTML theme 19 | 20 | We are in theory compatible with any theme, but are most tested with 21 | the sphinx_rtd_theme (which you need to set yourself): 22 | 23 | ``` 24 | html_theme = 'sphinx_rtd_theme' 25 | ``` 26 | 27 | The Jupyter Book (Executable Books Project) Sphinx theme 28 | ([sphinx-book-theme](https://sphinx-book-theme.readthedocs.io/en/latest/)) has some 29 | very nice features and also deserves some consideration. Using it 30 | should be clear: `html_theme = "sphinx_book_theme"`. You can see a 31 | preview of it [as a branch on github-pages](https://coderefinery.github.io/sphinx-lesson/branch/sphinx-book-theme/). 32 | 33 | ## Under the hood 34 | 35 | Adding `sphinx_lesson` as an extension adds these sub-extensions, and 36 | you could selectively enable only the parts you want: 37 | 38 | - `sphinx_lesson.directives` - see {doc}`directives`. 39 | - `sphinx_lesson.md_transforms` - see {doc}`md-transforms`. 40 | - `sphinx_lesson.exerciselist` - see {doc}`exercise-list`. 41 | - `sphinx_lesson.term_role_formatting` - makes glossary term 42 | references bold 43 | - Enables the [myst_notebook extension](https://myst-nb.readthedocs.io/en/latest/), which also enables 44 | [myst_parser](https://myst-parser.readthedocs.io/en/latest/index.html) 45 | (included as a dependencies) 46 | - Enables the [sphinx-copybutton extension](https://github.com/executablebooks/sphinx-copybutton) 47 | (included as a dependency) 48 | - Same for [sphinx-tabs](https://sphinx-tabs.readthedocs.io/) 49 | - Same for [sphinx-togglebutton](https://pypi.org/project/sphinx-togglebutton/) 50 | -------------------------------------------------------------------------------- /rst-to-myst-config.yaml: -------------------------------------------------------------------------------- 1 | # This is a config that can be used with rst2myst. It may need 2 | # adjusting to your needs, but covers the most common CodeRefinery 3 | # extensions: 4 | # 5 | # $ convert --config ../sphinx-lesson/rst-to-myst-config.yaml -S -W content/**.rst 6 | # 7 | # https://rst-to-myst.readthedocs.io/en/latest/usage.html 8 | # 9 | # To verify it got everything, run: 10 | # $ grep -ri eval-rst content/ 11 | # 12 | # To add more roles, you can find the internal name using this 13 | # command. Make sure you specify the right extension to load, both in 14 | # this command and added to this list: 15 | # $ rst2myst directives show -e sphinx_tabs.tabs list-table 16 | 17 | extensions: 18 | - sphinx_lesson 19 | - sphinx_tabs.tabs 20 | - sphinx_lesson.directives 21 | - sphinx_lesson.exerciselist 22 | sphinx: true 23 | conversions: 24 | prereq: parse_all 25 | sphinx.directives.patches.CSVTable: direct 26 | docutils.parsers.rst.directives.tables.ListTable: direct 27 | 28 | sphinx_lesson.directives._BaseCRDirective: parse_all 29 | sphinx_lesson.directives.PrerequisitesDirective: parse_all 30 | sphinx_lesson.directives.DemoDirective: parse_all 31 | sphinx_lesson.directives.Type_AlongDirective: parse_all 32 | sphinx_lesson.directives.ExerciseDirective: parse_all 33 | sphinx_lesson.directives.SolutionDirective: parse_all 34 | sphinx_lesson.directives.HomeworkDirective: parse_all 35 | sphinx_lesson.directives.Instructor_NoteDirective: parse_all 36 | sphinx_lesson.directives.PrerequisitesDirective: parse_all 37 | sphinx_lesson.directives.DiscussionDirective: parse_all 38 | sphinx_lesson.directives.QuestionsDirective: parse_all 39 | sphinx_lesson.directives.ObjectivesDirective: parse_all 40 | sphinx_lesson.directives.KeypointsDirective: parse_all 41 | sphinx_lesson.directives.CalloutDirective: parse_all 42 | sphinx_lesson.directives.ChecklistDirective: parse_all 43 | sphinx_lesson.directives.TestimonialDirective: parse_all 44 | sphinx_lesson.directives.OutputDirective: parse_all 45 | sphinx_lesson.directives.: parse_all 46 | sphinx_lesson.directives.: parse_all 47 | sphinx_lesson.directives.: parse_all 48 | sphinx_lesson.directives.: parse_all 49 | 50 | sphinx_lesson.exerciselist.ExerciselistDirective: parse_all 51 | 52 | sphinx_tabs.tabs.TabsDirective: parse_all 53 | sphinx_tabs.tabs.TabDirective: parse_all 54 | sphinx_tabs.tabs.GroupTabDirective: parse_all 55 | sphinx_tabs.tabs.CodeTabDirective: parse_argument 56 | 57 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Sphinx test lesson 2 | ================== 3 | 4 | This is a Sphinx extension for software-carpentry style 5 | lessons. It is designed as a replacement for the Jekyll-based software 6 | templates. 7 | 8 | * `View the documentation (and example) on github pages 9 | `__. 10 | * `Template repository, to copy from 11 | `__. 12 | 13 | 14 | Features 15 | -------- 16 | 17 | - Sphinx, including power from all of its extensions. 18 | - ReST 19 | - Markdown via the `myst_parser` parser, so has access to all Sphinx 20 | directives natively 21 | - Jupyter as a source format, including executing the notebook (via 22 | ``myst_nb``). 23 | - Automatically building via Github Actions and automatic deployment 24 | to Github Pages. Included workflow file builds all branches, so you 25 | can also preview pull requests. 26 | - Directives for exercises/prereq/etc, works in both ReST and md. 27 | - The Sphinx part can be separated into a separately installable 28 | and versionable Python package, so we don't need git sub-modules. 29 | - Execute code cells in markdown (via ``myst_nb``). 30 | - Consists of sub-extensions for substitutions. Adding 31 | ``sphinx_lesson`` as an extension will bring in these: 32 | 33 | - ``sphinx_lesson.directives`` (the core directives) 34 | - ``sphinx_lesson.md_transforms`` (reprocess some other markdown 35 | format into myst_nb format) 36 | - ``myst_nb`` (not developed by us) 37 | 38 | 39 | 40 | Host Site Locally for Development 41 | --------------------------------- 42 | 43 | 1. Create a virtual python environment:: 44 | 45 | python -m venv venv 46 | 47 | 2. Activate the virtual environment:: 48 | 49 | source activate venv/bin/activate 50 | 51 | 3. Install python packages:: 52 | 53 | pip install -r requirements.txt 54 | 55 | 4. Build local files (this can also be used for deployment):: 56 | 57 | make html 58 | # Output in _build/html/ 59 | make clean html # clean + full rebuild 60 | 61 | 5. Or, start a live-compiled service for your compiled site for local development:: 62 | 63 | make livehtml 64 | 65 | Then view created site in your browser at `http://localhost:8000 `__ (follow the link in your console). 66 | 67 | 68 | 69 | Status 70 | ------ 71 | 72 | In beta use by CodeRefinery and active development. External users 73 | would be fine (but let us know so we know to keep things stable). 74 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: PyPI release 2 | 3 | # Make a PyPI release. This action: 4 | # - builds the release files 5 | # - checks the tag version matches the source version 6 | # - releases on PyPI using the action 7 | # 8 | # The first time, you have to upload the release yourself to get the 9 | # API key, to add to gh-secrets. 10 | # 11 | # I upload it with: 12 | # python setup.py sdist bdist_wheel 13 | # twine upload dist/* 14 | # 15 | # To configure the secrets, see steps here: 16 | # https://github.com/pypa/gh-action-pypi-publish 17 | # secret name= pypi_password 18 | 19 | 20 | # If MOD_NAME not defined, infer it from the current directory. If 21 | # inferred from the directory, '-' is replaced with '_'. This is used 22 | # when checking the version name. 23 | #env: 24 | # MOD_NAME: numpy 25 | 26 | on: 27 | # For Github release-based publishing 28 | #release: 29 | # types: [published] 30 | # For tag-based (instead of Github-specific release-based): 31 | push: 32 | tags: 33 | - '*' 34 | 35 | jobs: 36 | build: 37 | runs-on: ubuntu-latest 38 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 39 | permissions: 40 | contents: read 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | 45 | - name: Set up Python 46 | uses: actions/setup-python@v4 47 | #with: 48 | # python-version: 3.8 49 | 50 | - name: Install dependencies 51 | run: | 52 | python -m pip install --upgrade pip 53 | pip install -r requirements.txt 54 | pip install twine wheel flit 55 | 56 | - name: Build 57 | run: | 58 | flit build 59 | 60 | # Verify that the git tag has the same version as the python 61 | # project version. 62 | - uses: rkdarst/action-verify-python-version@main 63 | 64 | - uses: actions/upload-artifact@v4 65 | with: 66 | name: dist-directory 67 | path: dist/ 68 | 69 | 70 | upload: 71 | runs-on: ubuntu-latest 72 | needs: build 73 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 74 | permissions: 75 | id-token: write # for trusted publishing 76 | steps: 77 | 78 | - uses: actions/download-artifact@v4 79 | with: 80 | name: dist-directory 81 | path: dist/ 82 | 83 | - name: Publish on PyPI 84 | uses: pypa/gh-action-pypi-publish@release/v1 85 | #with: 86 | #user: __token__ 87 | #password: ${{ secrets.pypi_password }} 88 | #repository_url: https://test.pypi.org/legacy/ 89 | -------------------------------------------------------------------------------- /content/jupyter.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Jupyter notebook page\n", 8 | "This is a raw Jupyter notebook page, in `.ipynb` format." 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "## Usage" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "Basic code execution works:" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "!hostname\n", 32 | "print(sum(range(10)))" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "Directives work as well, since it is recursively executed by the same parser as normal Markdown files. Of course, Sphinx directives won't be interperted within Jupyter itself, but that is probably OK for development purposes:" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "```{challenge}\n", 47 | "\n", 48 | "This is a challenge block\n", 49 | "```" 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "metadata": {}, 55 | "source": [ 56 | "You can learn more about this at the [myst-nb docs](https://myst-nb.readthedocs.io/en/latest/use/execute.html). Most this applies equally to the `.md` files." 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "## Configuration\n", 64 | "\n", 65 | "The `nb_execution_mode` variable in conf.py controls execution. Currently, we don't set a default for this, which makes it `auto`. You can read more about this [upstream](https://myst-nb.readthedocs.io/en/latest/computation/execute.html). Possible values include:\n", 66 | "\n", 67 | "* `off`: don't execute\n", 68 | "* `auto`: exceute *only if there are missing outputs\n", 69 | "* `force`: always re-execute\n", 70 | "* `cache`: always execute, but cache the output. Don't re-execute if the input is unchanged." 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [] 79 | } 80 | ], 81 | "metadata": { 82 | "kernelspec": { 83 | "display_name": "Python 3", 84 | "language": "python", 85 | "name": "python3" 86 | }, 87 | "language_info": { 88 | "codemirror_mode": { 89 | "name": "ipython", 90 | "version": 3 91 | }, 92 | "file_extension": ".py", 93 | "mimetype": "text/x-python", 94 | "name": "python", 95 | "nbconvert_exporter": "python", 96 | "pygments_lexer": "ipython3", 97 | "version": "3.7.3" 98 | } 99 | }, 100 | "nbformat": 4, 101 | "nbformat_minor": 4 102 | } 103 | -------------------------------------------------------------------------------- /content/convert-old-lessons.md: -------------------------------------------------------------------------------- 1 | # Converting an old lesson 2 | 3 | **This was the old CodeRefinery procedure. It is now unmaintained 4 | but might be useful for someone. Let us know if you need it revived.** 5 | 6 | % highlight: console 7 | 8 | ## Convert a Jekyll lesson 9 | 10 | This brings in the necessary files 11 | 12 | Add the template lesson as a new remote: 13 | 14 | ``` 15 | git remote add s-l-t https://github.com/coderefinery/sphinx-lesson-template.git 16 | git fetch s-l-t 17 | ``` 18 | 19 | Check out some basic files into your working directory. Warning: if 20 | you add a `.github/workflows/sphinx.yml` file, even a push to a 21 | branch will **override github pages**: 22 | 23 | ``` 24 | git checkout s-l-t/main -- requirements.txt 25 | git checkout s-l-t/main -- .github/workflows/sphinx.yml 26 | ``` 27 | 28 | If you need more Sphinx files: 29 | 30 | ``` 31 | git checkout s-l-t/main -- content/conf.py 32 | git checkout s-l-t/main -- .gitignore Makefile make.bat 33 | ``` 34 | 35 | If you need the full content (only `index.rst` for now): 36 | 37 | ``` 38 | git checkout s-l-t/main -- content/ 39 | ``` 40 | 41 | (if jekyll conversion) Move content over: 42 | 43 | ``` 44 | git mv _episodes/* content/ 45 | ``` 46 | 47 | (if jekyll conversion) Copy stuff from `index.md` into `content/index.rst`. 48 | 49 | (if jekyll conversion) Remove old jekyll stuff: 50 | 51 | ``` 52 | git rm jekyll-common/ index.md _config.yml Gemfile .gitmodules 53 | ``` 54 | 55 | Set up github pages (first commit to trigger CI), see {doc}`installation`: 56 | 57 | ``` 58 | git checkout -b gh-pages origin/gh-pages 59 | git commit -m 'empty commit to trigger gh-pages' --allow-empty 60 | git push 61 | ``` 62 | 63 | Do all the rest of the fixing... all the bad, non-portable, 64 | non-relative markdown and so on. This is the hard part. Common 65 | problems: 66 | 67 | - Non-consecutive section headings 68 | - Multiple top-level section headings (there should be one top-level 69 | section heading that is the page title) 70 | - Weird relative links (most work though) 71 | 72 | You can also update your local view of the default branch: 73 | 74 | ``` 75 | git remote set-head origin --auto 76 | ``` 77 | 78 | ## Joint history 79 | 80 | This option joins the histories of the two repositories, so that you 81 | could merge from the template repository to keep your files up to 82 | date. **This may not currently work**, and also may not have any 83 | value (but is kept here for reference until later). 84 | 85 | Merge the two unrelated histories: 86 | 87 | ``` 88 | $ git remote add template https://github.com/coderefinery/sphinx-lesson-template 89 | $ git fetch template 90 | $ git merge template/main --allow-unrelated-histories 91 | # Resolve any possible merge conflicts 92 | $ git checkout --theirs .gitignore 93 | $ git checkout --ours LICENSE 94 | $ git add .gitignore LICENSE 95 | $ git commit -m 'merge sphinx-lesson history to this lesson' 96 | ``` 97 | 98 | Then proceed like the previous section shows. 99 | -------------------------------------------------------------------------------- /content/indexing.md: -------------------------------------------------------------------------------- 1 | # Indexing 2 | 3 | An index lets you efficiently look up any topic. In the age of 4 | full-text search, you are right to wonder what the point of indexes 5 | are. They could be seen as companion of cheatsheet: instead of 6 | searching and hoping you find the hright place, you can index the 7 | actual locations to which one would refer. 8 | 9 | As you might expect, there is nothing special to in sphinx-lesson 10 | about indexing: see the Sphinx documentation on {rst:dir}`index`. 11 | 12 | ```{index} ! Index 13 | ``` 14 | 15 | ## Basic concepts 16 | 17 | **Headings** are the terms which can be looked up in the index. When 18 | choosing headings, consider: 19 | 20 | - What is useful to a reader to locate 21 | - How would a reader look it up? For example, `commit` or 22 | `committing` is useful, but `how to commit` is not. Phrase it 23 | with the most important terms first (big-endian) 24 | - And index can have sub-entries. For example, under the entry 25 | `git`, there can be subentries for each git command, such as 26 | `commit`. 27 | 28 | ## Syntax 29 | 30 | :::{seealso} 31 | The Sphinx documentation on {rst:dir}`index`. 32 | ::: 33 | 34 | ```{highlight} rst 35 | ``` 36 | 37 | The `index` directive and role are the main ways to add index 38 | entries. The semicolon (`;`) character separates entries and 39 | subentries. 40 | 41 | ```{index} pair: index; ReST 42 | ``` 43 | 44 | ### Index a block with a directive 45 | 46 | MyST: 47 | 48 | ````md 49 | ```{index} commit; amend 50 | ``` 51 | 52 | ```{index} 53 | commit 54 | commit; mesage 55 | pair: commit; amend 56 | ``` 57 | ```` 58 | 59 | ReST: 60 | 61 | ``` 62 | .. index:: commit; amend 63 | ``` 64 | 65 | Or ReST, multiple: 66 | 67 | ``` 68 | .. index:: 69 | commit 70 | commit; message 71 | pair: commit; amend 72 | ``` 73 | 74 | 75 | ### Index a single word with the role 76 | 77 | ```{index} pair: index; MyST 78 | ``` 79 | 80 | MyST: 81 | 82 | ``` 83 | Simple entry: {index}`commit` 84 | Pair entry: {index}`loop variables ` 85 | ``` 86 | 87 | ```{index} index; single index; pair index; see index; seealso 88 | ``` 89 | 90 | ReST: 91 | 92 | ``` 93 | This sentence has an index entry for :index:`commit`. If you want the 94 | indexed term to be different, standard syntax applies such as 95 | :index:`loop variables `. 96 | ``` 97 | 98 | 99 | ### Styles of indexes 100 | 101 | - `TERM`, same as below 102 | - `single: TERM` (the default): create just a single entry 103 | - `pair: TERM; TERM`: create entries for `x; y` and `y; x` 104 | - `see: TOPIC; OTHER`: creates a "see other" entry for "topic". 105 | - `seealso: TOPIC; OTHER`: creates a "seealso" entry, like above 106 | 107 | ## Glossaries 108 | 109 | If you make a glossary using the {rst:dir}`glossary directive 110 | `, the terms automatically get added to the index 111 | 112 | ## See also 113 | 114 | - {rst:dir}`index` directive 115 | - Sphinx {rst:dir}`glossary` 116 | -------------------------------------------------------------------------------- /content/index.md: -------------------------------------------------------------------------------- 1 | # sphinx-lesson: structured lessons with Sphinx 2 | 3 | :::{seealso} 4 | See a real demo lesson at 5 | . 6 | ::: 7 | 8 | sphinx-lesson is a set of Sphinx extensions and themes for creating 9 | interactive, hands-on lessons. It was originally made to replace the 10 | CodeRefinery jekyll themes, but is designed to be used by others. 11 | 12 | The broad idea is that making teaching materials should be quite 13 | similar to good software documentation. Sphinx-lesson adds a model 14 | and a few nice extras to bring them closer together. 15 | 16 | As the name says, it is based on the [Sphinx documentation generator](https://www.sphinx-doc.org/). It is also inspired by and based on 17 | [jupyter-book](https://jupyterbook.org/), but both is jupyter-book 18 | and isn't. It *is* because jupyter-book is based on Sphinx and 19 | modular, we reuse all of those same Sphinx extensions which 20 | jupyter-book has made. It *isn't* jupyter-book because we configure 21 | Sphinx directly, instead of wrapping it through jupyter-book 22 | configuration and building. Thus, we get full control and high 23 | compatibility. 24 | 25 | Features: 26 | 27 | - Separate content and presentation: easy to adjust theme or control 28 | the parts independently. 29 | - Based on jupyter-book, cross-compatible. 30 | - Built with Sphinx, providing a structured, controlled output. 31 | - Distributed as Python pip packages 32 | - Markdown and ReStructured equally supported as input formats (yes, including all 33 | directives). 34 | - Jupyter notebooks as an input format. Can execute code (in jupyter 35 | and other formats, too) 36 | - Transparent transformation of jekyll-style markdown styles into 37 | CommonMark with directives (mainly of use for old CodeRefinery lessons) 38 | - Also renders with sphinx-book-theme (theme of jupyterbook) 39 | ([preview](https://coderefinery.github.io/sphinx-lesson/branch/sphinx-book-theme/)) 40 | and other Sphinx themes. 41 | 42 | This is in heavy internal use, and about ready for other users who are 43 | interested. 44 | 45 | :::{prereq} 46 | - If you know Sphinx, it helps some. If not, it's easy to copy 47 | - Markdown or ReStructured text 48 | - Hosting is usually by github-pages 49 | ::: 50 | 51 | ```{toctree} 52 | :caption: Getting started 53 | :maxdepth: 1 54 | 55 | getting-started 56 | installation 57 | contributing-to-a-lesson 58 | building 59 | changelog 60 | ``` 61 | 62 | ```{toctree} 63 | :caption: Basic syntax 64 | :maxdepth: 1 65 | 66 | md-and-rst 67 | toctree 68 | directives 69 | figures 70 | ``` 71 | 72 | ```{toctree} 73 | :caption: Examples 74 | :maxdepth: 1 75 | 76 | sample-episode-myst 77 | sample-episode-rst 78 | ``` 79 | 80 | ```{toctree} 81 | :caption: Advanced features 82 | :maxdepth: 1 83 | 84 | intersphinx 85 | md-transforms 86 | jupyter 87 | executing-code 88 | substitutions-replacements 89 | gh-action 90 | presentation-mode 91 | indexing 92 | exercise-list 93 | convert-old-lessons 94 | ``` 95 | 96 | ```{toctree} 97 | :caption: Extras 98 | :maxdepth: 1 99 | 100 | cheatsheet 101 | ``` 102 | 103 | - {ref}`genindex` 104 | - {ref}`search` 105 | -------------------------------------------------------------------------------- /content/intersphinx.md: -------------------------------------------------------------------------------- 1 | # Intersphinx: easy linking 2 | 3 | There is a common problem: you want to link to documentation in other 4 | sites, for example the documentation of `list.sort`. Isn't it nice 5 | to have a structured way to do this so you don't have to a) look up a 6 | URL yourself b) risk having links break? Well, what do you know, 7 | Sphinx has a native solution for this: {py:mod}`Intersphinx 8 | `. 9 | 10 | ## Enable the extension 11 | 12 | It's built into Sphinx, and in the sphinx-lesson-template `conf.py` but 13 | commented out. Enable it: 14 | 15 | ```python 16 | extensions.append('sphinx.ext.intersphinx') 17 | intersphinx_mapping = { 18 | 'python': ('https://docs.python.org/3', None), 19 | } 20 | ``` 21 | 22 | Configuration details and how to link to other sites are found at 23 | {py:mod}`the docs for intersphinx `. 24 | For most Sphinx-documented projects, use the URL of the documentation 25 | base. See "Usage" below for how to verify the URLs. 26 | 27 | ## Usage 28 | 29 | Just like `:doc:` is a structured way to link to other documents, 30 | there are other **domains** of links, such as `:py:class:`, 31 | `:py:meth:`, and so on. So we can link to documentation of a class 32 | or method like this: 33 | 34 | :::{admonition} Rendered (note the links) 35 | The {py:class}`list` class {py:meth}`sort ` method. 36 | ::: 37 | 38 | ```rst 39 | # Restructured Text 40 | The :py:class:`list` class :py:meth:`sort ` method. 41 | ``` 42 | 43 | ```md 44 | # MyST markdown 45 | The {py:class}`list` class {py:meth}`sort ` method. 46 | ``` 47 | 48 | Note that this is structured information, and thus has no concept in 49 | Markdown, only MyST "markdown". This is, in fact, a major reason why 50 | plain markdown is not that great for structured documentation. 51 | 52 | ## Available linking domains and roles 53 | 54 | Of course, the domains are extendable. Presumably, when you use 55 | sphinx-lesson, you will be referring to other things. The most 56 | common roles in the Python domain are: 57 | 58 | - `:py:mod:`: modules, e.g. {py:mod}`multiprocessing` 59 | - `:py:func:`: modules, e.g. {py:func}`itertools.combinations` 60 | - `:py:class:`: modules, e.g. {py:class}`list` 61 | - `:py:meth:`: modules, e.g. {py:meth}`list.sort` 62 | - `:py:attr:`: modules, e.g. {py:attr}`re.Pattern.groups` 63 | - `:py:data:`: modules, e.g. {py:data}`datetime.MINYEAR` 64 | - Also `:py:exc:`, `:py:data:`, `:py:obj:`, `::`, `::` 65 | - There are also built-in domains for C, C++, JavaScript (see 66 | [the info on Sphinx domains](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html) for what the roles are). 67 | Others are added by Sphinx extensions. 68 | 69 | You can list all available reference targets at some doc using a 70 | command line command. You can get the URL from the conf.py file (and 71 | use this to verify URLs before you put it in the conf.py file): 72 | 73 | ```shell 74 | # Note we need to append `objects.inv`: 75 | python -m sphinx.ext.intersphinx https://docs.python.org/3/objects.inv 76 | # In conf.py: 'python': ('https://docs.python.org/3', None), 77 | ``` 78 | 79 | You usually use the fully qualified name of an object, for example 80 | `matplotlib.pyplot.plot`. In Python this is usually pretty obvious, 81 | due to clear namespacing. You'll have to look at other languages 82 | yourself. 83 | 84 | ## See also 85 | 86 | - [Sphinx: domains](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html) - how to 87 | document classes/functions to be referrable this way, and link to them. 88 | - {py:mod}`Intersphinx `. 89 | -------------------------------------------------------------------------------- /sphinx_lesson/_static/sphinx_lesson.css: -------------------------------------------------------------------------------- 1 | /* sphinx_lesson.css 2 | * https://webaim.org/resources/contrastchecker/?fcolor=00000&bcolor=FCE762 3 | * */ 4 | :root { 5 | --sphinx-lesson-selection-bg-color: #fce762; 6 | --sphinx-lesson-selection-fg-color: #000000; 7 | } 8 | 9 | /* https://webaim.org/resources/contrastchecker/?fcolor=FFFFFF&bcolor=745315 10 | * when dark theme is selected the some themes use this attirbute 11 | */ 12 | html[data-theme='dark'], body[data-theme='dark'] { 13 | --sphinx-lesson-selection-bg-color: #745315; 14 | --sphinx-lesson-selection-fg-color: #ffffff; 15 | } 16 | 17 | /* when browser/system theme is dark and no theme is selected */ 18 | @media (prefers-color-scheme: dark) { 19 | html[data-theme='auto'], body[data-theme='auto'] { 20 | --sphinx-lesson-selection-bg-color: #745315; 21 | --sphinx-lesson-selection-fg-color: #ffffff; 22 | } 23 | } 24 | 25 | body.wy-body-for-nav img.with-border { 26 | border: 2px solid; 27 | } 28 | 29 | .rst-content .admonition-no-content { 30 | padding-bottom: 0px; 31 | } 32 | 33 | .rst-content .demo > .admonition-title::before { 34 | content: "\01F440"; /* Eyes */ } 35 | .rst-content .type-along > .admonition-title::before { 36 | content: "\02328\0FE0F"; /* Keyboard */ } 37 | .rst-content .exercise > .admonition-title::before { 38 | content: "\0270D\0FE0F"; /* Hand */ } 39 | .rst-content .solution > .admonition-title::before { 40 | content: "\02714\0FE0E"; /* Check mark */ } 41 | .rst-content .homework > .admonition-title::before { 42 | content: "\01F4DD"; /* Memo */ } 43 | .rst-content .discussion > .admonition-title::before { 44 | content: "\01F4AC"; /* Speech balloon */ } 45 | .rst-content .questions > .admonition-title::before { 46 | content: "\02753\0FE0E"; /* Question mark */ } 47 | .rst-content .prerequisites > .admonition-title::before { 48 | content: "\02699"; /* Gear */ } 49 | .rst-content .seealso > .admonition-title::before { 50 | content: "\027A1\0FE0E"; /* Question mark */ } 51 | 52 | 53 | /* instructor-note */ 54 | .rst-content .instructor-note { 55 | background: #e7e7e7; 56 | } 57 | .rst-content .instructor-note > .admonition-title { 58 | background: #6a6a6a; 59 | } 60 | .rst-content .instructor-note > .admonition-title::before { 61 | content: ""; 62 | } 63 | 64 | 65 | /* sphinx_toggle_button, make the font white */ 66 | .rst-content .toggle.admonition button.toggle-button { 67 | color: white; 68 | } 69 | 70 | /* sphinx-togglebutton, remove underflow when toggled to hidden mode */ 71 | .rst-content .admonition.toggle-hidden { 72 | padding-bottom: 0px; 73 | } 74 | 75 | /* selection / highlight colour uses a yellow background and a black text */ 76 | /*** Works on common browsers ***/ 77 | ::selection { 78 | background-color: var(--sphinx-lesson-selection-bg-color); 79 | color: var(--sphinx-lesson-selection-fg-color); 80 | } 81 | 82 | /*** Mozilla based browsers ***/ 83 | ::-moz-selection { 84 | background-color: var(--sphinx-lesson-selection-bg-color); 85 | color: var(--sphinx-lesson-selection-fg-color); 86 | } 87 | 88 | /***For Other Browsers ***/ 89 | ::-o-selection { 90 | background-color: var(--sphinx-lesson-selection-bg-color); 91 | color: var(--sphinx-lesson-selection-fg-color); 92 | } 93 | 94 | ::-ms-selection { 95 | background-color: var(--sphinx-lesson-selection-bg-color); 96 | color: var(--sphinx-lesson-selection-fg-color); 97 | } 98 | 99 | /*** For Webkit ***/ 100 | ::-webkit-selection { 101 | background-color: var(--sphinx-lesson-selection-bg-color); 102 | color: var(--sphinx-lesson-selection-fg-color); 103 | } 104 | -------------------------------------------------------------------------------- /content/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Sphinx-lesson' 21 | copyright = '2020-2024, CodeRefinery' 22 | author = 'CodeRefinery' 23 | 24 | # roundabout way to get version. "import sphinx_lesson" would be easier, but 25 | # then it becomes harder to have the same github action for this repo and 26 | # the lessons themselves. 27 | version_ns = { } 28 | exec(open('../sphinx_lesson/_version.py').read(), version_ns) 29 | version = version_ns['__version__'] 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | import sys 37 | sys.path.append('.') 38 | extensions = [ 39 | # githubpages just adds a .nojekyll file 40 | 'sphinx.ext.githubpages', 41 | # myst_parser is not needed, because myst_nb replaces and conflicts with it 42 | # (provides all functionality and more). But, myst_parser has fewer 43 | # dependencies so could be used instead. 44 | #'myst_parser', 45 | 'sphinx_lesson', 46 | #'myst_nb', # now done as part of sphinx_lesson 47 | 'sphinx_rtd_theme_ext_color_contrast', 48 | ] 49 | 50 | # Sphinx-lesson config: 51 | sphinx_lesson_transform_html_img = False 52 | 53 | # Settings for myst_nb: 54 | # https://myst-nb.readthedocs.io/en/latest/computation/execute.html#notebook-execution-modes 55 | #nb_execution_mode = "off" 56 | #nb_execution_mode = "auto" # *only* execute if at least one output is missing. 57 | #nb_execution_mode = "force" 58 | nb_execution_mode = "cache" 59 | 60 | # https://myst-parser.readthedocs.io/en/latest/syntax/optional.html 61 | myst_enable_extensions = [ 62 | "colon_fence", 63 | ] 64 | 65 | # Add any paths that contain templates here, relative to this directory. 66 | templates_path = ['_templates'] 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This pattern also affects html_static_path and html_extra_path. 71 | exclude_patterns = ['README*', '_build', 'Thumbs.db', '.DS_Store', 72 | 'jupyter_execute', '*venv*'] 73 | 74 | 75 | # -- Options for HTML output ------------------------------------------------- 76 | 77 | # The theme to use for HTML and HTML Help pages. See the documentation for 78 | # a list of builtin themes. 79 | # 80 | html_theme = 'sphinx_rtd_theme' 81 | 82 | # Add any paths that contain custom static files (such as style sheets) here, 83 | # relative to this directory. They are copied after the builtin static files, 84 | # so a file named "default.css" will overwrite the builtin "default.css". 85 | #html_static_path = ['_static'] 86 | 87 | # favicon location 88 | html_favicon = 'favicon.ico' 89 | 90 | # Github link in the theme 91 | html_context = { 92 | 'display_github': True, 93 | 'github_user': 'coderefinery', 94 | 'github_repo': 'sphinx-lesson', 95 | 'github_version': 'master', 96 | 'conf_py_path': '/content/', 97 | } 98 | 99 | # Intersphinx mapping. For example, with this you can use 100 | # :py:mod:`multiprocessing` to link straight to the Python docs of that module. 101 | # List all available references: 102 | # python -msphinx.ext.intersphinx https://docs.python.org/3/objects.inv 103 | extensions.append('sphinx.ext.intersphinx') 104 | intersphinx_mapping = { 105 | 'python': ('https://docs.python.org/3', None), 106 | 'sphinx': ('https://www.sphinx-doc.org/', None), 107 | # #'numpy': ('https://numpy.org/doc/stable/', None), 108 | # #'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), 109 | # #'pandas': ('https://pandas.pydata.org/docs/', None), 110 | # #'matplotlib': ('https://matplotlib.org/', None), 111 | # 'seaborn': ('https://seaborn.pydata.org/', None), 112 | } 113 | -------------------------------------------------------------------------------- /content/exercise-list.md: -------------------------------------------------------------------------------- 1 | # Exercise list 2 | 3 | The `exercise-list` directive inserts a list of all exercise and 4 | solution {doc}`directives ` (and maybe more). This can be 5 | useful as an overall summary of the entire lesson flow, onboarding new 6 | instructors and helpers, and more. 7 | 8 | ## Usage 9 | 10 | MyST: 11 | 12 | ```` 13 | ```{exerciselist}``` 14 | ```` 15 | 16 | ReST: 17 | 18 | ``` 19 | .. exerciselist:: 20 | ``` 21 | 22 | One can give the optional directive arguments to specify lists of 23 | admonition classes to include (default: `exercise`, `solution`, 24 | `exerciselist-include`) or exclude (default: 25 | `exerciselist-exclude`) if you want to (any {doc}`directives 26 | ` which match any `include`, and do not match any 27 | `exclude` are included). Specify the options this way (ReST): 28 | 29 | MyST: 30 | 31 | ````md 32 | ```{exerciselist} 33 | :include: exercise solution instructor-note 34 | :exclude: exclude-this 35 | ``` 36 | 37 | :::{exercise} Exercise title 38 | :class: exclude-this 39 | 40 | Exercise content 41 | ::: 42 | ```` 43 | 44 | 45 | ReST: 46 | 47 | ```rst 48 | .. exerciselist:: 49 | :include: exercise solution instructor-note 50 | :exclude: exclude-this 51 | 52 | .. exercise:: Exercise title 53 | :class: exclude-this 54 | 55 | Exercise content 56 | ``` 57 | 58 | This feature is new as of early 2022, there may be possible problems 59 | in it still - please report. Currently, only sphinx-lesson 60 | admonitions can be included due to technical considerations (see 61 | source for hint on fixing). 62 | 63 | (exerciselist-recommendations)= 64 | ## Recommendations to make a useful list 65 | 66 | - Context is important! Give your exercises a name other than the 67 | default of "Exercise", so that someone quickly scanning the exercise 68 | list can follow the overall flow. 69 | 70 | - Making good summaries is really an important skill for organizing 71 | anything - give this the attention it needs. 72 | - Think of an exercise leader or helper coming to help someone, seeing 73 | the exercise, and needing to help someone: not just what to do, 74 | but what the core lesson and task is, so that they can focus on 75 | giving the right help (and telling the learners what they don't 76 | need to worry about). 77 | - Context can be both in the exercise title and in the exercise body 78 | itself. 79 | 80 | - Name the exercises well. Best is to think of it like a version 81 | control commit: an imperative sentence stating what the person will 82 | do in the exercise. For example: 83 | 84 | ``` 85 | Make your first git commit 86 | Resolve the conflict 87 | ``` 88 | 89 | - In the title, include any other important information (see below): 90 | 91 | ``` 92 | Create a setup.py file for your package (15 min) 93 | (optional) Install your package in editable mode using ``pip install -e`` (5 min) 94 | (advanced) Also create packaging using pyproject.toml and compare (20 min) 95 | ``` 96 | 97 | - Consider giving your exercises permanent identifiers. They are 98 | intentionally not 99 | auto-numbered yet (what happens when more exercises are 100 | added/removed?), but if you give them an ID, they will be findable 101 | even later. Suggestion is `Episodetopic-N`: 102 | 103 | ``` 104 | Basic-1: Verify git is installed 105 | Basic-2: Initialize the repository 106 | Conflicts-2: Create a new branch for the other commit. 107 | Internals-1: (advanced): Inspect individual objects with ``git cat-file`` 108 | ``` 109 | 110 | - It could include not just what you do, but a bit about why you are 111 | doing it and what you are learning. 112 | 113 | - The list includes only `exercise`, `type-along`, and `solution`. For 114 | backwards compatibility, `challenge` is also included. 115 | 116 | - Optional or advanced exercises should clearly state it in the 117 | exercise title, since people will browse the list separate from the 118 | main lesson material. 119 | 120 | - Try to minimize use of `:include:` and `:exclude:` and use the 121 | defaults and adjust your directives to match sphinx-lesson 122 | semantics. Excess use of this may over-optimize for particular 123 | workshops 124 | 125 | ## Example 126 | 127 | **This section contains the exercise list of sphinx-lesson. Since 128 | sphinx-lesson has many examples of exercises, the list below is 129 | confusing and doesn't make a lot of sense. You can see a better 130 | example at [git-intro's exercise 131 | list](https://coderefinery.github.io/git-intro/exercises/).** 132 | 133 | **Example begins:** 134 | 135 | :::{exerciselist} 136 | ::: 137 | -------------------------------------------------------------------------------- /content/contributing-to-a-lesson.md: -------------------------------------------------------------------------------- 1 | # Quickstart: Contributing to a lesson 2 | 3 | If you are at this page, you might want to quickly contribute to some 4 | existing material using the `sphinx-lesson` format. Luckily, this 5 | is fairly easy: 6 | 7 | - Get the source 8 | - Edit the material in the `content/` directory 9 | - (optional) Set up the Python environment and preview 10 | - Send your contribution 11 | 12 | In summary, each lesson is like a Python project, with the lesson 13 | content as its documentation in the `content/` directory (and no 14 | Python code). Everything is fairly standard: it uses the Sphinx 15 | documentation system, which is a popular, extendable tool. We have 16 | only minor extensions to make it suitable to lessons. 17 | 18 | Instead of going through this process, you can also open an issue 19 | instead with your proposed change, and let someone else add it. 20 | 21 | ```{highlight} console 22 | ``` 23 | 24 | ## Get the lesson material 25 | 26 | You need to consult with the lesson you would like to edit. If this 27 | is using the `git` version control system on Github, you could clone 28 | it like this: 29 | 30 | ``` 31 | $ git clone git://github.com/ORGANIZATION/LESSON.git 32 | ``` 33 | 34 | [CodeRefinery's git-intro lesson](https://coderefinery.github.io/git-intro/) explains more. 35 | 36 | ## Edit the material 37 | 38 | The material is in the `content/` directory. Depending on the 39 | lesson, in may be in MyST Markdown, ReStructured Text, or Jupyter 40 | notebooks. 41 | 42 | ### ReStructured Text and MyST Markdown 43 | 44 | You will probably copy existing examples, but you can also see 45 | {doc}`our quick guide `. The main thing to note is that 46 | this is not unstructured Markdown, but there are particular 47 | (non-display) **directives** and **roles** to tag blocks and inline 48 | text. (In fact, "markdown" is a broad concept and everyone uses some 49 | different extensions of it). 50 | 51 | - {doc}`sphinx-lesson directives for markup ` 52 | - {doc}`md-and-rst` 53 | - [MyST reference](https://myst-parser.readthedocs.io/en/latest/using/syntax.html) 54 | - {ref}`ReStructured Text reference ` 55 | 56 | *Do not worry about getting syntax right*. Send your improvement, and 57 | editing is easy and you will learn something. 58 | 59 | ### Jupyter notebooks 60 | 61 | Jupyter notebooks are a common format for computational narratives, 62 | and can be natively used with Sphinx via [myst-nb](https://myst-nb.readthedocs.io/). Note that you should use MyST 63 | Markdown directives and roles (see previous section) in the notebook 64 | to give structure to the material. 65 | 66 | Again, *do not worry about getting the syntax right*. This is the 67 | least important part of things. 68 | 69 | ## Build and test locally 70 | 71 | Generic: The `requirements.txt` file includes all Python dependencies 72 | to build the lesson. The lesson can be built with `sphinx-build -M 73 | html content/ _build`, or `make html` if you have Make installed. 74 | 75 | Or in more detail: 76 | 77 | Create a virtual environment to install the requirements (a conda 78 | environment would work just as well): 79 | 80 | ``` 81 | $ python3 -m venv venv/ 82 | $ source venv/bin/activate 83 | ``` 84 | 85 | :::{note} 86 | if `python3 -m venv venv/` does not work, try with `python -m venv venv/` 87 | ::: 88 | 89 | Then upgrade pip inside the virtual environment and install dependencies (it is recommended that conda base environment is deactivated): 90 | 91 | ``` 92 | $ pip install --upgrade pip 93 | $ pip install -r requirements.txt 94 | ``` 95 | 96 | You can build it using either of these commands: 97 | 98 | ``` 99 | $ sphinx-build -M html content/ _build 100 | $ make html # if you have make installed 101 | ``` 102 | 103 | And then view it with your web browser. Remove the `_build` 104 | directory to force a clean rebuild (or `make clean`). 105 | 106 | Or you can use the **Sphinx autobuilder**, which will start a process 107 | that rebuilds it on every change, and starts a web server to view it. 108 | It will tell you how to access the server: 109 | 110 | ``` 111 | $ sphinx-autobuild content/ _build/ 112 | ... 113 | [I ...] Serving on http://127.0.0.1:8000 114 | ``` 115 | 116 | ## Sending your changes back 117 | 118 | This depends on the project, but can be done using Github pull 119 | requests. [CodeRefinery's git-collaborative lesson](https://coderefinery.github.io/git-collaborative/) goes into 120 | details about pull requests. 121 | 122 | ## Other things to keep in mind 123 | 124 | - Make sure that you have rights to submit your change. In general, 125 | if you reuse anything else that already exists, explain this in your 126 | pull request. 127 | - *Content and ideas are more important than markup*. Don't worry 128 | about doing something wrong, that is why we have review! 129 | - Many different people use the lessons. Ask before doing things that 130 | make the lesson too specific to your use case. 131 | -------------------------------------------------------------------------------- /content/directives.md: -------------------------------------------------------------------------------- 1 | # Directives 2 | 3 | :::{seealso} 4 | In [Sphinx/Docutils, directives have a different meaning](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html). 5 | Directives in sphinx-lesson are actually the special case of the 6 | generic directive class called **admonitions**. 7 | ::: 8 | 9 | **Directives** are used to set off a certain block of text. They can 10 | be used as an aside or block (e.g. `exercise`, `instructor-note`). 11 | If the content of the box would be long (e.g. an entire episode is a 12 | `type-along`, or an entire section is an `exercise`), you could use 13 | the `type-along` directive to introduce the start of it but not put 14 | all content in that directive. 15 | 16 | Sphinx and docutils calls these type of directives **admonitions** 17 | (and "directive" is a more general concept) 18 | 19 | ## How to use 20 | 21 | Example of `exercise`: 22 | 23 | :::{exercise} 24 | Some body text 25 | ::: 26 | 27 | :::{exercise} Custom title 28 | Some body text 29 | ::: 30 | 31 | :::{exercise} 32 | ::: 33 | 34 | ````{list-table} 35 | * * Markdown 36 | 37 | ```` 38 | ```{exercise} 39 | 40 | Some body text 41 | ``` 42 | ```` 43 | 44 | ```` 45 | ```{exercise} Custom title 46 | 47 | Some body text 48 | ``` 49 | ```` 50 | 51 | ```` 52 | ```{exercise} 53 | ``` 54 | ```` 55 | 56 | 57 | * ReST: 58 | 59 | ```` 60 | .. exercise:: 61 | 62 | Some body text 63 | ```` 64 | 65 | ```` 66 | .. exercise:: Custom title 67 | 68 | Some body text 69 | ```` 70 | 71 | ```` 72 | .. exercise:: 73 | ```` 74 | ```` 75 | 76 | You notice these directives can have optional a custom title. This is 77 | an addition from regular Sphinx admonitions, and is *not* usable in 78 | regular Sphinx admonition directives. Also, unlike regular Sphinx 79 | admonitions, the content in our directives is optional, if you want to 80 | use it as a simple section header. 81 | 82 | The `solution` directive begins collapsed (via [sphinx-togglebutton](https://github.com/executablebooks/sphinx-togglebutton)): 83 | 84 | :::{solution} 85 | This is a solution 86 | ::: 87 | 88 | Directives are implemented in the Python package 89 | `sphinx_lesson.directives` and can be used independently of the rest 90 | of `sphinx-lesson`. 91 | 92 | ## List 93 | 94 | Many directives are available. 95 | 96 | The following directives are used for exercises/solutions/homework. 97 | They all render as green ("important" class: 98 | 99 | - `demo` 100 | - `exercise` 101 | - `solution` (toggleable, default hidden) 102 | - `type-along` (most of the lessons are hands-on, so this is a bit 103 | redundant. Use this when emphasizing a certain section is "follow 104 | along", as opposed to watching or working alone.) 105 | - `homework` 106 | 107 | Other miscellaneous directives: 108 | 109 | - `discussion` 110 | - `instructor-note` 111 | - `prerequisites` 112 | 113 | The following are Sphinx default directives that may be especially 114 | useful to lessons. These do *not* accept an optional Title argument, 115 | the title is hard-coded. 116 | 117 | - `see-also` 118 | - `note` 119 | - `important` (green) 120 | - `warning` (yellow) 121 | - `danger` (red 122 | 123 | The following are available, for compatibility with Carpentries styles: 124 | 125 | - `callout` 126 | - `challenge` (alias to `exercise`) 127 | - `checklist` 128 | - `keypoints` (bottom of lesson) 129 | - `objectives` (top of lesson) 130 | - `prereq` (use `prerequisites` instead) 131 | - `solution` (begins collapsed) 132 | - `testimonial` 133 | - `output` (use code blocks instead) 134 | - `questions` (top of lesson) 135 | 136 | ## Gallery 137 | 138 | This is a demonstration of all major directives 139 | 140 | ### sphinx-lesson 141 | 142 | :::{demo} 143 | demo 144 | ::: 145 | 146 | :::{demo} 147 | ::: 148 | 149 | :::{type-along} 150 | type-along 151 | ::: 152 | 153 | :::{type-along} 154 | ::: 155 | 156 | :::{exercise} 157 | exercise 158 | ::: 159 | 160 | :::{solution} 161 | solution 162 | ::: 163 | 164 | :::{homework} 165 | homework 166 | ::: 167 | 168 | :::{discussion} 169 | discussion 170 | ::: 171 | 172 | :::{instructor-note} 173 | instructor-note 174 | ::: 175 | 176 | :::{prerequisites} 177 | prerequisites 178 | ::: 179 | 180 | ### Sphinx default 181 | 182 | :::{note} 183 | note 184 | ::: 185 | 186 | :::{important} 187 | important 188 | ::: 189 | 190 | :::{seealso} 191 | seealso 192 | ::: 193 | 194 | :::{warning} 195 | warning 196 | ::: 197 | 198 | :::{danger} 199 | danger 200 | ::: 201 | 202 | ### Carpentries holdovers 203 | 204 | :::{questions} 205 | questions 206 | ::: 207 | 208 | :::{objectives} 209 | objectives 210 | ::: 211 | 212 | :::{keypoints} 213 | keypoints 214 | ::: 215 | 216 | :::{callout} 217 | callout 218 | ::: 219 | 220 | :::{challenge} 221 | challenge 222 | ::: 223 | 224 | :::{checklist} 225 | checklist 226 | ::: 227 | 228 | :::{prereq} 229 | prereq 230 | ::: 231 | 232 | :::{testimonial} 233 | testimonial 234 | ::: 235 | 236 | :::{output} 237 | output 238 | ::: 239 | -------------------------------------------------------------------------------- /content/md-and-rst.md: -------------------------------------------------------------------------------- 1 | # Markdown and ReST 2 | 3 | Sites can be written in Markdown on ReStructured Text. Actually, in 4 | theory any format that has a Sphinx parser could be used, however you 5 | will be slightly limited without directive support. 6 | 7 | The most important thing to note is that: To make a structured lesson, 8 | one needs to write in something a bit more structured than HTML. 9 | 10 | ## Markdown 11 | 12 | Markdown is the most common syntax for CodeRefinery lessons. It's not 13 | raw markdown but the MyST flavor, which has *much* more structured 14 | directives than plain markdown. These come straight from Sphinx and 15 | what we use to 16 | 17 | [MyST syntax reference](https://myst-parser.readthedocs.io/en/latest/using/syntax.html) 18 | 19 | 20 | :::{note} 21 | What is Markdown? Markdown isn't a single language. Its native form 22 | is a simple syntax for HTML, and isn't very structured. There are many different 23 | flavors, some of which add extra syntax which gets it closer to 24 | enough, but for our purposes these are different enough that they 25 | should count as different languages (as similar as "markdown" and 26 | ReST). Since the Markdown creator says that [Markdown shouldn't 27 | evolve or be strictly defined](https://en.wikipedia.org/wiki/Markdown#CommonMark), Markdown is 28 | essentially a dead syntax: we should always specific which living 29 | sub-syntax you are referring to. 30 | 31 | sphinx-lesson uses the [MyST-parser] (MarkedlY Structured Text), 32 | which is both completely compatible with CommonMark, and also supports 33 | *all ReStructured Text directives*, unlike most other non-ReST Sphinx 34 | parsers. Thus, we finally have a way to write equivalent ReST and 35 | Markdown without any compromises (though other CommonMark parsers 36 | aren't expected to know Sphinx directives). 37 | ::: 38 | 39 | ## ReStructured Text 40 | 41 | ReStructured Text has native support for roles and directives, which 42 | makes it a more structured language such as LaTeX, rather than HTML. 43 | It came before Sphinx, but Sphinx made it even more popular. 44 | 45 | [ReST reference (from Sphinx)](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html) 46 | 47 | ## MD and ReST syntax 48 | 49 | This is a brief comparison of basic syntax: 50 | 51 | ReST syntax (Sphinx has a good [restructured text primer](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html): 52 | 53 | MyST markdown syntax: 54 | 55 | ````md 56 | *italic* 57 | **bold** 58 | `literal` 59 | # Heading 60 | 61 | [inline link](https://example.com) 62 | [link to page](relative-page-path) 63 | 64 | structured links: 65 | {doc}`link to page ` 66 | {ref}`page anchor link ` 67 | {py:mod}`intersphinx link ` 68 | 69 | 70 | ``` 71 | code block 72 | ``` 73 | ```` 74 | 75 | ```rst 76 | *italic* 77 | **bold** 78 | ``literal`` 79 | 80 | Heading 81 | ------- 82 | 83 | `inline link `__ 84 | `out-of-line link `__ 85 | 86 | .. _example: https://example.com 87 | 88 | :doc:`page-filename` 89 | :ref:`ref-name` 90 | :py:mod:`multiprocessing` 91 | 92 | :doc:`link to page ` 93 | :ref:`page anchor link ` 94 | :py:mod:`intersphinx link ` 95 | 96 | 97 | :: 98 | 99 | code block that is standalone (two 100 | colons before it and indented) 101 | 102 | Code block after paragraph:: 103 | 104 | The paragraph will end with 105 | a single colon. 106 | ``` 107 | 108 | The most interesting difference is the use of single backquote for 109 | literals in Markdown, and double in ReST. This is because ReST uses 110 | single quotes for *roles* - notice how there is a dedicated syntax for 111 | inter-page links, references, and so on (it can be configured to 112 | "figure it out" if you want). This is very important for 113 | things like verifying referential integrity of all of our pages. But 114 | this is configurable with [default_role](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-default_role): 115 | set to `any` to automatically detect documents/references/anthing, 116 | or `literal` to automatically be the same as literal text. 117 | 118 | ## Directives 119 | 120 | A core part of any Sphinx site is the directives: this provides 121 | structured parsing for blocks of text. For example, we have an 122 | `exercise` directive, which formats a text block into an exercise 123 | callout. This is not just a CSS class, it can do anything during the 124 | build phase (but in practice we don't do such complex things). 125 | 126 | ### MyST directives 127 | 128 | MyST-parser directives are done like this: 129 | 130 | ````md 131 | :::{exercise} 132 | :option: value 133 | 134 | content 135 | ::: 136 | ```` 137 | 138 | 139 | ### ReST directives 140 | 141 | ReST directives are done like this: 142 | 143 | ```rst 144 | .. exercise:: Optional title, some default otherwise 145 | :option: value 146 | 147 | This is the body 148 | 149 | You can put *arbitrary syntax* here. 150 | ``` 151 | 152 | 153 | ## Roles 154 | 155 | Roles are for inline text elements. A lot like directives, they can 156 | be as simple as styling or do arbitrary transformations in Python. 157 | 158 | ### ReST roles 159 | 160 | Like this: 161 | 162 | ```rst 163 | :rolename:`interpreted text` 164 | ``` 165 | 166 | ### MyST roles 167 | 168 | Like this: 169 | 170 | ```md 171 | {rolename}`interpreted text` 172 | ``` 173 | 174 | [myst-parser]: https://github.com/executablebooks/myst-parser 175 | -------------------------------------------------------------------------------- /sphinx_lesson/md_transforms.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=E701 2 | 3 | import os 4 | import re 5 | import textwrap 6 | 7 | from docutils import nodes 8 | from docutils.parsers.rst.directives.admonitions \ 9 | import Admonition as AdmonitionDirective 10 | from sphinx.util.docutils import SphinxDirective 11 | from sphinx.util.logging import getLogger 12 | 13 | from . import __version__ 14 | LOG = getLogger(__name__) 15 | 16 | 17 | 18 | def transform_code_fences(app, docname, source): 19 | """Transform a code fence when read. 20 | 21 | Transform this:: 22 | 23 | ``` 24 | blah 25 | ``` 26 | {: output} 27 | 28 | into this:: 29 | 30 | ```{output} 31 | blah 32 | ``` 33 | 34 | """ 35 | if not app.config.sphinx_lesson_transform_code_fences: 36 | return 37 | LOG.debug('transform_code_fences: beginning %s', docname) 38 | content = source[0] 39 | LOG.debug(content) 40 | code_fence_re = re.compile( 41 | r'^(?P```.*?)(?P\n.*?)(?P^``` ?\n)(?:\{:(?P[^}\n]+)\}$)?', 42 | re.DOTALL|re.MULTILINE, 43 | ) 44 | def sub_fence(m): 45 | if m.group('class'): 46 | LOG.debug("matched: %s", m.group(0)) 47 | LOG.debug("class: %s", m.group('class')) 48 | return m.group('before') + '{%s}'%m.group('class').strip() + m.group('content') + m.group('after') 49 | else: 50 | return m.group(0) 51 | 52 | newcontent = code_fence_re.sub(sub_fence, content) 53 | LOG.debug(newcontent) 54 | source[0] = newcontent 55 | 56 | def transform_block_quotes(app, docname, source): 57 | """ 58 | Transform this:: 59 | 60 | > ## some-heading 61 | > text 62 | > text 63 | {: .block-class} 64 | 65 | into this:: 66 | 67 | ```{block-class} some-heading 68 | text 69 | text 70 | ``` 71 | 72 | """ 73 | if not app.config.sphinx_lesson_transform_block_quotes: 74 | return 75 | LOG.debug('sphinx_lesson: transform_block_quotes: %s', docname) 76 | content = source[0] 77 | LOG.debug(content) 78 | 79 | block_quote_re = re.compile( 80 | r'(?P> ?#+[^\n]*$\n)?(?P(?:>[^\n]*$\n)+)\{: +(?P[^\}]+)\}', 81 | re.DOTALL|re.MULTILINE, 82 | ) 83 | 84 | def sub_block(m): 85 | """Handle each detected block quote""" 86 | if m.group('class'): 87 | LOG.debug("matched: %s", m.group(0)) 88 | LOG.debug("class: %s", m.group('class')) 89 | # Extract the class, remove leading characters 90 | class_ = m.group('class') 91 | class_ = re.sub('^[ .]*', '', class_) 92 | # heading: tranform explicit heading into directive heading 93 | if m.group('heading'): 94 | heading = m.group('heading') 95 | heading = ' ' + re.sub('^[ >#]*', '', heading) 96 | else: 97 | heading = '' 98 | # content: remove one leading '>' character 99 | contentlines = m.group('content').split('\n') 100 | contentlines = [ re.sub('^> ?', '', line) for line in contentlines ] 101 | 102 | LOG.debug(contentlines) 103 | return ("```{%s}%s\n"%(class_, heading) 104 | + '\n'.join(contentlines) 105 | + "```" 106 | ) 107 | 108 | else: 109 | return m.group(0) 110 | 111 | newcontent = block_quote_re.sub(sub_block, content) 112 | LOG.debug(newcontent) 113 | source[0] = newcontent 114 | 115 | def transform_html_img(app, docname, source): 116 | """ 117 | Transform this:: 118 | 119 | 120 | 121 | into this:: 122 | 123 | ```{figure} /path/to/img.png 124 | ``` 125 | 126 | Exclude any possible `{{ ... }}` template variables. 127 | 128 | """ 129 | if not app.config.sphinx_lesson_transform_html_img: 130 | return 131 | LOG.debug('sphinx_lesson: transform_html_img: %s', docname) 132 | content = source[0] 133 | LOG.debug(content) 134 | 135 | html_img_re = re.compile( 136 | r']+src="(?:{{[^\{\}\{"<>]+}})?(?P[^<>"]+)"[^<>]*>', 137 | ) 138 | 139 | def sub_img(m): 140 | """Handle each detected block quote""" 141 | raw = m.group(0) 142 | if re.search(r'style="[^"]*border:', raw): 143 | border = textwrap.dedent("""\ 144 | --- 145 | class: with-border 146 | --- 147 | """) 148 | else: 149 | border = "" 150 | repl = textwrap.dedent(""" 151 | ```{figure} %(src)s 152 | %(border)s``` 153 | """)%{'src':m.group('src'), 'border': border} 154 | LOG.debug(repl) 155 | return repl 156 | 157 | newcontent = html_img_re.sub(sub_img, content) 158 | LOG.debug(newcontent) 159 | source[0] = newcontent 160 | 161 | 162 | 163 | def setup(app): 164 | "Sphinx extension setup" 165 | app.setup_extension('myst_nb') 166 | # Code frence transformation 167 | app.add_config_value('sphinx_lesson_transform_code_fences', True, 'env') 168 | app.add_config_value('sphinx_lesson_transform_block_quotes', True, 'env') 169 | app.add_config_value('sphinx_lesson_transform_html_img', True, 'env') 170 | app.connect('source-read', transform_code_fences) 171 | app.connect('source-read', transform_block_quotes) 172 | app.connect('source-read', transform_html_img) 173 | 174 | return { 175 | 'version': __version__, 176 | 'parallel_read_safe': True, 177 | 'parallel_write_safe': True, 178 | } 179 | -------------------------------------------------------------------------------- /sphinx_lesson/directives.py: -------------------------------------------------------------------------------- 1 | """Add some sphinx-docutils directives related to lessons. 2 | """ 3 | # pylint: disable=E701 4 | 5 | import os 6 | import warnings 7 | 8 | from docutils import nodes 9 | from docutils.parsers.rst.directives.admonitions \ 10 | import Admonition as AdmonitionDirective 11 | from sphinx.util.docutils import SphinxDirective 12 | from sphinx.util.logging import getLogger 13 | 14 | from . import __version__ 15 | LOG = getLogger(__name__) 16 | 17 | 18 | def class_name_to_slug(name): 19 | """Strip Directive and turn name into slug 20 | 21 | Example: 22 | Hands_OnDirective --> hands-on 23 | """ 24 | return name.split('Directive')[0].lower().replace('_', '-') 25 | 26 | 27 | # This includes a heading, to not then have 28 | class _BaseCRDirective(AdmonitionDirective, SphinxDirective): 29 | """A directive to handle CodeRefinery styles 30 | """ 31 | # node_class = challenge 32 | required_arguments = 0 33 | optional_arguments = 1 34 | final_argument_whitespace = True 35 | extra_classes = [ ] 36 | allow_empty = True 37 | 38 | @classmethod 39 | def get_cssname(cls): 40 | """Return the CSS class name and Sphinx directive name. 41 | 42 | - Remove 'Directive' from the name of the class 43 | - All lowercase 44 | - '_' replaced with '-' 45 | """ 46 | return class_name_to_slug(cls.__name__) 47 | @classmethod 48 | def cssname(cls): 49 | """Backwards compatibility for get_cssname, do not use.""" 50 | warnings.warn( 51 | "You should use `get_cssname` (#71, 2021-08-22) and update old code to use it. This may be removed someday.\n", 52 | category=FutureWarning, 53 | stacklevel=2) 54 | return class_name_to_slug(cls.__name__) 55 | 56 | def run(self): 57 | """Run the normal admonition class, but add in a new features. 58 | 59 | title_text: some old classes had a title which was added at the 60 | CSS level. If this is set, then this title will be added by the 61 | directive. 62 | """ 63 | name = self.get_cssname() 64 | self.node_class = nodes.admonition 65 | # Some jekyll-common nodes have CSS-generated titles, some don't. The 66 | # Admonition class requires a title. Add one if missing. The title is 67 | # the first argument to the directive. 68 | if len(self.arguments) == 0: 69 | if hasattr(self, 'title_text'): 70 | self.arguments = [self.title_text] 71 | else: 72 | self.arguments = [name.title()] 73 | # Run the upstream directive 74 | ret = super().run() 75 | # Set CSS classes 76 | ret[0].attributes['classes'].append(name) 77 | ret[0].attributes['classes'].extend(self.extra_classes) 78 | # Give it a reference 79 | target_id = name+'-%d' % self.env.new_serialno(name) 80 | targetnode = nodes.target('', '', ids=[target_id]) 81 | ret[0].target_id = target_id 82 | ret[0].target_docname = self.env.docname 83 | ret.insert(0, targetnode) 84 | return ret 85 | 86 | def assert_has_content(self): 87 | """Allow empty directive blocks. 88 | 89 | This override skips the content check, if self.allow_empty is set 90 | to True. This adds the admonition-no-content to the CSS 91 | classes, which reduces a bit of the empty space. This is a hack 92 | of docutils, and may need fixing later on. 93 | """ 94 | if not self.allow_empty: 95 | return super().assert_has_content() 96 | if not self.content: 97 | #if not hasattr(self, 'extra_classes'): 98 | # self.extra_classes = [ ] 99 | self.extra_classes = list(self.extra_classes) + ['admonition-no-content'] 100 | return 101 | 102 | 103 | # These are the priamirly recommend directives 104 | class DemoDirective(_BaseCRDirective): 105 | title_text = "Demo" 106 | class Type_AlongDirective(_BaseCRDirective): 107 | extra_classes = ['important'] 108 | class ExerciseDirective(_BaseCRDirective): 109 | extra_classes = ['important'] 110 | class SolutionDirective(_BaseCRDirective): 111 | extra_classes = ['important', 'dropdown'] #'toggle-shown' = visible by default 112 | class HomeworkDirective(_BaseCRDirective): 113 | extra_classes = ['important'] 114 | class Instructor_NoteDirective(_BaseCRDirective): 115 | title_text = "Instructor note" 116 | class PrerequisitesDirective(_BaseCRDirective): 117 | title_text = "Prerequisites" 118 | class DiscussionDirective(_BaseCRDirective): 119 | extra_classes = ['important'] 120 | 121 | # These are hold-over for carpentries 122 | class QuestionsDirective(_BaseCRDirective): 123 | """Used at top of lesson for questions which will be answered""" 124 | pass 125 | class ObjectivesDirective(_BaseCRDirective): 126 | """Used at top of lesson""" 127 | pass 128 | class KeypointsDirective(_BaseCRDirective): 129 | """Used at bottom of lesson""" 130 | pass 131 | class CalloutDirective(_BaseCRDirective): pass 132 | ChallengeDirective = ExerciseDirective 133 | class ChecklistDirective(_BaseCRDirective): pass 134 | PrereqDirective = PrerequisitesDirective 135 | class TestimonialDirective(_BaseCRDirective): pass 136 | class OutputDirective(_BaseCRDirective): 137 | title_text = 'Output' 138 | 139 | # This does work, to add 140 | # from sphinx.writers.html5 import HTML5Translator 141 | # def visit_node(self, node): 142 | # #import pdb ; pdb.set_trace() 143 | # node.attributes['classes'] += [node.__class__.__name__] 144 | # self.visit_admonition(node) 145 | 146 | 147 | # Add our custom CSS to the headers. 148 | def init_static_path(app): 149 | static_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 150 | '_static')) 151 | #print('sphinx_lesson static path:', static_path) 152 | app.config.html_static_path.append(static_path) 153 | 154 | 155 | def setup(app): 156 | "Sphinx extension setup" 157 | app.setup_extension('myst_nb') 158 | for name, obj in globals().items(): 159 | #print(name, obj) 160 | if (name.endswith('Directive') 161 | and issubclass(obj, _BaseCRDirective) 162 | and not name.startswith('_')): 163 | #print(name, obj.get_cssname()) 164 | directive_name = class_name_to_slug(name) 165 | app.add_directive(directive_name, obj) 166 | 167 | # Add CSS to build 168 | # Hint is from https://github.com/choldgraf/sphinx-copybutton/blob/master/sphinx_copybutton/__init__.py # pylint: ignore=E501 169 | app.connect('builder-inited', init_static_path) 170 | app.add_css_file("sphinx_lesson.css") 171 | 172 | return { 173 | 'version': __version__, 174 | 'parallel_read_safe': True, 175 | 'parallel_write_safe': True, 176 | } 177 | -------------------------------------------------------------------------------- /.github/workflows/sphinx.yml: -------------------------------------------------------------------------------- 1 | # Deploy Sphinx. This could be shorter, but we also do some extra 2 | # stuff. 3 | # 4 | # License: CC-0. This is the canonical location of this file, which 5 | # you may want to link to anyway: 6 | # https://github.com/coderefinery/sphinx-lesson-template/blob/main/.github/workflows/sphinx.yml 7 | # https://raw.githubusercontent.com/coderefinery/sphinx-lesson-template/main/.github/workflows/sphinx.yml 8 | 9 | 10 | name: sphinx 11 | on: [push, pull_request, workflow_dispatch] 12 | 13 | env: 14 | DEFAULT_BRANCH: "main" 15 | # If these SPHINXOPTS are enabled, then be strict about the 16 | # builds and fail on any warnings. 17 | #SPHINXOPTS: "-W --keep-going -T" 18 | GENERATE_PDF: true # to enable, must be 'true' lowercase 19 | GENERATE_SINGLEHTML: true # to enable, must be 'true' lowercase 20 | PDF_FILENAME: lesson.pdf 21 | MULTIBRANCH: true # to enable, must be 'true' lowercase 22 | 23 | 24 | jobs: 25 | build: 26 | name: Build 27 | runs-on: ubuntu-latest 28 | permissions: 29 | contents: read 30 | 31 | steps: 32 | # https://github.com/marketplace/actions/checkout 33 | - uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 0 36 | lfs: true 37 | 38 | # https://github.com/marketplace/actions/setup-python 39 | # ^-- This gives info on matrix testing. 40 | - name: Install Python 41 | uses: actions/setup-python@v4 42 | with: 43 | python-version: '3.11' 44 | cache: 'pip' 45 | 46 | # https://docs.github.com/en/actions/guides/building-and-testing-python#installing-dependencies 47 | # ^-- This gives info on installing dependencies with pip 48 | - name: Install dependencies 49 | run: | 50 | python -m pip install --upgrade pip 51 | pip install -r requirements.txt 52 | 53 | # Debug 54 | - name: Debugging information 55 | env: 56 | ref: ${{github.ref}} 57 | event_name: ${{github.event_name}} 58 | head_ref: ${{github.head_ref}} 59 | base_ref: ${{github.base_ref}} 60 | run: | 61 | echo "github.ref: ${ref}" 62 | echo "github.event_name: ${event_name}" 63 | echo "github.head_ref: ${head_ref}" 64 | echo "github.base_ref: ${base_ref}" 65 | echo "GENERATE_PDF: ${GENERATE_PDF}" 66 | echo "GENERATE_SINGLEHTML: ${GENERATE_SINGLEHTML}" 67 | set -x 68 | git rev-parse --abbrev-ref HEAD 69 | git branch 70 | git branch -a 71 | git remote -v 72 | python -V 73 | pip list --not-required 74 | pip list 75 | 76 | 77 | # Build 78 | - uses: ammaraskar/sphinx-problem-matcher@master 79 | - name: Build Sphinx docs (dirhtml) 80 | # SPHINXOPTS used via environment variables 81 | run: | 82 | make dirhtml 83 | # This fixes broken copy button icons, as explained in 84 | # https://github.com/coderefinery/sphinx-lesson/issues/50 85 | # https://github.com/executablebooks/sphinx-copybutton/issues/110 86 | # This can be removed once these PRs are accepted (but the 87 | # fixes also need to propagate to other themes): 88 | # https://github.com/sphinx-doc/sphinx/pull/8524 89 | # https://github.com/readthedocs/sphinx_rtd_theme/pull/1025 90 | sed -i 's/url_root="#"/url_root=""/' _build/dirhtml/index.html || true 91 | 92 | # singlehtml 93 | - name: Generate singlehtml 94 | if: ${{ env.GENERATE_SINGLEHTML == 'true' }} 95 | run: | 96 | make singlehtml 97 | mv _build/singlehtml/ _build/dirhtml/singlehtml/ 98 | 99 | # PDF if requested 100 | - name: Generate PDF 101 | if: ${{ env.GENERATE_PDF == 'true' }} 102 | run: | 103 | #pip install https://github.com/rkdarst/sphinx_pyppeteer_builder/archive/refs/heads/main.zip 104 | pip install sphinx_pyppeteer_builder 105 | make pyppeteer 106 | mv _build/pyppeteer/*.pdf _build/dirhtml/${PDF_FILENAME} 107 | 108 | # Stage all deployed assets in _gh-pages/ for simplicity, and to 109 | # prepare to do a multi-branch deployment. 110 | - name: Copy deployment data to _gh-pages/ 111 | if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} 112 | run: 113 | rsync -a _build/dirhtml/ _gh-pages/ 114 | 115 | # Use gh-pages-multibranch to multiplex different branches into 116 | # one deployment. See 117 | # https://github.com/coderefinery/gh-pages-multibranch 118 | - name: gh-pages multibranch 119 | uses: coderefinery/gh-pages-multibranch@main 120 | if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && env.MULTIBRANCH == 'true' }} 121 | with: 122 | directory: _gh-pages/ 123 | default_branch: ${{ env.DEFAULT_BRANCH }} 124 | publish_branch: gh-pages 125 | 126 | # Add the .nojekyll file 127 | - name: nojekyll 128 | if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} 129 | run: | 130 | touch _gh-pages/.nojekyll 131 | 132 | # Save artifact for the next step. 133 | - uses: actions/upload-artifact@v4 134 | if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} 135 | with: 136 | name: gh-pages-build 137 | path: _gh-pages/ 138 | 139 | # Deploy in a separate job so that write permissions are restricted 140 | # to the minimum steps. 141 | deploy: 142 | name: Deploy 143 | runs-on: ubuntu-latest 144 | needs: build 145 | # This if can't use the env context - find better way later. 146 | if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} 147 | permissions: 148 | contents: write 149 | 150 | steps: 151 | - uses: actions/download-artifact@v4 152 | if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && ( env.MULTIBRANCH == 'true' || github.ref == format('refs/heads/{0}', env.DEFAULT_BRANCH )) }} 153 | with: 154 | name: gh-pages-build 155 | path: _gh-pages/ 156 | 157 | # As of 2023, we could publish to pages via a Deployment. This 158 | # isn't done yet to give it time to stabilize (out of beta), and 159 | # also having a gh-pages branch to check out is rather 160 | # convenient. 161 | 162 | # Deploy 163 | # https://github.com/peaceiris/actions-gh-pages 164 | - name: Deploy 165 | uses: peaceiris/actions-gh-pages@v3 166 | if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && ( env.MULTIBRANCH == 'true' || github.ref == format('refs/heads/{0}', env.DEFAULT_BRANCH )) }} 167 | with: 168 | publish_branch: gh-pages 169 | github_token: ${{ secrets.GITHUB_TOKEN }} 170 | publish_dir: _gh-pages/ 171 | force_orphan: true 172 | -------------------------------------------------------------------------------- /content/sample-episode-myst.md: -------------------------------------------------------------------------------- 1 | # Sample episode in MyST (Markdown) 2 | 3 | :::{questions} 4 | - What syntax is used to make a lesson? 5 | - How do you structure a lesson effectively for teaching? 6 | 7 | `questions` are at the top of a lesson and provide a starting 8 | point for what you might learn. It is usually a bulleted list. 9 | (The history is a holdover from carpentries-style lessons, and is 10 | not required.) 11 | ::: 12 | 13 | :::{objectives} 14 | - Show a complete lesson page with all of the most common 15 | structures. 16 | 17 | - ... 18 | 19 | This is also a holdover from the carpentries-style. It could 20 | usually be left off. 21 | ::: 22 | 23 | A first paragraph really motivating *why* you would need the material 24 | presented on this page, and why it is exciting. Don't go into details. 25 | 26 | Then, another paragraph going into the big picture of *what* you will 27 | do and *how* you will do it. Not details, but enough so that someone 28 | knows the overall path. 29 | 30 | \[For the syntax of ReST, you really want to browse this page alongside the 31 | source of it, to see how this is implemented. See the links at the to 32 | right of the page.\] 33 | 34 | ## Section titles should be enough to understand the page 35 | 36 | The first paragraph of each section should again summarize what you 37 | will do in it. 38 | 39 | Top-level section titles are the map through the page and should make 40 | sense together. 41 | 42 | This is text. 43 | 44 | A code block with preceeding paragraph: 45 | 46 | ``` 47 | import multiprocessing 48 | ``` 49 | 50 | - A bullet list 51 | 52 | - Bullet list 53 | 54 | - Sub-list: 55 | 56 | ``` 57 | code block (note indention) 58 | ``` 59 | 60 | :::{note} 61 | directive within a list (note indention) 62 | ::: 63 | 64 | ````{tabs} 65 | ```{code-tab} py 66 | import bisect 67 | a = 1 + 2 68 | ``` 69 | 70 | ```{code-tab} r R 71 | library(x) 72 | a <- 1 + 2 73 | ``` 74 | ```` 75 | 76 | :::{discussion} This is a `discussion` directive 77 | Discussion content. 78 | ::: 79 | 80 | ## Exercise: \[the general topic\] 81 | 82 | These exercises will show basic exercise conventions. It might be 83 | useful for the first paragraph of a multi-exercise section to tie them 84 | together to the overall point, but that isn't necessary. 85 | 86 | \[Exercises get their own section, so that they can be linked and found 87 | in the table of contents.\] 88 | 89 | :::{exercise} ReST-1 Imperative statement of what will happen in the exercise. 90 | An intro paragraph about the exercise, if not obvious. Expect that 91 | learners and exercise leaders will end up here without having 92 | browsed the lesson above. Make sure that they understand the 93 | general idea of what is going on and *why* the exercise exists 94 | (what the learning objective is roughly, for example there is a big 95 | difference between making a commit and focusing on writing a good 96 | commit message and knowing the command line arguments!) 97 | 98 | 1. Bullet list if multiple parts. 99 | 100 | 2. Despite the names, most exercises are not really "exercises" in 101 | that the are difficult. Most are rather direct applications of 102 | what has been learned (unless they are `(advanced)`). 103 | 104 | 3. When writing the exercise steps, try to make it clear enough 105 | that a helper/exercise leader who knows the general tools 106 | somewhat well (but doesn't know the lesson) can lead the 107 | exercise just by looking at the text in the box. 108 | 109 | - Of course that's not always possible, sometimes they actually 110 | are difficult. 111 | ::: 112 | 113 | :::{solution} 114 | - Solution here. 115 | ::: 116 | 117 | :::{exercise} (optional) ReST-2 Imperative statement of what will happen in the exercise. 118 | 1. Optional exercises are prefixed with `(optional)` 119 | 2. It's better to have more exercises be optional than many that 120 | are made optional ad-hoc. Every instructor may do something 121 | different, but it's better to seem like you are covering all the 122 | main material than seem like you are skipping parts. 123 | ::: 124 | 125 | :::{solution} 126 | - Solution to that one. 127 | ::: 128 | 129 | ::::{exercise} (optional) ReST-3: Exercise with embedded solution 130 | 1. This exercise has the solution within its box itself. This is a 131 | stylistic difference more than anything. 132 | 133 | :::{solution} 134 | - Solution to that one. 135 | ::: 136 | :::: 137 | 138 | :::{exercise} (advanced) ReST-4: Exercise with embedded solution 139 | 1. `(advanced)` is the tag for things which really require 140 | figuring out stuff on your own. Can also be `(advanced, 141 | optional)` but that's sort of implied. 142 | 2. This also demonstrates an exercise with a {doc}`link `, 143 | or {ref}`internal reference `. 144 | ::: 145 | 146 | ## This entire section is an exercise 147 | 148 | :::{admonition} Exercise leader setup 149 | :class: dropdown 150 | 151 | This admonition is a drop-down and can be used for instructor or 152 | exercise-leader specific setup. (see also / compare with 153 | `instructor-note`. 154 | ::: 155 | 156 | :::{exercise} In this section, we will \[do something\] 157 | Standard intro paragraph of the exercise. 158 | 159 | Describe how this exercise is following everything that is in this 160 | section. 161 | ::: 162 | 163 | Do this. 164 | 165 | Then do that. 166 | 167 | And so on. 168 | 169 | ## Another section 170 | 171 | :::{instructor-note} 172 | This is an instructor note. It may be hidden, collapsed, or put to 173 | the sidebar in a later style. You should use it for things that 174 | the instructor should see while teaching, but should be 175 | de-emphasized for the learners. Still, we don't hide them for 176 | learners (instructors often present from the same view.) 177 | ::: 178 | 179 | These tab synchronize with those above: 180 | 181 | ````{tabs} 182 | ```{code-tab} py 183 | import cmath 184 | a = 10 / 2 185 | ``` 186 | 187 | ```{code-tab} r R 188 | library(x) 189 | a <- 10 / 2 190 | ``` 191 | ```` 192 | 193 | :::{admonition} Advanced info that should be hidden 194 | :class: dropdown 195 | 196 | Any advanced information can be hidden behind any admonition by 197 | adding a `dropdown` class to it (syntax: `:class: dropdown` as 198 | first line separated by a space). 199 | 200 | This can be useful for advanced info that should not be show in the 201 | main body of text.. 202 | ::: 203 | 204 | ### A subsection 205 | 206 | Subsections are fine, use them as you want. But make sure the main 207 | sections tell the story and provide a good table of contents to the 208 | episode. 209 | 210 | :::{figure} img/sample-image.png 211 | Figure caption here. 212 | ::: 213 | 214 | :::{figure} img/sample-image.png 215 | :class: with-border 216 | 217 | Figure caption here, which explains the content in text so that 218 | it's accessible to screen readers. 219 | ::: 220 | 221 | ## Other directives 222 | 223 | :::{seealso} 224 | A reference to something else. Usually used at the top of a 225 | section or page to highlight that the main source of information is 226 | somewhere else. Regular-importance "see also" is usually at a 227 | section at the bottom of the page or an a regular paragraph text. 228 | ::: 229 | 230 | :::{important} 231 | This is used for things that should be highlighted to prevent 232 | significant confusion. It's not *that* often used. 233 | ::: 234 | 235 | :::{warning} 236 | Something which may result in data loss, security, or massive 237 | confusion. It's not *that* often used. 238 | ::: 239 | 240 | ## What's next? 241 | 242 | Pointers to what someone can learn about next to expand on this topic, 243 | if relevant. 244 | 245 | ## Summary 246 | 247 | A summary of what you learned. 248 | 249 | ## See also 250 | 251 | A "see also" section is good practice to show that you have researched 252 | the topic well and your lesson becomes a hub pointing to the other 253 | best possible resources. 254 | 255 | - Upstream information 256 | - Another course 257 | 258 | :::{keypoints} 259 | - What the learner should take away 260 | 261 | - point 2 262 | 263 | - ... 264 | 265 | This is another holdover from the carpentries style. This perhaps 266 | is better done in a "summary" section. 267 | ::: 268 | -------------------------------------------------------------------------------- /sphinx_lesson/exerciselist.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from docutils import nodes 4 | from docutils.parsers.rst import Directive 5 | import sphinx.util 6 | 7 | # Currently only sphinx-lesson admonitions can be included. Hint to fix: 8 | # nodes.admonition to nodes.Admonition and then you have to examine the Python 9 | # object class name. 10 | DEFAULT_EXERCISELIST_CLASSES = { 11 | 'exercise', 12 | 'solution', 13 | 'challenge', # backwards compatibility 14 | 'exerciselist-include', 15 | } 16 | 17 | DEFAULT_EXERCISELIST_CLASSES_EXCLUDE = { 18 | 'exerciselist-exclude', 19 | } 20 | 21 | class exerciselist(nodes.General, nodes.Element): 22 | """Node for exercise list 23 | 24 | Gets replaced with contents in the second pass. 25 | """ 26 | include_classes = frozenset() 27 | exclude_classes = frozenset() 28 | def __init__(self, *args, 29 | include_classes=DEFAULT_EXERCISELIST_CLASSES, 30 | exclude_classes=DEFAULT_EXERCISELIST_CLASSES_EXCLUDE): 31 | super().__init__(*args) 32 | self.include_classes = set(include_classes) 33 | self.exclude_classes = set(exclude_classes) 34 | 35 | class ExerciselistDirective(Directive): 36 | """Run when a directive is parsed, returns the node (which is handled later) 37 | """ 38 | option_spec = {'include': str, 'exclude': str} 39 | 40 | def run(self): 41 | kwargs = { } 42 | if 'include' in self.options: 43 | kwargs['include_classes'] = re.split('[ ,]+', self.options['include']) 44 | if 'exclude' in self.options: 45 | kwargs['exclude_classes'] = re.split('[ ,]+', self.options['exclude']) 46 | el = exerciselist('', **kwargs) 47 | return [el] 48 | 49 | 50 | def is_exercise_node(node, 51 | include_classes=DEFAULT_EXERCISELIST_CLASSES, 52 | exclude_classes=DEFAULT_EXERCISELIST_CLASSES_EXCLUDE): 53 | """Should a single node be included in the exercise list?""" 54 | # If not an admonition, never include 55 | if not isinstance(node, nodes.admonition): 56 | return False 57 | # If wrong classes, exclude 58 | if not hasattr(node, 'attributes'): 59 | return False 60 | classes = node.attributes.get('classes', ()) 61 | if not include_classes.intersection(classes): 62 | return False 63 | if exclude_classes.intersection(classes): 64 | return False 65 | # If parent is included, we don't need to include us. 66 | # TODO: higher level parents 67 | if (hasattr(node, 'parent') 68 | and is_exercise_node( 69 | node.parent, 70 | include_classes=include_classes, 71 | exclude_classes=exclude_classes, 72 | )): 73 | #import pdb ; pdb.set_trace() 74 | return False 75 | return True 76 | 77 | def find_exerciselist_nodes(app, env): 78 | """Find all nodes for the exercise list, in order. 79 | 80 | Go through all documents (in toctree order) and make a list of all 81 | docnames. Then, from those docnames, find all admonitions that 82 | match certain classes. Store this for the next round. 83 | """ 84 | 85 | env = app.builder.env 86 | 87 | # Find all docnames in toctree order. 88 | docnames = [ ] 89 | def process_docname(docname): 90 | """Process this doc and children""" 91 | if docname in docnames: # already visited 92 | return 93 | docnames.append(docname) 94 | for docname2 in env.toctree_includes.get(docname, []): # children 95 | process_docname(docname2) 96 | # Sphinx 4.0 renamed master_doc > root_doc 97 | root_doc = app.config.root_doc if hasattr(app.config, 'root_doc') else app.config.master_doc 98 | if root_doc not in env.toctree_includes: 99 | logger = sphinx.util.logging.getLogger(__name__) 100 | logger.error(f'sphinx_lesson.exerciselist could not find root doc {root_doc}') 101 | return 102 | process_docname(root_doc) 103 | 104 | # The list of all the exercises will be stored here. 105 | if not hasattr(env, 'sphinx_lesson_all_admonitions'): 106 | env.sphinx_lesson_all_admonitions = [ ] 107 | all_admonitions = env.sphinx_lesson_all_admonitions 108 | 109 | # Now go through and collect all admonitions from these docnames, in order. 110 | for docname in docnames: 111 | doctree = env.get_doctree(docname) 112 | for node in doctree.traverse(nodes.admonition): 113 | all_admonitions.append(node) 114 | 115 | 116 | def process_exerciselist_nodes(app, doctree, fromdocname): 117 | """Find 'exerciselist' directives and replace with the exercise list. 118 | """ 119 | env = app.builder.env 120 | # List of exercises 121 | if not hasattr(env, 'sphinx_lesson_all_admonitions'): 122 | env.sphinx_lesson_all_admonitions = [ ] 123 | all_admonitions = env.sphinx_lesson_all_admonitions 124 | 125 | for exerciselist_node in doctree.traverse(exerciselist): 126 | content = [] # This new content in place of 'exerciselist' 127 | last_docname = None 128 | for exercise_node in all_admonitions: 129 | # Skipp all admonitions which don't match our criteria 130 | if not is_exercise_node(exercise_node, include_classes=exerciselist_node.include_classes, exclude_classes=exerciselist_node.exclude_classes): 131 | continue 132 | # Set title of the document. We need to make a new section with a 133 | # 'title' node for this. 134 | if exercise_node.target_docname != last_docname: 135 | # find the page title 136 | last_docname = exercise_node.target_docname 137 | doctree = env.get_doctree(exercise_node.target_docname) 138 | page_title = next(iter(doctree.traverse(nodes.title))) 139 | # make section with the stuff 140 | section = nodes.section() 141 | content.append(section) 142 | section += page_title 143 | slug = page_title.rawsource.replace(' ', '-').lower() 144 | slug = re.sub(r'[^\w\d_-]', '', slug) 145 | section['ids'].append(slug) 146 | 147 | filename = env.doc2path(exercise_node.target_docname, base=None) 148 | par = nodes.paragraph() 149 | 150 | # Create a reference 151 | newnode = nodes.reference(f'In {filename}:', f'In {filename}:') 152 | newnode['refdocname'] = exercise_node.target_docname 153 | newnode['refuri'] = app.builder.get_relative_uri( 154 | fromdocname, exercise_node.target_docname) 155 | newnode['refuri'] += '#' + exercise_node.target_id 156 | par += newnode 157 | section.append(par) 158 | section.append(exercise_node) 159 | 160 | exerciselist_node.replace_self(content) 161 | 162 | 163 | 164 | from sphinx.transforms import SphinxTransform 165 | # Find the transform priority of MystReferenceResolver and be less than it. 166 | # But don't fail if use this extension without myst_parser 167 | try: 168 | from myst_parser.myst_refs import MystReferenceResolver 169 | # This is 8 in 2022 (thus the default value below) 170 | el_transform_priority = MystReferenceResolver.default_priority - 1 171 | except ImportError: 172 | el_transform_priority = 8 173 | 174 | class ExerciseListTransform(SphinxTransform): 175 | """Expand exerciselist nodes into the list of the exercises. 176 | 177 | This has to run before references are resolved 178 | """ 179 | # myst_parser.myst_refs.MystReferenceResolver is priority 9 180 | default_priority = el_transform_priority 181 | def apply(self): 182 | doctree = self.document 183 | #import IPython ; IPython.embed() 184 | process_exerciselist_nodes(self.app, doctree, self.env.docname) 185 | 186 | 187 | 188 | def setup(app): 189 | app.add_node(exerciselist) 190 | app.add_directive('exerciselist', ExerciselistDirective) 191 | app.connect('env-check-consistency', find_exerciselist_nodes) 192 | app.add_post_transform(ExerciseListTransform) 193 | return { 194 | 'version': '0.2', 195 | 'parallel_read_safe': True, 196 | 'parallel_write_safe': True, 197 | } 198 | -------------------------------------------------------------------------------- /content/sample-episode-rst.rst: -------------------------------------------------------------------------------- 1 | Sample episode in ReST 2 | ====================== 3 | 4 | .. questions:: 5 | 6 | - What syntax is used to make a lesson? 7 | - How do you structure a lesson effectively for teaching? 8 | 9 | ``questions`` are at the top of a lesson and provide a starting 10 | point for what you might learn. It is usually a bulleted list. 11 | (The history is a holdover from carpentries-style lessons, and is 12 | not required.) 13 | 14 | .. objectives:: 15 | 16 | - Show a complete lesson page with all of the most common 17 | structures. 18 | - ... 19 | 20 | This is also a holdover from the carpentries-style. It could 21 | usually be left off. 22 | 23 | 24 | A first paragraph really motivating *why* you would need the material 25 | presented on this page, and why it is exciting. Don't go into details. 26 | 27 | Then, another paragraph going into the big picture of *what* you will 28 | do and *how* you will do it. Not details, but enough so that someone 29 | knows the overall path. 30 | 31 | [For the syntax of ReST, you really want to browse this page alongside the 32 | source of it, to see how this is implemented. See the links at the to 33 | right of the page.] 34 | 35 | 36 | 37 | Section titles should be enough to understand the page 38 | ------------------------------------------------------ 39 | 40 | The first paragraph of each section should again summarize what you 41 | will do in it. 42 | 43 | Top-level section titles are the map through the page and should make 44 | sense together. 45 | 46 | This is text. 47 | 48 | A code block with preceeding paragraph:: 49 | 50 | import multiprocessing 51 | 52 | * A bullet list 53 | 54 | * Bullet list 55 | 56 | * Sub-list:: 57 | 58 | code block (note indention) 59 | 60 | .. note:: 61 | 62 | directive within a list (note indention) 63 | 64 | .. tabs:: 65 | 66 | .. code-tab:: py 67 | 68 | import bisect 69 | a = 1 + 2 70 | 71 | .. code-tab:: r R 72 | 73 | library(x) 74 | a <- 1 + 2 75 | 76 | .. discussion:: This is a `discussion` directive 77 | 78 | Discussion content. 79 | 80 | 81 | Exercise: [the general topic] 82 | ----------------------------- 83 | 84 | These exercises will show basic exercise conventions. It might be 85 | useful for the first paragraph of a multi-exercise section to tie them 86 | together to the overall point, but that isn't necessary. 87 | 88 | [Exercises get their own section, so that they can be linked and found 89 | in the table of contents.] 90 | 91 | .. exercise:: ReST-1 Imperative statement of what will happen in the exercise. 92 | 93 | An intro paragraph about the exercise, if not obvious. Expect that 94 | learners and exercise leaders will end up here without having 95 | browsed the lesson above. Make sure that they understand the 96 | general idea of what is going on and *why* the exercise exists 97 | (what the learning objective is roughly, for example there is a big 98 | difference between making a commit and focusing on writing a good 99 | commit message and knowing the command line arguments!) 100 | 101 | 1. Bullet list if multiple parts. 102 | 2. Despite the names, most exercises are not really "exercises" in 103 | that the are difficult. Most are rather direct applications of 104 | what has been learned (unless they are ``(advanced)``). 105 | 3. When writing the exercise steps, try to make it clear enough 106 | that a helper/exercise leader who knows the general tools 107 | somewhat well (but doesn't know the lesson) can lead the 108 | exercise just by looking at the text in the box. 109 | 110 | - Of course that's not always possible, sometimes they actually 111 | are difficult. 112 | 113 | .. solution:: 114 | 115 | * Solution here. 116 | 117 | 118 | .. exercise:: (optional) ReST-2 Imperative statement of what will happen in the exercise. 119 | 120 | 1. Optional exercises are prefixed with ``(optional)`` 121 | 2. It's better to have more exercises be optional than many that 122 | are made optional ad-hoc. Every instructor may do something 123 | different, but it's better to seem like you are covering all the 124 | main material than seem like you are skipping parts. 125 | 126 | .. solution:: 127 | 128 | * Solution to that one. 129 | 130 | 131 | .. exercise:: (optional) ReST-3: Exercise with embedded solution 132 | 133 | 1. This exercise has the solution within its box itself. This is a 134 | stylistic difference more than anything. 135 | 136 | .. solution:: 137 | 138 | * Solution to that one. 139 | 140 | .. exercise:: (advanced) ReST-4: Exercise with embedded solution 141 | 142 | 1. ``(advanced)`` is the tag for things which really require 143 | figuring out stuff on your own. Can also be ``(advanced, 144 | optional)`` but that's sort of implied. 145 | 2. This also demonstrates an exercise with a :doc:`link `, 146 | or :ref:`internal reference `. 147 | 148 | 149 | 150 | This entire section is an exercise 151 | ---------------------------------- 152 | 153 | .. admonition:: Exercise leader setup 154 | :class: dropdown 155 | 156 | This admonition is a drop-down and can be used for instructor or 157 | exercise-leader specific setup. (see also / compare with 158 | ``instructor-note``. 159 | 160 | .. exercise:: In this section, we will [do something] 161 | 162 | Standard intro paragraph of the exercise. 163 | 164 | Describe how this exercise is following everything that is in this 165 | section. 166 | 167 | Do this. 168 | 169 | Then do that. 170 | 171 | And so on. 172 | 173 | 174 | 175 | Another section 176 | --------------- 177 | 178 | .. instructor-note:: 179 | 180 | This is an instructor note. It may be hidden, collapsed, or put to 181 | the sidebar in a later style. You should use it for things that 182 | the instructor should see while teaching, but should be 183 | de-emphasized for the learners. Still, we don't hide them for 184 | learners (instructors often present from the same view.) 185 | 186 | 187 | These tab synchronize with those above: 188 | 189 | .. tabs:: 190 | 191 | .. code-tab:: py 192 | 193 | import cmath 194 | a = 10 / 2 195 | 196 | .. code-tab:: r R 197 | 198 | library(x) 199 | a <- 10 / 2 200 | 201 | .. admonition:: Advanced info that should be hidden 202 | :class: dropdown 203 | 204 | Any advanced information can be hidden behind any admonition by 205 | adding a ``dropdown`` class to it (syntax: ``:class: dropdown`` as 206 | first line separated by a space). 207 | 208 | This can be useful for advanced info that should not be show in the 209 | main body of text.. 210 | 211 | 212 | 213 | 214 | A subsection 215 | ~~~~~~~~~~~~ 216 | 217 | Subsections are fine, use them as you want. But make sure the main 218 | sections tell the story and provide a good table of contents to the 219 | episode. 220 | 221 | .. figure:: img/sample-image.png 222 | 223 | Figure caption here. 224 | 225 | 226 | .. figure:: img/sample-image.png 227 | :class: with-border 228 | 229 | Figure caption here, which explains the content in text so that 230 | it's accessible to screen readers. 231 | 232 | 233 | Other directives 234 | ---------------- 235 | 236 | .. seealso:: 237 | 238 | A reference to something else. Usually used at the top of a 239 | section or page to highlight that the main source of information is 240 | somewhere else. Regular-importance "see also" is usually at a 241 | section at the bottom of the page or an a regular paragraph text. 242 | 243 | .. important:: 244 | 245 | This is used for things that should be highlighted to prevent 246 | significant confusion. It's not *that* often used. 247 | 248 | .. warning:: 249 | 250 | Something which may result in data loss, security, or massive 251 | confusion. It's not *that* often used. 252 | 253 | 254 | 255 | What's next? 256 | ------------ 257 | 258 | Pointers to what someone can learn about next to expand on this topic, 259 | if relevant. 260 | 261 | 262 | 263 | Summary 264 | ------- 265 | 266 | A summary of what you learned. 267 | 268 | 269 | 270 | See also 271 | -------- 272 | 273 | A "see also" section is good practice to show that you have researched 274 | the topic well and your lesson becomes a hub pointing to the other 275 | best possible resources. 276 | 277 | * Upstream information 278 | * Another course 279 | 280 | 281 | 282 | .. keypoints:: 283 | 284 | - What the learner should take away 285 | - point 2 286 | - ... 287 | 288 | This is another holdover from the carpentries style. This perhaps 289 | is better done in a "summary" section. 290 | --------------------------------------------------------------------------------