Начальная редакция
commit
d524359250
|
@ -0,0 +1,49 @@
|
||||||
|
syntax: glob
|
||||||
|
|
||||||
|
.gitignore
|
||||||
|
node_modules
|
||||||
|
.c9revisions
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Packages
|
||||||
|
*.egg
|
||||||
|
*.egg-info
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
eggs
|
||||||
|
parts
|
||||||
|
bin
|
||||||
|
var
|
||||||
|
sdist
|
||||||
|
develop-eggs
|
||||||
|
.installed.cfg
|
||||||
|
lib
|
||||||
|
lib64
|
||||||
|
include
|
||||||
|
man
|
||||||
|
__pycache__
|
||||||
|
public
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
.coverage
|
||||||
|
.tox
|
||||||
|
nosetests.xml
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
cms/webpack
|
||||||
|
|
||||||
|
# Ignore IDEA files
|
||||||
|
.idea
|
|
@ -0,0 +1,4 @@
|
||||||
|
0.0
|
||||||
|
---
|
||||||
|
|
||||||
|
- Initial version.
|
|
@ -0,0 +1,2 @@
|
||||||
|
include *.txt *.ini *.cfg *.rst
|
||||||
|
recursive-include cms *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
|
|
@ -0,0 +1,12 @@
|
||||||
|
Radium Flat-file CMS
|
||||||
|
==========
|
||||||
|
|
||||||
|
Getting Started
|
||||||
|
---------------
|
||||||
|
|
||||||
|
- Install flatfilecms
|
||||||
|
|
||||||
|
pip install flatfilecms
|
||||||
|
|
||||||
|
- Use flatfilecms:main as a WSGI application
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
from pyramid.config import Configurator
|
||||||
|
from pyramid.path import AssetResolver
|
||||||
|
from .resources import Root
|
||||||
|
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class PagesEventHandler(FileSystemEventHandler):
|
||||||
|
def __init__(self, root, root_path):
|
||||||
|
super(PagesEventHandler, self).__init__()
|
||||||
|
self.root = root
|
||||||
|
self.root_path = root_path
|
||||||
|
|
||||||
|
def find_dir(self, path):
|
||||||
|
folder = self.root
|
||||||
|
path = path.relative_to(self.root_path)
|
||||||
|
for name in path.parts[:-1]:
|
||||||
|
folder = folder[name]
|
||||||
|
return folder
|
||||||
|
|
||||||
|
def on_created(self, event):
|
||||||
|
super(PagesEventHandler, self).on_created(event)
|
||||||
|
path = Path(event.dest_path if hasattr(event, 'dest_path')
|
||||||
|
else event.src_path)
|
||||||
|
folder = self.find_dir(path)
|
||||||
|
if event.is_directory:
|
||||||
|
folder.create_dir(path)
|
||||||
|
else:
|
||||||
|
folder.create_file(path)
|
||||||
|
|
||||||
|
def on_deleted(self, event):
|
||||||
|
super(PagesEventHandler, self).on_deleted(event)
|
||||||
|
path = Path(event.src_path)
|
||||||
|
folder = self.find_dir(path)
|
||||||
|
name = path.stem
|
||||||
|
if path.suffix not in ['.md', '.yaml', '.j2', '.jinja2'] \
|
||||||
|
and path.name != 'index.html':
|
||||||
|
name = path.name
|
||||||
|
del folder[name]
|
||||||
|
|
||||||
|
def on_moved(self, event):
|
||||||
|
super(PagesEventHandler, self).on_moved(event)
|
||||||
|
self.on_deleted(event)
|
||||||
|
self.on_created(event)
|
||||||
|
|
||||||
|
|
||||||
|
def main(global_config, **settings):
|
||||||
|
""" This function returns a Pyramid WSGI application.
|
||||||
|
"""
|
||||||
|
from pyramid.settings import asbool
|
||||||
|
watchdog = asbool(settings.get(
|
||||||
|
'watchdog', 'false'))
|
||||||
|
settings['watchdog'] = watchdog
|
||||||
|
settings.setdefault('pages_dir', 'pages')
|
||||||
|
settings.setdefault('data_dir', 'data')
|
||||||
|
|
||||||
|
pages = Root(settings['pages_dir'])
|
||||||
|
|
||||||
|
def factory(request):
|
||||||
|
return pages
|
||||||
|
|
||||||
|
config = Configurator(settings=settings,
|
||||||
|
root_factory=factory)
|
||||||
|
config.include('pyramid_jinja2')
|
||||||
|
config.include('.routes')
|
||||||
|
config.scan()
|
||||||
|
if watchdog:
|
||||||
|
path = AssetResolver().resolve(
|
||||||
|
settings['pages_dir']).abspath()
|
||||||
|
observer = Observer()
|
||||||
|
event_handler = PagesEventHandler(pages, path)
|
||||||
|
observer.schedule(
|
||||||
|
event_handler,
|
||||||
|
path,
|
||||||
|
recursive=True)
|
||||||
|
observer.start()
|
||||||
|
return config.make_wsgi_app()
|
|
@ -0,0 +1,57 @@
|
||||||
|
from jinja2 import contextfilter, Markup
|
||||||
|
import markdown
|
||||||
|
from pathlib import Path
|
||||||
|
from markdown.extensions import Extension
|
||||||
|
from markdown.blockprocessors import BlockProcessor
|
||||||
|
|
||||||
|
|
||||||
|
class Jinja2Processor(BlockProcessor):
|
||||||
|
def test(self, parent, block):
|
||||||
|
return block.startswith('{%') or block.startswith('{{')
|
||||||
|
|
||||||
|
def run(self, parent, blocks):
|
||||||
|
block = blocks.pop(0)
|
||||||
|
sibling = self.lastChild(parent)
|
||||||
|
if sibling is not None:
|
||||||
|
if sibling.tail:
|
||||||
|
sibling.tail = sibling.tail + block
|
||||||
|
else:
|
||||||
|
sibling.tail = block
|
||||||
|
else:
|
||||||
|
if parent.text:
|
||||||
|
parent.text = parent.text + block
|
||||||
|
else:
|
||||||
|
parent.text = block
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class IgnoreExtension(Extension):
|
||||||
|
def extendMarkdown(self, md, md_globals):
|
||||||
|
md.parser.blockprocessors.add(
|
||||||
|
'jinja2',
|
||||||
|
Jinja2Processor(md.parser),
|
||||||
|
">hashheader")
|
||||||
|
|
||||||
|
|
||||||
|
@contextfilter
|
||||||
|
def markdown2html(context, text, render=True):
|
||||||
|
result = markdown.markdown(
|
||||||
|
text,
|
||||||
|
extensions=[IgnoreExtension(), 'markdown.extensions.extra'],
|
||||||
|
output_format='html5',
|
||||||
|
tab_length=2)
|
||||||
|
if render:
|
||||||
|
result = context.environment.from_string(result).render(context)
|
||||||
|
if context.eval_ctx.autoescape:
|
||||||
|
result = Markup(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def merge_dict(a, b):
|
||||||
|
if isinstance(a, list):
|
||||||
|
return [merge_dict(i, b) for i in a]
|
||||||
|
return {**a, **b}
|
||||||
|
|
||||||
|
|
||||||
|
def fileglob(path, root):
|
||||||
|
return [p.relative_to(root) for p in Path(root).glob(path)]
|
|
@ -0,0 +1,75 @@
|
||||||
|
from pyramid.path import AssetResolver
|
||||||
|
from pathlib import PurePath
|
||||||
|
|
||||||
|
|
||||||
|
def flat(d, path):
|
||||||
|
structure = []
|
||||||
|
for k, v in d.items():
|
||||||
|
if isinstance(v, dict):
|
||||||
|
structure.extend(flat(v, f"{path}/{k}"))
|
||||||
|
else:
|
||||||
|
if k == 'index':
|
||||||
|
structure.append(f"{path}/")
|
||||||
|
else:
|
||||||
|
structure.append(f"{path}/{k}")
|
||||||
|
return structure
|
||||||
|
|
||||||
|
|
||||||
|
class Folder(dict):
|
||||||
|
def __init__(self, name, parent, path):
|
||||||
|
self.__path__ = path
|
||||||
|
self.__name__ = name
|
||||||
|
self.__parent__ = parent
|
||||||
|
for entry in AssetResolver().resolve(path).listdir():
|
||||||
|
asset = f"{path}/{entry}"
|
||||||
|
if AssetResolver().resolve(asset).isdir():
|
||||||
|
self.create_dir(asset)
|
||||||
|
else:
|
||||||
|
self.create_file(asset)
|
||||||
|
|
||||||
|
def create_file(self, asset):
|
||||||
|
path = PurePath(asset)
|
||||||
|
if path.suffix == '.md':
|
||||||
|
self[path.stem] = Markdown(path.stem, self, asset)
|
||||||
|
elif path.suffix == '.yaml':
|
||||||
|
self[path.stem] = YAML(path.stem, self, asset)
|
||||||
|
elif path.suffix == '.j2' or path.suffix == '.jinja2':
|
||||||
|
self[path.stem] = Jinja2(path.stem, self, asset)
|
||||||
|
else:
|
||||||
|
name = path.name
|
||||||
|
# Если имя файла не index.html,
|
||||||
|
# то отдавать по имени файла
|
||||||
|
if name == 'index.html':
|
||||||
|
name = 'index'
|
||||||
|
self[name] = Document(name, self, asset)
|
||||||
|
|
||||||
|
def create_dir(self, asset):
|
||||||
|
path = PurePath(asset)
|
||||||
|
self[path.name] = Folder(path.name, self, asset)
|
||||||
|
|
||||||
|
def structure(self, base_dir=''):
|
||||||
|
return flat(self, base_dir)
|
||||||
|
|
||||||
|
|
||||||
|
class Root(Folder):
|
||||||
|
def __init__(self, path):
|
||||||
|
super(Root, self).__init__('', None, path)
|
||||||
|
|
||||||
|
|
||||||
|
class Document(object):
|
||||||
|
def __init__(self, name, parent, path):
|
||||||
|
self.__name__ = name
|
||||||
|
self.__parent__ = parent
|
||||||
|
self.__path__ = path
|
||||||
|
|
||||||
|
|
||||||
|
class Markdown(Document):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class YAML(Document):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Jinja2(Document):
|
||||||
|
pass
|
|
@ -0,0 +1,7 @@
|
||||||
|
def includeme(config):
|
||||||
|
settings = config.get_settings()
|
||||||
|
if 'static' in settings:
|
||||||
|
config.add_static_view(
|
||||||
|
'static',
|
||||||
|
settings['static'],
|
||||||
|
cache_max_age=3600)
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends "layout.jinja2" %}
|
||||||
|
|
||||||
|
{% set title="404: страница не найдена!" %}
|
||||||
|
{% set description="Попробуйте найти нужную вам страницу в меню сайта." %}
|
||||||
|
{% set image="https://source.unsplash.com/Q1p7bh3SHj8" %}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html ⚡>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<script async src="https://cdn.ampproject.org/v0.js"></script>
|
||||||
|
<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
|
||||||
|
<link rel="canonical" href="{{canonical | default(request.url)}}">
|
||||||
|
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
|
||||||
|
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block body %}
|
||||||
|
{{content}}
|
||||||
|
{% endblock %}
|
||||||
|
{% include 'amp-counter.jinja2' %}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,29 @@
|
||||||
|
{%- from 'macros.jinja2' import b -%}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{request.locale_name}}">
|
||||||
|
<head>
|
||||||
|
{% block head -%}
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="{{description}}">
|
||||||
|
<meta name="generator" content="flatfilecms">
|
||||||
|
<link rel="icon" type="image/png" href="/static/img/favicon.png">
|
||||||
|
<title>{{title}}</title>
|
||||||
|
{% block links %}
|
||||||
|
{%- endblock %}
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="theme-color" content="#333333">
|
||||||
|
{%- if json_ld %}
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{{ json_ld | merge_dict({"@context": "http://schema.org"}) | tojson }}
|
||||||
|
</script>
|
||||||
|
{%- endif -%}
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% include 'counter.jinja2' %}
|
||||||
|
{% block body %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div style="background: url({{jumbotron_image}}) no-repeat center center; background-size: cover">
|
||||||
|
<div class="jumbotron container {{jumbotron_class|default('text-light bg-darken')}}">
|
||||||
|
<h2>{{jumbotron_header}}</h2>
|
||||||
|
{{jumbotron_content}}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<nav class="nav justify-content-left container py-3 footer-menu">
|
||||||
|
{%- for item in menu recursive %}
|
||||||
|
{% if item.href -%}
|
||||||
|
<a href="{{item.href|e}}" class="nav-link text-light py-0">{{item.title}}</a>
|
||||||
|
{%- endif -%}
|
||||||
|
{{ loop(item.children) }}
|
||||||
|
{%- endfor %}
|
||||||
|
</nav>
|
|
@ -0,0 +1,60 @@
|
||||||
|
<div{% if image %} style="background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), url({{image}}) no-repeat center center; background-size:cover"{% endif %}>
|
||||||
|
<nav class="navbar navbar-expand-sm navbar-dark container">
|
||||||
|
<a href="/" class="navbar-brand">
|
||||||
|
{% include 'logo.jinja2' %}
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div id="navbarSupportedContent" class="collapse navbar-collapse">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
{%- for item in menu %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
{%- if item.children %}
|
||||||
|
<a role="button" class="nav-link dropdown-toggle cursor-default" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{item.title}}</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{%- if item.href %}
|
||||||
|
<li>
|
||||||
|
<a href="{{item.href|e}}" class="dropdown-item">{{item.title}}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
</li>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- for subitem in item.children recursive -%}
|
||||||
|
{%- if subitem.children %}
|
||||||
|
<li class="dropright">
|
||||||
|
<button class="dropdown-item dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{subitem.title}}</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{%- if subitem.href %}
|
||||||
|
<li>
|
||||||
|
<a href="{{subitem.href|e}}" class="dropdown-item">{{subitem.title}}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
</li>
|
||||||
|
{%- endif -%}
|
||||||
|
{{ loop(subitem.children) }}
|
||||||
|
</ul>
|
||||||
|
{%- else -%}
|
||||||
|
<li>
|
||||||
|
<a href="{{subitem.href|e}}" class="dropdown-item">{{subitem.title}}</a>
|
||||||
|
{%- endif -%}
|
||||||
|
</li>
|
||||||
|
{%- endfor %}
|
||||||
|
</ul>
|
||||||
|
{%- else %}
|
||||||
|
<a href="{{item.href|e}}" class="nav-link">{{item.title}}</a>
|
||||||
|
{%- endif %}
|
||||||
|
</li>
|
||||||
|
{%- endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="jumbotron container text-white bg-transparent">
|
||||||
|
<h1 class="display-4">{{title}}</h1>
|
||||||
|
{%- if description %}
|
||||||
|
<p class="lead">{{description}}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends "layout.jinja2" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{{ contents|safe }}
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,26 @@
|
||||||
|
{% extends "base.jinja2" %}
|
||||||
|
{% block links %}
|
||||||
|
{% webpack 'layout', '.js' -%}
|
||||||
|
<script src="{{ ASSET.url }}" async defer></script>
|
||||||
|
{% endwebpack %}
|
||||||
|
{% webpack 'layout', '.css' -%}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ ASSET.url }}">
|
||||||
|
{% endwebpack %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<header>
|
||||||
|
{% include "header.jinja2" %}
|
||||||
|
</header>
|
||||||
|
<main class="py-4">
|
||||||
|
<article class="container">
|
||||||
|
{% block content %}
|
||||||
|
{% if content %}
|
||||||
|
{{content|markdown}}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<footer class="page-footer">
|
||||||
|
{% include "footer.jinja2" %}
|
||||||
|
</footer>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{% macro b(class=None, tag="div", id=None) -%}
|
||||||
|
<{{ tag }}
|
||||||
|
{%- if class is not none %} class="{{class}}" {%- endif -%}
|
||||||
|
{%- if id is not none %} id="{{id}}" {%- endif -%}
|
||||||
|
>
|
||||||
|
{%- if caller %}
|
||||||
|
{{ caller() }}
|
||||||
|
{% endif -%}
|
||||||
|
</{{ tag }}>
|
||||||
|
{%- endmacro %}
|
|
@ -0,0 +1,42 @@
|
||||||
|
{% extends "base.jinja2" %}
|
||||||
|
{% block links %}
|
||||||
|
{% webpack 'onepage', '.js' -%}
|
||||||
|
<script src="{{ ASSET.url }}" async defer></script>
|
||||||
|
{% endwebpack %}
|
||||||
|
{% webpack 'onepage', '.css' -%}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ ASSET.url }}">
|
||||||
|
{% endwebpack %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<header>
|
||||||
|
<nav id="nav-page" class="navbar navbar-expand-sm navbar-dark fixed-top">
|
||||||
|
<a href="/" class="navbar-brand">
|
||||||
|
{% include 'logo.jinja2' %}
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse justify-content-end" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
{%- for item in nav %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{item.link}}" class="nav-link">{{item.name}}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<article>
|
||||||
|
{% block content %}
|
||||||
|
{% if content %}
|
||||||
|
{{content|markdown}}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<footer class="page-footer">
|
||||||
|
{% include "footer.jinja2" %}
|
||||||
|
</footer>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% extends "layout.jinja2" %}
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
{%- for item in request.root.structure() %}{% if item != '/sitemap.xml' and item not in data.ignore %}
|
||||||
|
<url>
|
||||||
|
<loc>{{data.base_url}}{{item}}</loc>
|
||||||
|
</url>
|
||||||
|
{%- endif %}{%- endfor %}
|
||||||
|
</urlset>
|
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "layout.jinja2" %}
|
||||||
|
{% block content %}
|
||||||
|
{% filter markdown %}
|
||||||
|
{% if content %}
|
||||||
|
{{content|safe}}
|
||||||
|
{% endif %}
|
||||||
|
{% for name in ['why', 'what', 'who', 'call'] %}
|
||||||
|
{% if name in data %}
|
||||||
|
{{("{% call b(tag='section', id='"+name+"') %}")|safe}}
|
||||||
|
|
||||||
|
{{data[name]|safe}}
|
||||||
|
|
||||||
|
{{"{% endcall %}"|safe}}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if 'abbr' in data %}
|
||||||
|
{{data['abbr']|safe}}
|
||||||
|
{% endif %}
|
||||||
|
{% endfilter %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,7 @@
|
||||||
|
from pyramid.view import notfound_view_config
|
||||||
|
|
||||||
|
|
||||||
|
@notfound_view_config(renderer='../templates/404.jinja2')
|
||||||
|
def notfound_view(request):
|
||||||
|
request.response.status = 404
|
||||||
|
return {}
|
|
@ -0,0 +1,120 @@
|
||||||
|
from pyramid.view import (
|
||||||
|
view_config,
|
||||||
|
render_view_to_response,
|
||||||
|
)
|
||||||
|
from pyramid.renderers import render_to_response
|
||||||
|
from pyramid.httpexceptions import (HTTPFound, HTTPNotFound)
|
||||||
|
from pyramid.response import FileResponse
|
||||||
|
from pyramid.path import AssetResolver
|
||||||
|
from pathlib import PurePath
|
||||||
|
from ..resources import (Folder, Document, Markdown, YAML, Jinja2)
|
||||||
|
import yaml
|
||||||
|
import frontmatter
|
||||||
|
|
||||||
|
|
||||||
|
class Loader(yaml.Loader):
|
||||||
|
def __init__(self, stream):
|
||||||
|
super(Loader, self).__init__(stream)
|
||||||
|
Loader.add_constructor('!include', Loader.include)
|
||||||
|
|
||||||
|
def include(self, node):
|
||||||
|
if isinstance(node, yaml.ScalarNode):
|
||||||
|
return self.extractFile(self.construct_scalar(node))
|
||||||
|
|
||||||
|
elif isinstance(node, yaml.SequenceNode):
|
||||||
|
return [self.extractFile(filename)
|
||||||
|
for filename in self.construct_sequence(node)]
|
||||||
|
|
||||||
|
elif isinstance(node, yaml.MappingNode):
|
||||||
|
result = {}
|
||||||
|
for k, v in self.construct_mapping(node).items():
|
||||||
|
result[k] = self.extractFile(v)
|
||||||
|
return result
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise yaml.constructor.ConstructorError(
|
||||||
|
"Error:: unrecognised node type in !include statement")
|
||||||
|
|
||||||
|
def extractFile(self, filename):
|
||||||
|
path = PurePath(self.data_dir) / filename
|
||||||
|
f = AssetResolver().resolve(str(path)).stream()
|
||||||
|
if f:
|
||||||
|
if path.suffix in ['.yaml', '.yml', '.json']:
|
||||||
|
return yaml.load(f, Loader)
|
||||||
|
return f.read().decode()
|
||||||
|
|
||||||
|
|
||||||
|
def LoaderFactory(data_dir):
|
||||||
|
cl = Loader
|
||||||
|
cl.data_dir = data_dir
|
||||||
|
return cl
|
||||||
|
|
||||||
|
|
||||||
|
class CustomYAMLHandler(frontmatter.YAMLHandler):
|
||||||
|
def __init__(self, data_dir):
|
||||||
|
self.loader = LoaderFactory(data_dir)
|
||||||
|
|
||||||
|
def load(self, fm, **kwargs):
|
||||||
|
return yaml.load(fm, self.loader)
|
||||||
|
|
||||||
|
|
||||||
|
class PagesView:
|
||||||
|
def __init__(self, context, request):
|
||||||
|
self.context = context
|
||||||
|
self.request = request
|
||||||
|
|
||||||
|
@view_config(context=Folder)
|
||||||
|
def folder(self):
|
||||||
|
if 'index' not in self.context:
|
||||||
|
raise HTTPNotFound
|
||||||
|
return render_view_to_response(self.context['index'], self.request)
|
||||||
|
|
||||||
|
@view_config(context=Document)
|
||||||
|
def document(self):
|
||||||
|
return FileResponse(
|
||||||
|
AssetResolver().resolve(self.context.__path__).abspath(),
|
||||||
|
request=self.request)
|
||||||
|
|
||||||
|
def process_yaml(self):
|
||||||
|
if 'redirect' in self.post:
|
||||||
|
return HTTPFound(location=self.post.redirect)
|
||||||
|
if 'menu' not in self.post:
|
||||||
|
self.post['menu'] = yaml.load(
|
||||||
|
AssetResolver().resolve(
|
||||||
|
str(PurePath(self.request.registry.settings['data_dir']) /
|
||||||
|
'menu/default.yaml')).stream(),
|
||||||
|
LoaderFactory(self.request.registry.settings['data_dir']))
|
||||||
|
response = render_to_response(
|
||||||
|
'{0}.jinja2'.format(
|
||||||
|
self.post.get('template', 'default')),
|
||||||
|
self.post,
|
||||||
|
request=self.request
|
||||||
|
)
|
||||||
|
if 'content_type' in self.post:
|
||||||
|
response.content_type = self.post['content_type']
|
||||||
|
return response
|
||||||
|
|
||||||
|
@view_config(context=YAML)
|
||||||
|
def yaml(self):
|
||||||
|
self.post = yaml.load(
|
||||||
|
AssetResolver().resolve(
|
||||||
|
self.context.__path__).stream(),
|
||||||
|
LoaderFactory(self.request.registry.settings['data_dir']))
|
||||||
|
return self.process_yaml()
|
||||||
|
|
||||||
|
@view_config(context=Markdown)
|
||||||
|
def markdown(self):
|
||||||
|
self.post = frontmatter.load(
|
||||||
|
AssetResolver().resolve(
|
||||||
|
self.context.__path__).stream(),
|
||||||
|
handler=CustomYAMLHandler(self.request.registry.settings['data_dir']),
|
||||||
|
).to_dict()
|
||||||
|
return self.process_yaml()
|
||||||
|
|
||||||
|
@view_config(context=Jinja2)
|
||||||
|
def jinja2(self):
|
||||||
|
return render_to_response(
|
||||||
|
self.context.path,
|
||||||
|
{},
|
||||||
|
request=self.request
|
||||||
|
)
|
|
@ -0,0 +1,64 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
with open(os.path.join(here, 'README.txt')) as f:
|
||||||
|
README = f.read()
|
||||||
|
with open(os.path.join(here, 'CHANGES.txt')) as f:
|
||||||
|
CHANGES = f.read()
|
||||||
|
|
||||||
|
requires = [
|
||||||
|
'plaster_pastedeploy',
|
||||||
|
'pyramid >= 1.9a',
|
||||||
|
'pyramid_debugtoolbar',
|
||||||
|
'pyramid_jinja2',
|
||||||
|
'pyramid_retry',
|
||||||
|
'pyramid_tm',
|
||||||
|
'transaction',
|
||||||
|
'waitress',
|
||||||
|
'Markdown',
|
||||||
|
'PyYAML',
|
||||||
|
'python-frontmatter',
|
||||||
|
'watchdog < 0.9.0',
|
||||||
|
'pyramid-htmlmin',
|
||||||
|
'pyramid-webpack',
|
||||||
|
]
|
||||||
|
|
||||||
|
tests_require = [
|
||||||
|
'WebTest >= 1.3.1', # py3 compat
|
||||||
|
'pytest',
|
||||||
|
'pytest-cov',
|
||||||
|
]
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='flatfilecms',
|
||||||
|
version='0.0',
|
||||||
|
description='Radium CMS',
|
||||||
|
long_description=README + '\n\n' + CHANGES,
|
||||||
|
classifiers=[
|
||||||
|
'Programming Language :: Python',
|
||||||
|
'Framework :: Pyramid',
|
||||||
|
'Topic :: Internet :: WWW/HTTP',
|
||||||
|
'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
|
||||||
|
],
|
||||||
|
author='Anton Kasimov',
|
||||||
|
author_email='kasimov@radium-it.ru',
|
||||||
|
url='http://www.radium.group',
|
||||||
|
keywords='web pyramid pylons flat-file CMS',
|
||||||
|
packages=find_packages(),
|
||||||
|
include_package_data=True,
|
||||||
|
zip_safe=False,
|
||||||
|
extras_require={
|
||||||
|
'testing': tests_require,
|
||||||
|
},
|
||||||
|
install_requires=requires,
|
||||||
|
entry_points={
|
||||||
|
'paste.app_factory': [
|
||||||
|
'main = flatfilecms:main',
|
||||||
|
],
|
||||||
|
'console_scripts': [
|
||||||
|
'generate_static_site = flatfilecms.scripts.generate:main',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
Reference in New Issue