diff --git a/.gitignore b/.gitignore index 3332741..259f1ed 100644 --- a/.gitignore +++ b/.gitignore @@ -234,7 +234,7 @@ pip-selfcheck.json ### VisualStudioCode ### .vscode/* -!.vscode/settings.json +.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 1af7993..e1b6e7b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "python.pythonPath": "/home/d/.local/share/virtualenvs/ginpar-rqgfLzln/bin/python" + "python.pythonPath": "/home/d/dev/ginpar/venv/bin/python3.7", + "python.formatting.provider": "black" } \ No newline at end of file diff --git a/Pipfile b/Pipfile index a0bf059..674fa09 100644 --- a/Pipfile +++ b/Pipfile @@ -5,11 +5,16 @@ verify_ssl = true [dev-packages] pylint = "*" +black = "*" [packages] Jinja2 = "*" PyYAML = "*" click = "*" +livereload = "*" [requires] python_version = "3.7" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock index a9d1629..70d8121 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5b6ad66e50b2a2f0c9f3edbb7cb10e567b4821be1841a8345187755f4fe2ceb8" + "sha256": "782ef3faa76d74260740dfbecb3defd15f4be68cdd9b329d1f7d16597ff16888" }, "pipfile-spec": 6, "requires": { @@ -32,6 +32,14 @@ "index": "pypi", "version": "==2.10.3" }, + "livereload": { + "hashes": [ + "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", + "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66" + ], + "index": "pypi", + "version": "==2.6.1" + }, "markupsafe": { "hashes": [ "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", @@ -83,9 +91,35 @@ ], "index": "pypi", "version": "==5.1.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "tornado": { + "hashes": [ + "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", + "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", + "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", + "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", + "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", + "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", + "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5" + ], + "version": "==6.0.3" } }, "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, "astroid": { "hashes": [ "sha256:09a3fba616519311f1af8a461f804b68f0370e100c9264a035aa7846d7852e33", @@ -93,6 +127,29 @@ ], "version": "==2.3.2" }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "black": { + "hashes": [ + "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", + "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" + ], + "index": "pypi", + "version": "==19.3b0" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "index": "pypi", + "version": "==7.0" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", @@ -143,7 +200,14 @@ "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" ], - "version": "==1.12" + "version": "==1.12.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" }, "typed-ast": { "hashes": [ diff --git a/README.md b/README.md index d2ac505..8937de9 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![PyPI](https://img.shields.io/pypi/v/ginpar)](https://pypi.org/project/ginpar/) [![Build](https://github.com/davidomarf/ginpar/workflows/build/badge.svg)](https://github.com/davidomarf/ginpar/actions?workflow=build) +[![Documentation Status](https://readthedocs.org/projects/ginpar/badge/?version=latest)](https://ginpar.readthedocs.io/en/latest/?badge=latest) --- @@ -9,191 +10,279 @@ --- -Ginpar is a **static website generator** for interactive P5.js sketches, +Ginpar is a **static content generator** for interactive P5.js sketches, awkwardly named after **Generative Interactive Parametrisable Canvases**. -Key features: - -- Generate an individual page for each sketch in your project. -- Generate forms to control the parameters of the sketch on the go. -- Specify what parameters from the sketch you want to control. -- Generate an index page that links to every sketch. - -Ginpar aims to generate portfolios for generative artists. - -## Contents - -- [How to use](#how-to-use) - - [tl;dr](#tldr) - - [Installing](#Installing) - - [Initializing](#initializing) - - [Quickstarting](#quickstarting) - - [Creating sketch files](#creating-sketch-files) - - [sketch.js](#sketchjs) - - [data.yaml](#datayaml) - - [Building](#building) - - [Deploying](#Deploying) - - [Netlify](#netlify) -- [Built with](#built-with) -- [Versioning](#Versioning) -- [Contributors](#Contributors) -- [License](#License) - -## How to use - -### tl;dr: - -1. Install - ```sh - $ pip install ginpar - ``` -1. Initialize a new project - ```sh - $ ginpar init - ``` -1. Build - ```sh - $ ginpar build - ``` - -Alternatively to `init`, you can use `quickstart` and import a working example -automatically. - -Use `ginpar --help` to see a list of commands and options for each one. - -### Installing - -For now the only way to get ginpar running is by installing the PyPi package: - -```bash -$ pip install ginpar -``` +By separating the primary development and the parametric experimentation, +it allows you to stop thinking about code once your pieces have reached an +acceptable level of complexity, freeing you to experiment in a GUI for the +browser. -### Initializing +Features: -```sh -$ ginpar init -``` +- Simple API to define the controllable variables in your sketch. +- Easy to adapt existing sketches. +- Easy replicability of each final result. +- Index page to list all your sketches. -Ginpar will prompt you for the variables of your site, such as `name`, -`description`, `author`, etc. +The following Introduction is part of the [Ginpar Documentation][ginpar-docs] and +may be read at [Introduction][docs-intro] -This will create a new directory under the name you specified for `name`. +## Table of contents -Available flags: +- [Introduction](#introduction) + - [Prerequisites](#prerequisites) + - [Installation](#installation) + - [Quickstart](#quickstart) + - [Initialization](#initialization) + - [Creating new sketches](#creating-new-sketches) + - [Adapting existing sketches](#adapting-existing-sketches) + - [Specifying the parameters](#specifying-the-parameters) + - [Serving & Building](#serving-&-building) + - [Deploying](#deploying) +- [Build with](#built-with) +- [Versioning](#versioning) +- [Contributors](#contributors) +- [License](#license) -- `--quick, -q`: Skip the prompt and load the default values -- `--force, -f`: If there's a directory with the same name, remove it. +## Introduction -### Quickstarting +This is a quick introductory documentation for the Ginpar static content +generator. -```sh -$ ginpar-quickstart -``` +Ginpar works similarly to other engines such as Jekyll, Hugo, or Pelican, but +with a narrower and deeper set of functionalities, since it's very specific in +its task. -Ginpar includes a working example so you can modify its contents, and learn -how to set your own projects if current docs are not enough (they're not). +The two main objectives of Ginpar are: -Available flags: +- Allowing artists to stop thinking about code when experimenting with the + parameters that control the results of the artwork, achieving a **quicker + feedback loop.** -- `--force, -f`: If there's a directory with the same name, remove it. +- Making interactive websites to share the artist's work, letting users play + with the same GUI. -### Creating sketch files +For a live example of a website built with Ginpar, check this example_. -Every directory inside the `sketches/` folder will be considered a sketch if -it contains: +### Prerequisites -- `sketch.js` -- `data.yaml` +For now Ginpar only runs using Python >= 3.6. +Future versions will add compatibility with Python 2. -For example, for a sketch named `rectangle`, you'd need this file structure: +### Installation -``` -sketches/ - |- rectangle/ - |- sketch.js - |- data.yaml -``` +The easiest way to install the latest version of Ginpar is using pip: + + pip install ginpar + +To make sure it's installed and working, run: + + ginpar --version + +### Quickstart + +Ginpar has an example project ready for installation, it contains the default +project structure and a sketch example. + +If you're new to static content generators, this may be the best way to start. + + ginpar quickstart + +This will create the following directory structure:: + + . + ├── config.yaml + ├── sketches/ + │ └── domino-dancing/ + │ ├── sketch.js + │ └── data.yaml + └── themes/ + └── gart + └── ... + +To open this project in your browser, run: + + ginpar serve + +Now, start modifying the contents of ``config.json`` and ``sketches/``. + +Next, you should read [Creating new sketches](#creating-new-sketches), +[Serving & Building](#serving-building), or [Deploying](#deploying). + +### Initialization + +Alternatively, if you want to start a new project without importing anything +extra, run: + + ginpar init + +This will prompt you for the values to build your configuration file and then +create the project using those values. + +With this command, you may configure things like the destination and source +directories (``public`` and ``sketches`` by default). + +Check [ginpar init][ginpar-init] or run ``ginpar init --help`` for more +information. -#### sketch.js +### Creating new sketches -This is the script for the sketch. The only modifications you need to do to -be able to use ginpar are: +Ginpar has a handy command to start new projects with some configuration +already set: -- Add a `.parent("artwork-container")` to the `createCanvas` instruction. + ginpar new [SKETCH] -#### data.yaml +This will create a new sketch inside your predefined source directory. +You can set the name when running the command, but it's optional. -The `data.yaml` file will contain the list of variables that you'll be able to -control in the final sketch page. +Check [cli:ginpar new][ginpar-new] or run ``ginpar new --help`` for more +information. -The structure is this: +Now, you must be [specifying the parameters](#specifying-the-parameters). -```yaml +### Adapting existing sketches + +For Ginpar to build the interactive page, you'll need to add some modifications +to your sketch code. + +#### Adding it to the list of sketches + +First, make your sketch detectable for Ginpar: + +1. Create a directory ``my-sketch/`` inside ``sketches/``. +1. Copy your existent sketch script inside ``my-sketch`` and rename it to + ``sketch.js``. +1. Create a ``data.yaml`` file. + +You should end with a structure like this:: + + . + └── sketches/ + └── my-sketch/ + ├── sketch.js + └── data.yaml + +#### Making your sketch compatible with Ginpar + +In your ``createCanvas`` instruction, add ``.parent("artwork-container")``. + +Now, you must be [specifying the parameters](#specifying-the-parameters). + +### Specifying the parameters + +Each sketch is a directory that contains two files: ``sketch.js`` and +``data.yaml``. The ``data.yaml`` file is where the parameters specification +takes place. + +To create a parameters list, add this to your data file: + + ```yaml --- -# The name of the variable to control in your sketch.js file -- var: NUMBER_OF_POINTS - # Valid HTML input attributes, or ones that fit our API - attrs: - type: number - value: 30 - step: 1 -- var: SOME_RATIO - # You can specify a custom name to display in the HTML form - name: Minimum column height factor - attrs: - type: range - value: 0.1 - step: 0.01 - # These are all valid HTML attributes - min: 0 - max: 1 +# ... other data +# ... + +# Key that contains a list of parameters +params: + + # The name of the parameter must be the key of the element + # It must match a variable in your sketch.js file + - MY_VARIABLE: + + # Ginpar parameters definition keys. All optional. + # For a full list check the API + randomizable: True + name: My displayed variable name + + # HTML valid attributes + attrs: + type: number + value: 30 + step: 1 + min: 0 + max: 100 ``` -Ginpar will automatically produce the HTML forms, and the scripts to update the -script variables everytime the input values change. - -You don't need to declare these values in your JS file, but you can do it. If -you decide to, **declare them with either `let` or `var`, not with `const`.** +Once parsed, Ginpar will produce: + +- A form containing each of the items in the ``parameters`` list: + ```html +
+
+ + +
+ +
+ ``` + +- A JS code fragment to update each of the parameters using the form values: + ```JS + + function updateVars() { + MY_VARIABLE = document.getElementByID("my-variable").value; + // More variable updates. One for each params element. + } + ``` -### Building +--- -To build, simply run: +To use this parameters inside your sketch, just use the same name you used as +key: -```sh -ginpar build +```js +console.log(MY_VARIABLE) ``` -### Deploying +### Serving & Building + +Ginpar has two different commands to build your site: + + ginpar build -For now, we've only deployed in Netlify. However, using any other server -to deliver static content should be easy. +Will build your site into the ``build_directory`` path, which by default is +``public``. -### Netlify + ginpar serve -You need to specify: +Will start a new server on ``localhost:8000`` and open your default web +browser. You can specify the port with ``--port``. -- the python version to run - ```sh - $ echo "3.7" > runtime.txt - ``` -- add `ginpar` as dependency - ```sh - $ echo "ginpar" > requirements.txt - ``` -- tell Netlify how to build - ```sh - $ echo -e "[build]\n command = \"ginpar build\"\n publish = \"public\"" > netlify.toml - ``` +Check [ginpar serve][ginpar-serve] and [ginpar build][ginpar-build], or run +``ginpar serve --help``, ``ginpar build --help`` to see the full list of +options and arguments available. -Then just make a deployment and you'll be ready to go. +### Deploying + +Ginpar also has a command to create the deployment configuration files for +Netlify. Future versions will also generate the configuration files for other +deployments enviroments. + +**This command won't deploy your site. It'll just create the config files**. -To see a site in production, check [gen.algorithms][algo] +For Netlify, this means creating ``requirements.txt``, ``runtime.txt``, and +``netlify.toml``. + +If you want to set the configuration files manually for other engines, you +need to: + +- Specify the Python version to be above 3.6, +- Add `ginpar` to the dependencies, usually in a ``requirements.txt`` file, +- Make the deploy path the same as the :ref:`config:build_path`, +- Set ``ginpar build`` as the build command. ## Built With - [Jinja2][jinja] - Templating language. - [Click][click] - CLI Tool composer. +- [PyYAML][pyyaml] - YAML framework. + ## Versioning We use [SemVer][semver] for versioning. For the versions @@ -221,4 +310,11 @@ This project is licensed under the MIT License - see the [jinja]: https://jinja.palletsprojects.com/ [click]: https://click.palletsprojects.com/ [pelican]: https://getpelican.com -[algo]: https://github.com/davidomarf/gen.algorithms \ No newline at end of file +[algo]: https://github.com/davidomarf/gen.algorithms +[ginpar-docs]: https://ginpar.readthedocs.io +[docs-intro]: https://ginpar.readthedocs.io/en/latest/intro.html +[ginpar-serve]: https://ginpar.readthedocs.io/en/latest/cli.html#ginpar-serve +[ginpar-build]: https://ginpar.readthedocs.io/en/latest/cli.html#ginpar-build +[ginpar-init]: https://ginpar.readthedocs.io/en/latest/cli.html#ginpar-init +[ginpar-new]: https://ginpar.readthedocs.io/en/latest/cli.html#ginpar-new +[pyyaml]:https://pyyaml.org \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..6247f7e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..cac17fb --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,6 @@ +ginpar +pallets_sphinx_themes +pallets_sphinx_themes +sphinx-prompt +sphinx_click +sphinxcontrib-programoutput \ No newline at end of file diff --git a/docs/source/api.rst b/docs/source/api.rst new file mode 100644 index 0000000..8c012e5 --- /dev/null +++ b/docs/source/api.rst @@ -0,0 +1,9 @@ +API Docs +======== + +.. toctree:: + :maxdepth: 2 + + config + data + params diff --git a/docs/source/cli.rst b/docs/source/cli.rst new file mode 100644 index 0000000..34736f4 --- /dev/null +++ b/docs/source/cli.rst @@ -0,0 +1,36 @@ +CLI Commands +============ + +This is a list of the available CLI commands for Ginpar. + +This page contains the same information you'd get if you run + +.. prompt:: bash + + ginpar --help + +and then, + +.. prompt:: bash + + ginpar COMMAND --help + +for every command available. + +.. click:: ginpar.cli:cli + :prog: ginpar + +.. click:: ginpar.cli:build + :prog: ginpar build + +.. click:: ginpar.cli:init + :prog: ginpar init + +.. click:: ginpar.cli:new + :prog: ginpar new + +.. click:: ginpar.cli:quickstart + :prog: ginpar quickstart + +.. click:: ginpar.cli:serve + :prog: ginpar serve diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..5ade7e8 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,81 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +from pallets_sphinx_themes import ProjectLink + +# -- Project information ----------------------------------------------------- + +project = 'Ginpar' +copyright = '2019, David Omar Flores Chavez' +author = 'David Omar Flores Chavez' + +# The full version, including alpha/beta/rc tags +release = 'v0.6.0' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "pallets_sphinx_themes", + "sphinx-prompt", + "sphinx_click.ext", + "sphinxcontrib.programoutput", + "sphinx.ext.autosectionlabel", +] + +master_doc = 'index' + + +autosectionlabel_prefix_document = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'click' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +html_context = { + "project_links": [ + ProjectLink("Source Code", "https://github.com/davidomarf/ginpar/"), + ProjectLink("PyPI releases", "https://pypi.org/project/ginpar/"), + ProjectLink("Example website", "https://gen.davidomar.com"), + ProjectLink("Introduction", "/intro.html") + ] +} + +html_sidebars = { + "index": ["project.html", "localtoc.html", "searchbox.html"], + "**": ["localtoc.html", "relations.html", "searchbox.html"], +} + +singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]} \ No newline at end of file diff --git a/docs/source/config.rst b/docs/source/config.rst new file mode 100644 index 0000000..ed36743 --- /dev/null +++ b/docs/source/config.rst @@ -0,0 +1,114 @@ +Site configuration +================== + +This is the API documentation for the configuration file of a Ginpar site: +``config.yaml``. + +This file contains all the metadata for your site, such as your name, the name +of the site, the repository, social links, etc. + +The following data fields are the ones used by Ginpar. However, you can design +a custom theme or template that makes uses of extra fields. + +author +------ + +*String, optional* [**Not defined by default**] + +The name of the author of the site. This will be used to set Copyright messages +and meta tags. + +sitename +-------- + +*String, optional* [**Not defined by default**] + +The name of the site. This will be used to set Copyright messages +and meta tags. + +description +----------- + +*String, optional* [**Not defined by default**] + +The description of the site. This will be used as a meta tag, and will appear +in the index of the site. + +url +--- + +*String, optional* [**Not defined by default**] + +The URL you'll be using to redirect to the site's content. + +theme +----- + +*String, required* [**davidomarf/gart**] + +If the theme you want to use is a GitHub repository, set this value to +``AUTHOR/REPO``. + +If it's a git repository in a different server, add the ``.git`` address. + +Alternatively, you can add a string that matches the name of one directory +inside the ``themes/`` folder for a locally designed theme. + +The default value is ``davidomarf/ginpar`` + +content_path +------------ + +*String, required* [**sketches**] + +The directory that contains the project sketches. This path is referenced +when you run :ref:`cli:ginpar build` and :ref:`cli:ginpar serve`. + +build_path +---------- + +*String, required* [**public**] + +The directory Ginpar will use to build the site. + +scripts +------- + +*List, Optional* [**Not defined by default**] + +This is a list of scripts and the url to fetch them. You can later reference +the items of this list to include them in individual sketches. + +The structure is this: + +.. code-block:: yaml + + scripts: + p5: + "p5-url" + extra: + "extra-url" + lib: + "lib-url" + +sketch_defaults +--------------- + +*Object, Optional* [**Not defined by default**] + +Here you'll add the same content you'd otherwise manually add to every sketch +in your project. + +For example, if you'd like all your sketches to **not allow a global_seed**, +you'd need to add this to your config.yaml file: + +.. code-block:: YAML + + sketch_defaults: + global_seed: False + +Now, all your sketches will automatically have the value +``global_seed: False``. However, you can manually replace that value for a +single sketch. + +For all the available values, check :ref:`data:Sketch data`. diff --git a/docs/source/data.rst b/docs/source/data.rst new file mode 100644 index 0000000..68510b2 --- /dev/null +++ b/docs/source/data.rst @@ -0,0 +1,115 @@ +Sketch data +=========== + +This is the API documentation for the data files of single sketches: +``data.yaml``. + +Data files contain instructions to build the pages for individual sketches, +such as the sketch parameters, source code obfuscation, sketch hashing, etc. + +All these values are converted into a Python dictionary and then associated +with the sketch. Ginpar will use this dictionary to render +``sketch templates``. + +Check Jinja_ to learn how this templating works. + +The following keys are the ones that: + +- Ginpar uses to determine build flow, or +- Gart (the default theme) uses in its templates to render final pages. + +The only indispensable key is ``params``. Every other key it's optional. + +**If you want to use a configuration value for all your sketches, you may want +to read** :ref:`config:sketch_defaults`. + +params +~~~~~~ + +**List, Required** + +This is the most important and the only required key for the data file. +In ``params``, you specify the sketch parameters and their attributes. + +The key of every element **must match the variable name in your sketch.js**. + +.. code-block:: YAML + + params: + - YEAR: + attrs: + type: number + value: 2019 + step: 1 + - RATIO: + attrs: + type: number + value: 0.2 + step: 0.05 + min: 0 + max: 1 + +.. code-block:: JS + + console.log(NAME, YEAR, RATIO) + // ==> "Ginpar", 2019, 0.2 + +For most of the variables, those attributes will suffice. + +Ginpar will automatically remove low dashes and capitalize the parameter name, +however, you can also specify the name to display in the form: + +.. code-block:: YAML + + params: + - YEAR: + name: Current year + attrs: + # ... + +For a complete list of the fields you can specify for the ``params`` list, +check :ref:`params:Params API`. + +randomizable +~~~~~~~~~~~~ + +*Boolean, Optional* [**False**] + +When **True**, Ginpar will add a button ``Randomize`` at the end of the form +that will select random values between the ``min`` and ``max`` values of each +parameter that has ``randomize: True``. + +Check :ref:`params:randomize`. + +obfuscate +~~~~~~~~~ + +*Boolean, Optional* [**False**] + +When **True**, Ginpar will obfuscate the sketch source code before creating +the script file in the final build. + +This option will use `Javascript Obfuscator`_ + +draft +~~~~~ + +*Boolean, Optional* [**False**] + +When **True**, Ginpar will skip this sketch in the building proccess. + + +global_seed +~~~~~~~~~~~ + +*Boolean, Optional* [**True**] + +When **True**, Ginpar will create a unique base 64 seed for each sketch +result, and allow the user to put that ID as an input field so it +automatically sets all the parameter values necessary to generate the same +result again. + +.. Links + +.. _Jinja: https://jinja.palletsprojects.com/en/2.10.x/ +.. _`Javascript Obfuscator`: https://obfuscator.io diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..31ba0f1 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,44 @@ +.. Ginpar documentation master file, created by + sphinx-quickstart on Sat Oct 19 02:27:25 2019. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Ginpar +====== + +Ginpar is a static content generator for interactive P5.js sketches, +awkwardly named after **Generative Interactive Parametrisable Canvases**. + +By separating the primary development and the parametric experimentation, +it allows you to stop thinking about code once your pieces have reached +an acceptable level of complexity, freeing you to experiment in a GUI for +the browser. + +Features: + +- Simple API to define the controllable variables in your sketch. +- Easy to adapt existing sketches. +- Easy replicability of each final result. +- Index page to list all your sketches. + +Documentation +------------- + +If you want to start using Ginpar to build your own website, this links +will guide you. + +We recommend starting with the introduction and then move up to the API docs. + +.. toctree:: + :maxdepth: 2 + + intro + api + cli + +Other links +----------- + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/intro.rst b/docs/source/intro.rst new file mode 100644 index 0000000..9c8e896 --- /dev/null +++ b/docs/source/intro.rst @@ -0,0 +1,264 @@ +Introduction +============ + +This is a quick introductory documentation for the Ginpar static content +generator. + +Ginpar works similarly to other engines such as Jekyll, Hugo, or Pelican, but +with a narrower and deeper set of functionalities, since it's very specific in +its task. + +The two main objectives of Ginpar are: + +- Allowing artists to stop thinking about code when experimenting with the + parameters that control the results of the artwork, achieving a **quicker + feedback loop.** + +- Making interactive websites to share the artist's work, letting users play + with the same GUI. + +For a live example of a website built with Ginpar, check this example_. + +Prerequisites +------------- + +For now Ginpar only runs using Python >= 3.6. +Future versions will add compatibility with Python 2. + +Installation +------------ + +The easiest way to install the latest version of Ginpar is using pip: + +.. prompt:: bash + + pip install ginpar + +To make sure it's installed and working, run: + +.. prompt:: bash + + ginpar --version + +Quickstart +---------- + +Ginpar has an example project ready for installation, it contains the default +project structure and a sketch example. + +If you're new to static content generators, this may be the best way to start. + +.. prompt:: bash + + ginpar quickstart + +This will create the following directory structure:: + + . + ├── config.yaml + ├── sketches/ + │ └── domino-dancing/ + │ ├── sketch.js + │ └── data.yaml + └── themes/ + └── gart + └── ... + +To open this project in your browser, run: + +.. prompt:: bash + + ginpar serve + +Now, start modifying the contents of ``config.json`` and ``sketches/``. + +Next, you should read `Creating new sketches`_, `Serving & Building`_, or +`Deploying`_. + +Initialization +-------------- + +Alternatively, if you want to start a new project without importing anything +extra, run: + +.. prompt:: bash + + ginpar init + +This will prompt you for the values to build your configuration file and then +create the project using those values. + +With this command, you may configure things like the destination and source +directories (``public`` and ``sketches`` by default). + +Check :ref:`cli:ginpar init` or run ``ginpar init --help`` for more +information. + +Creating new sketches +--------------------- + +Ginpar has a handy command to start new projects with some configuration +already set: + +.. prompt:: bash + + ginpar new [SKETCH] + +This will create a new sketch inside your predefined source directory. +You can set the name when running the command, but it's optional. + +Check :ref:`cli:ginpar new` or run ``ginpar new --help`` for more information. + +Now, you must be `specifying the parameters`_. + +Adapting existing sketches +-------------------------- + +For Ginpar to build the interactive page, you'll need to add some modifications +to your sketch code. + +Adding it to the list of sketches +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, make your sketch detectable for Ginpar: + +#. Create a directory ``my-sketch/`` inside ``sketches/``. +#. Copy your existent sketch script inside ``my-sketch`` and rename it to + ``sketch.js``. +#. Create a ``data.yaml`` file. + +You should end with a structure like this:: + + . + └── sketches/ + └── my-sketch/ + ├── sketch.js + └── data.yaml + +Making your sketch compatible with Ginpar +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In your ``createCanvas`` instruction, add ``.parent("artwork-container")``. + +Now, you must be `specifying the parameters`_. + +Specifying the parameters +------------------------- + +Each sketch is a directory that contains two files: ``sketch.js`` and +``data.yaml``. The ``data.yaml`` file is where the parameters specification +takes place. + +To create a parameters list, add this to your data file: + +.. code-block:: yaml + + --- + # ... other data + # ... + + # Key that contains a list of parameters + params: + + # The name of the parameter must be the key of the element + # It must match a variable in your sketch.js file + - MY_VARIABLE: + + # Ginpar parameters definition keys. All optional. + # For a full list check the API + randomizable: True + name: My displayed variable name + + # HTML valid attributes + attrs: + type: number + value: 30 + step: 1 + min: 0 + max: 100 + +Once parsed, Ginpar will produce: + +- A form containing each of the items in the ``parameters`` list: + .. code-block:: HTML + +
+
+ + +
+ +
+ +- A JS code fragment to update each of the parameters using the form values: + .. code-block:: JavaScript + + function updateVars() { + MY_VARIABLE = document.getElementByID("my-variable").value; + // More variable updates. One for each params element. + } + +---- + +To use this parameters inside your sketch, just use the same name you used as +key: + +.. code-block:: JavaScript + + console.log(MY_VARIABLE) + +Serving & Building +------------------ + +Ginpar has two different commands to build your site: + +.. prompt:: bash + + ginpar build + +Will build your site into the ``build_directory`` path, which by default is +``public``. + +.. prompt:: bash + + ginpar serve + +Will start a new server on ``localhost:8000`` and open your default web +browser. You can specify the port with ``--port``. + +Check :ref:`cli:ginpar serve` and :ref:`cli:ginpar build`, or run +``ginpar serve --help``, ``ginpar build --help`` to see the full list of +options and arguments available. + +Deploying +--------- + +Ginpar also has a command to create the deployment configuration files for +Netlify. Future versions will also generate the configuration files for other +deployments enviroments. + +**This command won't deploy your site. It'll just create the config files**. + +For Netlify, this means creating ``requirements.txt``, ``runtime.txt``, and +``netlify.toml``. + +If you want to set the configuration files manually for other engines, you +need to: + +- Specify the Python version to be above 3.6, +- Add `ginpar` to the dependencies, usually in a ``requirements.txt`` file, +- Make the deploy path the same as the :ref:`config:build_path`, +- Set ``ginpar build`` as the build command. + +.. Links + +.. _example: https://genp.netlify.com +.. _CLI: /cli +.. _data: /data +.. _config: /config diff --git a/docs/source/params.rst b/docs/source/params.rst new file mode 100644 index 0000000..3d7718d --- /dev/null +++ b/docs/source/params.rst @@ -0,0 +1,57 @@ +Params API +========== + +This is the API documentation for the ``params`` field inside a single sketch +data file ``data.yaml``. Check :ref:`data:Sketch data` + +Data files contain instructions to build the pages for individual sketches, +such as the sketch parameters, source code obfuscation, sketch hashing, etc. + +All these values are converted into a Python dictionary and then associated +with the sketch. Ginpar will use this dictionary to render +``sketch templates``. + + +randomize +--------- + +*Boolean, Optional* [**False**] + +This will allow this field to get a random value between its +``min`` and ``max`` values using the ``Randomize`` button. + +Setting this to **True** requires both ``min`` and ``max`` inside the values +:ref:`params:attrs`. + + +name +---- + +*Boolean, Optional* [**Not defined by default**] + +Ginpar will automatically generate a ``name`` when processing the parameter +info. This value is generated by removing *snake_case* from the string and +capitalizing:: + + MY_VERY_VERBOSE_VARIABLE ==> My very verbose variable + +However, if you'd like to use a different name instead of the generated one, +you can specify it in its values. + +attrs +----- + +*List, Required* + +All the *key-value* pairs inside the ``attrs`` field will be added to the +input tag as attributes. + +In ``attrs`` you can (and should) specify all the +`input tag attributes`_ you'd like to include in your input. + +The only required field is ``value``, however, Ginpar will produce a better +output if you specify ``type``, ``value``, ``step``, ``min``, ``max``. + +.. Links + +.. _`input tag attributes`: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes diff --git a/ginpar/__init__.py b/ginpar/__init__.py index 1922b2c..e49a2e5 100644 --- a/ginpar/__init__.py +++ b/ginpar/__init__.py @@ -1,137 +1,4 @@ -import os -import shutil -import yaml -import click - -from jinja2 import Environment, FileSystemLoader - -from ginpar.settings import read_config -import ginpar.generators as gg - -_SITE_FILE = "config.json" - - -def parse(path): - return eval('f"""' + open(path).read() + '"""') - - -def build_sketch(sketch): - content = sketch - return content - - -def build_link(sketch): - title = sketch.split("/")[-1].split(".")[0] - return f'{title}
\n' - - -def build_index(sketches): - content = "" - for s in sketches: - content += build_link(s) - return content - - -def unkebab(s): - return " ".join(s.split("-")) - - -def get_sketches_list(path): - sketches = [] - # Create a list with all the directories inside path - for r, d, _ in os.walk(path): - for sketch in d: - sketches.append( - { - "name": sketch, - "script": os.path.join(r, sketch, "sketch.js"), - "data": os.path.join(r, sketch, "data.yaml"), - } - ) - - # Remove all the directories that don't contain both a `sketch.js` and `data.yaml` file - sketches[:] = filter( - lambda a: os.path.isfile(a["script"]) and os.path.isfile(a["data"]), sketches - ) - return sketches - - -def convert_information(sketch): - path = sketch["data"] - with open(path, "r") as stream: - try: - parsed_data = yaml.safe_load(stream) - except yaml.YAMLError as exc: - print(exc) - sketch["data"] = parsed_data - return sketch - +from ginpar.cli import cli def main(): - _SITE = read_config(_SITE_FILE) - - _THEME = _SITE["theme"] - - _TEMPLATES_PATH = os.path.join("themes", _THEME, "templates") - - _jinja_env = Environment(loader=FileSystemLoader(_TEMPLATES_PATH), trim_blocks=True) - - _jinja_env.filters["unkebab"] = unkebab - - ## Remove existent /public folder and create an empty one - if os.path.exists("public"): - shutil.rmtree("public") - - os.mkdir("public") - - ## Copy the static/ folder of the theme - shutil.copytree( - os.path.join("themes", _THEME, "static"), os.path.join("public", "static") - ) - - ## Create a sketches array - sketches_path = "./sketches" - sketches = get_sketches_list(sketches_path) - sketches[:] = map(convert_information, sketches) - - ## Create an index to contain all the sketches - _index_template = _jinja_env.get_template("index.html") - index = open("public/index.html", "w") - index.write( - _index_template.render(sketches=map(lambda a: a["name"], sketches), site=_SITE) - ) - index.close() - - for s in sketches: - ## Create a directory with the sketch title - os.mkdir(f"public/{s['name']}") - - ## Convert the form JSON into a dict - form_dict = s["data"] - - ## Add name key to the dict elements - form_dict = gg.add_name(form_dict) - - ## Create index.html - _sketch_template = _jinja_env.get_template("sketch.html") - sketch_index = open(f"public/{s['name']}/index.html", "w+") - sketch_index.write( - _sketch_template.render( - sketch=unkebab(s["name"]), form=gg.sketch_index(form_dict), site=_SITE - ) - ) - sketch_index.close() - - ## Create sketch.js - sketch_path = f"public/{s['name']}/sketch.js" - sketch = open(sketch_path, "w+") - - ## Copy all the content from original sketches/{title}.js to sketch.js - sf = open(s["script"], "r") - - sketch.write(gg.makeValueGetter(form_dict)) - - for x in sf.readlines(): - sketch.write(x) - sf.close() - sketch.close() + cli() \ No newline at end of file diff --git a/ginpar/build.py b/ginpar/build.py index d18ac91..279be75 100644 --- a/ginpar/build.py +++ b/ginpar/build.py @@ -1,49 +1,68 @@ -""" - ginpar.build - ~~~~~~~~~~~~ +"""Build command for Ginpar projects. + +This module implements the building command for the ginpar static content +generator. + +`build` will read the configuration file in search for `build_path` and +`source_path`. If not defined, `build` will use `"public"` and +`"sketches`, respectively. + +Examples +-------- + +To build your project according to your specifications in `config.yaml`:: + + ginpar build - Implements the generation of the static site. +To build targeting a custom path `_site/`:: + + ginpar build --path="_site" + +Notes +----- + +You cannot specify the content path. It is either ``config.content_path`` or +``"sketches"``. """ + import os import shutil + import yaml import click - from jinja2 import Environment, FileSystemLoader -from ginpar.settings import read_config import ginpar.generators as gg - from ginpar.utils.echo import echo, success from ginpar.utils.strings import unkebab import click -_SITE_FILE = "config.json" -_SITE = read_config(_SITE_FILE) -_THEME = _SITE["theme"] -_TEMPLATES_PATH = os.path.join("themes", _THEME, "templates") -_SKETCHES_PATH = _SITE["content_path"] -_jinja_env = Environment(loader=FileSystemLoader(_TEMPLATES_PATH), trim_blocks=True) -_jinja_env.filters["unkebab"] = unkebab +def get_sketches(content_path): + """Obtain the list of **valid** sketches inside `path`. -def build_link(sketch): - title = sketch.split("/")[-1].split(".")[0] - return f'{title}
\n' + Valid sketches are directories containing at least two files: + `sketch.js` and `data.yaml`. + This function will create a list of sketch objects containing + `name`, `script`, and `data`. -def build_index(sketches): - content = "" - for s in sketches: - content += build_link(s) - return content + Parameters + ---------- + content_path : str + The path containing the sketches to fetch. + Returns + ------- + list + Individual elements contain `{"name", "script", "data"}`. + """ -def get_sketches(path): sketches = [] + # Create a list with all the directories inside path - for r, d, _ in os.walk(path): + for r, d, _ in os.walk(content_path): for sketch in d: sketches.append( { @@ -65,6 +84,21 @@ def get_sketches(path): def convert_information(sketch): + """Convert the `["data"]` field of a sketch into a Python dictionary. + + Parameters + ---------- + sketch : dict + It contains the sketch information. Must contain `["data"]`, and + `["data"]` must be a YAML-valid string. + + + Returns + ------- + dictionary + `sketch` but with the updated `["data"]` field. + """ + path = sketch["data"] with open(path, "r") as stream: try: @@ -75,81 +109,184 @@ def convert_information(sketch): return sketch -def create_publishing_directory(path): - if os.path.exists(path): - shutil.rmtree(path) - os.mkdir(path) +def create_publishing_directory(build_path): + """Remove existing directories with the same name, and create again. + + Parameters + ---------- + build_path : str + Path of the build. + """ + + if os.path.exists(build_path): + shutil.rmtree(build_path) + os.mkdir(build_path) + + +def copy_theme(build_path, theme): + """Copy the theme static content into the build static directory. + + Parameters + ---------- + build_path : str + Path of the build. + theme : str + Name of the theme to install. + """ -def copy_theme(path, theme_path): ## Copy the static/ folder of the theme shutil.copytree( - os.path.join("themes", theme_path, "static"), os.path.join(path, "static") + os.path.join("themes", theme, "static"), os.path.join(build_path, "static") ) -def render_index(path, sketches, site): - ## Create an index to contain all the sketches - _index_template = _jinja_env.get_template("index.html") - index = open(os.path.join(path, "index.html"), "w") +def render_index(build_path, sketches, site, page_template): + """Render the index using the list of sketches and site configuration + + The index is rendered using a Jinja2 template inside the theme `templates/` + directory. + + The index template must receive `sketches`, containing a list of the sketches + names; and `site`, containing the site configuration from ``config.yaml``. + + Parameters + ---------- + build_path : str + Path of the build. + + sketches : list + Contains all the sketches in the project. Must contain at leas `["name"]`. + + site : dict + Contains the site information, such as `sitename` and `author`. + + page_template : Jinja2.Template + Jinja2 template to render the sketch. + """ + + # Open the index in the build path for writing + index = open(os.path.join(build_path, "index.html"), "w") + + # Write the contents of the rendered template into the index file index.write( - _index_template.render(sketches=map(lambda a: a["name"], sketches), site=site) + page_template.render(sketches=map(lambda a: a["name"], sketches), site=site) ) index.close() -def render_sketch_page(path, s, site): +def render_sketch_page(build_path, sketch, site, page_template): + """Render a sketch page + + This generates the page for a single sketch. This will convert the + `sketch["data"]` into a form that will control the variables of the + script. + + When `sketch["data"]` doesn't define fields that may be used at the moment + of the form generation, Ginpar will instead look up for those fields in + `site["sketch_defaults"]`. + + When both `sketch["data"]` and `site["sketch_defaults"]` don't define those + fields, Ginpar will use the default values. + + `Ginpar default values for sketch data `_ + + Parameters + ---------- + + build_path : str + Path of the build. + + sketch : dict + Sketch information. Must contain `["data"]` and `["name"]` + + site : dict + Site configuration. + + page_template : Jinja2.Template + Jinja2 template to render the sketch. + """ ## Create a directory with the sketch title - os.mkdir(f"public/{s['name']}") + os.mkdir(os.path.join(build_path, sketch["name"])) ## Convert the form JSON into a dict - form_dict = s["data"] + form_dict = sketch["data"] ## Add name key to the dict elements form_dict = gg.add_name(form_dict) ## Create index.html - _sketch_template = _jinja_env.get_template("sketch.html") - sketch_index = open(f"public/{s['name']}/index.html", "w+") + sketch_index = open(f"public/{sketch['name']}/index.html", "w+") sketch_index.write( - _sketch_template.render( - sketch=unkebab(s["name"]), form=gg.sketch_index(form_dict), site=_SITE + page_template.render( + sketch=unkebab(sketch["name"]), form=gg.sketch_index(form_dict), site=site ) ) sketch_index.close() ## Create sketch.js - sketch_path = f"public/{s['name']}/sketch.js" - sketch = open(sketch_path, "w+") + sketch_path = f"public/{sketch['name']}/sketch.js" + sketch_script = open(sketch_path, "w+") ## Copy all the content from original sketches/{title}.js to sketch.js - sf = open(s["script"], "r") + sf = open(sketch["script"], "r") - sketch.write(gg.makeValueGetter(form_dict)) + sketch_script.write(gg.makeValueGetter(form_dict)) for x in sf.readlines(): - sketch.write(x) + sketch_script.write(x) sf.close() - sketch.close() + sketch_script.close() + +def read_config(path): + """Create a dictionary out of the YAML file received + + Paremeters + ---------- + path : str + Path of the YAML file. + """ + with open(path, "r") as stream: + try: + config = yaml.safe_load(stream) + except yaml.YAMLError as exc: + print(exc) + return config def build(path): + """Main function of the module. This is what `ginpar build` calls. + + Parameters + ---------- + build_path : str + Path of the build. + """ + + _SITE_FILE = "config.yaml" + _SITE = read_config(_SITE_FILE) + _THEME = _SITE["theme"] + _TEMPLATES_PATH = os.path.join("themes", _THEME, "templates") + _SKETCHES_PATH = _SITE["content_path"] + _jinja_env = Environment(loader=FileSystemLoader(_TEMPLATES_PATH), trim_blocks=True) + _jinja_env.filters["unkebab"] = unkebab + create_publishing_directory(path) echo(f"Building in `{os.path.abspath(path)}`") copy_theme(path, _THEME) echo(f"Building using theme `{_THEME}`") - ## Create a sketches array + ## Create the sketches list sketches = list(get_sketches(_SKETCHES_PATH)) echo(f"Found {len(sketches)} sketch(es)") - render_index(path, sketches, _SITE) + render_index(path, sketches, _SITE, _jinja_env.get_template("index.html")) echo("Building main page") echo("Building sketches:") - for s in sketches: - echo(f" Building {s['name']}") - render_sketch_page(path, s, _SITE) + for sketch in sketches: + echo(f" Building {sketch['name']}") + render_sketch_page(path, sketch, _SITE, _jinja_env.get_template("sketch.html")) success("Success.") diff --git a/ginpar/cli.py b/ginpar/cli.py index 22b8d52..d69dfeb 100644 --- a/ginpar/cli.py +++ b/ginpar/cli.py @@ -1,8 +1,14 @@ -""" - ginpar.cli - ~~~~~~~~~~ +"""Definition of the CLI commands for Ginpar. + +This module defines the different commands available for the ginpar static +content generator. + +Examples +-------- + +To get the list of available commands and options run:: - Implements the command line application to manage ginpar projects. + ginpar """ import click @@ -12,7 +18,8 @@ @click.version_option(message="%(prog)s v%(version)s") def cli(): """ - Ginpar is an extra simple static content generator for interactive and parametrisable p5 canvases. + Ginpar is a static content generator for interactive P5.js sketches, + awkwardly named after Generative Interactive Parametrisable Canvases. """ pass @@ -23,10 +30,22 @@ def cli(): "-p", default="public", type=click.Path(), - help="The PATH for the generated site.\nDefault = public", + help=( + "The PATH where the site will be built. [ , public ] " + "This path is relative to the current directory. When no option is provided " + "Ginpar will read the from the configuration file." + ), ) def build(path): - """Build a static website in PATH""" + """Build the project content into PATH. + + `ginpar build` will read your configuration file, fetch all the sketches inside + your , and build your static site inside PATH, which + defaults to , or public if it doesn't exist. + + This operation will wipe all the content from PATH in each run, so you must not + make modifications you expect to preserve. + """ from ginpar.build import build as ginpar_build click.echo("") @@ -40,46 +59,52 @@ def build(path): "-f", default=False, is_flag=True, - help="Remove existing directories that may interfere with the initialization", + help=( + "If Ginpar finds an existing directory with the same name of the " + "project being initialized, it'll force its removal. " + "Only do this if you're completely sure you want to do it." + ), ) @click.option( "--quick", "-q", default=False, is_flag=True, - help="Skip the configuration prompts and use the default values", + help=( + "Skip the prompts and use the default values for the configuration file. " + "You can still modify the variables later by manually updating your " + "configuration file." + ), ) -@click.option( - "--path", - "-p", - default="", - help="The PATH to initialize the project. Defaults to ./", -) -def init(force, path, quick): - """Initialize a new project in PATH""" +def init(force, quick): + """Initialize a new project in PATH. + + `ginpar init` will prompt you for a series of values that will be used to + generate the configuration and file structure of your project. + """ from ginpar.init import init as ginpar_init click.echo("") - ginpar_init(force, path, quick) + ginpar_init(force, quick) click.echo("") @cli.command() -@click.argument("sketch", default="new-sketch") -@click.option( - "--path", - "-p", - default="sketches", - type=click.Path(), - help="The path for the newly created sketch", -) -def new(sketch, path): - """Create a new SKETCH in PATH""" - click.secho(f"Attemping to create `{sketch}` in `{path}/`", fg="blue") +@click.argument("sketch") +def new(sketch): + """Create a new SKETCH. + + `ginpar new` will create a new sketch structure inside your . + + You must specify the name of the sketch. + + If there's an existing sketch with the same name, it'll throw an error and ask + for a different name. + """ from ginpar.new import new as ginpar_new click.echo("") - ginpar_new(sketch, path) + ginpar_new(sketch) click.echo("") @@ -89,24 +114,36 @@ def new(sketch, path): "-f", default=False, is_flag=True, - help="Remove existing directories that may interfere the quickstart", -) -@click.option( - "--path", "-p", default="./", help="The path the demo content will be copied to." + help=( + "If Ginpar finds an existing directory with the same name of the sample content, " + "it'll force its removal. " + "Only do this if you're completely sure you want to do it." + ), ) -def quickstart(force, path): - """Load a working example in PATH""" +def quickstart(force): + """Import a working sample project. + + `ginpar quickstart` will download the contents of the sample project, hosted + at github: davidomarf/ginpar-quickstart in the current directory. + """ from ginpar.quickstart import quickstart as ginpar_quickstart click.echo("") - ginpar_quickstart(force, path) + ginpar_quickstart(force) click.echo("") @cli.command() -@click.option("--port", "-p", default="8080", help="Port for the web server") +@click.option("--port", "-p", default=8080, help="Port of the server") def serve(port): - """Serve the content using PORT""" + """Start a new server in localhost:PORT. + + `ginpar serve` will trigger `ginpar build`, and start a new server inside + . + + Every time you modify a file that is part of the project's source code the + site gets built again. + """ from ginpar.serve import serve as ginpar_serve click.echo("") diff --git a/ginpar/init.py b/ginpar/init.py index 9283b2d..860c790 100644 --- a/ginpar/init.py +++ b/ginpar/init.py @@ -1,80 +1,116 @@ -""" - ginpar.init - ~~~~~~~~~~~ +"""Init command for Ginpar projects. + +This module implements the initialization command for the ginpar static content +generator. + +`init` will prompt for a series of values to write the site configuration file. + +Examples +-------- + +To initialize a project in a standard way to specify the configuration values:: - Implements the initialization of a new project. + ginpar init + +To skip the prompts and initialize the project with the default values:: + + ginpar init --quick + ginpar init --q + +To force the initialization in case there is a directory with the same name +of the project to initialize:: + + ginpar init --force + ginpar init -f """ + import os + import click +import yaml from ginpar.utils.echo import info, echo, success, error, alert from ginpar.utils.files import create_file, create_folder, try_remove from ginpar.utils.strings import space_to_kebab -from jinja2 import Environment, FileSystemLoader - -_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") -_jinja_env = Environment(loader=FileSystemLoader(_TEMPLATES_DIR), trim_blocks=True) +def prompt_site_config(quick): + """Echo the prompts and create the configuration dict. + + Echo the instructions and configuration fields, store each input, + and create a dictionary containing those values. + + Parameters + ---------- + quick : bool + Returns the default values immediatle if True. + + Returns + ------- + dict + Used to generate the site configuration file. + """ + site = { + "author": "David Omar", + "sitename": "My site", + "description": "This is a Ginpar project", + "url": "/", + "theme": "gart", + "content_path": "sketches", + "build_path": "public", + } + if quick: + return site -def prompt_site_config(): info("Welcome to ginpar! We'll ask for some values to initialize your project.") - click.pause() echo("") - sitename = click.prompt("Site name", default="My Site") - description = click.prompt("Description", default="Cool site") - author = click.prompt("Author", default="John Doe") - url = click.prompt("url", default="johndoe.com") + site["sitename"] = click.prompt("Site name", default=site["sitename"]) + site["description"] = click.prompt("Description", default=site["description"]) + site["author"] = click.prompt("Author", default=site["author"]) + site["url"] = click.prompt("url", default=site["url"]) info("\nIf you're unsure about the next prompts, accept the defaults") - click.pause() echo("") - theme = click.prompt("Theme", default="gart") - content_path = click.prompt("Sketches path", default="sketches") - build_path = click.prompt("Build path", default="public") - return { - "author": author, - "sitename": sitename, - "description": description, - "url": url, - "theme": theme, - "content_path": content_path, - "build_path": build_path, - } + site["theme"] = click.prompt("Theme", default=site["theme"]) + site["content_path"] = click.prompt("Sketches path", default=site["content_path"]) + site["build_path"] = click.prompt("Build path", default=site["build_path"]) + return site -def init(force, path, quick): - """""" - _config_template = _jinja_env.get_template("config.json.jinja2") + +def init(force, quick): + """Main function of the module. This is what `ginpar init` calls. + + Parameters + ---------- + force : bool + Remove conflicting files when true. + + quick : bool + Skip prompts when true. + """ if force: alert("You're forcing the initialization.") alert("This will replace any existent file relevant to the project.") click.confirm("Do you want to proceed?", abort=True) - if quick: - content_path = os.path.join("my-site", "sketches") - config_json = os.path.join("my-site", "config.json") - config_dict = _config_template.render() - else: - site = prompt_site_config() - - path = space_to_kebab(site["sitename"]).lower() - print(site["content_path"]) - echo("\n---\n") + site = prompt_site_config(quick) + path = space_to_kebab(site["sitename"]).lower() - content_path = os.path.join(path, site["content_path"]) - config_json = os.path.join(path, "config.json") - config_dict = _config_template.render(site) + content_path = os.path.join(path, site["content_path"]) + config_yaml = os.path.join(path, "config.yaml") + echo("\n---\n") + if force: echo("\n---\n") - try_remove(content_path) - try_remove(config_json) + try_remove(path) echo("\n---\n") create_folder(content_path) - create_file(config_json, config_dict) + with open(config_yaml, "w") as file: + yaml.dump(site, file) echo("\n---\n") success( diff --git a/ginpar/new.py b/ginpar/new.py index a4b3f89..b329902 100644 --- a/ginpar/new.py +++ b/ginpar/new.py @@ -1,11 +1,81 @@ -""" - ginpar.new - ~~~~~~~~~~ +"""New sketch creation command for Ginpar projects. + +This module implements the sketch creation command for the ginpar static content +generator. + +`new` will read the configuration file in search for `source_path` and +create a new directory in there with the specified name, which by default is +`new-sketch-{n}`. + +This directory will contain the two required files with a boilerplate code. + +Examples +-------- - Implements the creation of a new sketch in the project. +To create a new sketch `rectangle`:: + + ginpar new rectangle + +To start a new sketch with the default name `new-sketch-{n}`:: + + ginpar new """ +import os +import yaml + import click +from jinja2 import Environment, FileSystemLoader + +from ginpar.utils.files import create_folder, create_file +from ginpar.utils.echo import echo, error, success + +## TODO: Move read_config into a shared library inside utils +def read_config(path): + """Create a dictionary out of the YAML file received + + Paremeters + ---------- + path : str + Path of the YAML file. + """ + with open(path, "r") as stream: + try: + config = yaml.safe_load(stream) + except yaml.YAMLError as exc: + print(exc) + return config + + +def new(sketch): + """Main function of the module. This is what `ginpar new` calls. + + Parameters + ---------- + sketch : str + Name of the sketch to create + """ + + _SITE = "config.yaml" + site = read_config(_SITE) + + path = os.path.join(site["content_path"], sketch) + + _TEMPLATES_DIR = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "templates", "sketch" + ) + _jinja_env = Environment(loader=FileSystemLoader(_TEMPLATES_DIR), trim_blocks=True) + + if os.path.isdir(path): + error(f"Failure.") + echo(f"{path} already exists.") + raise click.Abort() + + create_folder(path) + + sketch_template = _jinja_env.get_template("sketch.js") + data_template = _jinja_env.get_template("data.yaml") + create_file(os.path.join(path, "sketch.js"), sketch_template.render()) + create_file(os.path.join(path, "data.yaml"), data_template.render()) -def new(sketch, path): - click.secho("You're in new", fg="blue") + echo(f"\nYour new sketch {path} is ready.\n") \ No newline at end of file diff --git a/ginpar/quickstart.py b/ginpar/quickstart.py index ec4da98..caab562 100644 --- a/ginpar/quickstart.py +++ b/ginpar/quickstart.py @@ -1,60 +1,109 @@ -""" - ginpar.quickstart - ~~~~~~~~~~~~~~~~~ +"""Quickstart command for Ginpar projects. + +This module implements the quickstart command for the ginpar static content +generator. + +`quickstart` will download the contents of the sample repository hosted at +`davidomarf/ginpar-quickstart `_ +into `./quickstart`. + +This is aimed to provide an easier and faster way to start working in a Ginpar +project for people who isn't familiar with static content generators. + +Example +------- - Implements the importing of demo content into a new project. +To create `./quickstart` and copy the contents of the sample repository:: + + ginpar quickstart + +If there's another directory named `quickstart`, you can force this command, +removing the existing directory:: + + ginpar quickstart --force + ginpar quickstart -f """ import os -import shutil -from pathlib import Path -import sys +import subprocess -from jinja2 import Environment, FileSystemLoader +import click -from ginpar.utils.echo import alert, success, error, info, echo -from ginpar.utils.files import try_remove, copy_folder +from ginpar.utils.echo import success, alert, echo, error +from ginpar.utils.files import try_remove -import click +def clone_repo(repo, path): + """Clone the contents of a repository in a custom path -_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + Parameters + ---------- + repo : str + GitHub repository as in "{USER}/{REPO}" + path : str + Path to clone the repository to + """ -_THEMES_DIR = os.path.join(Path(os.path.dirname(os.path.abspath(__file__))), "themes") + repo_url = f"https://github.com/{repo}.git" + + echo(f"> Cloning {repo} in `{path}`") + try: + subprocess.call(["git", "clone", repo_url, path, "--quiet"]) + except OSError: + error("You don't have git installed in your machine.") + echo("Please install git and rerun or download the files manually:") + echo(f"\t{repo_url}") + return 1 + + success(f"Successfully cloned {repo}.\n") + return 0 -_SKETCHES_DIR = os.path.join( - Path(os.path.dirname(os.path.abspath(__file__))), "sketches" -) -_jinja_env = Environment(loader=FileSystemLoader(_TEMPLATES_DIR), trim_blocks=True) +def delete_git_files(path): + """Delete the git files to only keep the relevant files + Parameters + ---------- + path : str + Path to look for git files + """ -def init_config(): - click.secho("\n> Creating `config.json` using template:") + echo("> Deleting .git files") try: - config = open("config.json", "r") + try_remove(os.path.join(path, ".git")) + success("Successfully deleted .git files") except: - try: - config = open("config.json", "w+") - except: - error("Failure.") - else: - _template = _jinja_env.get_template("config.json.jinja2") - config.write(_template.render()) - config.close() - success("Success.") - else: - error("Failure. It already exists.") + error(f"Couldn't delete files. Delete all .git files manually in `{path}`") + return 1 + + return 0 -def quickstart(force, path): +def quickstart(force): + """Main function of the module. This is what `ginpar quickstart` calls. + + Parameters + ---------- + force : bool + Remove conflicting files when true. + """ + + repo = "davidomarf/ginpar-quickstart" + path = os.path.abspath("quickstart") + if force: - alert("Forcing quickstart. This will replace existent directories and files.") - try_remove("sketches") - try_remove("themes") - try_remove("config.json") + alert("Forcing quickstart. This will replace existent directories and files.\n") + try_remove("quickstart") echo("") - info(f"Copying demo content into `{os.path.abspath(path)}`") - copy_folder(_THEMES_DIR, "themes") - copy_folder(_SKETCHES_DIR, "sketches") - init_config() # FIXME Use a copy_file or render_file option imported from utils instead + if os.path.isdir(path): + error(f"`{path}` already exists.") + echo("Delete it manually or run `ginpar quickstart -f` to force") + raise click.Abort() + + if clone_repo(repo, path) == 0: + if delete_git_files(path) == 0: + echo(f"\nThe Ginpar sample site is ready.\n") + echo("Run `cd quickstart` to move to the project directory.") + echo("Then run `ginpar build` or `ginpar serve` to see it working.") + else: + raise click.Abort() \ No newline at end of file diff --git a/ginpar/serve.py b/ginpar/serve.py index a3a0b92..0312051 100644 --- a/ginpar/serve.py +++ b/ginpar/serve.py @@ -1,11 +1,55 @@ -""" - ginpar.serve - ~~~~~~~~~~~~ +"""Serve command for Ginpar projects. + +This module implements the server starting command for the ginpar static content +generator. + +`serve` will start a live-reloading server in a specified port. + +Examples +-------- + +To start a new server in the default port `8080`:: + + ginpar serve + +To start a new server in a custom port:: - Implements the serving of the generated content. + ginpar serve --port=3000 + ginpar serve -p=3000 """ import click +import yaml +from livereload import Server, shell + + +## TODO: Move read_config into a shared library inside utils +def read_config(path): + """Create a dictionary out of the YAML file received + + Paremeters + ---------- + path : str + Path of the YAML file. + """ + with open(path, "r") as stream: + try: + config = yaml.safe_load(stream) + except yaml.YAMLError as exc: + print(exc) + return config def serve(port): - click.secho("You're serving", fg="blue") + """Main function of the module. This is what `ginpar serve` calls. + + Parameters + ---------- + port : int + The port of the server + """ + site = read_config("config.yaml") + + server = Server() + + server.watch(site["content_path"], 'ginpar build') + server.serve(port=port, root=site["build_path"]) \ No newline at end of file diff --git a/ginpar/settings.py b/ginpar/settings.py deleted file mode 100644 index 93d9f62..0000000 --- a/ginpar/settings.py +++ /dev/null @@ -1,7 +0,0 @@ -import json - - -def read_config(path): - with open(path, "r") as f: - config = json.load(f) - return config diff --git a/ginpar/sketches/domino-dancing/data.yaml b/ginpar/sketches/domino-dancing/data.yaml deleted file mode 100644 index 4f4d9bd..0000000 --- a/ginpar/sketches/domino-dancing/data.yaml +++ /dev/null @@ -1,34 +0,0 @@ ---- -- var: DIMENSIONS - attrs: - type: dimensions - value: - - 2048 - - 2560 -- var: NUMBER_OF_COLUMNS - attrs: - type: numbera - value: 30 - step: 1 -- var: COLUMN_Y_SD - name: Columns offset sd - attrs: - type: number - value: 300 - step: 1 -- var: COLUMN_HF_MIN - name: Minimum column height factor - attrs: - type: range - value: 0.1 - step: 0.01 - min: 0 - max: 1 -- var: COLUMN_HF_MAX - name: Maximum column height factor - attrs: - type: range - value: 0.6 - step: 0.01 - min: 0 - max: 1 diff --git a/ginpar/sketches/domino-dancing/sketch.js b/ginpar/sketches/domino-dancing/sketch.js deleted file mode 100644 index fa4a21c..0000000 --- a/ginpar/sketches/domino-dancing/sketch.js +++ /dev/null @@ -1,287 +0,0 @@ -/** - * Standard function of p5js - */ -function setup() { - createCanvas(DIMENSIONS[0], DIMENSIONS[1]).parent("artwork-container"); - - // Call draw() only once - noLoop(); -} - -/** - * Standard function of p5js - */ -function draw() { - // Set a background color - background(225); - - // Don't draw the stroke of the shapes, and fill with gray - noStroke(); - fill(30); - - let columns = generateColumns(NUMBER_OF_COLUMNS); - - // The index starts at 2 and ends at (30 - 2) to avoid drawing - // the two leftmost and rightmost columns in the canvas. - for (let i = 2; i < NUMBER_OF_COLUMNS-2; i++) { - drawColumn(columns[i]); - let squares = generateSquaresForColumn(columns[i]); - drawSquares(squares, columns[i]); - } -} - -/** - * Generates a custom number of columns across the width and height - * of the canvas - * - * @param {number} n Indicates the number of columns to generate - */ -function generateColumns(n) { - let columns = []; - - for (let i = 0; i < n; i++) { - // Create a column. Here, x represents the top left corner, - // and y the y coordinate of the middle point. - let column = { - x: i * (DIMENSIONS[0] / n), - y: DIMENSIONS[1] / 2 + randomGaussian(0, COLUMN_Y_SD), - "name": "Column Y standard deviation", - width: DIMENSIONS[0] / (n * 1.3), - height: random(DIMENSIONS[1] * COLUMN_HF_MIN, DIMENSIONS[1] * COLUMN_HF_MAX) - }; - - // Change the x value to be the x coordinate of the middle point. - column.x = column.x + column.width / 2; - - // Set the borders of the column - column.borders = { - top: column.y - column.height / 2, - bottom: column.y + column.height / 2, - left: column.x - column.width / 2, - right: column.x + column.width / 2 - }; - - columns.push(column); - } - - return columns; -} - -/** - * Calls the function to draw a rectangle by calling it with attributes - * of the column object - * @param {Object} column Contains the information to build a column - * At least {x, y, width, height} - */ -function drawColumn(column) { - handDrawRectangleMiddle( - { x: column.x, y: column.y }, - column.width, - column.height - ); -} - -/** - * Generate the individual little squares that are aligned with a single - * column - * @param {Object} column Contains the information to build a column - * At least {wi} - */ -function generateSquaresForColumn(column) { - let squares = []; - - // Don't need to count. Will break when the middle of the squares lies - // outside the canvas dimensions - for (let j = 0; ; j++) { - squares.push({ - middle: { - x: column.x, - y: (column.width * j + column.width * 0.5 * j) / 2 - }, - size: 1 - }); - - // Square is outside the canvas dimensions - if (squares[j].middle.y >= DIMENSIONS[1]) break; - } - return squares; -} - -/** - * Draws all the squares in a vertical line associated with a single column - * - * @param {Object[]} squares Contains all the squares from a single column - * @param {Object} column Contains the information to build a column - */ -function drawSquares(squares, column) { - // Push new p5js style configuration values - push(); - - // This makes p5js rect() take the center coordinates as the first two - // parameters, instead of the top left corner - rectMode(CENTER); - - // Don't draw the stroke of the shapes - noStroke(); - - for (let j = 0; j < squares.length; j++) { - // Set a base color of (almost-)white that will be used in the default case - colorMode(RGB); - fill(225); - - // If the square is outside the column, the color is a black one, with a - // variable opacity that gets lower the further away from the center - if (isSquareOutsideColumn(squares[j], column)) { - // Create a custom RGBA color just to change the opacity - fill( - `rgba(30, 30, 30, ${1 - - abs(DIMENSIONS[1] / 2 - squares[j].middle.y) / (DIMENSIONS[1] / 2)})` - ); - - // The squares outside the column are subject to a probability of not - // being drawn. This probability increases the further away from the center. - if (random() * 0.9 < abs(DIMENSIONS[1] / 2 - squares[j].middle.y) / (DIMENSIONS[1] / 2)) - continue; - } - - // With a proability of .1, change the color to one of the three using the - // hues 40, 350, and 220. - // This overrules the color of the squares outside columns, including opacity. - if (random() > 0.9) { - colorMode(HSL); - fill(random([40, 350, 220]), 60, 40); - } - - // Draw a single square that deviates from its middle point, and with a - // height that deviates from the standard (0.25 of the width of the column) - handDrawRectangleMiddle( - { - x: randomGaussian(squares[j].middle.x, 2), - y: randomGaussian(squares[j].middle.y, 2) - }, - column.width * 0.4, - randomGaussian(column.width * squares[j].size - column.width * 0.75, 6) - ); - } - - // Return to the previous p5js specifications set outside this function - pop(); -} - -/* ---------------------------- Helping Functions --------------------------- */ - -/** - * Determines if a square is outside the column - * - * @param {Object} square Contains the information to build a square - * At least {middle.y} - * @param {Object} column Contains the information to build a column - * At least {top, bottom} - */ -function isSquareOutsideColumn(square, column) { - return ( - square.middle.y < column.borders.top || - square.middle.y > column.borders.bottom - ); -} - -/** - * Calculates the four {x, y} points that represent the corners of a - * rectangle with specified middle, and height and width values. - * - * @param {Object} middle Coordinates of the middle of the rectangle - * @param {number} width Width of the rectangle - * @param {number} height Height of the rectangle - * @returns {Object} Contains the corners in the order - * 1---2 - * | | - * 4---3 - */ -function getCornersFromMiddle(middle, width, height) { - let halfW = width / 2; - let halfH = height / 2; - return [ - { x: middle.x - halfW, y: middle.y - halfH }, - { x: middle.x + halfW, y: middle.y - halfH }, - { x: middle.x + halfW, y: middle.y + halfH }, - { x: middle.x - halfW, y: middle.y + halfH } - ]; -} - -/** - * Angle of the line that joins two different points - * - * @param {Object} p1 {x, y} coordinates of the first point - * @param {Object} p2 {x, y} coordinates of the second point - * @returns {number} Angle in radians - */ -function angleBetweenPoints(p1, p2) { - return atan2(p2.y - p1.y, p2.x - p1.x); -} - -/** - * Produce the array of points that emulates a shaky-straight line - * joining two points a, b. - * @param {Object} a {x, y} coordinates of the first point - * @param {Object} b {x, y} coordinates of the second point - */ -function getHandDrawnLine(a, b) { - // Generating a different seed for every line avoids getting - // the same curve-pattern in different lines. - noiseSeed(random(1000000)); - - // The number of points is calculated in function to the distance - // between the points. - let distance = dist(a.x, a.y, b.x, b.y); - let internalPoints = distance; - let angle = angleBetweenPoints(a, b); - - let pointsInLine = []; - - // Create a line with every point perfectly aligned - for (let i = 0; i < internalPoints; i++) { - pointsInLine.push({ - x: a.x + i * ((distance * cos(angle)) / internalPoints), - y: a.y + i * ((distance * sin(angle)) / internalPoints) - }); - } - - // Modify each point by displacing it in a perpendicular direction - // to the one of the line - for (let i = 0; i < pointsInLine.length; i++) { - let displacement = 3 * noise(i * 0.2) - 0.5; - let perpendicular = angle + PI / 2; - pointsInLine[i].x += displacement * cos(perpendicular); - pointsInLine[i].y += displacement * sin(perpendicular); - } - - return pointsInLine; -} - -/** - * Draw a rectangle in a hand drawn style specifiying the coordinates of - * the midpoint and the width and height values. - * - * @param {Object} middle {x, y} coordinates of the midpoint. - * @param {number} width Width of the rectangle - * @param {number} height Height of the rectangle - */ -function handDrawRectangleMiddle(middle, width, height) { - beginShape(); - - // Get the coordinates of every corner of the rectangle - let corners = getCornersFromMiddle(middle, width, height); - - // Draw a line joining every contiguous corner - for (let i = 0; i < 4; i++) { - let handLine = getHandDrawnLine( - corners[i], - corners[(i + 1) % corners.length] - ); - - // For every point in the hand drawn line, create a vertex - handLine.map(e => vertex(e.x, e.y)); - } - - endShape(CLOSE); -} diff --git a/ginpar/templates/config.json.jinja2 b/ginpar/templates/config.json.jinja2 deleted file mode 100644 index 2761fb4..0000000 --- a/ginpar/templates/config.json.jinja2 +++ /dev/null @@ -1,13 +0,0 @@ -{ - "author": "{{author or "David Omar"}}", - "sitename": "{{sitename or "Ginpar"}}", - "description": "{{description or "Interactive Sketches"}}", - "url": "{{url or "./"}}", - "theme": "{{theme or "gart"}}", - "content_path": "{{content_path or "./sketches"}}", - "build_path": "{{build_path or "./public"}}", - "scripts": [{ - "tag": "p5", - "url": "https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js" - }] -} \ No newline at end of file diff --git a/ginpar/templates/sketch/data.yaml b/ginpar/templates/sketch/data.yaml new file mode 100644 index 0000000..2ce5a11 --- /dev/null +++ b/ginpar/templates/sketch/data.yaml @@ -0,0 +1,7 @@ +--- +- var: DIMENSIONS + attrs: + type: dimensions + value: + - 2048 + - 2048 diff --git a/ginpar/templates/sketch/sketch.js b/ginpar/templates/sketch/sketch.js new file mode 100644 index 0000000..c8ea081 --- /dev/null +++ b/ginpar/templates/sketch/sketch.js @@ -0,0 +1,23 @@ +/* You don't need to initialize your variables in here. However, if you decide + * to do it, you **must** use `var` instead of `let` or `const`. + */ + +var DIMENSIONS = [400, 2048] + +/** + * Standard function of p5js + */ +function setup() { + createCanvas(DIMENSIONS[0], DIMENSIONS[1]).parent("artwork-container"); + + // Call draw() only once + noLoop(); +} + +/** + * Standard function of p5js + */ +function draw() { + // Set a background color + background(225); +} diff --git a/ginpar/tools/quickstart.py b/ginpar/tools/quickstart.py deleted file mode 100644 index 923ab45..0000000 --- a/ginpar/tools/quickstart.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -import shutil -from pathlib import Path -import sys - -from jinja2 import Environment, FileSystemLoader - - -_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - -_THEMES_DIR = os.path.join( - Path(os.path.dirname(os.path.abspath(__file__))).parent, "themes" -) - -_SKETCHES_DIR = os.path.join( - Path(os.path.dirname(os.path.abspath(__file__))).parent, "sketches" -) - -_jinja_env = Environment(loader=FileSystemLoader(_TEMPLATES_DIR), trim_blocks=True) - - -def argv_to_flag_dict(argv): - """Take an arguments vector and convert it into a dictionary""" - possible_args = ["--force"] - flags = {} - for arg in possible_args: - if arg in argv: - flags[arg] = True - return flags - - -def create_folder(folder): - print(f"Creating `{folder}`:", end="\n\t") - try: - os.mkdir(folder) - except FileExistsError: - print(f"Failure. It already exists.") - except: - print(f"Failure.") - else: - print(f"Sucess") - - -def init_config(): - print("\nCreating `config.json` using template:", end="\n\t") - try: - config = open("config.json", "r") - except: - try: - config = open("config.json", "w+") - except: - print("Failure.") - else: - _template = _jinja_env.get_template("config.json.jinja2") - config.write(_template.render()) - config.close() - print("Success.") - else: - print("Failure. It already exists.") - - -def copy_folder(fr, to): - print(f"\nCopying `{to}` from `{fr}`:", end="\n\t") - try: - shutil.copytree(fr, to) - except FileExistsError: - print(f"Failure. It already exists.") - else: - print(f"Success.") - - -def try_remove(path): - if os.path.isdir(path): - print(f"`{path}` already exists. Attemping removal:", end="\n\t") - try: - shutil.rmtree(path) - except: - print(f"Failure. Restart or delete manually.") - else: - print(f"Success.") - elif os.path.isfile(path): - print(f"`{path}` already exists. Attemping removal:", end="\n\t") - try: - os.remove(path) - except: - print(f"Failure. Restart or delete manually.") - else: - print(f"Success.") - else: - print(f"`{path}` doesn't exist. Skipping.") - - -def main(): - flags = argv_to_flag_dict(sys.argv) - - if "--force" in flags: - print( - "\n---\nForcing quickstart. This will replace existent directories and files.\n---\n" - ) - try_remove("sketches") - try_remove("themes") - try_remove("config.json") - - print("\n---\nInitializing the project with default values\n---\n") - copy_folder(_THEMES_DIR, "themes") - copy_folder(_SKETCHES_DIR, "sketches") - init_config() - - -if __name__ == "__main__": - main() diff --git a/ginpar/utils.py b/ginpar/utils.py deleted file mode 100644 index 619263f..0000000 --- a/ginpar/utils.py +++ /dev/null @@ -1,35 +0,0 @@ -""" - ginpar.utils - ~~~~~~~~~~~~ - - Implements the utilities used across modules in this package. -""" - -import click - - -def unkebab(s): - return " ".join(s.split("-")) - - -# ------------------------------ Throw messages ------------------------------ # - - -def echo(m): - click.echo(m) - - -def info(m): - click.secho(m, fg="blue") - - -def success(m): - click.secho(m, fg="green") - - -def error(m): - click.secho(m, fg="red") - - -def alert(m): - click.secho(m, fg="yellow") diff --git a/requirements.txt b/requirements.txt index 7f74540..7b3dbd5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,56 @@ +alabaster==0.7.12 +appdirs==1.4.3 +argh==0.26.2 astroid==2.3.2 +attrs==19.3.0 +Babel==2.7.0 +black==19.3b0 +certifi==2019.9.11 +chardet==3.0.4 Click==7.0 +doc8==0.8.0 +docutils==0.15.2 +idna==2.8 +imagesize==1.1.0 +importlib-metadata==0.23 isort==4.3.21 Jinja2==2.10.3 lazy-object-proxy==1.4.2 +livereload==2.6.1 MarkupSafe==1.1.1 mccabe==0.6.1 +more-itertools==7.2.0 +packaging==19.2 +Pallets-Sphinx-Themes==1.2.2 +pathtools==0.1.2 +pbr==5.4.3 +port-for==0.3.1 +Pygments==2.4.2 pylint==2.4.3 +pyparsing==2.4.2 +pytz==2019.3 PyYAML==5.1.2 +requests==2.22.0 +restructuredtext-lint==1.3.0 six==1.12.0 +snowballstemmer==2.0.0 +Sphinx==2.1.2 +sphinx-autobuild==0.7.1 +sphinx-click==2.3.0 +sphinx-prompt==1.1.0 +sphinxcontrib-applehelp==1.0.1 +sphinxcontrib-devhelp==1.0.1 +sphinxcontrib-htmlhelp==1.0.2 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-programoutput==0.15 +sphinxcontrib-qthelp==1.0.2 +sphinxcontrib-serializinghtml==1.1.3 +stevedore==1.31.0 +toml==0.10.0 +tornado==6.0.3 typed-ast==1.4.0 +urllib3==1.25.6 virtualenv==16.7.6 +watchdog==0.9.0 wrapt==1.11.2 +zipp==0.6.0 diff --git a/setup.py b/setup.py index f007751..e48bb6b 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ import setuptools -version = '0.5.0' +version = '0.6.0' requires = ['jinja2 >= 2.7', 'pyyaml', 'click'] diff --git a/ginpar/tools/__init__.py b/tests/__init__.py similarity index 100% rename from ginpar/tools/__init__.py rename to tests/__init__.py diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 0000000..2065149 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,10 @@ +from click.testing import CliRunner +from ginpar import cli + + +def test_init(): + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(cli, ["init", "-q"]) + assert result.exit_code == 0 + assert "Done" in result.output diff --git a/tests/test_new.py b/tests/test_new.py new file mode 100644 index 0000000..5fa475e --- /dev/null +++ b/tests/test_new.py @@ -0,0 +1,41 @@ +import os + +from click.testing import CliRunner +from ginpar import cli + +def test_new(): + """New works properly on the default scenario. + + Default scenario is when the directory of the new sketch doesn't + exist in the source_path.. + """ + runner = CliRunner() + with runner.isolated_filesystem(): + ## Initialize a new project and cd it + runner.invoke(cli, ["init", "-q"]) + os.chdir("my-site") + + result = runner.invoke(cli, ["new", "test"]) + + data_path = os.path.join("sketches", "test", "data.yaml") + sketch_path = os.path.join("sketches", "test", "sketch.js") + assert result.exit_code == 0 + assert os.path.isfile(data_path) + assert os.path.isfile(sketch_path) + + +def test_new_existent(): + """New works properly when the sketch already exists + """ + runner = CliRunner() + with runner.isolated_filesystem(): + ## Initialize a new project and cd it + runner.invoke(cli, ["init", "-q"]) + os.chdir("my-site") + + ## Create test + runner.invoke(cli, ["new", "test"]) + + result = runner.invoke(cli, ["new", "test"]) + assert result.exit_code == 1 + \ No newline at end of file diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py new file mode 100644 index 0000000..1ec66bd --- /dev/null +++ b/tests/test_quickstart.py @@ -0,0 +1,72 @@ +import os + +from click.testing import CliRunner + +from ginpar import cli + + +def test_quickstart(): + """Quickstart works properly on the default scenario. + + Default scenario is when the directory of the quickstart project doesn't + exist in the current path: `./quickstart`. + This also doesn't need to send the force flag. + """ + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(cli, ["quickstart"]) + assert result.exit_code == 0 + assert "The Ginpar sample site is ready." in result.output + + +def test_quickstart_existent_path(): + """Quickstart works properly when `./quickstart` exists. + """ + runner = CliRunner() + with runner.isolated_filesystem(): + os.mkdir("quickstart") + file_path = os.path.join("quickstart", "a.txt") + + with open(file_path, "w+") as f: + f.write("a") + + result = runner.invoke(cli, ["quickstart"]) + assert result.exit_code == 1 + + +def test_quickstart_existent_path_force(): + """Quickstart works properly when `./quickstart` exists and --force + """ + runner = CliRunner() + with runner.isolated_filesystem(): + os.mkdir("quickstart") + file_path = os.path.join("quickstart", "a.txt") + + with open(file_path, "w+") as f: + f.write("a") + + result = runner.invoke(cli, ["quickstart", "--force"]) + assert result.exit_code == 0 + + +def test_quickstart_removes_git_files(): + """Quickstart removes git files after cloning + + Check for the existence of any file that starts with `.git`. + This will report the existent files. + """ + import os + + runner = CliRunner() + with runner.isolated_filesystem(): + git_files = [] + runner.invoke(cli, ["quickstart"]) + for _, d, f in os.walk("quickstart"): + for name in d: + if name.startswith(".git"): + git_files.append(name) + for file in f: + if file.startswith(".git"): + git_files.append(file) + + assert git_files == [] diff --git a/tests/test_serve.py b/tests/test_serve.py new file mode 100644 index 0000000..226837f --- /dev/null +++ b/tests/test_serve.py @@ -0,0 +1,13 @@ +import os + +from click.testing import CliRunner +from ginpar import cli + +def test_serve(): + runner = CliRunner() + with runner.isolated_filesystem(): + runner.invoke(cli, ["init", "-q"]) + os.chdir("my-site") + result = runner.invoke(cli, ["serve", "--help"]) + assert result.exit_code == 0 + \ No newline at end of file