Skip to content

Commit

Permalink
Merge pull request #26 from norm/add-recipe-view
Browse files Browse the repository at this point in the history
Add recipe view
  • Loading branch information
norm committed Jul 23, 2020
2 parents c29b168 + 86581cd commit 2f29b1c
Show file tree
Hide file tree
Showing 6 changed files with 369 additions and 32 deletions.
5 changes: 4 additions & 1 deletion documentation/release-notes.markdown
Expand Up @@ -16,9 +16,12 @@
now only uses `PATHS`, and how generators are declared has changed. See
the documentation on [adding paths][ap].
* Set Atom feeds to include at most 20 items by default.
* Bug fixes and performance improvements.
* Can use [Sectile][sec] for template assembly. Experimental, so not yet
supported or documented.
* When previewing the site, you can add `?showrecipe` to the URL to see
a "recipe" page detailing the template(s) and context used to generate
the page.
* Bug fixes and performance improvements.

[sass]: https://flourish.readthedocs.io/en/latest/api-flourish-generators-sass/
[cal]: https://flourish.readthedocs.io/en/latest/api-flourish-generators-calendar/
Expand Down
9 changes: 9 additions & 0 deletions flourish/__init__.py
Expand Up @@ -164,6 +164,15 @@ def generate_path(self, path, report=False):
path = self._paths[key]
path.generate(report, tokens=[args])

def path_recipe(self, path):
handlers = self.get_handler_for_path(path)
for key, args in handlers:
path = self._paths[key]
recipe = path.get_recipe(tokens=[args])
if recipe:
return recipe
return None

def set_global_context(self, global_context):
self.global_context = global_context

Expand Down
143 changes: 130 additions & 13 deletions flourish/command_line.py
Expand Up @@ -2,15 +2,115 @@
from hashlib import md5
import mimetypes
import os
from textwrap import dedent

import boto3
from flask import Flask, send_from_directory
from flask import Flask, request, send_from_directory
from jinja2 import Template

from . import Flourish, __version__
from .examples import example_files
from .lib import relative_list_of_files_in_directory


recipe_html = Template(dedent("""\
<html>
<head>
<style>
#dimensions dt {
float: left;
margin-right: 20px;
min-width: 100px;
}
#dimensions dd {
font-weight: bold;
}
#template ol {
margin: 0;
padding: 0;
}
#template ol ol li {
padding: 0 0 0 17px;
border-left: 1px solid #999;
margin-left: 3px;
margin-bottom: 15px;
}
#template li {
list-style: none;
margin-top: 10px;
}
#template li i {
color: #66a;
font-size: 0.8em;
}
#template li.missing {
margin-bottom: 10px;
}
#template li.missing > i {
font-weight: bold;
color: #f99;
}
#template li pre {
margin: 0;
}
</style>
</head>
<body>
<h1>Recipe for {{path|e}}</h1>
<div id='template'>
{% if sectile_dimensions %}
<h2>Dimensions</h2>
<dl id='dimensions'>
{% for dim in sectile_dimensions %}
<dt>{{dim}}</dt>
<dd>{{sectile_dimensions[dim]}}</dd>
{% endfor %}
</dl>
{% endif %}
<h2>Template: <i>{{template_name|e}}</i></h2>
{% if sectile_fragments %}
{% set depth = namespace(previous=-1) %}
{% for fragment in sectile_fragments %}
{% if depth.previous < fragment.depth %}
<ol>
{% elif depth.previous > fragment.depth %}
{% for i in range(fragment.depth, depth.previous) %}
</li></ol>
{% endfor %}
{% else %}
</li>
{% endif %}
<li class='{% if not fragment.found %}missing{% endif %}'>
{% if not fragment.found %}
<i>{{fragment.file}}: None</i>
{% else %}
<i>{{fragment.found}}</i>
<pre>{{fragment.fragment|e}}</pre>
{% endif %}
{% if depth.previous < fragment.depth %}
{% elif depth.previous > fragment.depth %}
{% else %}
</li>
{% endif %}
{% set depth.previous = fragment.depth %}
{% endfor %}
</ol>
{% for i in range(depth.previous) %}
</li></ol>
{% endfor %}
<p>Result:</p>
{% endif %}
<pre>{{template|e}}</pre>
</div>
<h2>Context:</h2>
<div id='context'>{{context|e}}</div>
</body>
</html>
"""))


