Compare commits

...

25 Commits
0.0.1 ... main

Author SHA1 Message Date
digitalstudium 1bd46b2105 Add mimify_html 2024-01-20 15:07:43 +03:00
Digital Studium 98912ac09b Fix observer' screen 2023-10-14 18:46:50 +03:00
Digital Studium d29570739b Sorting by date 2023-10-14 13:53:51 +03:00
Digital Studium 2c30a3bb30 Add minifiers 2023-07-28 20:35:34 +03:00
Digital Studium 024ba1f56a Fix readme 2023-07-23 22:52:34 +03:00
Digital Studium c9181c9292 Fix readme 2023-07-23 22:50:57 +03:00
Digital Studium 42cf006f4f Add image resize 2023-07-23 22:46:55 +03:00
Digital Studium 3c5e1cc3c8 Add robots 2023-07-23 20:47:37 +03:00
Digital Studium f0ecde0932 Rename theme 2023-07-23 19:58:37 +03:00
Digital Studium a2189d7638 Rename all 2023-07-23 19:52:46 +03:00
Digital Studium 092bc21064 Add css includes 2023-07-23 17:56:27 +03:00
Digital Studium 1c532fabb8 Fix js includes 2023-07-23 17:32:32 +03:00
Digital Studium 278831ee68 Fix js includes 2023-07-23 17:30:20 +03:00
Digital Studium 5ecbb0f0a5 Fix README 2023-07-23 16:45:56 +03:00
Digital Studium 4078e5e957 Fix indent 2023-07-23 16:44:56 +03:00
Digital Studium 374e1fdd1f Fix release link 2023-07-23 16:15:37 +03:00
Digital Studium 77ec607207 Fix serve_httpd() 2023-07-23 16:11:55 +03:00
Digital Studium 36eedecc1c Fix README 2023-07-23 16:03:48 +03:00
Digital Studium cabe5b632f Change to markdown2 2023-07-23 15:54:37 +03:00
Digital Studium 0f4dbd62f4 Update common 2023-07-23 11:44:35 +03:00
Digital Studium c5d7d48492 Refactor 2023-07-23 11:30:22 +03:00
Digital Studium 5fd1b91eba Update common 2023-07-22 23:26:54 +03:00
Digital Studium b4ce72dd92 Fix readme 2023-07-22 16:45:10 +03:00
Digital Studium 453a1e8268 Fix readme 2023-07-22 16:44:20 +03:00
Digital Studium 50868fbbd2 Fix readme 2023-07-22 16:43:17 +03:00
8 changed files with 236 additions and 148 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
.venv
.venv
__pycache__

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "common"]
path = common
url = ssh://git@git.digitalstudium.com:2222/digitalstudium/common_functions.git

View File

@ -1,7 +1,21 @@
# Crater
# franca
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
Example website: https://git.digitalstudium.com/digitalstudium/digitalstudium.com (https://digitalstudium.com)
## Installation (Linux):
```
curl https://git.digitalstudium.com/attachments/ceaf659d-f161-4ca4-86f9-172993b35e7e -o franca && chmod +x franca && sudo mv franca /usr/local/bin
```
## Usage:
Development:
```
franca
```
Generate production-ready public folder:
```
franca -p
```

1
common Submodule

@ -0,0 +1 @@
Subproject commit f551c17705ea4be7369423f5d829264911588523

139
crater.py
View File

@ -1,139 +0,0 @@
import csv
import os
import pathlib
import shutil
import http.server
import socketserver
from jinja2 import Environment, FileSystemLoader, select_autoescape
import yaml
import frontmatter
import markdown
import fire
translations = {}
def copy_file(src, dst):
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copy2(src, dst)
def load_translations(file_path, _dict):
with open(file_path) as f:
reader = csv.DictReader(f, delimiter='|')
for row in reader:
_dict[row['id']] = row
del row['id']
def translate(id, language):
global translations
return translations[id][language]
def develop():
PORT = 8000
os.chdir('public')
handler = http.server.SimpleHTTPRequestHandler
server = socketserver.TCPServer(("", PORT), handler)
print("Server started at port 8000. Press CTRL+C to close the server.")
try:
server.serve_forever()
except KeyboardInterrupt:
server.server_close()
print("Server Closed")
def site_creator(prod=False):
global translations
# Read config of site
with open('config.yaml', 'r') as file:
config = yaml.safe_load(file)
# 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
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']:
path = f"content/{language}"
content_dir = pathlib.Path(path)
content_files = content_dir.rglob("*.md")
posts[language] = {}
# Create posts dict
for item in list(content_files):
with open(item, 'r') as f:
data = frontmatter.loads(f.read())
post_path = str(item).replace(
"content", "public").rstrip(".md")
os.makedirs(post_path, exist_ok=True)
content = "{% import 'shortcodes.j2' as shortcodes %}" + \
markdown.markdown(data.content)
content = templates.from_string(content).render()
description = content.partition('<!--more-->')[0]
url = post_path.replace(f"public/{language}", "")
section = url.split('/')[1]
if not os.path.dirname(url) == "/":
posts[language].setdefault(section, {})
posts[language][section][url] = {
'title': data['title'],
'description': description,
'date': data['date'],
'content': content
}
image = 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():
html = base.render(config=config, section=section,
language=language, posts=posts)
with open(f"public/{language}/{section}/index.html", 'w') as f:
f.write(html)
for url, post in urls.items():
html = base.render(config=config, post=post,
language=language, url=url, posts=posts)
with open(f"public/{language}{url}/index.html", 'w') as f:
f.write(html)
html = base.render(config=config, posts=posts,
language=language, home=True)
with open(f"public/{language}/index.html", 'w') as f:
f.write(html)
shutil.copytree('static', 'public', dirs_exist_ok=True)
shutil.copytree(
f"themes/{config['theme']}/static", 'public', dirs_exist_ok=True)
if not prod:
develop()
if __name__ == '__main__':
fire.Fire(site_creator)

View File

@ -1,5 +1,5 @@
pip install -r requirements.txt
python -m PyInstaller --onefile crater.py
chmod +x dist/crater
sudo mv dist/crater /usr/local/bin/
rm -rf dist build crater.spec
python -m PyInstaller --onefile franca.py
chmod +x dist/franca
sudo mv dist/franca /usr/local/bin/
rm -rf dist build franca.spec

202
franca.py Executable file
View File

@ -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)

View File

@ -1,11 +1,17 @@
altgraph==0.17.3
fire==0.5.0
Jinja2==3.1.2
Markdown==3.4.3
markdown2==2.4.9
MarkupSafe==2.1.3
minify_html==0.15.0
Pillow==10.0.0
Pygments==2.15.1
pyinstaller==5.13.0
pyinstaller-hooks-contrib==2023.6
python-frontmatter==1.0.0
PyYAML==6.0.1
six==1.16.0
svgwrite==1.4.3
termcolor==2.3.0
watchdog==3.0.0
wavedrom==2.0.3.post3