diff --git a/.gitignore b/.gitignore
index eed4564..858bc3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,5 @@ node_modules
build
!tests/dist
.coverage
-htmlcov
\ No newline at end of file
+htmlcov
+_build
\ No newline at end of file
diff --git a/README.md b/README.md
index ce0b9bf..0ac9b1d 100644
--- a/README.md
+++ b/README.md
@@ -1,106 +1,42 @@
-# Django Manifest Loader
+# Django Manifest Loader
-[![Build Status](https://img.shields.io/travis/shonin/django-manifest-loader/main?label=latest%20published%20branch&style=flat-square
+[![Build Status](https://img.shields.io/travis/shonin/django-manifest-loader/main?label=stable%20branch&style=flat-square
)](https://travis-ci.org/shonin/django-manifest-loader)
[![Build Status](https://img.shields.io/travis/shonin/django-manifest-loader/dev?label=development%20branch&style=flat-square
)](https://travis-ci.org/shonin/django-manifest-loader)
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](#)
-_Always have access to the latest webpack assets, with minimal configuration. Wraps Django's built in
-`{% static %}` templatetag to allow you to link to assets according to a webpack manifest file. Handles webpack's
-split chunks._
+
+Reads a manifest file to import your assets into a Django template. Find
+the URL for a single asset or the URLs for multiple assets by using
+pattern matching against the file names. Path resolution handled using
+Django's built-in `staticfiles` app. Minimal configuraton,
+cache-busting, split chunks.
+
+## [Documentation](https://django-manifest-loader.readthedocs.io/en/latest/index.html)
+
+## About
**Turns this**
```djangotemplate
{% load manifest %}
-
+
```
**Into this**
```djangotemplate
-
+
```
+* [Official documentation](https://django-manifest-loader.readthedocs.io/en/latest/index.html)
* For an in-depth look at this package, check out [this blog post here](https://medium.com/@shonin/django-and-webpack-now-work-together-seamlessly-a90cffdbab8e)
-* [Quick start guide](https://medium.com/@shonin/django-and-webpack-in-4-short-steps-b39bd3380c71)
-
-
-## Installation
-
-```shell script
-pip install django-manifest-loader
-```
-
-## Django Setup
-
-```python
-# settings.py
-
-INSTALLED_APPS = [
- ...
- 'manifest_loader', # add to installed apps
- ...
-]
-
-STATICFILES_DIRS = [
- BASE_DIR / 'dist' # the directory webpack outputs to
-]
-```
-
-You must add webpack's output directory to the `STATICFILES_DIRS` list.
-The above example assumes that your webpack configuration is setup to output all files into a directory `dist/` that is
-in the `BASE_DIR` of your project.
-
-`BASE_DIR`'s default value, as set by `$ djagno-admin startproject` is `BASE_DIR = Path(__file__).resolve().parent.parent`, in general
-you shouldn't be modifying it.
+* [Quick start blog post](https://medium.com/@shonin/django-and-webpack-in-4-short-steps-b39bd3380c71)
-**Optional settings,** default values shown.
-```python
-# settings.py
+## Quick reference:
-MANIFEST_LOADER = {
- 'output_dir': None, # where webpack outputs to, if not set will search in STATICFILES_DIRS for the manifest.
- 'manifest_file': 'manifest.json', # name of your manifest file
- 'cache': False, # recommended True for production, requires a server restart to pickup new values from the manifest.
-}
-```
-
-## Webpack configuration
-
-You must install the `WebpackManifestPlugin`. Optionally, but recommended, is to install the `CleanWebpackPlugin`.
-
-```shell script
-npm i --save-dev webpack-manifest-plugin clean-webpack-plugin
-```
-
-```javascript
-// webpack.config.js
-
-const { CleanWebpackPlugin } = require('clean-webpack-plugin');
-const ManifestPlugin = require('webpack-manifest-plugin');
-
-module.exports = {
- ...
- plugins: [
- new CleanWebpackPlugin(), // removes outdated assets from the output dir
- new ManifestPlugin(), // generates the required manifest.json file
- ],
- ...
-};
-```
-
-# Usage
-
-Django Manifest Loader comes with two template tags that house all logic. The `manifest` tag takes a single string
-input, such as `'main.js'`, looks it up against the webpack manifest, and then outputs the url to that compiled file.
-It works just like Django's built it `static` tag, except it's finding the correct filename.
-
-The `manifest_match` tag takes two arguments, a sting to pattern match filenames against, and a string to embed matched file
-urls into. See the `manifest_match` section for more information.
-
-## Single file use (for cache busting) (`manifest` tag)
+### Manifest tag
```djangotemplate
{% load manifest %}
@@ -114,116 +50,21 @@ turns into
```
-Where the argument to the tag will be the original filename of a file processed by webpack. If in doubt, check your
-`manifest.json` file generated by webpack to see what files are available.
-
-The reason this is worth while is because of the content hash after the original filename, which will invalidate the
-browser cache every time the file is updated. This ensures that your users always have the latest assets.
-
-## Split chunks (`manifest_match` tag)
+### Manifest match tag
```djangotemplate
{% load manifest %}
-{% manifest_match '*.js' '' %}
+{% manifest_match '*.js' '' %}
```
turns into
```html
-
-
-```
-
-This tag takes two arguments, a pattern to match against, according to the rules of the python fnmatch package,
-and a string to input the file urls into. The second argument must contain the string `{match}`, as it is what
-is replaced with the urls.
-
-## URLs in Manifest File
-
-If your manifest file points to full urls, instead of file names, the full url will be output instead of pointing
-to the static file directory in Django.
-
-Example:
-
-```json
-{
- "main.js": "http://localhost:8080/main.js"
-}
-```
-
-```djangotemplate
-{% load manifest %}
-
-
-```
-
-Will output as:
-
-```html
-
-```
-
-# About
-
-At it's heart Django Manifest Loader is an extension to Django's built-in `static` templatetag.
-When you use the provided `{% manifest %}` templatetag, all the manifest loader is doing is
-taking the input string, looking it up against the manifest file, modifying the value, and then
-passing along the result to the `{% static %}` template tag. The `{% manifest_match %}` tag works
-similarly, just with a bit of additional logic to find all the necessary files and to render the output.
-
-### Suggested Project Structure
-
-```
-BASE_DIR
-├── dist
-│ ├── main.f82c02a005f7f383003c.js
-│ └── manifest.json
-├── frontend
-│ ├── apps.py
-│ ├── src
-│ │ └── index.js
-│ ├── templates
-│ │ └── frontend
-│ │ └── index.html
-│ └── views.py
-├── manage.py
-├── package.json
-├── project
-│ ├── settings.py
-│ ├── urls.py
-│ └── wsgi.py
-├── requirements.txt
-└── webpack.config.js
-```
-
-### Cache Busting and Split Chunks (the problem this package solves)
-
-* [What is cache busting?](https://www.keycdn.com/support/what-is-cache-busting)
-* [The 100% correct way to split your chunks with Webpack](https://medium.com/hackernoon/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758)
-
-### Tests and Code Coverage
-
-Run unit tests and verify 100% code coverage with:
-
-```
-git clone https://github.com/shonin/django-manifest-loader.git
-cd django-manifest-loader
-pip install -e .
-
-# run tests
-python runtests.py
-
-# check code coverage
-pip install coverage
-coverage run --source=manifest_loader/ runtests.py
-coverage report
+
+
```
-### Contributing
-
-Do it. Please feel free to file an issue or open a pull request. The code of conduct is basic human kindness.
-
### License
Django Manifest Loader is distributed under the [3-clause BSD license](https://opensource.org/licenses/BSD-3-Clause).
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..d4bb2cb
--- /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 = .
+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/conf.py b/docs/conf.py
new file mode 100644
index 0000000..3fc935c
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,61 @@
+# 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('../manifest_loader/templatetags'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'django-manifest-loader'
+copyright = '3-Clause BSD License'
+author = 'django-manifest-loader'
+
+# The full version, including alpha/beta/rc tags
+release = '1.01'
+
+
+# -- 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 = ['sphinx.ext.autodoc','recommonmark', 'sphinx_rtd_theme'
+]
+
+# 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 = ['_build', 'Thumbs.db', '.DS_Store']
+
+
+# -- 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 = 'sphinx_rtd_theme'
+
+# 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 = []
+
+source_suffix = {
+ '.rst': 'restructuredtext',
+ '.txt': 'markdown',
+ '.md': 'markdown',
+}
\ No newline at end of file
diff --git a/docs/docs/about_install.md b/docs/docs/about_install.md
new file mode 100644
index 0000000..d01f256
--- /dev/null
+++ b/docs/docs/about_install.md
@@ -0,0 +1,124 @@
+# About
+
+Django Manifest Loader reads a manifest file to import your assets into a Django template. Find
+the URL for a single asset OR find the URLs for multiple assets by using
+pattern matching against the file names. Path resolution handled using
+Django's built-in `staticfiles` app. Minimal configuraton, cache-busting, split chunks.
+Designed for webpack, ready for anything.
+
+**Turns this**
+
+```html
+{% load manifest %}
+
+```
+
+**Into this**
+
+```html
+
+```
+
+* For an in-depth tutorial, check out [this blog post here](https://medium.com/@shonin/django-and-webpack-now-work-together-seamlessly-a90cffdbab8e)
+* [Quick start blog post](https://medium.com/@shonin/django-and-webpack-in-4-short-steps-b39bd3380c71)
+
+## Additional resources
+
+* [What is cache busting?](https://www.keycdn.com/support/what-is-cache-busting)
+* [The 100% correct way to split your chunks with Webpack](https://medium.com/hackernoon/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758)
+
+# Installation
+
+```shell
+pip install django-manifest-loader
+```
+
+
+## Django Setup
+
+```python
+# settings.py
+
+INSTALLED_APPS = [
+ ...
+ 'manifest_loader', # add to installed apps
+ ...
+]
+
+STATICFILES_DIRS = [
+ BASE_DIR / 'dist' # the directory webpack outputs to
+]
+```
+
+You must add webpack's output directory to the `STATICFILES_DIRS` list.
+The above example assumes that your webpack configuration is set up to output all files into a directory `dist/` that is
+in the `BASE_DIR` of your project.
+
+`BASE_DIR`'s default value, as set by `$ djagno-admin startproject` is `BASE_DIR = Path(__file__).resolve().parent.parent`, in general
+you shouldn't be modifying it.
+
+**Optional settings,** default values shown.
+```python
+# settings.py
+
+MANIFEST_LOADER = {
+ 'output_dir': None, # where webpack outputs to, if not set, will search in STATICFILES_DIRS for the manifest.
+ 'manifest_file': 'manifest.json', # name of your manifest file
+ 'cache': False, # recommended True for production, requires a server restart to pick up new values from the manifest.
+ 'loader': DefaultLoader # how the manifest files are interacted with
+}
+```
+
+
+## webpack configuration
+
+_webpack is not technically required: Django Manifest Loader by default expects a manifest file in the form output by [webpack Manifest Plugin](https://github.com/shellscape/webpack-manifest-plugin). See the section on custom loaders for information on how to use a different type of manifest file._
+
+You must install the `WebpackManifestPlugin`. Optionally, but recommended, is to install the `CleanWebpackPlugin`.
+
+```shell
+npm i --save-dev webpack-manifest-plugin clean-webpack-plugin
+```
+
+```javascript
+// webpack.config.js
+
+const { CleanWebpackPlugin } = require('clean-webpack-plugin');
+const ManifestPlugin = require('webpack-manifest-plugin');
+
+module.exports = {
+ ...
+ plugins: [
+ new CleanWebpackPlugin(), // removes outdated assets from the output dir
+ new ManifestPlugin(), // generates the required manifest.json file
+ ],
+ ...
+};
+```
+
+_For a deep dive into a supported webpack configuration, read the blog post introducting this package [here](https://medium.com/@shonin/django-and-webpack-now-work-together-seamlessly-a90cffdbab8e)_
+
+# Example Project Structure
+
+```
+BASE_DIR
+├── dist # webpack's output directory
+│ ├── index.f82c02a005f7f383003c.js
+│ └── manifest.json
+├── frontend # a django app
+│ ├── apps.py
+│ ├── src
+│ │ └── index.js
+│ ├── templates
+│ │ └── frontend
+│ │ └── index.html
+│ └── views.py
+├── manage.py
+├── package.json
+├── project
+│ ├── settings.py
+│ ├── urls.py
+│ └── wsgi.py
+├── requirements.txt
+└── webpack.config.js
+```
\ No newline at end of file
diff --git a/docs/docs/docs_license.md b/docs/docs/docs_license.md
new file mode 100644
index 0000000..7f746c0
--- /dev/null
+++ b/docs/docs/docs_license.md
@@ -0,0 +1,34 @@
+# Improve Documentation
+
+Thanks to everyone who has and who will one day contribute to the documentation for this project. Pull requests or issues filed for documentation fixes, clarifications, and restructuring are all welcome. [Open a pull request or issue here](https://github.com/shonin/django-manifest-loader).
+
+Documentation is developed using [Sphinx](https://www.sphinx-doc.org/en/master/usage/configuration.html).
+
+
+## Installation
+In order to install sphinx
+
+```shell
+pip install -U sphinx
+```
+
+## Dependencies for installation
+To use .md with Sphynx, it requires Recommonmark.
+
+
+```shell
+pip install recommonmark
+```
+
+## How to run
+After installation of sphinx and recommonmark, to generate the `_build` directory that has doc trees and html, `cd docs` then `make html`.
+
+
+# Contributing
+
+Do it. Please feel free to file an issue or open a pull request. The code of conduct is basic human kindness. [See the project on Github here](https://github.com/shonin/django-manifest-loader)
+
+# License
+
+Django Manifest Loader is distributed under the [3-clause BSD license](https://opensource.org/licenses/BSD-3-Clause).
+This is an open source license granting broad permissions to modify and redistribute the software.
diff --git a/docs/docs/philosophy.md b/docs/docs/philosophy.md
new file mode 100644
index 0000000..57bb260
--- /dev/null
+++ b/docs/docs/philosophy.md
@@ -0,0 +1,19 @@
+# The two ways to build a front end
+
+There are two fundamental ways to connect a javascript front end to Django: coupled or decoupled. Django Manifest loader is specifically for the coupled option.
+
+A coupled front end and back end means that Django is responsible for the front ends asset files. As a user you point your web browser to the Django app, and the Django app in turn makes sure you get the front end.
+
+A decoupled front and back end means they are hosted separately. Django has no knowledge of front end asset files, and does not serve them. As a user you point your browser at the staticly hosted front end app and that app interacts with Django through an API.
+
+I typically choose the coupled option as
+* I don't want to manage multiple repos
+* or multiple servers
+* Django is powerful
+
+The decoupled option is good for if
+* you value the performance gain of using a static file server
+* your front end and django app are managed by different teams
+* you want micro services
+
+It's a tradeoff. Django Manifest Loader makes the coupled option much easier than it was before.
\ No newline at end of file
diff --git a/docs/docs/reference.md b/docs/docs/reference.md
new file mode 100644
index 0000000..b5631b8
--- /dev/null
+++ b/docs/docs/reference.md
@@ -0,0 +1,174 @@
+
+## API Reference
+
+### Manifest Tag
+Returns the manifest tag
+
+```python
+@register.tag('manifest')
+def do_manifest(parser, token):
+
+ return ManifestNode(token)
+
+```
+### Manifest Match Tag
+Returns manifest_match tag
+
+```python
+@register.tag('manifest_match')
+def do_manifest_match(parser, token):
+ return ManifestMatchNode(token)
+
+```
+### ManifestNode
+Initializes and renders the creation of the manifest tag and
+
+
+```python
+ class ManifestNode(template.Node):
+ """ Initalizes the creation of the manifest template tag"""
+ def __init__(self, token):
+ bits = token.split_contents()
+ if len(bits) < 2:
+ raise template.TemplateSyntaxError(
+ "'%s' takes one argument (name of file)" % bits[0])
+ self.bits = bits
+
+
+ def render(self, context):
+ """Renders the creation of the manifest tag"""
+ manifest_key = get_value(self.bits[1], context)
+ manifest = get_manifest()
+ manifest_value = manifest.get(manifest_key, manifest_key)
+ return make_url(manifest_value, context)
+```
+### ManifestMatch Node
+Initalizes and renders the creation of the manifest match tag
+
+```python
+class ManifestMatchNode(template.Node):
+ """ Initalizes the creation of the manifest match template tag"""
+ def __init__(self, token):
+ self.bits = token.split_contents()
+ if len(self.bits) < 3:
+ raise template.TemplateSyntaxError(
+ "'%s' takes two arguments (pattern to match and string to "
+ "insert into)" % self.bits[0]
+ )
+
+ def render(self, context):
+ """ Renders the manifest match tag"""
+ urls = []
+ search_string = get_value(self.bits[1], context)
+ output_tag = get_value(self.bits[2], context)
+
+ manifest = get_manifest()
+
+ matched_files = [file for file in manifest.keys() if
+ fnmatch.fnmatch(file, search_string)]
+ mapped_files = [manifest.get(file) for file in matched_files]
+
+ for file in mapped_files:
+ url = make_url(file, context)
+ urls.append(url)
+ output_tags = [output_tag.format(match=file) for file in urls]
+ return '\n'.join(output_tags)
+
+
+def get_manifest():
+ """ Returns the manifest file from the output directory """
+ cached_manifest = cache.get('webpack_manifest')
+ if APP_SETTINGS['cache'] and cached_manifest:
+ return cached_manifest
+
+ if APP_SETTINGS['output_dir']:
+ manifest_path = os.path.join(APP_SETTINGS['output_dir'],
+ APP_SETTINGS['manifest_file'])
+ else:
+ manifest_path = find_manifest_path()
+
+ try:
+ with open(manifest_path) as manifest_file:
+ data = json.load(manifest_file)
+ except FileNotFoundError:
+ raise WebpackManifestNotFound(manifest_path)
+
+ if APP_SETTINGS['cache']:
+ cache.set('webpack_manifest', data)
+
+ return data
+```
+
+
+### Finding the Manifest File
+Returns manifest_file
+```python
+def find_manifest_path():
+ static_dirs = settings.STATICFILES_DIRS
+ if len(static_dirs) == 1:
+ return os.path.join(static_dirs[0], APP_SETTINGS['manifest_file'])
+ for static_dir in static_dirs:
+ manifest_path = os.path.join(static_dir, APP_SETTINGS['manifest_file'])
+ if os.path.isfile(manifest_path):
+ return manifest_path
+ raise WebpackManifestNotFound('settings.STATICFILES_DIRS')
+
+```
+### String Validator
+Method validates if it's a string
+
+```python
+
+def is_quoted_string(string):
+ if len(string) < 2:
+ return False
+ return string[0] == string[-1] and string[0] in ('"', "'")
+```
+
+### Value Validator
+Method validates the value
+
+```python
+
+def get_value(string, context):
+
+ if is_quoted_string(string):
+ return string[1:-1]
+ return context.get(string, '')
+```
+
+
+### URL Validator
+Function validates if it's a URL
+
+```python
+
+def is_url(potential_url):
+
+
+ validate = URLValidator()
+ try:
+ validate(potential_url)
+ return True
+ except ValidationError:
+ return False
+
+```
+
+### URL Generator
+Returns the URL that will be outputed to the static file directory
+
+```python
+def make_url(manifest_value, context):
+
+
+ if is_url(manifest_value):
+ url = manifest_value
+ else:
+ url = StaticNode.handle_simple(manifest_value)
+ if context.autoescape:
+ url = conditional_escape(url)
+ return url
+
+
+```
\ No newline at end of file
diff --git a/docs/docs/usage.md b/docs/docs/usage.md
new file mode 100644
index 0000000..5494c5f
--- /dev/null
+++ b/docs/docs/usage.md
@@ -0,0 +1,167 @@
+# Basic Usage
+
+Django Manifest Loader comes with two template tags that house all logic. The `manifest` tag takes a single string
+input, such as `'main.js'`, looks it up against the webpack manifest, and then outputs the URL to that compiled file.
+It works just like Django's built it `static` tag, except it's finding the filename according to your manifest file.
+
+The `manifest_match` tag takes two arguments, a string to pattern match filenames against and a string to embed matched file urls into. See the `manifest_match` section for more information.
+
+## Manifest tag
+
+```html
+{% load manifest %}
+
+
+```
+
+turns into
+
+```html
+
+```
+
+Where the argument to the tag will be the original filename of a file processed by webpack. If in doubt, check your
+`manifest.json` file generated by webpack to see what files are available.
+
+This is worthwhile because of the content hash after the original filename, which will invalidate the browser cache every time the file is updated, which will ensure that your users always have the latest assets.
+
+## Manifest match tag
+
+```html
+{% load manifest %}
+
+{% manifest_match '*.js' '' %}
+```
+
+turns into
+
+```html
+
+
+```
+
+This tag takes two arguments, a pattern to match against, according to the python `fnmatch` package rules,
+and a string to input the file URLs into. The second argument must contain the string `{match}`, as it is replaced with the URLs.
+
+# Advanced Usage
+
+## Custom Loaders
+
+Custom loaders allow you to implement your own means of extracting data from your manifest file. If your manifest
+file is not the default structure of [webpack manifest plugin](https://github.com/shellscape/webpack-manifest-plugin), this is how you can tell `django-manifest-loader` how to read it.
+
+First import the loader parent abstract class, and subclass it in your new loader class.
+
+```python
+from manifest_loader.loaders import LoaderABC
+
+class MyCustomLoader(LoaderABC):
+```
+
+Your new loader must have two static methods that each take two required arguments:
+`get_single_match(manifest, key)` and `get_multi_match(manifest, pattern)`.
+
+```python
+from manifest_loader.loaders import LoaderABC
+
+class MyCustomLoader(LoaderABC):
+ @staticmethod
+ def get_single_match(manifest, key):
+ pass
+
+ @staticmethod
+ def get_multi_match(manifest, pattern):
+ pass
+```
+
+* `get_single_match` - returns a `String`, finds a single file in your manifest file, according to the `key`
+* `get_multi_match` - returns a `List` of files in your manifest, according to the `pattern`
+* `manifest` - this is your full manifest file, after being processed by `json.load()`. It will be a dictionary or list
+ depending on which it is in your manifest file.
+* `key` - `String`; the argument passed into the `manifest` template tag. e.g.: in the template tag `{% manifest 'index.js' %}`,
+ the string `'index.js'` is sent to `get_single_match` as `key` (without surrounding quotes)
+* `pattern` - `String`; the first argument passed into the `manifest_match` template tag. e.g.: in the template tag
+ `{% manifest_match '*.js' '' %}`, the string `'*.js'` is sent to `get_multi_match`
+ as `pattern` (without surrounding quotes)
+
+**Below is the code for the default loader, which is a good starting point:**
+
+```python
+import fnmatch
+from manifest_loader.loaders import LoaderABC
+
+class DefaultLoader(LoaderABC):
+ @staticmethod
+ def get_single_match(manifest, key):
+ return manifest.get(key, key)
+
+ @staticmethod
+ def get_multi_match(manifest, pattern):
+ matched_files = [file for file in manifest.keys() if
+ fnmatch.fnmatch(file, pattern)]
+ return [manifest.get(file) for file in matched_files]
+```
+
+In the above example, `get_single_match` retrieves the value on the `manifest` dictionary that matches the key `key`. If
+the key does not exist on the dictionary, it instead returns the key.
+
+`get_multi_match` uses the recommended `fnmatch` python standard library to do pattern matching. You could also use
+regex in it's place. Here, it iterates through all the keys in the manifest file, and builds a list of the keys that
+match the given `pattern`. It then returns a list of the values associated with those matched keys.
+
+### Activating the custom loader
+
+To put the custom loader into use it needs to be registered in your `settings.py`.
+
+```python
+# settings.py
+from my_app.utils import MyCustomLoader
+
+MANIFEST_LOADER = {
+ ...
+ 'loader': MyCustomLoader
+}
+```
+
+## URLs in Manifest File
+
+If your manifest file points to full URLs, instead of file names, the full URL will be output instead of pointing to the static file directory in Django.
+
+Example:
+
+```json
+{
+ "main.js": "http://localhost:8080/main.js"
+}
+```
+
+```html
+{% load manifest %}
+
+
+```
+
+Will output as:
+
+```html
+
+```
+
+
+# Tests and Code Coverage
+
+Run unit tests and verify 100% code coverage with:
+
+```
+git clone https://github.com/shonin/django-manifest-loader.git
+cd django-manifest-loader
+pip install -e .
+
+# run tests
+python runtests.py
+
+# check code coverage
+pip install coverage
+coverage run --source=manifest_loader/ runtests.py
+coverage report
+```
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..ccd3c94
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,26 @@
+Django Manifest Loader
+=========================
+
+| |Stable Status| |Dev Status| |contributions welcome|
+
+Django Manifest Loader reads a manifest file to import your assets into a Django template. Find
+the URL for a single asset OR find the URLs for multiple assets by using
+pattern matching against the file names. Path resolution handled using
+Django's built-in ``staticfiles`` app. Minimal configuraton, cache-busting, split chunks.
+Designed for webpack, ready for anything.
+
+.. toctree::
+ :maxdepth: 1
+
+ docs/about_install
+ docs/usage
+ docs/philosophy
+ docs/reference
+ docs/docs_license
+
+.. |Stable Status| image:: https://img.shields.io/travis/shonin/django-manifest-loader/main?label=stable%20branch&style=flat-square
+ :target: https://travis-ci.org/shonin/django-manifest-loader
+.. |Dev Status| image:: https://img.shields.io/travis/shonin/django-manifest-loader/dev?label=development%20branch&style=flat-square
+ :target: https://travis-ci.org/shonin/django-manifest-loader
+.. |contributions welcome| image:: https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square
+ :target: #
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..2119f51
--- /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=.
+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/index.rst b/index.rst
new file mode 100644
index 0000000..64d4532
--- /dev/null
+++ b/index.rst
@@ -0,0 +1,17 @@
+.. django-manifest-loader documentation master file, created by
+ sphinx-quickstart on Thu Dec 17 17:41:38 2020.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Django-manifest-loader's documentation!
+==================================================
+
+.. toctree::
+ :maxdepth: 1
+
+ docs/README
+ docs/usage
+ docs/reference
+ docs/docs_license
+
+
diff --git a/manifest_loader/exceptions.py b/manifest_loader/exceptions.py
index feb28e7..c7abef7 100644
--- a/manifest_loader/exceptions.py
+++ b/manifest_loader/exceptions.py
@@ -4,3 +4,10 @@ def __init__(self, path, message='Manifest file not found. '
'settings are wrong or you still need to '
'generate the file.'):
super().__init__(message.format(path))
+
+
+class CustomManifestLoaderNotValid(Exception):
+ def __init__(self, message='Custom manifest loader defined in settings.py '
+ 'must inherit from '
+ 'manifest_loader.loaders.LoaderABC'):
+ super().__init__(message)
diff --git a/manifest_loader/loaders.py b/manifest_loader/loaders.py
new file mode 100644
index 0000000..4fabf57
--- /dev/null
+++ b/manifest_loader/loaders.py
@@ -0,0 +1,26 @@
+import fnmatch
+from abc import ABCMeta, abstractmethod
+
+
+class LoaderABC(metaclass=ABCMeta):
+ @staticmethod
+ @abstractmethod
+ def get_single_match(manifest, key):
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def get_multi_match(manifest, pattern):
+ pass
+
+
+class DefaultLoader(LoaderABC):
+ @staticmethod
+ def get_single_match(manifest, key):
+ return manifest.get(key, key)
+
+ @staticmethod
+ def get_multi_match(manifest, pattern):
+ matched_files = [file for file in manifest.keys() if
+ fnmatch.fnmatch(file, pattern)]
+ return [manifest.get(file) for file in matched_files]
\ No newline at end of file
diff --git a/manifest_loader/templatetags/manifest.py b/manifest_loader/templatetags/manifest.py
index 86d6780..8237f5c 100644
--- a/manifest_loader/templatetags/manifest.py
+++ b/manifest_loader/templatetags/manifest.py
@@ -1,6 +1,5 @@
import json
import os
-import fnmatch
from django import template
from django.templatetags.static import StaticNode
@@ -11,7 +10,9 @@
from django.core.exceptions import ValidationError
-from manifest_loader.exceptions import WebpackManifestNotFound
+from manifest_loader.exceptions import WebpackManifestNotFound, \
+ CustomManifestLoaderNotValid
+from manifest_loader.loaders import DefaultLoader, LoaderABC
register = template.Library()
@@ -19,7 +20,8 @@
APP_SETTINGS = {
'output_dir': None,
'manifest_file': 'manifest.json',
- 'cache': False
+ 'cache': False,
+ 'loader': DefaultLoader
}
if hasattr(settings, 'MANIFEST_LOADER'):
@@ -27,16 +29,19 @@
@register.tag('manifest')
-def do_manifest(parser, token):
+def do_manifest(parser, token):
+ """Returns the manifest tag """
return ManifestNode(token)
-
@register.tag('manifest_match')
def do_manifest_match(parser, token):
+ """Returns manifest match tag"""
return ManifestMatchNode(token)
+
class ManifestNode(template.Node):
+ """ Initalizes the creation of the manifest template tag"""
def __init__(self, token):
bits = token.split_contents()
if len(bits) < 2:
@@ -44,14 +49,18 @@ def __init__(self, token):
"'%s' takes one argument (name of file)" % bits[0])
self.bits = bits
+
def render(self, context):
+ """Renders the creation of the manifest tag"""
manifest_key = get_value(self.bits[1], context)
manifest = get_manifest()
- manifest_value = manifest.get(manifest_key, manifest_key)
+ manifest_value = load_from_manifest(manifest, key=manifest_key)
+
return make_url(manifest_value, context)
class ManifestMatchNode(template.Node):
+ """ Initalizes the creation of the manifest match template tag"""
def __init__(self, token):
self.bits = token.split_contents()
if len(self.bits) < 3:
@@ -61,17 +70,16 @@ def __init__(self, token):
)
def render(self, context):
+ """ Renders the manifest match tag"""
urls = []
search_string = get_value(self.bits[1], context)
output_tag = get_value(self.bits[2], context)
manifest = get_manifest()
- matched_files = [file for file in manifest.keys() if
- fnmatch.fnmatch(file, search_string)]
- mapped_files = [manifest.get(file) for file in matched_files]
+ files = load_from_manifest(manifest, pattern=search_string)
- for file in mapped_files:
+ for file in files:
url = make_url(file, context)
urls.append(url)
output_tags = [output_tag.format(match=file) for file in urls]
@@ -79,6 +87,7 @@ def render(self, context):
def get_manifest():
+ """ Returns the manifest file from the output directory """
cached_manifest = cache.get('webpack_manifest')
if APP_SETTINGS['cache'] and cached_manifest:
return cached_manifest
@@ -102,6 +111,7 @@ def get_manifest():
def find_manifest_path():
+ """ Returns manifest_file """
static_dirs = settings.STATICFILES_DIRS
if len(static_dirs) == 1:
return os.path.join(static_dirs[0], APP_SETTINGS['manifest_file'])
@@ -113,18 +123,35 @@ def find_manifest_path():
def is_quoted_string(string):
+ """Method validates if it's a stirng"""
if len(string) < 2:
return False
return string[0] == string[-1] and string[0] in ('"', "'")
def get_value(string, context):
+ """Method validates the value of the string"""
if is_quoted_string(string):
return string[1:-1]
return context.get(string, '')
+def load_from_manifest(manifest, key=None, pattern=None):
+ loader = APP_SETTINGS['loader']
+
+ if not issubclass(loader, LoaderABC):
+ raise CustomManifestLoaderNotValid
+
+ if key:
+ return loader.get_single_match(manifest, key)
+ elif pattern:
+ return loader.get_multi_match(manifest, pattern)
+ return ''
+
+
def is_url(potential_url):
+ """Function validates if it's a URL """
+
validate = URLValidator()
try:
validate(potential_url)
@@ -134,6 +161,8 @@ def is_url(potential_url):
def make_url(manifest_value, context):
+ """ Returns the URL that will be outputed to the static file directory"""
+
if is_url(manifest_value):
url = manifest_value
else:
@@ -141,3 +170,4 @@ def make_url(manifest_value, context):
if context.autoescape:
url = conditional_escape(url)
return url
+
diff --git a/setup.cfg b/setup.cfg
index 10d708e..0577fe2 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = django-manifest-loader
-version = 0.1.1
+version = 0.2.0
description = A Django app to load webpack assets.
long_description = file: README.md
url = https://www.github.com/shonin/django-manifest-loader
diff --git a/tests/tests.py b/tests/tests.py
index 4f8bde6..f6f727e 100644
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -8,9 +8,9 @@
get_manifest, APP_SETTINGS, is_quoted_string, is_url
from manifest_loader.apps import ManifestLoader
-
-from manifest_loader.exceptions import WebpackManifestNotFound
-
+from manifest_loader.exceptions import WebpackManifestNotFound, \
+ CustomManifestLoaderNotValid
+from manifest_loader.loaders import LoaderABC, DefaultLoader
NEW_STATICFILES_DIRS = [
settings.BASE_DIR / 'foo',
@@ -284,3 +284,28 @@ def test_match_urls(self):
class AppConfigTests(SimpleTestCase):
def test_the_django_app(self):
self.assertTrue(issubclass(ManifestLoader, AppConfig))
+
+
+class LoadFromManifestTests(SimpleTestCase):
+ def test_loader_not_subclass(self):
+ class Foo:
+ pass
+
+ APP_SETTINGS.update({'loader': Foo})
+
+ with self.assertRaises(CustomManifestLoaderNotValid):
+ render_template(
+ '{% load manifest %}'
+ '{% manifest "main.js" %}'
+ )
+
+ APP_SETTINGS.update({'loader': DefaultLoader})
+
+
+class LoaderABCTests(SimpleTestCase):
+ def test_if_meta(self):
+ self.assertTrue(hasattr(LoaderABC, 'register'))
+
+ def test_methods_not_implemented(self):
+ self.assertIsNone(LoaderABC.get_single_match('foo', 'bar'))
+ self.assertIsNone(LoaderABC.get_multi_match('foo', 'bar'))