def main():
version = ('Flourish static site generator, version %s -- '
'http://withaflourish.net' % __version__)
Expand All @@ -37,6 +137,14 @@ def main():
default='templates',
help='Directory containing templates (default: %(default)s)',
)
parser.add_argument(
'--fragments',
default='fragments',
help=(
'Directory containing Sectile template fragments'
' (experimental feature, no default)'
),
)
parser.add_argument(
'--output',
default='output',
Expand Down Expand Up @@ -119,7 +227,11 @@ def main():
if args.base is not None:
args.source = os.path.join(args.base, args.source)
args.templates = os.path.join(args.base, args.templates)
args.fragments = os.path.join(args.base, args.fragments)
args.output = os.path.join(args.base, args.output)
# find default 'fragments' directory but without forcing it
if not os.path.isdir(args.fragments):
args.fragments = None
action = ACTIONS[args.action]
action(args)

Expand All @@ -128,6 +240,7 @@ def generate(args):
flourish = Flourish(
source_dir=args.source,
templates_dir=args.templates,
fragments_dir=args.fragments,
output_dir=args.output,
)
if args.path:
Expand All @@ -142,6 +255,7 @@ def preview_server(args):
flourish = Flourish(
source_dir=args.source,
templates_dir=args.templates,
fragments_dir=args.fragments,
output_dir=args.output,
)
app = Flask(__name__)
Expand All @@ -154,20 +268,23 @@ def preview_server(args):
def send_file(path=''):
generate = '/%s' % path

# fix URLs for .html
filename = os.path.basename(path)
if filename == '':
path = '%sindex.html' % path
_, ext = os.path.splitext(path)
if ext == '':
path += '.html'
if 'showrecipe' in request.args:
return recipe_html.render(flourish.path_recipe(generate))
else:
# fix URLs for .html
filename = os.path.basename(path)
if filename == '':
path = '%sindex.html' % path
_, ext = os.path.splitext(path)
if ext == '':
path += '.html'

# regenerate if requested
if args.generate:
flourish._rescan_sources()
flourish.generate_path(generate, report=True)
# regenerate if requested
if args.generate:
flourish._rescan_sources()
flourish.generate_path(generate, report=True)

return send_from_directory(output_dir, path)
return send_from_directory(output_dir, path)

@app.after_request
def add_header(response):
Expand Down
35 changes: 35 additions & 0 deletions flourish/generators/mixins.py
@@ -1,5 +1,8 @@
import os
import re

from sectile import Sectile


class MissingValue(Exception):
pass
Expand Down Expand Up @@ -152,6 +155,38 @@ def generate_path(self, tokens):
self.get_objects(tokens)
self.output_to_file()

def get_recipe(self, tokens):
for tokenset in tokens:
self.tokens = tokenset
self.get_current_path(tokenset)
self.get_objects(tokenset)
context = self.get_context_data()
name = self.get_template_name()
recipe = {
'path': self.current_path,
'context': context,
'template_name': name,
}
if self.flourish.using_sectile:
sectile = Sectile(fragments=self.flourish.fragments_dir)
dimensions = {
'generator': self.name,
}
for dimension in self.flourish.jinja.loader.dimensions():
if dimension in context:
dimensions[dimension] = context[dimension]
recipe['template'], recipe['sectile_fragments'] = sectile.generate(
self.current_path,
name,
**dimensions
)
recipe['sectile_dimensions'] = dimensions
else:
filename = os.path.join(self.flourish.templates_dir, name)
with open(filename, 'r') as handle:
recipe['template'] = handle.read()
return recipe


class TemplateMixin:
def render_output(self):
Expand Down
57 changes: 39 additions & 18 deletions flourish/sectileloader.py
@@ -1,43 +1,64 @@
from collections import OrderedDict
import hashlib
import json

from jinja2 import BaseLoader, TemplateNotFound
from sectile import Sectile




class SectileLoader(BaseLoader):
CACHE_SIZE=100

def __init__(self, fragments_dir):
self.sectile = Sectile(fragments=fragments_dir)
self.prepared_path = None
self.prepared_base = None
self.prepared_dimensions = None
self.contents = {}
self.fragments = {}
# FIXME LRU cache, not everything ever
self.cache = OrderedDict()

def dimensions(self):
return self.sectile.get_dimensions_list()

def prepare_template(self, path, base_template, **dimensions):
self.prepared_path = path
self.prepared_base = base_template
self.prepared_dimensions = dimensions
content, fragments = self.sectile.generate(
path,
base_template,
**dimensions
)
def generate_template(self, path, base_template, **dimensions):
fingerprint = '%s-%s-%s' % (
path,
base_template,
json.dumps(dimensions)
)
digest = hashlib.sha256(fingerprint.encode('utf8')).hexdigest()
self.contents[digest] = content
self.fragments[digest] = fragments
return digest
# print('==', fingerprint)
if fingerprint in self.cache:
print('** cache hit')
self.cache.move_to_end(fingerprint, last=True)
else:
# print('++ cache miss')
content, fragments = self.sectile.generate(
path,
base_template,
**dimensions
)
self.cache[fingerprint] = {
'path': path,
'fingerprint': fingerprint,
'base_template': base_template,
'dimensions': dimensions,
'content': content,
'fragments': fragments,
}
if len(self.cache) > self.CACHE_SIZE:
# print('-- evict cache key')
self.cache.popitem(last=False)

return self.cache[fingerprint]

def prepare_template(self, path, base_template, **dimensions):
generated = self.generate_template(path, base_template, **dimensions)
return generated['fingerprint']

def get_source(self, environment, digest):
if self.contents[digest] is None:
def get_source(self, environment, fingerprint):
if self.cache[fingerprint] is None:
raise TemplateNotFound(
"%s, %s {%s}" % (
self.prepared_path,
Expand All @@ -46,4 +67,4 @@ def get_source(self, environment, digest):
)
)
else:
return self.contents[digest], None, None
return self.cache[fingerprint]['content'], None, None

0 comments on commit 2f29b1c

Please sign in to comment.