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