Compare commits
14 Commits
Author | SHA1 | Date |
---|---|---|
digitalstudium | 1bd46b2105 | |
Digital Studium | 98912ac09b | |
Digital Studium | d29570739b | |
Digital Studium | 2c30a3bb30 | |
Digital Studium | 024ba1f56a | |
Digital Studium | c9181c9292 | |
Digital Studium | 42cf006f4f | |
Digital Studium | 3c5e1cc3c8 | |
Digital Studium | f0ecde0932 | |
Digital Studium | a2189d7638 | |
Digital Studium | 092bc21064 | |
Digital Studium | 1c532fabb8 | |
Digital Studium | 278831ee68 | |
Digital Studium | 5ecbb0f0a5 |
|
@ -1 +1,2 @@
|
||||||
.venv
|
.venv
|
||||||
|
__pycache__
|
12
README.md
12
README.md
|
@ -1,21 +1,21 @@
|
||||||
# Crater
|
# franca
|
||||||
|
|
||||||
Multilingual static site generator based on Jinja templates.
|
Multilingual static site generator based on Jinja templates.
|
||||||
|
|
||||||
Example theme: https://git.digitalstudium.com/digitalstudium/crater-theme-digitalstudium
|
Example theme: https://git.digitalstudium.com/digitalstudium/franca-theme-blog
|
||||||
|
|
||||||
Example website: https://git.digitalstudium.com/digitalstudium/digitalstudium.com (https://digitalstudium.com)
|
Example website: https://git.digitalstudium.com/digitalstudium/digitalstudium.com (https://digitalstudium.com)
|
||||||
|
|
||||||
## Installation (Linux):
|
## Installation (Linux):
|
||||||
```
|
```
|
||||||
curl https://git.digitalstudium.com/attachments/29f029cb-1f35-4286-803b-5d3908e32cd2 -o crater && chmod +x crater && sudo mv crater /usr/local/bin
|
curl https://git.digitalstudium.com/attachments/ceaf659d-f161-4ca4-86f9-172993b35e7e -o franca && chmod +x franca && sudo mv franca /usr/local/bin
|
||||||
```
|
```
|
||||||
## Usage:
|
## Usage:
|
||||||
Development:
|
Development:
|
||||||
```
|
```
|
||||||
crater
|
franca
|
||||||
```
|
```
|
||||||
Generate public folder only:
|
Generate production-ready public folder:
|
||||||
```
|
```
|
||||||
crater -p
|
franca -p
|
||||||
```
|
```
|
2
common
2
common
|
@ -1 +1 @@
|
||||||
Subproject commit 50eddb5731ae9ba44513f9d0be7b4885fdff8b9e
|
Subproject commit f551c17705ea4be7369423f5d829264911588523
|
145
crater.py
145
crater.py
|
@ -1,145 +0,0 @@
|
||||||
import csv
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from watchdog.observers import Observer
|
|
||||||
from watchdog.events import FileSystemEventHandler
|
|
||||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
||||||
import yaml
|
|
||||||
import frontmatter
|
|
||||||
import markdown2
|
|
||||||
import fire
|
|
||||||
|
|
||||||
|
|
||||||
from common.functions import *
|
|
||||||
|
|
||||||
|
|
||||||
def load_translations(file_path, _dict):
|
|
||||||
with open(file_path) as f:
|
|
||||||
reader = csv.DictReader(f, delimiter='|') #
|
|
||||||
for row in reader: # проходим по строкам csv, каждая из которых является словарём
|
|
||||||
_dict[row['id']] = row # добавляем ключ - значение ключа id, значение - словарь row
|
|
||||||
del row['id'] # удаляем ключ по названием "id" из словаря row, так как его значение уже является ключом в словаре _dict
|
|
||||||
|
|
||||||
|
|
||||||
# Функции, доступные в теме
|
|
||||||
def translate(id, language):
|
|
||||||
global translations
|
|
||||||
return translations[id][language]
|
|
||||||
|
|
||||||
translations = {} # здесь будут переводы от темы и от сайта
|
|
||||||
config = yaml.safe_load(read_file('config.yaml')) # Чиатем конфиг сайта
|
|
||||||
running = False # нужно для проверки
|
|
||||||
|
|
||||||
# класс для watchdog.
|
|
||||||
# При обнаружении изменений в папках content и themes/{config['theme']}, перегенерировать папку public
|
|
||||||
class Develop(FileSystemEventHandler):
|
|
||||||
def on_modified(self, event):
|
|
||||||
print(f'event type: {event.event_type} path : {event.src_path}')
|
|
||||||
crater()
|
|
||||||
def on_created(self, event):
|
|
||||||
print(f'event type: {event.event_type} path : {event.src_path}')
|
|
||||||
crater()
|
|
||||||
def on_deleted(self, event):
|
|
||||||
print(f'event type: {event.event_type} path : {event.src_path}')
|
|
||||||
crater()
|
|
||||||
|
|
||||||
|
|
||||||
# Функция для запуска разработки
|
|
||||||
def develop(prod):
|
|
||||||
global running
|
|
||||||
if not prod and not running:
|
|
||||||
event_handler = Develop()
|
|
||||||
observer = Observer()
|
|
||||||
observer.schedule(event_handler, path='content', recursive=True)
|
|
||||||
observer.schedule(event_handler, path=f"themes/{config['theme']}", recursive=True)
|
|
||||||
observer.start()
|
|
||||||
running = True
|
|
||||||
try:
|
|
||||||
start_httpd(directory='public')
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Функция для генерации сайта
|
|
||||||
def crater(prod=False):
|
|
||||||
# Load theme's jinja templates
|
|
||||||
templates = Environment(loader=FileSystemLoader(
|
|
||||||
f"themes/{config['theme']}/templates/"), autoescape=select_autoescape())
|
|
||||||
|
|
||||||
# Load base jinja template
|
|
||||||
base = templates.get_template("base.j2")
|
|
||||||
|
|
||||||
# Load translations
|
|
||||||
global translations
|
|
||||||
load_translations(f"themes/{config['theme']}/i18n.csv", translations)
|
|
||||||
load_translations("i18n.csv", translations)
|
|
||||||
|
|
||||||
# Add functions to base template
|
|
||||||
base.globals.update({"translate": translate})
|
|
||||||
|
|
||||||
# Remove public folder if exists before generating new content
|
|
||||||
shutil.rmtree('public', ignore_errors=True)
|
|
||||||
|
|
||||||
posts = {}
|
|
||||||
# Create new public folder
|
|
||||||
for language in config['languages']:
|
|
||||||
content_dir = Path(f"content/{language}")
|
|
||||||
posts[language] = {}
|
|
||||||
|
|
||||||
# Create posts dict
|
|
||||||
for item in list(content_dir.rglob("*.md")):
|
|
||||||
post_data = frontmatter.loads(read_file(item))
|
|
||||||
|
|
||||||
post_path = str(item).replace(
|
|
||||||
"content", "public").rstrip(".md")
|
|
||||||
os.makedirs(post_path, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
content = markdown2.markdown(post_data.content, extras=['fenced-code-blocks'])
|
|
||||||
description = content.partition('<!--more-->')[0]
|
|
||||||
content = "{% import 'shortcodes.j2' as shortcodes %}" + content
|
|
||||||
|
|
||||||
url = post_path.replace(f"public/{language}", "")
|
|
||||||
section = "/" if len(url.split('/')) == 2 else url.split('/')[1]
|
|
||||||
|
|
||||||
posts[language].setdefault(section, {})
|
|
||||||
|
|
||||||
posts[language][section][url] = {
|
|
||||||
'title': post_data['title'],
|
|
||||||
'description': description,
|
|
||||||
'date': post_data['date'],
|
|
||||||
'content': templates.from_string(content).render()
|
|
||||||
}
|
|
||||||
|
|
||||||
image = post_data.get('image', None)
|
|
||||||
if image:
|
|
||||||
posts[language][section][url]['image'] = image
|
|
||||||
copy_file(f'assets{image}', f'public{image}')
|
|
||||||
|
|
||||||
for section, urls in posts[language].items():
|
|
||||||
if section != "/":
|
|
||||||
html = base.render(config=config, section=section,
|
|
||||||
language=language, posts=posts)
|
|
||||||
|
|
||||||
write_file(f"public/{language}/{section}/index.html", html)
|
|
||||||
|
|
||||||
for url, post in urls.items():
|
|
||||||
html = base.render(config=config, post=post,
|
|
||||||
language=language, url=url, posts=posts)
|
|
||||||
|
|
||||||
write_file(f"public/{language}{url}/index.html", html)
|
|
||||||
|
|
||||||
html = base.render(config=config, posts=posts,
|
|
||||||
language=language, home=True)
|
|
||||||
|
|
||||||
write_file(f"public/{language}/index.html", html)
|
|
||||||
|
|
||||||
for source_folder in ('static', f"themes/{config['theme']}/static"):
|
|
||||||
shutil.copytree(source_folder, 'public', dirs_exist_ok=True)
|
|
||||||
|
|
||||||
develop(prod)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
fire.Fire(crater)
|
|
|
@ -1,5 +1,5 @@
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
python -m PyInstaller --onefile crater.py
|
python -m PyInstaller --onefile franca.py
|
||||||
chmod +x dist/crater
|
chmod +x dist/franca
|
||||||
sudo mv dist/crater /usr/local/bin/
|
sudo mv dist/franca /usr/local/bin/
|
||||||
rm -rf dist build crater.spec
|
rm -rf dist build franca.spec
|
|
@ -0,0 +1,202 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import PatternMatchingEventHandler
|
||||||
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
|
import yaml
|
||||||
|
import frontmatter
|
||||||
|
import markdown2
|
||||||
|
import fire
|
||||||
|
import minify_html
|
||||||
|
|
||||||
|
from common.functions import *
|
||||||
|
|
||||||
|
|
||||||
|
def load_translations(file_path, _dict):
|
||||||
|
with open(file_path) as f:
|
||||||
|
reader = csv.DictReader(f, delimiter='|') #
|
||||||
|
for row in reader: # проходим по строкам csv, каждая из которых является словарём
|
||||||
|
_dict[row['id']] = row # добавляем ключ - значение ключа id, значение - словарь row
|
||||||
|
del row[
|
||||||
|
'id'] # удаляем ключ по названием "id" из словаря row, так как его значение уже является ключом в словаре _dict
|
||||||
|
|
||||||
|
|
||||||
|
# Функции, доступные в теме
|
||||||
|
def translate(id, language):
|
||||||
|
global translations
|
||||||
|
return translations[id][language]
|
||||||
|
|
||||||
|
|
||||||
|
translations = {} # здесь будут переводы от темы и от сайта
|
||||||
|
config = yaml.safe_load(read_file('config.yaml')) # Читаем конфиг сайта
|
||||||
|
running = False # нужно для проверки
|
||||||
|
|
||||||
|
|
||||||
|
# класс для watchdog
|
||||||
|
# Во время разработки, при обнаружении изменений в папках content и themes/{config['theme']},
|
||||||
|
# перегенерировать папку public
|
||||||
|
class Develop(PatternMatchingEventHandler):
|
||||||
|
def __init__(self):
|
||||||
|
PatternMatchingEventHandler.__init__(self, patterns=['*.md', '*.css'],
|
||||||
|
ignore_directories=True, case_sensitive=False)
|
||||||
|
|
||||||
|
def on_modified(self, event):
|
||||||
|
print(f'event type: {event.event_type} path : {event.src_path}')
|
||||||
|
franca()
|
||||||
|
|
||||||
|
def on_created(self, event):
|
||||||
|
print(f'event type: {event.event_type} path : {event.src_path}')
|
||||||
|
franca()
|
||||||
|
|
||||||
|
def on_deleted(self, event):
|
||||||
|
print(f'event type: {event.event_type} path : {event.src_path}')
|
||||||
|
franca()
|
||||||
|
|
||||||
|
|
||||||
|
# Функция для запуска разработки
|
||||||
|
def develop(prod):
|
||||||
|
global running
|
||||||
|
if not prod and not running:
|
||||||
|
event_handler = Develop()
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(event_handler, path='content', recursive=True)
|
||||||
|
observer.schedule(event_handler, path='assets', recursive=True)
|
||||||
|
observer.schedule(event_handler, path='static', recursive=True)
|
||||||
|
observer.schedule(event_handler, path='config.yaml')
|
||||||
|
observer.schedule(event_handler, path=f"themes/{config['theme']}", recursive=True)
|
||||||
|
observer.start()
|
||||||
|
running = True
|
||||||
|
try:
|
||||||
|
start_httpd(directory='public')
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Функция для генерации сайта
|
||||||
|
def franca(prod=False):
|
||||||
|
if not prod:
|
||||||
|
config['base_url'] = "http://127.0.0.1:8000"
|
||||||
|
# Load theme's jinja templates
|
||||||
|
templates = Environment(loader=FileSystemLoader(
|
||||||
|
f"themes/{config['theme']}/templates/"), autoescape=select_autoescape())
|
||||||
|
|
||||||
|
# Load base jinja template
|
||||||
|
base = templates.get_template("base.j2")
|
||||||
|
index = templates.get_template("index.j2")
|
||||||
|
|
||||||
|
# Load translations
|
||||||
|
global translations
|
||||||
|
load_translations(f"themes/{config['theme']}/i18n.csv", translations)
|
||||||
|
load_translations("i18n.csv", translations)
|
||||||
|
|
||||||
|
# Add functions to base template
|
||||||
|
base.globals.update({"translate": translate})
|
||||||
|
|
||||||
|
# Remove public folder if exists before generating new content
|
||||||
|
shutil.rmtree('public', ignore_errors=True)
|
||||||
|
|
||||||
|
posts = {}
|
||||||
|
# Create new public folder
|
||||||
|
for language in config['languages']:
|
||||||
|
content_dir = Path(f"content/{language}")
|
||||||
|
posts[language] = {}
|
||||||
|
|
||||||
|
# Create posts dict
|
||||||
|
for item in list(content_dir.rglob("*.md")):
|
||||||
|
post_data = frontmatter.loads(read_file(item))
|
||||||
|
|
||||||
|
post_path = str(item).replace(
|
||||||
|
"content", "public").rstrip(".md")
|
||||||
|
os.makedirs(post_path, exist_ok=True)
|
||||||
|
|
||||||
|
content = markdown2.markdown(post_data.content, extras=['fenced-code-blocks'])
|
||||||
|
description = content.partition('<!--more-->')[0]
|
||||||
|
content = "{% import 'shortcodes.j2' as shortcodes %}" + content
|
||||||
|
|
||||||
|
url = post_path.replace(f"public/{language}", "")
|
||||||
|
section = "/" if len(url.split('/')) == 2 else url.split('/')[1]
|
||||||
|
date = post_data['date']
|
||||||
|
posts[language].setdefault(date, {})
|
||||||
|
posts[language][date].setdefault(section, {})
|
||||||
|
|
||||||
|
posts[language][date][section][url] = {
|
||||||
|
'title': post_data['title'],
|
||||||
|
'description': description,
|
||||||
|
'date': date,
|
||||||
|
'content': templates.from_string(content).render()
|
||||||
|
}
|
||||||
|
|
||||||
|
image = post_data.get('image', None)
|
||||||
|
if image:
|
||||||
|
os.makedirs(os.path.dirname(f'public{image}'), exist_ok=True)
|
||||||
|
filename = image.split('/')[-1].split('.')[0]
|
||||||
|
|
||||||
|
create_thumbnail(f'assets{image}', f'public/images/{filename}_600.jpg', 600)
|
||||||
|
posts[language][date][section][url]['image'] = f'/images/{filename}_600.jpg'
|
||||||
|
|
||||||
|
create_thumbnail(f'assets{image}', f'public/images/{filename}_400.jpg', 400)
|
||||||
|
posts[language][date][section][url]['thumbnail'] = f'/images/{filename}_400.jpg'
|
||||||
|
|
||||||
|
posts[language] = dict(sorted(posts[language].items(), reverse=True))
|
||||||
|
|
||||||
|
for date, sections in posts[language].items():
|
||||||
|
for section, urls in sections.items():
|
||||||
|
if section != "/":
|
||||||
|
html = base.render(config=config, section=section,
|
||||||
|
language=language, posts=posts)
|
||||||
|
|
||||||
|
write_file(f"public/{language}/{section}/index.html", minify_html.minify(html, minify_js=True))
|
||||||
|
|
||||||
|
for url, post in urls.items():
|
||||||
|
html = base.render(config=config, post=post,
|
||||||
|
language=language, url=url, posts=posts)
|
||||||
|
|
||||||
|
write_file(f"public/{language}{url}/index.html", minify_html.minify(html, minify_js=True))
|
||||||
|
|
||||||
|
html = base.render(config=config, posts=posts,
|
||||||
|
language=language, home=True)
|
||||||
|
|
||||||
|
write_file(f"public/{language}/index.html", minify_html.minify(html, minify_js=True))
|
||||||
|
|
||||||
|
# copy images/css/js from theme
|
||||||
|
shutil.copytree(f"themes/{config['theme']}/static/images", 'public/images', dirs_exist_ok=True)
|
||||||
|
css = read_file(f"themes/{config['theme']}/static/css/style.css")
|
||||||
|
minify_css('public/css/style.css', css)
|
||||||
|
if 'css_includes' in config:
|
||||||
|
for include in config['css_includes']:
|
||||||
|
css = read_file(f"themes/{config['theme']}/static/css/{include}")
|
||||||
|
minify_css(f'public/css/{include}', css)
|
||||||
|
if 'js_includes' in config:
|
||||||
|
for include in config['js_includes']:
|
||||||
|
copy_file(f"themes/{config['theme']}/static/js/{include}", 'public/js/')
|
||||||
|
|
||||||
|
# copy css/images from site static folder
|
||||||
|
if 'custom_css' in config:
|
||||||
|
shutil.copytree('static/css', 'public/css', dirs_exist_ok=True)
|
||||||
|
|
||||||
|
copy_file('static/logo.svg', 'public/')
|
||||||
|
copy_file('static/favicon.ico', 'public/')
|
||||||
|
|
||||||
|
# Write main index.html
|
||||||
|
html = index.render(config=config)
|
||||||
|
write_file('public/index.html', minify_html.minify(html, minify_js=True))
|
||||||
|
# Write robots.txt
|
||||||
|
robots_content = "User-agent: *"
|
||||||
|
if not config.get('search_engines', None) == "allow":
|
||||||
|
robots_content += "\nDisallow: /"
|
||||||
|
write_file('public/robots.txt', robots_content)
|
||||||
|
|
||||||
|
global running
|
||||||
|
if not running:
|
||||||
|
if 'pagefind' in config:
|
||||||
|
os.system("npx pagefind --source public") # build search index
|
||||||
|
|
||||||
|
develop(prod)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
fire.Fire(franca)
|
|
@ -3,6 +3,8 @@ fire==0.5.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
markdown2==2.4.9
|
markdown2==2.4.9
|
||||||
MarkupSafe==2.1.3
|
MarkupSafe==2.1.3
|
||||||
|
minify_html==0.15.0
|
||||||
|
Pillow==10.0.0
|
||||||
Pygments==2.15.1
|
Pygments==2.15.1
|
||||||
pyinstaller==5.13.0
|
pyinstaller==5.13.0
|
||||||
pyinstaller-hooks-contrib==2023.6
|
pyinstaller-hooks-contrib==2023.6
|
||||||
|
|
Loading…
Reference in New Issue