Skip to content

Commit

Permalink
Add Markdown support (and more)
Browse files Browse the repository at this point in the history
  • Loading branch information
EpocDotFr committed Apr 25, 2024
1 parent 4b0338e commit d289bcd
Show file tree
Hide file tree
Showing 8 changed files with 490 additions and 274 deletions.
170 changes: 119 additions & 51 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
URL = 'https://github.com/EpocDotFr/staticjinjaplus'
EMAIL = 'contact.nospam@epoc.nospam.fr'
AUTHOR = 'Maxime "Epoc" Gross'
REQUIRES_PYTHON = '>=3.9'
REQUIRES_PYTHON = '>=3.10'
VERSION = None # Pulled from staticjinjaplus/__version__.py

REQUIRED = [
Expand All @@ -22,6 +22,7 @@
'cssutils~=2.10',
'jsmin~=3.0',
'environs~=11.0',
'markdown~=3.6',
]

EXTRAS = {
Expand All @@ -41,7 +42,6 @@
'Topic :: Text Processing :: Markup :: XML',
'Topic :: Text Processing :: Markup :: HTML',
'Topic :: Text Processing :: Markup :: Markdown',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
Expand Down
264 changes: 89 additions & 175 deletions staticjinjaplus/__init__.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
from staticjinjaplus.http import EnhancedThreadingHTTPServer, SimpleEnhancedHTTPRequestHandler
from staticjinjaplus import staticjinja_helpers, jinja_helpers
from webassets import Environment as AssetsEnvironment
from staticjinjaplus.__version__ import __version__ as staticjinjaplus_version
from staticjinja import __version__ as staticjinja_version
from typing import Dict, Any, Iterator, Tuple
from importlib import util as importlib_util
from jinja2 import select_autoescape
from staticjinja import Site, logger
from shutil import copytree, rmtree
from os import makedirs, path
from subprocess import call
from environs import Env
from typing import Dict
import locale


def load_config() -> Dict:
from glob import iglob
from os import path
import markdown.extensions.meta as markdown_meta

__generator__ = f'staticjinjaplus {staticjinjaplus_version} (staticjinja {staticjinja_version})'

# Set default config values
serve_port = 8080

config: Dict[str, Any] = {
'SERVE_PORT': serve_port,
'BASE_URL': f'http://localhost:{serve_port}/',
'MINIFY_XML': False,
'MINIFY_JSON': False,
'TEMPLATES_DIR': 'templates',
'OUTPUT_DIR': 'output',
'STATIC_DIR': 'static',
'ASSETS_DIR': 'assets',
'CONTEXTS': [],
'WEBASSETS_BUNDLES': [],
'JINJA_GLOBALS': {},
'JINJA_FILTERS': {},
'JINJA_EXTENSIONS': [],
'MARKDOWN_EXTENSIONS': {},
'MARKDOWN_DEFAULT_PARTIAL': None,
'USE_HTML_EXTENSION': True,
}


def load_config() -> None:
"""Load configuration from both `config.py` in the directory where staticjinjaplus is executed and environment
variables, returning a dict representation of this configuration. Only uppercase variables are loaded"""
global config

# Set default config values
serve_port = 8080

config = {
'LOCALE': None,
'SERVE_PORT': serve_port,
'BASE_URL': f'http://localhost:{serve_port}/',
'MINIFY_XML': False,
'MINIFY_JSON': False,
'TEMPLATES_DIR': 'templates',
'OUTPUT_DIR': 'output',
'STATIC_DIR': 'static',
'ASSETS_DIR': 'assets',
'ASSETS_BUNDLES': [],
'CONTEXTS': [],
'GLOBALS': {},
'FILTERS': {},
'EXTENSIONS': [],
}

# Load and erase default config values from config.py, if the file exists
# Load and override default config values from config.py, if the file exists
try:
spec = importlib_util.spec_from_file_location('config', 'config.py')
actual_config = importlib_util.module_from_spec(spec)
Expand All @@ -48,165 +48,79 @@ def load_config() -> Dict:
except FileNotFoundError:
pass

return config


def set_locale(config: Dict) -> None:
"""Set the system locale based on the LOCALE config"""
if not config['LOCALE']:
return

locale_successfully_set = False

for code in config['LOCALE']:
try:
locale.setlocale(locale.LC_ALL, code)

locale_successfully_set = True

logger.info(f'System locale set to {code}')

break
except locale.Error:
pass

if not locale_successfully_set:
logger.error('Unable to set system locale')


def build(config: Dict, watch: bool = False) -> None:
"""Build the site"""
set_locale(config)

webassets_cache = path.join(config['ASSETS_DIR'], '.webassets-cache')
def smart_build_url(filename: str) -> Tuple[str, str]:
"""Build a webserver and SEO-friendly (if configured so) URL to an HTML file"""
_, ext = path.splitext(filename)
ext = ext.lstrip('.')

makedirs(webassets_cache, exist_ok=True)
makedirs(config['STATIC_DIR'], exist_ok=True)
makedirs(config['OUTPUT_DIR'], exist_ok=True)
makedirs(config['ASSETS_DIR'], exist_ok=True)
url = '/' + filename.lstrip('/')

logger.info('Copying static files from "{STATIC_DIR}" to "{OUTPUT_DIR}"...'.format(**config))
if url.endswith(('/index.html', '/index.md')):
url = url.removesuffix('/index.html').removesuffix('/index.md') + '/'
elif ext in ('html', 'md') and not config['USE_HTML_EXTENSION']:
url, _ = path.splitext(url)

copytree(
config['STATIC_DIR'],
config['OUTPUT_DIR'],
dirs_exist_ok=True
)
return url, ext

logger.info('Building from "{TEMPLATES_DIR}" to "{OUTPUT_DIR}"...'.format(**config))

rules = [
r for r in [
(r'.*\.(xml|html|rss|atom)', staticjinja_helpers.minify_xml_template) if config['MINIFY_XML'] else None,
(r'.*\.json', staticjinja_helpers.minify_json_template) if config['MINIFY_JSON'] else None,
] if r is not None
]
def collect_templates() -> Iterator[Dict[str, Any]]:
"""Iterates over all valid files found in the templates directory and return several kind of information about
them."""
for filename in iglob(
f'**/[!_]*.*',
root_dir=config['TEMPLATES_DIR'],
recursive=True
):
filename = path.normpath(filename).replace('\\', '/')

jinja_globals = {
'config': config,
'url': jinja_helpers.url(config),
'icon': jinja_helpers.icon(config),
}
url, ext = smart_build_url(filename)

jinja_globals.update(config['GLOBALS'])

jinja_filters = {
'tojsonm': jinja_helpers.tojsonm(config),
'dictmerge': jinja_helpers.dictmerge,
}

jinja_filters.update(config['FILTERS'])

jinja_extensions = [
'webassets.ext.jinja2.AssetsExtension',
]

jinja_extensions.extend(config['EXTENSIONS'])

site = Site.make_site(
searchpath=config['TEMPLATES_DIR'],
outpath=config['OUTPUT_DIR'],
mergecontexts=True,
env_globals=jinja_globals,
filters=jinja_filters,
contexts=config['CONTEXTS'] or None,
rules=rules or None,
extensions=jinja_extensions,
env_kwargs={
'trim_blocks': True,
'lstrip_blocks': True,
'autoescape': select_autoescape(enabled_extensions=('html', 'htm', 'xml', 'rss', 'atom')),
data = {
'source': filename,
'type': ext,
'url': url,
}
)

site.env.assets_environment = AssetsEnvironment(
directory=config['OUTPUT_DIR'],
url='/',
cache=webassets_cache
)

site.env.assets_environment.append_path(config['ASSETS_DIR'])

for name, args, kwargs in config['ASSETS_BUNDLES']:
site.env.assets_environment.register(name, *args, **kwargs)

site.render(watch)


def clean(config: Dict) -> None:
"""Delete and recreate the output directory"""
logger.info('Deleting and recreating "{OUTPUT_DIR}"...'.format(**config))

if path.isdir(config['OUTPUT_DIR']):
rmtree(config['OUTPUT_DIR'])
if ext == 'md':
data['meta'] = {}

makedirs(config['OUTPUT_DIR'], exist_ok=True)
first_line = True

with open(path.join(config['TEMPLATES_DIR'], filename), 'r', encoding='utf-8') as f:
# The following code has been borrowed and adapted from the meta extension of Python's markdown package:
# https://github.com/Python-Markdown/markdown/blob/master/markdown/extensions/meta.py
for line in f:
if first_line:
first_line = False

def publish(config: Dict) -> None:
"""Build and publish the site (using `rsync` through SSH)"""
logger.info('Overriding some configuration values from environment variables...')
if markdown_meta.BEGIN_RE.match(line):
continue

env = Env()
m1 = markdown_meta.META_RE.match(line)

config.update({
'BASE_URL': env.str('BASE_URL'),
'MINIFY_XML': env.bool('MINIFY_XML', config['MINIFY_XML']),
'MINIFY_JSON': env.bool('MINIFY_JSON', config['MINIFY_JSON']),
'SSH_USER': env.str('SSH_USER'),
'SSH_HOST': env.str('SSH_HOST'),
'SSH_PORT': env.int('SSH_PORT', default=22),
'SSH_PATH': env.str('SSH_PATH'),
})
if line.strip() == '' or markdown_meta.END_RE.match(line):
break

clean(config)
build(config)
if m1:
key = m1.group('key').lower().strip()
value = m1.group('value').strip()

exit(call(
'rsync --delete --exclude ".DS_Store" -pthrvz -c '
'-e "ssh -p {SSH_PORT}" '
'{} {SSH_USER}@{SSH_HOST}:{SSH_PATH}'.format(
config['OUTPUT_DIR'].rstrip('/') + '/', **config
),
shell=True
))
try:
data['meta'][key] += f'\n{value}'
except KeyError:
data['meta'][key] = value
else:
m2 = markdown_meta.META_MORE_RE.match(line)

if m2 and key:
value = m2.group('value').strip()

def serve(config: Dict) -> None:
"""Serve the rendered site directory through HTTP"""
with EnhancedThreadingHTTPServer(
('', config['SERVE_PORT']),
SimpleEnhancedHTTPRequestHandler,
directory=config['OUTPUT_DIR']
) as server:
msg = 'Serving "{OUTPUT_DIR}" on http://localhost:{SERVE_PORT}/'.format(**config)
data['meta'][key] += f'\n{value}'
else:
break

if server.has_dualstack_ipv6:
msg += ' and http://[::1]:{SERVE_PORT}/'.format(**config)
yield data

logger.info(msg)

try:
server.serve_forever()
except KeyboardInterrupt:
pass
load_config()
2 changes: 1 addition & 1 deletion staticjinjaplus/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.1.2'
__version__ = '2.0.0'

0 comments on commit d289bcd

Please sign in to comment.