init
This commit is contained in:
commit
183d7b6396
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.egg-info/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
*~
|
63
README.md
Normal file
63
README.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Pēji
|
||||||
|
|
||||||
|
**Pēji** (Japanese: ページ, "page") is simple way to generate small static sites (one or more pages).
|
||||||
|
|
||||||
|
If you need to collect several pages from the Markdown, then Pēji are great for you.
|
||||||
|
|
||||||
|
**Note**: Pēji is not intended to generate a blog site. Its single pages only. You can link pages with hyper-links, but you have to do it manually.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
- [Python-Markdown](https://python-markdown.github.io/) is used.
|
||||||
|
- Code syntax highlighting via [Pygments](https://pygments.org/).
|
||||||
|
- [Jinja2](https://jinja.palletsprojects.com/en/2.11.x/) template engine.
|
||||||
|
- Custom style and layout for specific page.
|
||||||
|
- Site menu bar can be edited through the config.
|
||||||
|
- YAML config file.
|
||||||
|
|
||||||
|
## Installation and quickstart
|
||||||
|
|
||||||
|
Install Pēji globally or into virtual environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install Peji
|
||||||
|
```
|
||||||
|
|
||||||
|
Create your site:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
peji create mysite
|
||||||
|
cd mysite/
|
||||||
|
```
|
||||||
|
|
||||||
|
Create your first page and place it into `mysite/pages/`:
|
||||||
|
|
||||||
|
`index.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
title: My first page
|
||||||
|
---
|
||||||
|
|
||||||
|
# My heading
|
||||||
|
|
||||||
|
It works!
|
||||||
|
```
|
||||||
|
|
||||||
|
Build your site:
|
||||||
|
|
||||||
|
```
|
||||||
|
peji build
|
||||||
|
```
|
||||||
|
|
||||||
|
Site will be placed in `./mysite/build`.
|
||||||
|
|
||||||
|
Also you can run Python built-in HTTP Server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m http.server --directory build/
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Pēji is released under The Unlicense. See [https://unlicense.org/](https://unlicense.org/) for detais.
|
413
peji.py
Normal file
413
peji.py
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
##########################################################################
|
||||||
|
#
|
||||||
|
# Pēji — static site generator.
|
||||||
|
# Homepage: https://peji.gch.icu
|
||||||
|
#
|
||||||
|
# LICENSE: The Unlicense
|
||||||
|
#
|
||||||
|
# This is free and unencumbered software released into the public domain.
|
||||||
|
#
|
||||||
|
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
# distribute this software, either in source code form or as a compiled
|
||||||
|
# binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
# means.
|
||||||
|
#
|
||||||
|
# In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
# of this software dedicate any and all copyright interest in the
|
||||||
|
# software to the public domain. We make this dedication for the benefit
|
||||||
|
# of the public at large and to the detriment of our heirs and
|
||||||
|
# successors. We intend this dedication to be an overt act of
|
||||||
|
# relinquishment in perpetuity of all present and future rights to this
|
||||||
|
# software under copyright law.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
# OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
#
|
||||||
|
# For more information, please refer to <http://unlicense.org/>
|
||||||
|
#
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
__version__ = 1.0
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
import click
|
||||||
|
import jinja2
|
||||||
|
import pygments
|
||||||
|
import markdown
|
||||||
|
|
||||||
|
|
||||||
|
config_yaml_template = '''# Site title.
|
||||||
|
title: My site
|
||||||
|
# Site meta description.
|
||||||
|
description: My cool website.
|
||||||
|
# Site default theme.
|
||||||
|
theme: default
|
||||||
|
# Site menu.
|
||||||
|
menu:
|
||||||
|
Home: /
|
||||||
|
# Markdown extensions.
|
||||||
|
# See https://python-markdown.github.io/extensions/
|
||||||
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
|
- codehilite
|
||||||
|
- extra
|
||||||
|
- meta
|
||||||
|
- toc
|
||||||
|
markdown_extension_configs:
|
||||||
|
codehilite:
|
||||||
|
noclasses: true
|
||||||
|
use_pygments: true
|
||||||
|
pygments_style: default
|
||||||
|
'''
|
||||||
|
|
||||||
|
base_j2_template = '''<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<title>
|
||||||
|
{% block title %}{% endblock %}
|
||||||
|
</title>
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% if menu %}
|
||||||
|
<div class="menu">
|
||||||
|
<ul>
|
||||||
|
{% for link in menu.keys() %}
|
||||||
|
<li><a href="{{ menu.get(link) }}">{{ link }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% block content %}
|
||||||
|
Nothing here.
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''
|
||||||
|
|
||||||
|
index_j2_template = '''{% extends 'base.j2' %}
|
||||||
|
{% block title %}{{ title }} | {{ site_title }}{% endblock %}
|
||||||
|
{% block head %}{{ super() }}
|
||||||
|
<link rel="stylesheet" href="/themes/{{ theme }}/css/main.css">
|
||||||
|
<link rel="shortcut icon" href="/themes/{{ theme }}/images/favicon.ico"
|
||||||
|
type="image/x-icon">
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
{{ content | safe }}
|
||||||
|
{% endblock %}
|
||||||
|
'''
|
||||||
|
|
||||||
|
default_main_css = '''body {
|
||||||
|
margin: 32px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #000000;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a,
|
||||||
|
a:active,
|
||||||
|
a:visited {
|
||||||
|
color: #3f37c9;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
border-bottom: 1px dashed;
|
||||||
|
}
|
||||||
|
.menu {
|
||||||
|
margin: 18px 0 28px 0;
|
||||||
|
}
|
||||||
|
.menu ul {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.menu ul li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
padding-right: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.menu ul li::before {
|
||||||
|
content: '|';
|
||||||
|
position: relative;
|
||||||
|
left: -6px;
|
||||||
|
}
|
||||||
|
.menu ul li:first-child::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
dt {
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
dd {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.admonition {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
color: #055160;
|
||||||
|
background-color: #cff4fc;
|
||||||
|
border: 1px solid #055160;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
color: #084298;
|
||||||
|
background-color: #cfe2ff;
|
||||||
|
border: 1px solid #084298;
|
||||||
|
}
|
||||||
|
.tip {
|
||||||
|
color: #0f5132;
|
||||||
|
background-color: #d1e7dd;
|
||||||
|
border: 1px solid #0f5132;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: #664d03;
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border: 1px solid #664d03;
|
||||||
|
}
|
||||||
|
.danger {
|
||||||
|
color: #842029;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border: 1px solid #842029;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
pages = 'pages'
|
||||||
|
layouts = 'layouts'
|
||||||
|
themes = 'themes'
|
||||||
|
|
||||||
|
cwd = os.getcwd()
|
||||||
|
|
||||||
|
def read_file(filepath: str) -> str:
|
||||||
|
try:
|
||||||
|
with open(filepath, 'r') as file:
|
||||||
|
return file.read()
|
||||||
|
except IOError:
|
||||||
|
print('Error: cannot read file: {}'.format(filepath))
|
||||||
|
|
||||||
|
def write_to_file(filepath: str, data: str):
|
||||||
|
try:
|
||||||
|
with open(filepath, 'w') as file:
|
||||||
|
return file.write(data)
|
||||||
|
except IOError:
|
||||||
|
print('Error: cannot write to file: {}'.format(filepath))
|
||||||
|
|
||||||
|
def get_config() -> dict:
|
||||||
|
config = read_file(os.path.join(cwd, 'config.yaml'))
|
||||||
|
return yaml.safe_load(config)
|
||||||
|
|
||||||
|
def get_markdown_file_list(directory: str) -> list:
|
||||||
|
"""Return the list of Markdown files from
|
||||||
|
directory. Scan subdirectories too.
|
||||||
|
"""
|
||||||
|
file_list = []
|
||||||
|
for root, dirs, files in os.walk(directory):
|
||||||
|
for name in files:
|
||||||
|
if os.path.splitext(name)[1] == '.md':
|
||||||
|
file_list.append(os.path.join(root, name))
|
||||||
|
return file_list
|
||||||
|
|
||||||
|
def get_new_path(build_dir: str, filename: str) -> str:
|
||||||
|
"""Make dir and return file path. Example:
|
||||||
|
Before:
|
||||||
|
./mysite/pages/index.md
|
||||||
|
./mysite/pages/subfilder/index.md
|
||||||
|
After:
|
||||||
|
./mysite/build/index.html
|
||||||
|
./mysite/pages/subfilder/index.html
|
||||||
|
"""
|
||||||
|
relpath = os.path.relpath(filename, start = pages)
|
||||||
|
if os.path.dirname(relpath):
|
||||||
|
os.makedirs(
|
||||||
|
os.path.join(build_dir, os.path.dirname(relpath)),
|
||||||
|
exist_ok = True
|
||||||
|
)
|
||||||
|
return os.path.join(
|
||||||
|
build_dir,
|
||||||
|
os.path.splitext(relpath)[0] + '.html'
|
||||||
|
)
|
||||||
|
|
||||||
|
def copy_files(source_dir: str, destination_dir: str):
|
||||||
|
shutil.copytree(
|
||||||
|
source_dir,
|
||||||
|
destination_dir,
|
||||||
|
ignore = shutil.ignore_patterns('*.md'),
|
||||||
|
dirs_exist_ok = True
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_page_metadata(text: str) -> dict:
|
||||||
|
"""Parse metadata from Markdown as YAML or return None.
|
||||||
|
"""
|
||||||
|
# Find first paragraph that starts and ends with '---'.
|
||||||
|
pattern = r'\A\s*(^-{3}$)\n((.+\n)+\n*)(^-{3}$)'
|
||||||
|
metadata = re.search(pattern, text, re.MULTILINE)
|
||||||
|
if metadata:
|
||||||
|
metadata = metadata.group().replace('---', '')
|
||||||
|
return yaml.safe_load(metadata)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate_matadata(config: dict, metadata: dict) -> dict:
|
||||||
|
meta = {}
|
||||||
|
if metadata:
|
||||||
|
try:
|
||||||
|
meta['title'] = metadata['title']
|
||||||
|
except KeyError:
|
||||||
|
meta['title'] = ''
|
||||||
|
try:
|
||||||
|
meta['theme'] = metadata['theme']
|
||||||
|
except KeyError:
|
||||||
|
meta['theme'] = config['theme']
|
||||||
|
try:
|
||||||
|
meta['layout'] = metadata['layout']
|
||||||
|
except KeyError:
|
||||||
|
meta['layout'] = 'index.j2'
|
||||||
|
# Validate config meta
|
||||||
|
try:
|
||||||
|
meta['site_title'] = config['title']
|
||||||
|
except KeyError:
|
||||||
|
meta['site_title'] = ''
|
||||||
|
try:
|
||||||
|
meta['description'] = config['description']
|
||||||
|
except KeyError:
|
||||||
|
meta['description'] = ''
|
||||||
|
try:
|
||||||
|
meta['menu'] = config['menu']
|
||||||
|
except KeyError:
|
||||||
|
meta['menu'] = False
|
||||||
|
return meta
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def render_html_from_markdown(config: dict, text: str) -> str:
|
||||||
|
html = markdown.Markdown(
|
||||||
|
extensions = config['markdown_extensions'],
|
||||||
|
extension_configs = config['markdown_extension_configs']
|
||||||
|
)
|
||||||
|
return html.convert(text)
|
||||||
|
|
||||||
|
def render_template(template: str, **kwargs) -> str:
|
||||||
|
"""Render template from file.
|
||||||
|
Usage:
|
||||||
|
render_template('index.j2', title = 'My title')
|
||||||
|
"""
|
||||||
|
env = jinja2.Environment(loader = jinja2.FileSystemLoader(layouts))
|
||||||
|
return env.get_template(template).render(**kwargs)
|
||||||
|
|
||||||
|
def check_site(cwd: str):
|
||||||
|
"""Check site config existence and exit if not exist."""
|
||||||
|
if not os.path.exists(os.path.join(cwd, 'config.yaml')):
|
||||||
|
click.echo(
|
||||||
|
'Error: No such file: config.yaml\nAre you in site directory?',
|
||||||
|
err = True)
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def make_site_dirs(site: str):
|
||||||
|
default_dirs = [
|
||||||
|
'layouts',
|
||||||
|
'pages',
|
||||||
|
'themes',
|
||||||
|
'themes/default/css',
|
||||||
|
'themes/default/images',
|
||||||
|
'themes/default/js'
|
||||||
|
]
|
||||||
|
dirs = [os.path.join(site, dir) for dir in default_dirs]
|
||||||
|
for dir in dirs:
|
||||||
|
os.makedirs(os.path.join(cwd, dir), exist_ok = True)
|
||||||
|
|
||||||
|
def create_site(site: str):
|
||||||
|
make_site_dirs(site)
|
||||||
|
write_to_file(
|
||||||
|
os.path.join(site, 'config.yaml'),
|
||||||
|
config_yaml_template
|
||||||
|
)
|
||||||
|
write_to_file(
|
||||||
|
os.path.join(site, 'layouts/base.j2'),
|
||||||
|
base_j2_template
|
||||||
|
)
|
||||||
|
write_to_file(
|
||||||
|
os.path.join(site, 'layouts/index.j2'),
|
||||||
|
index_j2_template
|
||||||
|
)
|
||||||
|
write_to_file(
|
||||||
|
os.path.join(site, 'themes/default/css/main.css'),
|
||||||
|
default_main_css
|
||||||
|
)
|
||||||
|
|
||||||
|
def build_site(build_dir: str):
|
||||||
|
"""Render HTML files and place site data to `build_dir`."""
|
||||||
|
config = get_config()
|
||||||
|
markdown_files = get_markdown_file_list(pages)
|
||||||
|
for file in markdown_files:
|
||||||
|
text = read_file(file)
|
||||||
|
meta = validate_matadata(
|
||||||
|
config,
|
||||||
|
parse_page_metadata(text)
|
||||||
|
)
|
||||||
|
if meta:
|
||||||
|
md = render_html_from_markdown(config, text)
|
||||||
|
html = render_template(
|
||||||
|
meta['layout'],
|
||||||
|
theme = meta['theme'],
|
||||||
|
title = meta['title'],
|
||||||
|
site_title = meta['site_title'],
|
||||||
|
description = meta['description'],
|
||||||
|
menu = meta['menu'],
|
||||||
|
content = md
|
||||||
|
)
|
||||||
|
# Copy (or replace if exists) files to 'build/' dir.
|
||||||
|
copy_files(themes, os.path.join(build_dir, themes))
|
||||||
|
copy_files(pages, build_dir)
|
||||||
|
# Write rendered HTML into file.
|
||||||
|
write_to_file(get_new_path(build_dir, file), html)
|
||||||
|
else:
|
||||||
|
print('Error: Not rendered: No metadata found in: {}'.format(file))
|
||||||
|
|
||||||
|
@click.version_option(
|
||||||
|
version = __version__,
|
||||||
|
prog_name = 'Pēji')
|
||||||
|
@click.group()
|
||||||
|
def cli():
|
||||||
|
"""Static site generator."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument('site')
|
||||||
|
def create(site):
|
||||||
|
"""Create new site."""
|
||||||
|
site = os.path.join(cwd, site)
|
||||||
|
create_site(site)
|
||||||
|
click.echo('Site created: %s' % site)
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option('-d', '--dir', default = './build/', metavar = 'DIR',
|
||||||
|
help = 'Set destination directory. Default: ./build')
|
||||||
|
def build(dir):
|
||||||
|
"""Render HTML files."""
|
||||||
|
check_site(cwd)
|
||||||
|
build_site(dir)
|
||||||
|
click.echo('Built in: %s' % dir)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cli()
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
click>=7.1.2
|
||||||
|
markdown>=3.3.4
|
||||||
|
Jinja2>=2.11.3
|
||||||
|
Pygments>=2.8.1
|
||||||
|
PyYAML>=5.4.1
|
34
setup.py
Normal file
34
setup.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
|
||||||
|
with open("README.md", "r") as long_descr:
|
||||||
|
long_description = long_descr.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='Peji',
|
||||||
|
version='1.0',
|
||||||
|
author='gd',
|
||||||
|
description='Static site generator.',
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
url='https://peji.gch.icu/',
|
||||||
|
classifiers=[
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"License :: OSI Approved :: The Unlicense (Unlicense)",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
],
|
||||||
|
python_requires='>=3.6',
|
||||||
|
py_modules=['peji'],
|
||||||
|
install_requires = [
|
||||||
|
'click>=7.1.2',
|
||||||
|
'markdown>=3.3.4',
|
||||||
|
'Jinja2>=2.11.3',
|
||||||
|
'Pygments>=2.8.1',
|
||||||
|
'PyYAML>=5.4.1'
|
||||||
|
],
|
||||||
|
entry_points = {
|
||||||
|
'console_scripts': [
|
||||||
|
'peji = peji:cli'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user