diff --git a/.gitignore b/.gitignore index a9fe39ffc..2f91f4ed0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ +# generated docs items docs/site/ +docs/docs/_partials/termynal.md +docs/docs/_partials/*/*.html # test cache manual_test/ diff --git a/Makefile b/Makefile index cbcf80f0c..050cc8dfa 100644 --- a/Makefile +++ b/Makefile @@ -26,13 +26,13 @@ requirements: ## Format the code using isort and black format: - isort --profile black ccds hooks tests "{{ cookiecutter.repo_name }}/{{ cookiecutter.module_name }}" - black ccds hooks tests "{{ cookiecutter.repo_name }}/{{ cookiecutter.module_name }}" + isort --profile black ccds hooks tests docs/scripts "{{ cookiecutter.repo_name }}/{{ cookiecutter.module_name }}" + black ccds hooks tests docs/scripts "{{ cookiecutter.repo_name }}/{{ cookiecutter.module_name }}" lint: - flake8 ccds hooks tests "{{ cookiecutter.repo_name }}/{{ cookiecutter.module_name }}" - isort --check --profile black ccds hooks tests "{{ cookiecutter.repo_name }}/{{ cookiecutter.module_name }}" - black --check ccds hooks tests "{{ cookiecutter.repo_name }}/{{ cookiecutter.module_name }}" + flake8 ccds hooks tests docs/scripts "{{ cookiecutter.repo_name }}/{{ cookiecutter.module_name }}" + isort --check --profile black ccds hooks tests docs/scripts "{{ cookiecutter.repo_name }}/{{ cookiecutter.module_name }}" + black --check ccds hooks tests docs/scripts "{{ cookiecutter.repo_name }}/{{ cookiecutter.module_name }}" ### DOCS diff --git a/dev-requirements.txt b/dev-requirements.txt index 520e11a84..554a2e6a5 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,12 +1,17 @@ -e . +ansi2html black chardet flake8 isort mkdocs mkdocs-cinder +mkdocs-gen-files +mkdocs-include-markdown-plugin +pexpect pipenv pytest +termynal virtualenvwrapper; sys_platform != 'win32' virtualenvwrapper-win; sys_platform == 'win32' diff --git a/docs/docs/_partials/.gitkeep b/docs/docs/_partials/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/docs/docs/css/extra.css b/docs/docs/css/extra.css index d94b922e1..6c96ed962 100644 --- a/docs/docs/css/extra.css +++ b/docs/docs/css/extra.css @@ -1,3 +1,24 @@ h1, h2, h3 { margin-top: 77px; } + +#termynal { + height: 80ex !important; /* 40 lines of 2ex */ + min-height: 80ex !important; + max-height: 80ex !important; + overflow: scroll !important; + font-size: 1.5ex !important; +} + +[data-ty] { + line-height: 2ex !important; + white-space: pre; +} + +.newline { + line-height: 0 !important; +} + +.inline-input, .default-text { + display: inline-block !important; +} diff --git a/docs/docs/index.md b/docs/docs/index.md index e93e88706..1ae397c1f 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -83,7 +83,10 @@ ccds https://github.com/drivendata/cookiecutter-data-science ### Example - +{% + include-markdown "./_partials/termynal.md" +%} + ## Directory structure diff --git a/docs/docs/js/extra.js b/docs/docs/js/extra.js new file mode 100644 index 000000000..d81f81110 --- /dev/null +++ b/docs/docs/js/extra.js @@ -0,0 +1,28 @@ +/* Smooth scrolling for termynal replay */ + +function scrollToBottomOfContainer(container, element) { + var positionToScroll = element.offsetTop + element.offsetHeight - container.offsetHeight; + container.scrollTo({ + top: positionToScroll, + behavior: 'smooth' + }); +} + +// Select the node that will be observed for mutations +const targetNode = document.getElementById("termynal"); + +// Options for the observer (which mutations to observe) +const config = { attributes: false, childList: true, subtree: false }; + +// Callback function to execute when mutations are observed +const callback = (mutationList, observer) => { + for (const mutation of mutationList) { + scrollToBottomOfContainer(targetNode, mutation.target); + } +}; + +// Create an observer instance linked to the callback function +const observer = new MutationObserver(callback); + +// Start observing the target node for configured mutations +observer.observe(targetNode, config); diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 30ca202df..95194506c 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -8,5 +8,22 @@ google_analytics: ['UA-54096005-4', 'auto'] theme: cinder extra_css: - css/extra.css +extra_javascript: + - js/extra.js nav: - Home: index.md + +exclude_docs: | + _partials/termynal.md + +plugins: + - include-markdown + - termynal: + title: bash + buttons: macos + prompt_literal_start: + - "$" + - gen-files: + scripts: + - scripts/generate-termynal.py + diff --git a/docs/scripts/generate-termynal.py b/docs/scripts/generate-termynal.py new file mode 100644 index 000000000..b9601b016 --- /dev/null +++ b/docs/scripts/generate-termynal.py @@ -0,0 +1,149 @@ +import shutil +from pathlib import Path + +import pexpect +from ansi2html import Ansi2HTMLConverter + +CCDS_ROOT = Path(__file__).parents[2].resolve() + + +def execute_command_and_get_output(command, input_script): + input_script = iter(input_script) + child = pexpect.spawn(command, encoding="utf-8") + + interaction_history = [f"$ {command}\n"] + + prompt, user_input = next(input_script) + + try: + while True: + index = child.expect([prompt, pexpect.EOF, pexpect.TIMEOUT]) + + if index == 0: + output = child.before + child.after + interaction_history += [line.strip() for line in output.splitlines()] + + child.sendline(user_input) + + try: + prompt, user_input = next(input_script) + except StopIteration: + pass + + elif index == 1: # The subprocess has exited. + output = child.before + interaction_history += [line.strip() for line in output.splitlines()] + break + elif index == 2: # Timeout waiting for new data. + print("\nTimeout waiting for subprocess response.") + continue + + finally: + return interaction_history + + +ccds_script = [ + ("project_name", "My Analysis"), + ("repo_name", "my_analysis"), + ("module_name", ""), + ("author_name", "Dat A. Scientist"), + ("description", "This is my analysis of the data."), + ("python_version_number", "3.12"), + ("Choose from", "3"), + ("bucket", "s3://my-aws-bucket"), + ("aws_profile", ""), + ("Choose from", "2"), + ("Choose from", "1"), + ("Choose from", "2"), + ("Choose from", "2"), + ("Choose from", "1"), +] + + +def run_scripts(): + try: + output = [] + output += execute_command_and_get_output(f"ccds {CCDS_ROOT}", ccds_script) + return output + + finally: + # always cleanup + if Path("my_analysis").exists(): + shutil.rmtree("my_analysis") + + +def render_termynal(): + # actually execute the scripts and capture the output + results = run_scripts() + + # watch for inputs and format them differently + script = iter(ccds_script) + _, user_input = next(script) + + conv = Ansi2HTMLConverter(inline=True) + html_lines = [ + '