2022-03-11 13:35:37 +02:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
2023-08-29 10:27:14 +03:00
""" blog manager """
2022-03-11 13:35:37 +02:00
2023-08-29 10:30:28 +03:00
from __future__ import annotations
2023-08-29 10:27:14 +03:00
import datetime
2022-09-23 01:29:49 +03:00
import hashlib
2023-08-29 10:27:14 +03:00
import json
2022-03-11 13:35:37 +02:00
import os
2023-08-29 10:27:14 +03:00
import re
import shutil
2022-03-11 13:35:37 +02:00
import string
2023-08-29 10:27:14 +03:00
import subprocess
2022-03-11 13:35:37 +02:00
import sys
2023-08-29 10:27:14 +03:00
import tempfile
import typing
2022-11-04 01:53:48 +02:00
import xml . etree . ElementTree as etree
2023-10-29 17:41:58 +02:00
from collections import Counter
2022-03-11 17:47:54 +02:00
from glob import iglob
2022-03-11 13:35:37 +02:00
from html import escape as html_escape
from threading import Thread
from timeit import default_timer as code_timer
2023-08-29 10:27:14 +03:00
import mistune
import mistune . core
import mistune . inline_parser
import mistune . plugins
import unidecode
2023-10-08 07:37:55 +03:00
import web_mini
2024-12-15 02:53:32 +02:00
from mistune . renderers . html import HTMLRenderer
2023-04-26 02:41:11 +03:00
from readtime import of_markdown as read_time_of_markdown # type: ignore
2023-10-29 17:41:58 +02:00
from readtime . result import Result as MarkdownResult # type: ignore
2022-03-11 13:35:37 +02:00
2024-05-15 23:02:15 +03:00
# from warnings import filterwarnings as filter_warnings
2023-08-29 10:27:14 +03:00
__version__ : typing . Final [ int ] = 2
2023-10-26 18:03:32 +03:00
GEN : typing . Final [ str ] = f " ari-web blog generator version { __version__ } "
2023-08-29 10:27:14 +03:00
2024-12-15 02:53:32 +02:00
MEDIA_MIME : dict [ str , str ] = {
" image/jpeg " : " jpeg " ,
" image/png " : " png " ,
" image/gif " : " gif " ,
" image/webp " : " webp " ,
" image/avif " : " avif " ,
" image/svg+xml " : " svg " ,
" audio/mpeg " : " mp3 " ,
" audio/wav " : " wav " ,
" audio/ogg " : " ogg " ,
" audio/flac " : " flac " ,
}
2023-08-29 10:27:14 +03:00
OK : typing . Final [ int ] = 0
ER : typing . Final [ int ] = 1
CONFIG_FILE : typing . Final [ str ] = " blog.json "
2023-10-06 19:01:48 +03:00
DEFAULT_CONFIG : dict [ str , typing . Any ] = {
2023-08-29 10:27:14 +03:00
" title " : " blog " ,
" header " : " blog " ,
" description " : " my blog page " ,
" posts-dir " : " b " ,
" assets-dir " : " content " ,
" rss-file " : " rss.xml " ,
" blog-keywords " : [
" blog " ,
" blog page " ,
" blog post " ,
" personal " ,
" website " ,
2022-03-11 13:35:37 +02:00
] ,
2023-08-29 10:27:14 +03:00
" default-keywords " : [
" blog " ,
" blog page " ,
" blog post " ,
" personal " ,
" website " ,
] ,
" website " : " https://example.com " ,
" blog " : " https://blog.example.com " ,
" source " : " /git " ,
2023-05-19 18:24:02 +03:00
" visitor-count " : " /visit " ,
2023-08-29 10:27:14 +03:00
" comment " : " /c " ,
" theme " : {
" primary " : " #000 " ,
" secondary " : " #fff " ,
" type " : " dark " ,
} ,
" manifest " : {
" icons " : [
{
" src " : " /favicon.ico " ,
" sizes " : " 128x128 " ,
2023-10-29 19:34:24 +02:00
" type " : " image/x-icon " ,
2023-08-29 10:27:14 +03:00
} ,
] ,
} ,
" author " : " John Doe " ,
2023-10-26 18:03:32 +03:00
" email " : " me@example.com " ,
2023-08-29 10:27:14 +03:00
" locale " : " en_GB " ,
" recents " : 14 ,
" indent " : 4 ,
" markdown-plugins " : [ # good defaults
" speedup " ,
" strikethrough " ,
" insert " ,
" superscript " ,
" subscript " ,
" footnotes " ,
" abbr " ,
] ,
" editor " : [ " vim " , " -- " , " %s " ] ,
" context-words " : [
" the " ,
" a " ,
" about " ,
" etc " ,
" on " ,
" at " ,
" in " ,
" by " ,
" its " ,
" i " ,
" to " ,
" my " ,
" of " ,
" between " ,
" because " ,
" of " ,
" or " ,
" how " ,
" to " ,
" begin " ,
" is " ,
" this " ,
" person " ,
" important " ,
" homework " ,
" and " ,
" cause " ,
" how " ,
" what " ,
" for " ,
" with " ,
" without " ,
2023-08-30 19:03:58 +03:00
" using " ,
" im " ,
2023-08-29 10:27:14 +03:00
] ,
" wslug-limit " : 10 ,
" slug-limit " : 96 ,
" license " : " GPL-3.0-or-later " ,
2023-10-24 03:45:59 +03:00
" recent-title-trunc " : 16 ,
2023-08-29 10:27:14 +03:00
" server-host " : " 127.0.0.1 " ,
" server-port " : 8080 ,
2023-08-29 10:47:52 +03:00
" post-preview-size " : 196 ,
2023-08-29 11:45:09 +03:00
" read-wpm " : 150 ,
2023-10-29 17:41:58 +02:00
" top-words " : 64 ,
" top-tags " : 64 ,
2024-12-14 23:14:29 +02:00
" legal " : ' <b>Legal disclaimer</b>: The content of this blog post is my personal opinion, experience, and thoughts as an individual sharing her perspective on this topic. I am not a professional or expert in this area, and the information provided in this blog post is for general informational, educational, entertainment, and recreational purposes only. Accuracy is strived for, but no warranty is given in respect to the reliability or completeness of such content, express or implied. Your acceptance of any reliance on any part of such content from the blog is entirely at your own risk. Under no circumstances shall I, and any of my representatives be held liable for damages arising from access and use of the blog post or linked sites and sources. Furthermore, there might be some links from this blog post to other websites; I have no control over those and do not endorse their content. You are responsible for your actions and subsequent results in using this material. This disclaimer is provided as per the legal basis underlying <a href= " https://ari.lt/legal " >https://ari.lt/legal</a> and should be considered in further reference to privacy, data security, and liability. Your use of this blog post acknowledges your acceptance of the conditions, and exempts me from any liability that comes from any possible inaccuracy, incompleteness, unreliability, inconsistencies, (un)availability, or any other errors in the information provided herein. ' ,
2023-08-29 10:27:14 +03:00
" posts " : { } ,
2022-03-11 13:35:37 +02:00
}
2023-08-29 10:27:14 +03:00
NCI : bool = " CI " not in os . environ
2023-08-30 19:15:36 +03:00
NOCLR : bool = " NOCLR " in os . environ
2023-08-29 10:27:14 +03:00
LOG_CLR : str = " \033 [90m "
ERR_CLR : str = " \033 [1m \033 [31m "
NEW_CLR : str = " \033 [1m \033 [32m "
IMP_CLR : str = " \033 [1m \033 [35m "
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
HTML_BEGIN : typing . Final [
str
] = """ <!DOCTYPE html>
2023-10-06 17:53:29 +03:00
< html lang = " {lang} " >
2023-08-29 10:27:14 +03:00
< 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 " >
2023-10-29 19:34:24 +02:00
< link rel = " icon " href = " /favicon.ico " sizes = " 128x128 " type = " image/x-icon " / >
2023-08-29 10:27:14 +03:00
< meta
name = " keywords "
content = " {keywords} "
/ >
< meta
2023-10-08 21:19:31 +03:00
name = " robots "
content = " follow, index, max-snippet:-1, max-video-preview:-1, max-image-preview:large "
2023-08-29 10:27:14 +03:00
/ >
< meta name = " color-scheme " content = " {theme_type} " / >
< meta name = " theme-color " content = " {theme_primary} " / >
< link rel = " manifest " href = " /manifest.json " / >
< link rel = " canonical " href = " {blog} / {path} " >
2023-10-06 18:45:04 +03:00
< style type = " text/css " >
2023-10-08 07:37:55 +03:00
: root { { color - scheme : { theme_type } ; - - b : { theme_primary } ; - - f : { theme_secondary } } } \
2023-10-24 03:45:59 +03:00
html { { background - color : var ( - - b ) ; color : var ( - - f ) } } { critical_css }
2023-10-06 18:45:04 +03:00
< / style >
2023-10-06 17:53:29 +03:00
< link
2023-10-08 21:19:31 +03:00
href = " / {styles} "
rel = " preload "
referrerpolicy = " no-referrer "
type = " text/css "
as = " style "
onload = " this.onload=null;this.rel= ' stylesheet ' "
2023-10-06 17:53:29 +03:00
/ >
< noscript >
2023-08-29 10:27:14 +03:00
< link
2023-10-08 21:19:31 +03:00
href = " / {styles} "
rel = " stylesheet "
referrerpolicy = " no-referrer "
type = " text/css "
2023-08-29 10:27:14 +03:00
/ >
2023-10-06 17:53:29 +03:00
< / noscript >
2023-08-29 10:27:14 +03:00
< link
2023-10-08 21:19:31 +03:00
href = " / {rss} "
referrerpolicy = " no-referrer "
title = " {blog_title} "
rel = " alternate "
type = " application/rss+xml "
2023-08-29 10:27:14 +03:00
/ >
< meta name = " author " content = " {author} " / >
2023-10-26 18:03:32 +03:00
< meta name = " generator " content = " {gen} " / >
2023-08-29 10:27:14 +03:00
< meta property = " og:locale " content = " {locale} " / >
< meta name = " license " content = " {license} " >
< link rel = " sitemap " href = " /sitemap.xml " type = " application/xml " > """
POST_TEMPLATE : typing . Final [ str ] = (
HTML_BEGIN
+ """
2023-10-06 18:36:58 +03:00
< style type = " text/css " > { post_critical_css } < / style >
2023-08-29 10:27:14 +03:00
< title > { blog_title } - > { post_title } < / title >
< meta name = " description " content = " {post_title} by {author} at {post_creation_time} GMT -- {post_description} " / >
< meta property = " article:read_time " content = " {post_read_time} " / >
< meta property = " og:type " content = " article " / >
< / head >
< body >
2023-10-31 21:56:36 +02:00
< header role = " group " >
< h1 role = " heading " aria - level = " 1 " > { post_title } < / h1 >
2023-08-29 10:27:14 +03:00
2023-10-31 21:56:36 +02:00
< nav id = " info-bar " role = " menubar " >
< a role = " menuitem " aria - label = " skip " href = " #main " > skip < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2022-10-15 01:14:29 +03:00
2023-10-31 21:56:36 +02:00
< span role = " menuitem " > < time > { post_creation_time } < / time > GMT { post_edit_time } < / span >
< span role = " seperator " aria - hidden = " true " > | < / span >
2023-04-26 02:41:11 +03:00
2023-10-31 21:56:36 +02:00
< span role = " menuitem " > visitor < img src = " {visitor_count} " alt = " visitor count " / > < / span >
< span role = " seperator " aria - hidden = " true " > | < / span >
2023-05-19 18:24:02 +03:00
2023-10-31 21:56:36 +02:00
< span role = " menuitem " > < time > { post_read_time } < / time > read < / span >
< br role = " seperator " aria - hidden = " true " / >
2022-10-15 01:14:29 +03:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " / " > home < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2022-10-15 01:14:29 +03:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " /stats " > stats < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2023-10-29 17:41:58 +02:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " {comment} " > comment < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2022-10-15 01:14:29 +03:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " {website} " > website < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2022-10-15 01:14:29 +03:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " {source} " > src < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2023-08-08 17:49:12 +03:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " / {rss} " > rss < / a >
2022-10-15 01:14:29 +03:00
2023-10-31 21:56:36 +02:00
< hr aria - hidden = " true " role = " seperator " / >
< / nav >
< / header >
2024-12-14 23:14:29 +02:00
< main > < article id = " main " > < p id = legal > { legal } < / p > { post_content } < / article > < / main >
2023-10-31 21:56:36 +02:00
< footer > < p > { author } & lt ; < a href = " mailto: {email} " > { email } < / a > & gt ; + { license } < / p > < / footer >
2022-03-11 13:35:37 +02:00
< / body >
< / html > """
2023-08-29 10:27:14 +03:00
)
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
INDEX_TEMPLATE : typing . Final [ str ] = (
HTML_BEGIN
+ """
< title > { blog_title } < / title >
< meta name = " description " content = " {blog_description} " / >
< meta property = " og:type " content = " website " / >
2022-03-11 13:35:37 +02:00
< / head >
2022-10-15 01:14:29 +03:00
2023-08-29 10:27:14 +03:00
< body >
2023-10-31 21:56:36 +02:00
< header role = " group " >
< h1 role = " heading " aria - level = " 1 " > { blog_header } < / h1 >
2023-08-29 10:27:14 +03:00
2023-10-31 21:56:36 +02:00
< nav id = " info-bar " role = " menubar " >
< a role = " menuitem "
aria - label = " skip "
href = " #main " > skip < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2022-10-15 01:14:29 +03:00
2023-10-31 21:56:36 +02:00
< span role = " menuitem " > latest post : < a href = " / {latest_post_path} " > { latest_post_title_trunc } < / a > at < time > { latest_post_creation_time } < / time > GMT < / span >
< span role = " seperator " aria - hidden = " true " > | < / span >
2022-10-15 01:14:29 +03:00
2023-10-31 21:56:36 +02:00
< span role = " menuitem " > visitor < img src = " {visitor_count} " alt = " visitor count " / > < / span >
< br role = " seperator " aria - hidden = " true " / >
2022-10-15 01:14:29 +03:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " /stats " > stats < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2023-10-29 17:41:58 +02:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " {comment} " > comment < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2022-10-15 01:14:29 +03:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " {website} " > website < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2022-10-15 01:14:29 +03:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " {source} " > src < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2022-10-15 01:14:29 +03:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " / {rss} " > rss < / a >
2023-08-08 17:49:12 +03:00
2023-10-31 21:56:36 +02:00
< hr aria - hidden = " true " role = " seperator " / >
< / nav >
< / header >
< main > < article id = " main " > < ol reversed id = blist > { blog_list } < / ol > < / article > < / main >
< footer > < p > { author } & lt ; < a href = " mailto: {email} " > { email } < / a > & gt ; + { license } < / p > < / footer >
2023-08-29 10:27:14 +03:00
< / body >
< / html > """
)
2022-10-15 01:14:29 +03:00
2023-10-29 17:41:58 +02:00
STATS_TEMPLATE : typing . Final [ str ] = (
HTML_BEGIN
+ """
< title > { blog_title } - > stats < / title >
< meta name = " description " content = " stats of {blog_title} , {blog_description} " / >
< meta property = " og:type " content = " website " / >
< / head >
< body >
2023-10-31 21:56:36 +02:00
< header role = " group " >
< h1 role = " heading " aria - level = " 1 " > stats of { blog_header } < / h1 >
2023-10-29 17:41:58 +02:00
2023-10-31 21:56:36 +02:00
< nav id = " info-bar " role = " menubar " >
< a role = " menuitem "
aria - label = " skip "
href = " #main " > skip < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2023-10-29 17:41:58 +02:00
2023-10-31 21:56:36 +02:00
< span role = " menuitem "
> visitor < img src = " {visitor_count} " alt = " visitor count "
/ > < / span >
< br role = " seperator " aria - hidden = " true " / >
2023-10-29 17:41:58 +02:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " / " > home < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2023-10-29 17:41:58 +02:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " {comment} " > comment < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2023-10-29 17:41:58 +02:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " {website} " > website < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2023-10-29 17:41:58 +02:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " {source} " > src < / a >
< span role = " seperator " aria - hidden = " true " > | < / span >
2023-10-29 17:41:58 +02:00
2023-10-31 21:56:36 +02:00
< a role = " menuitem " href = " / {rss} " > rss < / a >
2023-10-29 17:41:58 +02:00
2023-10-31 21:56:36 +02:00
< hr aria - hidden = " true " role = " seperator " / >
< / nav >
< / header >
2023-10-29 17:41:58 +02:00
< main >
< article id = " main " >
2024-12-14 23:14:29 +02:00
2023-10-29 17:41:58 +02:00
< ul id = blist >
< li > total count of blog posts : < code > { post_count } < / code > < / li >
< li > edited post count : < code > { edited_post_count } < / code > , < code > { edited_post_count_p : .2 f } % < / code > < / li >
< li >
total read time : < time > { read_time } < / time >
< ul >
< li > average read time : < time > { avg_read_time } < / time > < / li >
< / ul >
< / li >
< li >
content
< ul >
< li > characters : < code > { char_count } < / code > < / li >
< ul >
< li > average count of characters : < code > { avg_chars : .2 f } < / code > < / li >
< / ul >
< / ul >
< ul >
< li > words : < code > { word_count } < / code > < / li >
< ul >
< li > average count of words : < code > { avg_words : .2 f } < / code > < / li >
< li > average word length : < code > { avg_word_len : .2 f } < / code > < / li >
< li >
top { top_words } used words
< ol > { word_most_used } < / ol >
< / li >
< / ul >
< / ul >
< ul >
< li > tags : < code > { tag_count } < / code > < / li >
< ul >
< li > average count of tags : < code > { avg_tags } < / code > < / li >
< li >
top { top_tags } used tags
< ol > { tags_most_used } < / ol >
< / li >
2023-10-29 17:54:19 +02:00
< li > default tags < ol > { default_tags } < / ol > < / li >
2023-10-29 17:41:58 +02:00
< / ul >
< / ul >
< / li >
< li >
time ( GMT )
< ul >
< li > average posts by year : { posts_by_yr_avg } < ol > { posts_by_yr } < / ol > < / li >
< li > average posts by month : { posts_by_month_avg } < ol > { posts_by_month } < / ol > < / li >
< li > average posts by day : { posts_by_day_avg } < ol > { posts_by_day } < / ol > < / li >
< li > average posts by hour : { posts_by_hr_avg } < ol > { posts_by_hr } < / ol > < / li >
< / ul >
< / li >
< / ul >
< / article >
2023-10-31 21:56:36 +02:00
< / main > < footer > < p > { author } & lt ; < a href = " mailto: {email} " > { email } < / a > & gt ; + { license } < / p > < / footer > < / body >
2023-10-29 17:41:58 +02:00
< / html > """
)
2023-08-29 10:27:14 +03:00
if NCI :
import http . server
2022-10-15 01:14:29 +03:00
2024-12-15 02:53:32 +02:00
import magic
2023-08-29 10:27:14 +03:00
import pyfzf # type: ignore
2024-12-15 02:53:32 +02:00
from PIL import Image
2023-08-29 10:27:14 +03:00
else :
pyfzf : typing . Any = None
http : typing . Any = None
2024-12-15 02:53:32 +02:00
magic : typing . Any = None
Image : typing . Any = None
2022-10-15 01:14:29 +03:00
2023-08-29 10:27:14 +03:00
class Commands :
def __init__ ( self ) - > None :
2023-10-06 20:28:31 +03:00
self . commands : dict [ str , typing . Callable [ [ dict [ str , typing . Any ] ] , int ] ] = { }
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
def new (
2023-10-06 19:01:48 +03:00
self , fn : typing . Callable [ [ dict [ str , typing . Any ] ] , int ]
) - > typing . Callable [ [ dict [ str , typing . Any ] ] , int ] :
2023-08-29 10:27:14 +03:00
self . commands [ fn . __name__ ] = fn
return fn
2022-03-11 13:35:37 +02:00
2023-10-06 20:28:31 +03:00
def __getitem__ ( self , name : str ) - > typing . Callable [ [ dict [ str , typing . Any ] ] , int ] :
2023-08-29 10:27:14 +03:00
return self . commands [ name ]
2023-02-02 15:57:41 +02:00
2023-08-29 10:27:14 +03:00
cmds : Commands = Commands ( )
ecmds : Commands = Commands ( )
2023-02-09 00:45:57 +02:00
2023-08-30 19:03:58 +03:00
def ctimer ( ) - > float :
return code_timer ( ) if NCI else 0
2022-10-31 03:28:33 +02:00
2023-08-30 19:03:58 +03:00
def log ( msg : str , clr : str = LOG_CLR ) - > int :
2023-08-29 10:27:14 +03:00
if NCI :
print (
2024-02-10 16:50:07 +02:00
(
f " { datetime . datetime . now ( ) } | { msg } "
if NOCLR
else f " { clr } { datetime . datetime . now ( ) } | { msg } \033 [0m "
) ,
2023-08-29 10:27:14 +03:00
file = sys . stderr ,
2022-10-31 03:28:33 +02:00
)
2023-08-29 10:27:14 +03:00
return OK
2022-10-31 03:28:33 +02:00
2023-08-29 10:27:14 +03:00
def llog ( msg : str ) - > int :
return log ( msg , " \033 [0m " )
2022-10-31 03:28:33 +02:00
2023-08-29 10:27:14 +03:00
def err ( msg : str ) - > int :
log ( msg , ERR_CLR )
return ER
2022-10-31 03:28:33 +02:00
2022-11-04 01:53:48 +02:00
2023-08-29 10:27:14 +03:00
def lnew ( msg : str ) - > int :
return log ( msg , NEW_CLR )
2022-11-04 01:53:48 +02:00
2022-10-31 16:50:02 +02:00
2023-08-29 10:27:14 +03:00
def imp ( msg : str ) - > int :
return log ( msg , IMP_CLR )
2022-11-04 01:53:48 +02:00
2022-10-31 16:50:02 +02:00
2023-08-29 10:27:14 +03:00
def slugify (
title : str ,
2023-10-06 19:01:48 +03:00
context_words : typing . Sequence [ str ] | None = None ,
2023-08-29 10:27:14 +03:00
wslug_limit : int = DEFAULT_CONFIG [ " wslug-limit " ] ,
slug_limit : int = DEFAULT_CONFIG [ " slug-limit " ] ,
) - > str :
return (
" - " . join (
[
w
for w in " " . join (
c
for c in unidecode . unidecode ( title ) . lower ( )
if c not in string . punctuation
) . split ( )
if w not in ( context_words or [ ] )
] [ : wslug_limit ]
) [ : slug_limit ] . strip ( " - " )
or " post "
)
2022-10-31 16:50:02 +02:00
2023-10-29 17:41:58 +02:00
def rf_format_time ( ts : float ) - > typing . Tuple [ datetime . datetime , str ] :
2024-05-15 23:02:15 +03:00
d : datetime . datetime = datetime . datetime . utcfromtimestamp ( ts )
2023-10-29 17:41:58 +02:00
return d , d . strftime ( " % Y- % m- %d % H: % M: % S " )
2023-08-29 10:27:14 +03:00
def rformat_time ( ts : float ) - > str :
2024-05-15 23:02:15 +03:00
return datetime . datetime . utcfromtimestamp ( ts ) . strftime ( " % Y- % m- %d % H: % M: % S " )
2022-11-04 01:53:48 +02:00
2023-08-29 10:27:14 +03:00
def format_time ( ts : float ) - > str :
return f " { rformat_time ( ts ) } GMT "
2022-11-04 01:53:48 +02:00
2023-10-06 19:01:48 +03:00
def select_multi ( options : typing . Sequence [ str ] ) - > list [ str ] :
2023-08-29 10:27:14 +03:00
if not options :
return [ ]
return pyfzf . FzfPrompt ( ) . prompt (
choices = options ,
fzf_options = " -m " ,
)
2023-02-09 00:45:57 +02:00
2023-08-29 10:27:14 +03:00
2023-10-06 20:28:31 +03:00
def select_posts ( posts : dict [ str , dict [ str , typing . Any ] ] ) - > tuple [ str , . . . ] :
2023-08-29 10:27:14 +03:00
return tuple (
map (
lambda opt : opt . split ( " | " , maxsplit = 1 ) [ 0 ] . strip ( ) ,
select_multi (
tuple (
f " { slug } | { post [ ' title ' ] } | { post [ ' description ' ] } "
for slug , post in posts . items ( )
) ,
) ,
2022-11-05 20:04:39 +02:00
)
2023-08-29 10:27:14 +03:00
)
2022-10-31 03:28:33 +02:00
2023-08-29 10:27:14 +03:00
if NCI :
2023-10-30 12:50:09 +02:00
try :
import readline
except Exception :
readline : typing . Any = None
2022-10-20 20:04:16 +03:00
2023-08-29 10:27:14 +03:00
def iinput ( prompt : str , default_text : str = " " , force : bool = True ) - > str :
default_text = default_text . strip ( )
2022-03-11 13:35:37 +02:00
2023-10-30 12:50:09 +02:00
if readline is not None and default_text :
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
def hook ( ) - > None :
readline . insert_text ( default_text )
readline . redisplay ( )
2022-09-20 04:50:25 +03:00
2023-08-29 10:27:14 +03:00
readline . set_pre_input_hook ( hook )
2022-09-20 04:50:25 +03:00
2023-08-29 10:27:14 +03:00
while not ( user_input := input ( f " \033 [1m { prompt } \033 [0m " ) . strip ( ) ) and force :
pass
2022-09-20 04:50:25 +03:00
2023-10-30 12:50:09 +02:00
if readline is not None :
readline . set_pre_input_hook ( )
2022-09-20 04:50:25 +03:00
2023-08-29 10:27:14 +03:00
return user_input
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
else :
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
def iinput ( prompt : str , default_text : str = " " , force : bool = True ) - > str :
raise ValueError (
f " cannot read user input in CI mode, prompt : { prompt !r} ; default text : { default_text !r} ; force : { force !r} "
)
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
def yn ( prompt : str , default : str = " y " ) - > bool :
return ( iinput ( f " { prompt } ? [y/n] " , default ) + default ) [ 0 ] . lower ( ) == " y "
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
def get_tmpfile ( name : str ) - > str :
2023-10-06 22:17:53 +03:00
return f " { tempfile . gettempdir ( ) } / { name } .md "
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
def open_file ( editor : typing . Sequence [ str ] , path : str ) - > None :
log ( f " formatting and running { editor !r} with { path !r} " )
2023-10-30 13:06:52 +02:00
try :
subprocess . run ( [ ( token . replace ( " %s " , path ) ) for token in editor ] )
except Exception as e :
sys . exit ( err ( f " failed to run editor : { e } " ) )
2022-06-01 16:19:08 +03:00
2023-08-29 10:51:42 +03:00
def trunc ( data : str , length : int , end : str = " ... " ) - > str :
return data [ : length ] + ( end if len ( data ) > length else " " )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
def read_post ( path : str ) - > str :
log ( f " reading { path !r} " )
2022-03-11 13:35:37 +02:00
try :
2023-08-29 10:27:14 +03:00
with open ( path , " r " ) as data :
return data . read ( ) . strip ( )
except Exception as e :
err ( f " failed to read { path !r} : { e . __class__ . __name__ } { e } " )
2022-03-11 13:35:37 +02:00
return " "
2023-10-08 07:37:55 +03:00
def min_css_file ( file : str , out : str ) - > None :
with open ( file , " r " ) as icss :
with open ( out , " w " ) as ocss :
ocss . write ( web_mini . css . minify_css ( icss . read ( ) ) )
2023-10-29 17:41:58 +02:00
def sorted_post_counter (
c : Counter [ int ] ,
pcount : int ,
fix : str ,
) - > typing . Dict [ str , typing . Any ] :
s : int = sum ( c . values ( ) )
avg : float = s / len ( c )
return {
f " posts_by_ { fix } " : " " . join (
f " <li><time> { v } </time> -- <code> { p } </code> post { ' ' if p == 1 else ' s ' } , <code> { p / pcount * 100 : .2f } %</code></li> "
for v , p in c . most_common ( )
) ,
f " posts_by_ { fix } _avg " : f " <code> { round ( avg , 2 ) } </code>, <code> { round ( avg / s * 100 , 2 ) } %</code> " ,
}
def s_to_str ( seconds : float ) - > str :
minutes , sec = divmod ( seconds , 60 )
hours , minutes = divmod ( minutes , 60 )
days , hours = divmod ( hours , 24 )
periods : typing . Tuple [ typing . Tuple [ float , str , str ] , . . . ] = (
( round ( days , 2 ) , " day " , " days " ) ,
( round ( hours , 2 ) , " hour " , " hours " ) ,
( round ( minutes , 2 ) , " minute " , " minutes " ) ,
( round ( sec , 2 ) , " second " , " seconds " ) ,
)
time_periods : typing . List [ str ] = [ ]
for period in periods :
if period [ 0 ] != 0 :
time_periods . append (
" {} {} " . format ( period [ 0 ] , period [ 1 ] if period [ 0 ] == 1 else period [ 2 ] )
)
readable_text : str = " , " . join ( time_periods [ : - 1 ] )
if len ( time_periods ) > 1 :
readable_text + = " and " + time_periods [ - 1 ]
else :
readable_text = time_periods [ 0 ]
2023-10-29 17:54:19 +02:00
return (
f " { readable_text } ( { round ( seconds , 2 ) } second { ' ' if seconds == 1 else ' s ' } ) "
)
2023-10-29 17:41:58 +02:00
2023-08-29 10:27:14 +03:00
# markdown
2022-03-11 13:35:37 +02:00
2023-08-30 02:47:32 +03:00
TITLE_LINKS_RE : typing . Final [ str ] = r " <#:[^>]+?> "
2024-12-15 02:53:32 +02:00
MEDIA_EMBED_RE : typing . Final [ str ] = r " <@:[^>]+?> "
MEDIA_INDEX : dict [ str , dict [ str , typing . Any ] ] = { }
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
def parse_inline_titlelink (
_ : mistune . inline_parser . InlineParser ,
m : re . Match [ str ] ,
state : mistune . core . InlineState ,
) - > int :
text : str = m . group ( 0 ) [ 3 : - 1 ]
2022-04-07 22:32:55 +03:00
2023-08-29 10:27:14 +03:00
state . append_token (
{
" type " : " link " ,
" children " : [ { " type " : " text " , " raw " : f " # { text } " } ] ,
" attrs " : { " url " : f " # { slugify ( text , [ ] , 768 , 768 ) } " } ,
}
)
2023-10-08 21:35:51 +03:00
2023-08-29 10:27:14 +03:00
return m . end ( )
2022-03-11 13:35:37 +02:00
2024-12-15 02:53:32 +02:00
def parse_inline_media_embed (
_ : mistune . inline_parser . InlineParser ,
m : re . Match [ str ] ,
state : mistune . core . InlineState ,
) - > int :
text : str = m . group ( 0 ) [ 3 : - 1 ]
if text not in MEDIA_INDEX :
state . append_token (
{
" type " : " block_div " ,
" children " : [
{
" type " : " strong_error " ,
" raw " : f " ERROR: Media ' { html_escape ( text ) } ' does not exist. " ,
}
] ,
" attrs " : { " classes " : " media " } ,
}
)
return m . end ( )
mdx : dict [ str , typing . Any ] = MEDIA_INDEX [ text ]
source : str = f " /media/ { text } . { mdx [ ' ext ' ] } "
if mdx [ " type " ] == " image " :
child : dict [ str , typing . Any ] = {
" type " : " image " ,
" raw " : source ,
" attrs " : {
" alt " : mdx [ " alt " ] ,
" type " : mdx [ " mime " ] ,
" width " : mdx [ " width " ] ,
" height " : mdx [ " height " ] ,
} ,
}
elif mdx [ " type " ] == " audio " :
child = {
" type " : " audio " ,
" raw " : source ,
" attrs " : {
" alt " : mdx [ " alt " ] ,
" type " : mdx [ " mime " ] ,
} ,
}
else :
state . append_token (
{
" type " : " block_div " ,
" children " : [
{
" type " : " strong_error " ,
" raw " : f " ERROR: Media ' { html_escape ( text ) } ' has an unsupported type. " ,
}
] ,
" attrs " : { " classes " : " media " } ,
}
)
return m . end ( )
state . append_token (
{
" type " : " block_div " ,
" children " : [
child ,
{ " type " : " linebreak " } ,
{
" type " : " emphasis " ,
2024-12-15 03:02:24 +02:00
" raw " : f " { mdx [ ' alt ' ] } | \" { mdx [ ' title ' ] } \" by { mdx [ ' credit ' ] } ( { mdx [ ' license ' ] } ). Purpose: { html_escape ( mdx [ ' purpose ' ] ) } . Uploaded on { datetime . datetime . utcfromtimestamp ( mdx [ ' created ' ] ) . strftime ( ' %a , %d % b % Y % H: % M: % S GMT ' ) } . " ,
} ,
{
" type " : " link " ,
" children " : [ { " type " : " text " , " raw " : f " (raw media here) " } ] ,
" attrs " : { " url " : source } ,
2024-12-15 02:53:32 +02:00
} ,
] ,
" attrs " : { " classes " : " media " } ,
}
)
return m . end ( )
2023-08-29 10:27:14 +03:00
def titlelink ( md : mistune . Markdown ) - > None :
md . inline . register ( " titlelink " , TITLE_LINKS_RE , parse_inline_titlelink , before = " link " ) # type: ignore
2022-03-11 13:35:37 +02:00
2024-12-15 02:53:32 +02:00
def media_embed ( md : mistune . Markdown ) - > None :
md . inline . register ( " media_embed " , MEDIA_EMBED_RE , parse_inline_media_embed , before = " link " ) # type: ignore
class BlogRenderer ( HTMLRenderer ) :
def heading ( self , text : str , level : int ) - > str :
2023-08-29 10:27:14 +03:00
slug : str = slugify ( text , [ ] , 768 , 768 )
level = max ( 2 , level )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
return f ' <h { level } id= " { slug } " h><a href= " # { slug } " >#</a> { text } </h { level } > '
2022-03-11 13:35:37 +02:00
2024-12-15 02:53:32 +02:00
def strong_error ( self , text : str ) - > str :
return f " <strong class=error> { text } </strong> "
def block_div ( self , text : str , classes : str ) :
return f ' <div class= " { html_escape ( classes ) } " > { text } </div> '
def image ( self , text : str , alt : str , type : str , width : int , height : int ) - > str :
return f ' <img title= " { html_escape ( alt ) } " src= " { text } " alt= " { html_escape ( alt ) } " type= " { type } " data-width= " { width } " data-height= " { height } " loading=lazy /> '
def audio ( self , text : str , alt : str , type : str ) - > str :
return f ' <audio controls title= " { html_escape ( alt ) } " > <source src= " { text } " type= " { type } " { alt } /> </audio> '
2022-03-12 21:56:41 +02:00
2023-10-06 19:01:48 +03:00
def markdown ( md : str , plugins : list [ typing . Any ] ) - > str :
2024-12-15 02:53:32 +02:00
return mistune . create_markdown ( plugins = plugins + [ titlelink , media_embed ] , renderer = BlogRenderer ( ) ) ( md ) # type: ignore
2022-03-11 13:35:37 +02:00
2022-04-07 22:32:55 +03:00
2023-08-29 10:27:14 +03:00
# edit commands
2022-04-17 17:06:42 +03:00
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
@ecmds.new
2023-10-06 19:01:48 +03:00
def title ( post : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
post [ " title " ] = iinput ( " post title " , post [ " title " ] )
return OK
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
@ecmds.new
2023-10-06 19:01:48 +03:00
def description ( post : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
post [ " description " ] = iinput ( " post description " , post [ " description " ] )
return OK
2022-03-11 13:35:37 +02:00
2022-06-21 13:27:59 +03:00
2023-08-29 10:27:14 +03:00
@ecmds.new
2023-10-06 19:01:48 +03:00
def content ( post : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" edit posts """
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
log ( " getting post markdown path " )
path : str = get_tmpfile ( post [ " slug " ] )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
log ( " writing content " )
with open ( path , " w " ) as p :
p . write ( post [ " content " ] )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
open_file ( post [ " editor " ] , path )
2022-10-31 03:28:33 +02:00
2023-08-29 10:27:14 +03:00
if not ( content := read_post ( path ) ) :
return err ( " post content cannot be empty " )
2022-10-31 03:28:33 +02:00
2023-08-29 10:27:14 +03:00
post [ " content " ] = content
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
return OK
2022-03-11 13:35:37 +02:00
2022-10-20 20:04:16 +03:00
2023-08-29 10:27:14 +03:00
@ecmds.new
2023-10-06 19:01:48 +03:00
def keywords ( post : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" edit keywords """
2022-10-20 20:04:16 +03:00
2023-08-29 10:27:14 +03:00
post [ " keywords " ] = tuple (
map (
lambda k : unidecode . unidecode ( k . strip ( ) ) ,
filter (
bool ,
set (
iinput ( " post keywords " , " , " . join ( post [ " keywords " ] ) , force = False )
. lower ( )
. split ( " , " )
) ,
) ,
)
)
return OK
2022-10-20 20:04:16 +03:00
2023-08-29 10:27:14 +03:00
# normal commands
2022-10-20 20:04:16 +03:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def help ( _ : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" print help """
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
return llog (
" \n \n "
+ " \n " . join (
f " { cmd } -- { fn . __doc__ or ' no help provided ' } "
for cmd , fn in cmds . commands . items ( )
)
)
2022-03-11 13:35:37 +02:00
2022-04-17 17:18:56 +03:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def sort ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" sort blog posts by creation time """
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
log ( " sorting posts by creation time " )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
config [ " posts " ] = dict (
map (
lambda k : ( k , config [ " posts " ] [ k ] ) ,
sorted (
config [ " posts " ] ,
key = lambda k : config [ " posts " ] [ k ] [ " created " ] ,
reverse = True ,
) ,
)
)
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
return lnew ( " sorted blog posts by creation time " )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def new ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" create a new blog post """
2023-04-26 02:41:11 +03:00
2023-08-29 10:27:14 +03:00
title : str = iinput ( " post title " )
2023-04-26 02:52:55 +03:00
2023-08-29 10:27:14 +03:00
log ( " creating a slug from the given title " )
2023-08-30 19:03:58 +03:00
slug : str = slugify (
title ,
config [ " context-words " ] ,
config [ " wslug-limit " ] ,
config [ " slug-limit " ] ,
)
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
if slug in ( posts := config [ " posts " ] ) :
slug + = f " - { sum ( map ( lambda k : k . startswith ( slug ) , posts ) ) } "
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
log ( " getting post markdown path " )
post_path : str = get_tmpfile ( slug )
2022-04-17 17:06:42 +03:00
2023-08-29 10:27:14 +03:00
open_file ( config [ " editor " ] , post_path )
2022-04-17 17:06:42 +03:00
2023-08-29 10:27:14 +03:00
if not ( content := read_post ( post_path ) ) :
return err ( " content cannot be empty " )
2022-03-11 13:35:37 +02:00
2023-10-06 19:01:48 +03:00
keywords : tuple [ str , . . . ] = tuple (
2023-08-29 10:27:14 +03:00
map (
lambda k : unidecode . unidecode ( k . strip ( ) ) ,
filter (
bool ,
set (
iinput ( " post keywords ( separated by `,` ) " , force = False )
. lower ( )
. split ( " , " )
) ,
) ,
)
)
2022-03-11 13:35:37 +02:00
2023-10-17 17:29:30 +03:00
description : str = iinput ( " post description " )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
lnew ( f " saving blog post { slug !r} " )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
posts [ slug ] = {
" title " : title ,
" description " : description . strip ( ) ,
" content " : content ,
" keywords " : keywords ,
2024-05-15 23:02:15 +03:00
" created " : datetime . datetime . utcnow ( ) . timestamp ( ) ,
2023-08-29 10:27:14 +03:00
}
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
return OK
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def ls ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" list all posts """
2022-03-11 13:35:37 +02:00
2023-11-02 23:16:41 +02:00
for slug , post in reversed ( config [ " posts " ] . items ( ) ) :
2023-08-29 10:27:14 +03:00
llog (
f """ post( { slug } )
2022-03-11 17:47:54 +02:00
2023-08-29 10:27:14 +03:00
title : { post [ " title " ] ! r }
description : { post [ " description " ] ! r }
2023-08-29 11:01:57 +03:00
content : { trunc ( post [ " content " ] , config [ " post-preview-size " ] ) ! r }
2023-08-29 10:27:14 +03:00
keywords : { " , " . join ( post [ " keywords " ] ) }
created : { format_time ( post [ " created " ] ) } """
+ (
" "
if ( ed := post . get ( " edited " ) ) is None
else f " \n edited : { format_time ( ed ) } "
)
2022-03-11 13:35:37 +02:00
)
2023-08-29 10:27:14 +03:00
return OK
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
@cmds.new
2024-03-19 01:57:53 +02:00
def ed ( config : dict [ str , typing . Any ] , major : bool = True ) - > int :
2023-08-29 10:27:14 +03:00
""" edit posts """
2022-03-11 13:35:37 +02:00
2023-10-06 19:01:48 +03:00
fields : list [ str ] = select_multi ( tuple ( ecmds . commands . keys ( ) ) )
2022-03-11 17:47:54 +02:00
2023-08-29 10:27:14 +03:00
for slug in select_posts ( config [ " posts " ] ) :
llog ( f " editing { slug !r} " )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
for field in fields :
log ( f " editing field { field !r} " )
2022-03-11 13:35:37 +02:00
2023-10-06 19:01:48 +03:00
post : dict [ str , typing . Any ] = config [ " posts " ] [ slug ]
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
post [ " slug " ] = slug
post [ " editor " ] = config [ " editor " ]
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
code : int = ecmds [ field ] ( post )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
del post [ " slug " ]
del post [ " editor " ]
2022-11-05 20:04:39 +02:00
2023-08-29 10:27:14 +03:00
if code is not OK :
return code
2022-03-11 13:35:37 +02:00
2024-03-19 01:57:53 +02:00
if major :
2024-05-15 23:02:15 +03:00
post [ " edited " ] = datetime . datetime . utcnow ( ) . timestamp ( )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
return OK
2022-03-11 13:35:37 +02:00
2022-03-14 23:24:28 +02:00
2024-03-19 01:57:53 +02:00
@cmds.new
def med ( config : dict [ str , typing . Any ] ) - > int :
""" minor edit posts """
return ed ( config = config , major = False ) # type: ignore
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def rm ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" remove posts """
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
for slug in select_posts ( config [ " posts " ] ) :
imp ( f " deleting { slug !r} " )
del config [ " posts " ] [ slug ]
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
return OK
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def build ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" build blog posts """
2022-03-11 13:35:37 +02:00
2023-10-30 13:06:52 +02:00
if not config [ " posts " ] :
return err ( " no posts to be built " )
2023-10-08 07:37:55 +03:00
log ( " compiling regex " )
web_mini . html . html_fns . compileall ( )
2023-08-29 10:27:14 +03:00
log ( " setting up posts directory " )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
if os . path . exists ( config [ " posts-dir " ] ) :
shutil . rmtree ( config [ " posts-dir " ] )
2022-03-11 13:35:37 +02:00
2023-10-29 17:41:58 +02:00
if os . path . exists ( " stats " ) :
shutil . rmtree ( " stats " )
2023-08-29 10:27:14 +03:00
os . makedirs ( config [ " posts-dir " ] , exist_ok = True )
2023-10-29 17:41:58 +02:00
os . makedirs ( " stats " , exist_ok = True )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
llog ( " building blog " )
2022-03-11 13:35:37 +02:00
2023-10-06 20:28:31 +03:00
t : Thread
2023-08-29 10:27:14 +03:00
blog_title : str = html_escape ( config [ " title " ] )
author : str = html_escape ( config [ " author " ] )
2023-10-06 20:28:31 +03:00
styles : str = f " { config [ ' assets-dir ' ] } /styles.min.css "
2023-10-06 17:53:29 +03:00
lang : str = config [ " locale " ] [ : 2 ]
2022-03-11 13:35:37 +02:00
2023-10-06 18:36:58 +03:00
crit_css : str = " "
post_crit_css : str = " "
2023-10-06 20:28:31 +03:00
if os . path . isfile ( critp := f " { config [ ' assets-dir ' ] } /critical.css " ) :
2023-10-08 01:44:35 +03:00
with open ( critp , " r " ) as fp :
2023-10-08 07:37:55 +03:00
crit_css = web_mini . css . minify_css ( fp . read ( ) )
2023-10-06 18:36:58 +03:00
2023-10-06 20:28:31 +03:00
if os . path . isfile ( critp := f " { config [ ' assets-dir ' ] } /post_critical.css " ) :
2023-10-08 01:44:35 +03:00
with open ( critp , " r " ) as fp :
2023-10-08 07:37:55 +03:00
post_crit_css = web_mini . css . minify_css ( fp . read ( ) )
2023-10-06 18:36:58 +03:00
2023-10-29 17:41:58 +02:00
rt : typing . List [ int ] = [ ]
cc : typing . List [ int ] = [ ]
ws : Counter [ str ] = Counter ( )
tgs : Counter [ str ] = Counter ( )
py : Counter [ int ] = Counter ( )
pm : Counter [ int ] = Counter ( )
pd : Counter [ int ] = Counter ( )
ph : Counter [ int ] = Counter ( )
2024-07-11 04:41:40 +03:00
w_regex : re . Pattern [ str ] = re . compile ( r " \ b[a-zA-Z ' ]+ \ b " )
url_regex : re . Pattern [ str ] = re . compile ( r " https?:// \ S+|www \ . \ S+ " )
2023-10-06 19:01:48 +03:00
def build_post ( slug : str , post : dict [ str , typing . Any ] ) - > None :
2023-08-30 19:03:58 +03:00
ct : float = ctimer ( )
2022-03-11 13:35:37 +02:00
2023-10-06 20:28:31 +03:00
post_dir : str = f " { config [ ' posts-dir ' ] } / { slug } "
2023-08-29 10:27:14 +03:00
os . makedirs ( post_dir )
2022-03-11 13:35:37 +02:00
2023-10-29 17:41:58 +02:00
rtm : MarkdownResult = read_time_of_markdown ( post [ " content " ] , config [ " read-wpm " ] )
2024-07-11 04:41:40 +03:00
cont : str = url_regex . sub ( " " , post [ " content " ] ) + " " + post [ " title " ]
2023-10-29 17:41:58 +02:00
rt . append ( rtm . seconds )
cc . append ( len ( cont ) )
2024-07-11 04:41:40 +03:00
ws . update ( Counter ( w_regex . findall ( cont . lower ( ) . strip ( ) ) ) )
2024-02-10 16:50:07 +02:00
tgs . update ( Counter ( list ( map ( str . lower , post [ " keywords " ] ) ) ) )
2023-10-29 17:41:58 +02:00
dt , s = rf_format_time ( post [ " created " ] )
py [ dt . year ] + = 1
pm [ dt . month ] + = 1
pd [ dt . day ] + = 1
ph [ dt . hour ] + = 1
2023-10-06 20:28:31 +03:00
with open ( f " { post_dir } /index.html " , " w " ) as html :
2023-08-29 10:27:14 +03:00
html . write (
2023-10-08 07:37:55 +03:00
web_mini . html . minify_html (
2023-08-29 10:27:14 +03:00
POST_TEMPLATE . format (
2023-10-06 17:53:29 +03:00
lang = lang ,
2023-08-29 10:27:14 +03:00
keywords = html_escape (
" , " . join (
set ( post [ " keywords " ] + config [ " default-keywords " ] )
)
) ,
theme_type = config [ " theme " ] [ " type " ] ,
theme_primary = config [ " theme " ] [ " primary " ] ,
2023-10-06 17:53:29 +03:00
theme_secondary = config [ " theme " ] [ " secondary " ] ,
2023-08-29 10:27:14 +03:00
styles = styles ,
2023-10-06 18:36:58 +03:00
critical_css = crit_css ,
post_critical_css = post_crit_css ,
2023-10-26 18:03:32 +03:00
gen = GEN ,
2023-08-29 10:27:14 +03:00
rss = config [ " rss-file " ] ,
blog_title = blog_title ,
post_title = html_escape ( post [ " title " ] ) ,
author = author ,
locale = config [ " locale " ] ,
2023-10-29 17:41:58 +02:00
post_creation_time = s ,
2023-08-29 10:27:14 +03:00
post_description = html_escape ( post [ " description " ] ) ,
2023-10-29 17:41:58 +02:00
post_read_time = rtm . text ,
2023-08-29 10:27:14 +03:00
post_edit_time = (
" "
if " edited " not in post
else f ' , edited on <time> { rformat_time ( post [ " edited " ] ) } </time> GMT '
) ,
visitor_count = config [ " visitor-count " ] ,
comment = config [ " comment " ] ,
website = config [ " website " ] ,
source = config [ " source " ] ,
post_content = markdown (
post [ " content " ] , config [ " markdown-plugins " ]
) ,
blog = config [ " blog " ] ,
2023-12-27 06:59:52 +02:00
path = f " { config [ ' posts-dir ' ] } / { slug } / " ,
2023-08-29 10:27:14 +03:00
license = config [ " license " ] ,
2023-10-26 18:03:32 +03:00
email = config [ " email " ] ,
2024-12-14 23:14:29 +02:00
legal = config [ " legal " ] ,
2023-10-06 20:28:31 +03:00
) ,
2023-08-29 10:27:14 +03:00
)
)
2022-03-11 13:35:37 +02:00
2023-08-30 19:03:58 +03:00
lnew ( f " built post { post [ ' title ' ] !r} in { ctimer ( ) - ct } s " )
2022-03-11 13:35:37 +02:00
2023-10-06 19:01:48 +03:00
ts : list [ Thread ] = [ ]
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
for slug , post in tuple ( config [ " posts " ] . items ( ) ) :
2023-10-06 20:28:31 +03:00
ts . append ( t := Thread ( target = build_post , args = ( slug , post ) , daemon = True ) )
t . start ( )
2022-03-11 13:35:37 +02:00
2023-10-06 20:28:31 +03:00
latest_post : tuple [ str , dict [ str , typing . Any ] ] = tuple ( config [ " posts " ] . items ( ) ) [ 0 ]
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
with open ( " index.html " , " w " ) as index :
index . write (
2023-10-08 07:37:55 +03:00
web_mini . html . minify_html (
2023-08-29 10:27:14 +03:00
INDEX_TEMPLATE . format ( # type: ignore
2023-10-06 17:53:29 +03:00
lang = lang ,
2023-10-29 17:41:58 +02:00
keywords = ( bkw := html_escape ( " , " . join ( config [ " blog-keywords " ] ) ) ) ,
2023-08-29 10:27:14 +03:00
theme_type = config [ " theme " ] [ " type " ] ,
theme_primary = config [ " theme " ] [ " primary " ] ,
2023-10-06 17:53:29 +03:00
theme_secondary = config [ " theme " ] [ " secondary " ] ,
2023-08-29 10:27:14 +03:00
blog = config [ " blog " ] ,
path = " " ,
styles = styles ,
2023-10-06 18:36:58 +03:00
critical_css = crit_css ,
2023-10-26 18:03:32 +03:00
gen = GEN ,
2023-08-29 10:27:14 +03:00
rss = config [ " rss-file " ] ,
blog_title = blog_title ,
author = author ,
locale = config [ " locale " ] ,
license = config [ " license " ] ,
2023-10-29 17:41:58 +02:00
blog_description = ( bd := html_escape ( config [ " description " ] ) ) ,
blog_header = ( bh := html_escape ( config [ " header " ] ) ) ,
2023-10-06 20:28:31 +03:00
latest_post_path = f " { config [ ' posts-dir ' ] } / { latest_post [ 0 ] } " ,
2023-08-29 10:27:14 +03:00
latest_post_title_trunc = html_escape (
trunc ( latest_post [ 1 ] [ " title " ] , config [ " recent-title-trunc " ] )
) ,
latest_post_creation_time = rformat_time ( latest_post [ 1 ] [ " created " ] ) ,
visitor_count = config [ " visitor-count " ] ,
comment = config [ " comment " ] ,
website = config [ " website " ] ,
source = config [ " source " ] ,
blog_list = " " . join (
2023-10-06 20:28:31 +03:00
f ' <li><a href= " / { config [ " posts-dir " ] } / { slug } " > { html_escape ( post [ " title " ] ) } </a></li> '
2023-08-29 10:27:14 +03:00
for slug , post in config [ " posts " ] . items ( )
) ,
2023-10-26 18:03:32 +03:00
email = config [ " email " ] ,
2023-10-06 20:28:31 +03:00
) ,
2023-08-29 10:27:14 +03:00
)
)
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
lnew ( f " generated { index . name !r} " )
2022-03-11 17:47:54 +02:00
2023-08-29 10:27:14 +03:00
for t in ts :
t . join ( )
2022-03-11 13:35:37 +02:00
2023-10-29 17:41:58 +02:00
char_count : int = sum ( cc )
post_count : int = len ( config [ " posts " ] )
epost_count : int = sum ( " edited " in p for p in config [ " posts " ] . values ( ) )
rts : int = sum ( rt )
wcs : int = sum ( ws . values ( ) )
wcl : int = len ( ws )
tcs : int = sum ( tgs . values ( ) )
tcl : int = len ( tgs )
avg_chars : float = char_count / post_count
avg_words : float = wcs / post_count
avg_tags : float = tcs / post_count
with open ( " stats/index.html " , " w " ) as stats :
stats . write (
web_mini . html . minify_html (
STATS_TEMPLATE . format (
lang = lang ,
keywords = bkw + " , stats, statistics " ,
theme_type = config [ " theme " ] [ " type " ] ,
theme_primary = config [ " theme " ] [ " primary " ] ,
theme_secondary = config [ " theme " ] [ " secondary " ] ,
blog = config [ " blog " ] ,
2023-12-27 06:59:52 +02:00
path = " stats/ " ,
2023-10-29 17:41:58 +02:00
styles = styles ,
critical_css = crit_css ,
gen = GEN ,
locale = config [ " locale " ] ,
blog_title = blog_title ,
blog_description = bd ,
blog_header = bh ,
visitor_count = config [ " visitor-count " ] ,
comment = config [ " comment " ] ,
website = config [ " website " ] ,
source = config [ " source " ] ,
rss = config [ " rss-file " ] ,
post_count = post_count ,
edited_post_count = epost_count ,
edited_post_count_p = epost_count / post_count * 100 ,
read_time = s_to_str ( rts ) ,
avg_read_time = s_to_str ( rts / post_count ) ,
char_count = char_count ,
avg_chars = avg_chars ,
word_count = wcs ,
avg_words = avg_words ,
avg_word_len = avg_chars / avg_words ,
top_words = config [ " top-words " ] ,
word_most_used = " " . join (
f " <li><code> { html_escape ( w ) } </code>, <code> { u } </code> use { ' ' if u == 1 else ' s ' } , <code> { u / wcl * 100 : .2f } %</code></li> "
for w , u in ws . most_common ( config [ " top-words " ] )
) ,
tag_count = tcs ,
avg_tags = avg_tags ,
top_tags = config [ " top-tags " ] ,
tags_most_used = " " . join (
f " <li><code> { html_escape ( w ) } </code>, <code> { u } </code> use { ' ' if u == 1 else ' s ' } , <code> { u / tcl * 100 : .2f } %</code></li> "
for w , u in tgs . most_common ( config [ " top-tags " ] )
) ,
2023-10-29 17:54:19 +02:00
default_tags = " " . join (
f " <li><code> { html_escape ( t ) } </code></li> "
for t in config [ " default-keywords " ]
) ,
2023-10-29 17:41:58 +02:00
* * sorted_post_counter ( py , post_count , " yr " ) ,
* * sorted_post_counter ( pm , post_count , " month " ) ,
* * sorted_post_counter ( pd , post_count , " day " ) ,
* * sorted_post_counter ( ph , post_count , " hr " ) ,
author = config [ " author " ] ,
email = config [ " email " ] ,
license = config [ " license " ] ,
)
)
)
lnew ( f " generated { stats . name !r} " )
2023-08-29 10:27:14 +03:00
return 0
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def css ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" build and minify css """
2022-03-11 13:35:37 +02:00
2023-10-06 20:28:31 +03:00
t : Thread
2023-10-06 19:01:48 +03:00
ts : list [ Thread ] = [ ]
2022-03-11 13:35:37 +02:00
2023-10-08 07:37:55 +03:00
log ( " compiling regex " )
web_mini . css . css_fns . compileall ( )
2023-10-06 20:28:31 +03:00
def _thread ( c : typing . Callable [ . . . , typing . Any ] , * args : str ) - > None :
2023-08-29 10:27:14 +03:00
def _c ( ) - > None :
2023-08-30 19:03:58 +03:00
ct : float = ctimer ( )
2023-10-06 20:28:31 +03:00
c ( * args )
lnew ( f " processed { args [ 0 ] !r} in { ctimer ( ) - ct } s " )
2022-03-11 13:35:37 +02:00
2023-10-06 20:28:31 +03:00
ts . append ( t := Thread ( target = _c , daemon = True ) )
t . start ( )
2022-03-11 13:35:37 +02:00
2023-10-06 20:28:31 +03:00
if os . path . isfile ( styles := f " { config [ ' assets-dir ' ] } /styles.css " ) :
lnew ( f " minifying { styles !r} " )
2023-10-08 07:37:55 +03:00
_thread ( min_css_file , styles , f " { config [ ' assets-dir ' ] } /styles.min.css " ) # type: ignore
2022-03-11 13:35:37 +02:00
2023-10-06 20:28:31 +03:00
if os . path . isdir ( fonts := f " { config [ ' assets-dir ' ] } /fonts " ) :
2023-08-29 10:27:14 +03:00
log ( f " minifying fonts in { fonts !r} " )
2022-03-11 13:35:37 +02:00
2023-10-06 20:28:31 +03:00
for fcss in iglob ( f " { fonts } /*.css " ) :
2023-08-29 10:27:14 +03:00
if fcss . endswith ( " .min.css " ) :
continue
2022-03-11 13:35:37 +02:00
2023-10-08 07:37:55 +03:00
_thread ( min_css_file , fcss , f " { os . path . splitext ( fcss ) [ 0 ] } .min.css " ) # type: ignore
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
for t in ts :
t . join ( )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
return OK
2022-03-11 17:47:54 +02:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def robots ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" generate a robots.txt """
llog ( " generating robots " )
with open ( " robots.txt " , " w " ) as r :
r . write (
f """ User-agent: *
Disallow : / { config [ " assets-dir " ] } / *
Allow : *
Sitemap : { config [ " blog " ] } / sitemap . xml """
)
2022-03-11 17:47:54 +02:00
2023-08-29 10:27:14 +03:00
lnew ( f " generated { r . name !r} " )
2022-03-11 17:47:54 +02:00
2023-08-29 10:27:14 +03:00
return OK
2022-03-11 17:47:54 +02:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def manifest ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" generate a manifest.json """
2022-03-11 17:47:54 +02:00
2023-08-29 10:27:14 +03:00
llog ( " generating a manifest " )
2022-04-02 20:43:37 +03:00
2023-08-29 10:27:14 +03:00
with open ( " manifest.json " , " w " ) as m :
json . dump (
2022-04-02 20:43:37 +03:00
{
" $schema " : " https://json.schemastore.org/web-manifest-combined.json " ,
2023-08-29 10:27:14 +03:00
" short_name " : config [ " header " ] ,
" name " : config [ " title " ] ,
" description " : config [ " description " ] ,
2022-04-02 20:43:37 +03:00
" start_url " : " . " ,
" display " : " standalone " ,
2023-08-29 10:27:14 +03:00
" theme_color " : config [ " theme " ] [ " primary " ] ,
" background_color " : config [ " theme " ] [ " secondary " ] ,
* * config [ " manifest " ] ,
2022-04-02 20:43:37 +03:00
} ,
2023-08-29 10:27:14 +03:00
m ,
2022-04-02 20:43:37 +03:00
)
2023-08-29 10:27:14 +03:00
lnew ( f " generated { m . name !r} " )
2023-04-04 23:52:46 +03:00
2023-08-29 10:27:14 +03:00
return OK
2023-04-04 23:52:46 +03:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def sitemap ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" generate a sitemap.xml """
2023-04-04 23:52:46 +03:00
2023-08-29 10:27:14 +03:00
llog ( " generating a sitemap " )
2023-04-04 23:52:46 +03:00
2024-05-15 23:02:15 +03:00
now : float = datetime . datetime . utcnow ( ) . timestamp ( )
2023-04-04 22:44:32 +03:00
2023-08-29 10:27:14 +03:00
root : etree . Element = etree . Element ( " urlset " )
root . set ( " xmlns " , " http://www.sitemaps.org/schemas/sitemap/0.9 " )
2022-09-23 01:29:49 +03:00
2023-08-29 10:27:14 +03:00
for slug , post in (
( " " , config [ " website " ] ) ,
( " " , config [ " blog " ] ) ,
( " " , f " { config [ ' blog ' ] } / { config [ ' rss-file ' ] } " ) ,
2023-10-29 17:41:58 +02:00
( " " , f ' { config [ " blog " ] } /stats ' ) ,
2023-08-29 10:27:14 +03:00
) + tuple ( config [ " posts " ] . items ( ) ) :
llog ( f " adding { slug or post !r} to sitemap " )
url : etree . Element = etree . SubElement ( root , " url " )
etree . SubElement ( url , " loc " ) . text = (
2023-10-06 20:28:31 +03:00
f " { config [ ' blog ' ] } / { config [ ' posts-dir ' ] } / { slug } " if slug else post
2023-08-29 10:27:14 +03:00
)
2024-05-15 23:02:15 +03:00
etree . SubElement ( url , " lastmod " ) . text = datetime . datetime . utcfromtimestamp (
post . get ( " edited " , post [ " created " ] ) if slug else now , # type: ignore
2023-08-29 10:27:14 +03:00
) . strftime ( " % Y- % m- %d T % H: % M: % S+00:00 " )
etree . SubElement ( url , " priority " ) . text = " 1.0 "
2022-09-23 01:29:49 +03:00
2023-08-29 10:27:14 +03:00
etree . ElementTree ( root ) . write ( " sitemap.xml " , encoding = " UTF-8 " , xml_declaration = True )
lnew ( " generated ' sitemap.xml ' " )
2022-04-02 20:43:37 +03:00
2023-08-29 10:27:14 +03:00
return OK
2022-04-02 20:43:37 +03:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def rss ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" generate an rss feed """
llog ( " generating an rss feed " )
2023-08-08 17:49:12 +03:00
ftime : str = " %a , %d % b % Y % H: % M: % S GMT "
2024-05-15 23:02:15 +03:00
now : datetime . datetime = datetime . datetime . utcnow ( )
2023-08-08 17:49:12 +03:00
root : etree . Element = etree . Element ( " rss " )
root . set ( " version " , " 2.0 " )
channel : etree . Element = etree . SubElement ( root , " channel " )
2023-08-29 10:27:14 +03:00
etree . SubElement ( channel , " title " ) . text = config [ " title " ]
etree . SubElement ( channel , " link " ) . text = config [ " blog " ]
etree . SubElement ( channel , " description " ) . text = config [ " description " ]
2023-10-26 18:03:32 +03:00
etree . SubElement ( channel , " generator " ) . text = GEN
2023-08-08 17:49:12 +03:00
etree . SubElement ( channel , " language " ) . text = (
config [ " locale " ] . lower ( ) . replace ( " _ " , " - " )
)
etree . SubElement ( channel , " lastBuildDate " ) . text = now . strftime ( ftime )
2023-08-29 10:27:14 +03:00
for slug , post in config [ " posts " ] . items ( ) :
llog ( f " adding { slug !r} to rss " )
2023-08-08 18:20:17 +03:00
2023-10-06 19:01:48 +03:00
created : float | None = post . get ( " edited " )
2023-09-07 16:57:24 +03:00
2023-08-08 17:49:12 +03:00
item : etree . Element = etree . SubElement ( channel , " item " )
2023-08-29 10:27:14 +03:00
2023-08-08 17:49:12 +03:00
etree . SubElement ( item , " title " ) . text = post [ " title " ]
2023-08-08 18:20:17 +03:00
etree . SubElement ( item , " link " ) . text = (
2023-10-06 20:28:31 +03:00
link := f " { config [ ' blog ' ] } / { config [ ' posts-dir ' ] } / { slug } "
2023-08-08 18:20:17 +03:00
)
2023-09-07 16:57:24 +03:00
etree . SubElement ( item , " description " ) . text = post [ " description " ] + (
2024-05-15 23:02:15 +03:00
f " [edited at { datetime . datetime . utcfromtimestamp ( created ) . strftime ( ftime ) } ] "
2023-09-07 16:57:24 +03:00
if created
else " "
)
2024-05-15 23:02:15 +03:00
etree . SubElement ( item , " pubDate " ) . text = datetime . datetime . utcfromtimestamp (
2024-05-15 22:59:11 +03:00
post [ " created " ] ,
2023-08-08 17:49:12 +03:00
) . strftime ( ftime )
2023-08-08 17:58:43 +03:00
etree . SubElement ( item , " guid " ) . text = link
2024-03-01 00:15:51 +02:00
etree . SubElement ( item , " author " ) . text = (
f " { config [ ' email ' ] } ( { config [ ' author ' ] } ) "
)
2023-08-08 17:49:12 +03:00
2023-08-29 10:27:14 +03:00
etree . ElementTree ( root ) . write (
config [ " rss-file " ] , encoding = " UTF-8 " , xml_declaration = True
)
2023-08-08 17:49:12 +03:00
2023-08-29 10:27:14 +03:00
lnew ( f " generated { config [ ' rss-file ' ] !r} " )
2023-08-08 17:49:12 +03:00
2023-08-29 10:27:14 +03:00
return OK
2023-08-08 17:49:12 +03:00
2022-04-02 20:43:37 +03:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def apis ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" generate and hash apis """
2022-04-02 20:43:37 +03:00
2023-08-29 10:27:14 +03:00
with open ( " recents.json " , " w " ) as recents :
2023-08-29 10:47:52 +03:00
json . dump (
dict (
map (
2023-10-17 17:29:30 +03:00
lambda kv : ( # type: ignore
2023-08-29 10:47:52 +03:00
kv [ 0 ] ,
{
" title " : kv [ 1 ] [ " title " ] ,
2023-08-29 11:01:57 +03:00
" content " : trunc (
kv [ 1 ] [ " content " ] , config [ " post-preview-size " ] , " "
) ,
2023-08-29 10:47:52 +03:00
" created " : kv [ 1 ] [ " created " ] ,
} ,
) ,
tuple ( config [ " posts " ] . items ( ) ) [ : config [ " recents " ] ] ,
)
) ,
recents ,
)
2023-08-29 10:27:14 +03:00
lnew ( f " generated { recents . name !r} " )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
for api in recents . name , CONFIG_FILE :
with open ( api , " rb " ) as content :
h : str = hashlib . sha256 ( content . read ( ) ) . hexdigest ( )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
with open ( f " { api . replace ( ' . ' , ' _ ' ) } _hash.txt " , " w " ) as hf :
hf . write ( h )
lnew ( f " generated { hf . name !r} " )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
return OK
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def clean ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" clean up the site """
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
def remove ( file : str ) - > None :
imp ( f " removing { file !r} " )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
try :
os . remove ( file )
except IsADirectoryError :
shutil . rmtree ( file )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
for pattern in (
config [ " posts-dir " ] ,
" index.html " ,
2023-10-06 20:28:31 +03:00
f " { config [ ' assets-dir ' ] } /*.min.* " ,
2023-08-29 10:27:14 +03:00
" blog_json_hash.txt " ,
" manifest.json " ,
2023-10-06 20:28:31 +03:00
f " { config [ ' assets-dir ' ] } /fonts/*.min.* " ,
2023-08-29 10:27:14 +03:00
" recents_json_hash.txt " ,
" recents.json " ,
config [ " rss-file " ] ,
" robots.txt " ,
" sitemap.xml " ,
2023-10-29 17:41:58 +02:00
" stats " ,
2023-08-29 10:27:14 +03:00
) :
if os . path . exists ( pattern ) :
remove ( pattern )
else :
for file in iglob ( pattern , recursive = True ) :
remove ( file )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
return OK
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def static ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" generate a full static site """
2022-04-07 22:32:55 +03:00
2023-08-30 19:03:58 +03:00
ct : float = ctimer ( )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
for stage in clean , build , css , robots , manifest , sitemap , rss , apis :
imp ( f " running stage { stage . __name__ !r} : { stage . __doc__ or stage . __name__ !r} " )
2022-03-11 13:35:37 +02:00
2023-08-30 19:03:58 +03:00
st : float = ctimer ( )
2022-04-02 20:43:37 +03:00
2023-08-29 10:27:14 +03:00
if ( code := stage ( config ) ) is not OK :
return code
2022-11-05 20:04:39 +02:00
2023-08-30 19:03:58 +03:00
imp ( f " stage finished in { ctimer ( ) - st } s " )
2022-03-11 13:35:37 +02:00
2023-08-30 19:03:58 +03:00
return log ( f " site built in { ctimer ( ) - ct } s " )
2023-08-29 10:27:14 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def serve ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:27:14 +03:00
""" simple server """
class RequestHandler ( http . server . SimpleHTTPRequestHandler ) :
def log_message ( self , format : str , * args : typing . Any ) - > None :
llog ( format % args )
def do_GET ( self ) - > None :
file_path : str = self . translate_path ( self . path ) # type: ignore
if os . path . isdir ( file_path ) : # type: ignore
2023-10-06 20:28:31 +03:00
file_path = f " { file_path } /index.html "
2023-08-29 10:27:14 +03:00
try :
with open ( file_path , " rb " ) as fp : # type: ignore
self . send_response ( 200 ) # type: ignore
2023-10-26 18:03:32 +03:00
self . send_header (
" Cache-Control " , " no-store, no-cache, must-revalidate "
)
self . send_header ( " Pragma " , " no-cache " )
2023-08-29 10:27:14 +03:00
self . end_headers ( ) # type: ignore
self . wfile . write ( fp . read ( ) ) # type: ignore
except Exception as e :
self . send_response ( 404 ) # type: ignore
2023-10-26 18:03:32 +03:00
self . send_header ( " Cache-Control " , " no-store, no-cache, must-revalidate " )
self . send_header ( " Pragma " , " no-cache " )
2023-08-29 10:27:14 +03:00
self . end_headers ( ) # type: ignore
self . wfile . write ( f " { e . __class__ . __name__ } : { e } " . encode ( ) ) # type: ignore
httpd : typing . Any = http . server . HTTPServer (
( config [ " server-host " ] , config [ " server-port " ] ) , RequestHandler
)
httpd . RequestHandlerClass . directory = " . "
try :
imp (
f " server running on http:// { httpd . server_address [ 0 ] } : { httpd . server_address [ 1 ] } / ^C to close it "
2022-04-02 20:55:43 +03:00
)
2023-08-29 10:27:14 +03:00
httpd . serve_forever ( )
except KeyboardInterrupt :
httpd . server_close ( )
imp ( " server shut down " )
2022-04-02 20:43:37 +03:00
2023-08-29 10:27:14 +03:00
return OK
2022-04-07 22:32:55 +03:00
2023-08-29 10:34:36 +03:00
@cmds.new
2023-10-06 19:01:48 +03:00
def dev ( config : dict [ str , typing . Any ] ) - > int :
2023-08-29 10:34:36 +03:00
""" generate a full static site + serve it """
if ( code := static ( config ) ) is not OK :
return code
return serve ( config )
2023-10-30 12:29:56 +02:00
@cmds.new
def blog ( config : dict [ str , typing . Any ] ) - > int :
""" generate a new blog """
log ( " changing config " )
config . update ( DEFAULT_CONFIG )
lnew ( " blog set to default values " )
return OK
2024-12-15 02:53:32 +02:00
@cmds.new
def media ( config : dict [ str , typing . Any ] ) - > int :
""" add media """
path : str = iinput ( " media path " )
path = os . path . expanduser ( path )
if not os . path . isfile ( path ) :
return err ( f " file { path !r} is not a file or does not exist " )
purpose : str = iinput ( " media purpose " )
title : str = iinput ( " media title " )
license : str = iinput ( " media license (SPDX) " )
credit : str = iinput ( " media credit " )
# MIME stuff
mime : str = magic . from_file ( path , mime = True )
if mime not in MEDIA_MIME :
return err ( f " mime { mime !r} is not a supported media type " )
ext : str = MEDIA_MIME [ mime ]
# Filename
sha256_hash : typing . Any = hashlib . sha256 ( ) # type: ignore
with open ( path , " rb " ) as fp :
for byte_block in iter ( lambda : fp . read ( 4096 ) , b " " ) :
sha256_hash . update ( byte_block )
hash_hex : str = sha256_hash . hexdigest ( )
# Create media dir + media index
os . makedirs ( " media " , exist_ok = True )
if os . path . exists ( " media/media.json " ) :
with open ( " media/media.json " , " r " ) as fp :
index : dict [ str , dict [ str , typing . Any ] ] = json . load ( fp )
else :
index = { }
# Check if it exists
if hash_hex in index :
return err ( f " media pointing to { path !r} already exists " )
# Process stuff
filename : str = f " { hash_hex } . { ext } "
fpath : str = f " media/ { filename } "
if mime . startswith ( " image/ " ) :
with Image . open ( path ) as img :
width , height = img . size
shutil . copy ( path , fpath )
if ext in { " jpeg " , " png " } :
2024-12-15 03:02:24 +02:00
quality_s : str = iinput ( " image quality % (1-100) " , " 100 " )
2024-12-15 02:53:32 +02:00
try :
quality : int = int ( quality_s )
quality = 100 if quality > 100 else quality
except Exception :
quality = 100
img . save (
fpath ,
format = ext ,
quality = quality ,
optimize = True ,
)
index [ hash_hex ] = {
" type " : " image " ,
" width " : width ,
" height " : height ,
" alt " : iinput ( " alt text " ) ,
}
elif mime . startswith ( " audio/ " ) :
shutil . copy ( path , fpath )
index [ hash_hex ] = {
" type " : " audio " ,
" alt " : iinput ( " alt text " ) ,
}
else :
return err ( f " unsupported MIME: { mime !r} " )
index [ hash_hex ] . update (
{
" purpose " : purpose ,
" title " : title ,
" license " : license ,
" credit " : credit ,
" ext " : ext ,
" mime " : mime ,
2024-12-15 03:02:24 +02:00
" created " : datetime . datetime . utcnow ( ) . timestamp ( ) ,
2024-12-15 02:53:32 +02:00
}
)
lnew ( f " media { hash_hex } created " )
# Update media.json
with open ( " media/media.json " , " w " ) as fp :
json . dump ( index , fp , indent = config [ " indent " ] )
with open ( " media/media_json_hash.txt " , " w " ) as fp :
with open ( " media/media.json " , " rb " ) as fk :
fp . write ( hashlib . sha256 ( fk . read ( ) ) . hexdigest ( ) )
lnew ( " Updated media.json and media_json_hash.txt " )
return OK
2023-08-29 10:27:14 +03:00
def main ( ) - > int :
2023-10-17 17:29:30 +03:00
""" entry / main function """
2023-08-29 10:27:14 +03:00
2023-08-30 19:03:58 +03:00
main_t : float = ctimer ( )
2023-08-29 10:27:14 +03:00
log ( " hello world " )
if len ( sys . argv ) < 2 :
return err ( " no arguments provided, see `help` " )
cfg : dict [ str , typing . Any ] = DEFAULT_CONFIG . copy ( )
if os . path . exists ( CONFIG_FILE ) :
with open ( CONFIG_FILE , " r " ) as config :
log ( f " using { config . name !r} config " )
cfg . update ( json . load ( config ) )
else :
lnew ( " using the default config " )
2024-12-15 02:53:32 +02:00
if os . path . exists ( " media/media.json " ) :
with open ( " media/media.json " , " r " ) as fp :
MEDIA_INDEX . update ( json . load ( fp ) )
log ( " Loaded the media index (media/media.json) " )
2023-08-29 10:27:14 +03:00
sort ( cfg )
log ( f " looking command { sys . argv [ 1 ] !r} up " )
try :
cmd : typing . Callable [ [ dict [ str , typing . Any ] ] , int ] = cmds [ sys . argv [ 1 ] ]
except KeyError :
return err ( f " command { sys . argv [ 1 ] !r} does not exist " )
2023-12-27 05:47:10 +02:00
for file in " .editorconfig " , " .clang-format " :
if os . path . isfile ( file ) :
log ( f " copying { file !r} to /tmp " )
shutil . copy ( file , f " /tmp/ { file } " )
2023-08-29 10:27:14 +03:00
log ( " calling and timing the command " )
if NCI :
print ( )
2023-08-30 19:03:58 +03:00
timer : float = ctimer ( )
2022-03-11 13:35:37 +02:00
2023-08-29 10:27:14 +03:00
code : int = cmd ( cfg )
2022-04-07 22:32:55 +03:00
2023-08-29 10:27:14 +03:00
if NCI :
print ( )
2023-08-30 19:03:58 +03:00
log ( f " command finished in { ctimer ( ) - timer } s " ) # type: ignore
sort ( cfg )
2022-04-07 22:32:55 +03:00
2023-08-29 10:27:14 +03:00
with open ( CONFIG_FILE , " w " ) as config :
log ( f " dumping config to { config . name !r} " )
json . dump ( cfg , config , indent = cfg [ " indent " ] if NCI else None )
2022-04-07 22:32:55 +03:00
2023-08-30 19:03:58 +03:00
log ( f " goodbye world, return { code } , total { ctimer ( ) - main_t } s " )
2022-03-11 13:35:37 +02:00
2022-11-05 20:04:39 +02:00
return code
2022-03-11 13:35:37 +02:00
if __name__ == " __main__ " :
2023-08-29 10:34:36 +03:00
assert (
main . __annotations__ . get ( " return " ) == " int "
) , " main() should return an integer "
2022-03-11 13:35:37 +02:00
2024-05-15 23:02:15 +03:00
# filter_warnings("error", category=Warning)
2023-01-05 18:59:20 +02:00
raise SystemExit ( main ( ) )