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
2024-12-18 02:43:42 +02:00
from mistune . util import safe_entity
2024-12-15 15:17:17 +02:00
from pygments import highlight
from pygments . formatters import HtmlFormatter , html
2024-12-26 05:02:33 +02:00
from pygments . lexers import TextLexer , get_lexer_by_name , guess_lexer
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 " ,
} ,
2024-12-15 18:05:39 +02:00
" favicon " : " https://ari.lt/favicon.ico " ,
2023-08-29 10:27:14 +03:00
" manifest " : {
" icons " : [
{
2024-12-15 18:05:39 +02:00
" src " : " https://ari.lt/favicon.ico " ,
2023-08-29 10:27:14 +03:00
" sizes " : " 128x128 " ,
2024-12-15 17:56:38 +02:00
" type " : " image/vnd.microsoft.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 ,
2024-12-15 15:40:37 +02:00
" license " : " AGPL-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-15 15:17:17 +02:00
" code-style " : " coffee " ,
2024-12-18 02:14:35 +02:00
" note " : ' This page uses the <a href= " https://github.com/ryanoasis/nerd-fonts " >Nerd Hack Font</a>, which is licensed under the OFL 1.1 License. All internal content, unless specified otherwise, is subject to their respective license terms as well as <a href= " https://ari.lt/legal " >Ari-web legal policies</a>. Treat the content and the source of the page as source code according to the license. ' ,
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 " >
2024-12-15 18:05:39 +02:00
< link rel = " icon " href = " {favicon} " sizes = " 128x128 " type = " image/vnd.microsoft.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} " / >
2024-12-15 17:56:38 +02:00
< meta property = " og:site_name " content = " {blog_title} " / >
2023-08-29 10:27:14 +03:00
< link rel = " manifest " href = " /manifest.json " / >
< link rel = " canonical " href = " {blog} / {path} " >
2024-12-15 14:11:46 +02:00
< link rel = " og:url " 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} " / >
2024-12-15 14:11:46 +02:00
< meta name = " license " content = " {license} " / >
< link rel = " sitemap " href = " /sitemap.xml " type = " application/xml " / > """
2023-08-29 10:27:14 +03:00
POST_TEMPLATE : typing . Final [ str ] = (
HTML_BEGIN
+ """
2024-12-15 15:17:17 +02:00
< style type = " text/css " > { post_critical_css } { code_css } < / style >
2023-08-29 10:27:14 +03:00
< title > { blog_title } - > { post_title } < / title >
2024-12-15 17:59:10 +02:00
< meta name = " title " content = " {blog_title} -> {post_title} " / >
2024-12-15 14:11:46 +02:00
< meta property = " og:title " content = " {blog_title} -> {post_title} " / >
2024-12-15 17:56:38 +02:00
< meta property = " twitter:title " content = " {blog_title} -> {post_title} " / >
2023-08-29 10:27:14 +03:00
< meta name = " description " content = " {post_title} by {author} at {post_creation_time} GMT -- {post_description} " / >
2024-12-15 14:11:46 +02:00
< meta property = " og:description " content = " {post_title} by {author} at {post_creation_time} GMT -- {post_description} " / >
2024-12-15 17:48:37 +02:00
< meta property = " twitter:description " content = " {post_title} by {author} at {post_creation_time} GMT -- {post_description} " / >
2023-08-29 10:27:14 +03:00
< meta property = " article:read_time " content = " {post_read_time} " / >
2024-12-15 13:59:44 +02:00
< meta property = " og:type " content = " article " / > { image_meta }
2023-08-29 10:27:14 +03:00
< / 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-15 15:39:11 +02:00
< main > < article id = " main " > { post_content } < / article > < / main >
< footer > < p > { note } < / p > < 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 >
2024-12-15 14:11:46 +02:00
< meta property = " og:title " content = " {blog_title} " / >
2023-08-29 10:27:14 +03:00
< meta name = " description " content = " {blog_description} " / >
2024-12-15 14:11:46 +02:00
< meta property = " og:description " content = " {blog_description} " / >
2024-12-26 04:32:01 +02:00
< meta property = " twitter:description " content = " {blog_description} " / >
2023-08-29 10:27:14 +03:00
< 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
2024-12-26 04:29:22 +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 >
2023-10-31 21:56:36 +02:00
< 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 >
2024-12-15 15:39:11 +02:00
< footer > < p > { note } < / p > < 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 >
2024-12-15 14:11:46 +02:00
< meta property = " og:title " content = " {blog_title} -> stats " / >
2024-12-26 04:29:22 +02:00
< meta name = " description " content = " Statistics of {blog_title} , {blog_description} " / >
< meta property = " og:description " content = " Statistics of {blog_title} , {blog_description} " / >
2023-10-29 17:41:58 +02:00
< meta property = " og:type " content = " website " / >
< / head >
< body >
2023-10-31 21:56:36 +02:00
< header role = " group " >
2024-12-26 04:29:22 +02:00
< h1 role = " heading " aria - level = " 1 " > Statistics 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 >
2024-12-26 04:29:22 +02:00
< 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 >
2023-10-29 17:41:58 +02:00
< li >
2024-12-26 04:29:22 +02:00
Total read time : < time > { read_time } < / time >
2023-10-29 17:41:58 +02:00
< ul >
2024-12-26 04:29:22 +02:00
< li > Average read time : < time > { avg_read_time } < / time > < / li >
2023-10-29 17:41:58 +02:00
< / ul >
< / li >
< li >
2024-12-26 04:29:22 +02:00
Content
2023-10-29 17:41:58 +02:00
< ul >
2024-12-26 04:29:22 +02:00
< li > Characters : < code > { char_count } < / code > < / li >
2023-10-29 17:41:58 +02:00
< ul >
2024-12-26 04:29:22 +02:00
< li > Average count of characters : < code > { avg_chars : .2 f } < / code > < / li >
2023-10-29 17:41:58 +02:00
< / ul >
< / ul >
< ul >
2024-12-26 04:29:22 +02:00
< li > Words : < code > { word_count } < / code > < / li >
2023-10-29 17:41:58 +02:00
< ul >
2024-12-26 04:29:22 +02:00
< li > Average count of words : < code > { avg_words : .2 f } < / code > < / li >
< li > Average word length : < code > { avg_word_len : .2 f } < / code > < / li >
2023-10-29 17:41:58 +02:00
< li >
2024-12-26 04:29:22 +02:00
Top { top_words } used words :
2023-10-29 17:41:58 +02:00
< ol > { word_most_used } < / ol >
< / li >
< / ul >
< / ul >
< ul >
2024-12-26 04:29:22 +02:00
< li > Tags : < code > { tag_count } < / code > < / li >
2023-10-29 17:41:58 +02:00
< ul >
2024-12-26 04:29:22 +02:00
< li > Average count of tags : < code > { avg_tags : .2 f } < / code > < / li >
2023-10-29 17:41:58 +02:00
< li >
2024-12-26 04:29:22 +02:00
Top { top_tags } used tags :
2023-10-29 17:41:58 +02:00
< ol > { tags_most_used } < / ol >
< / li >
2024-12-26 04:29:22 +02:00
< li > Default tags : < ol > { default_tags } < / ol > < / li >
2023-10-29 17:41:58 +02:00
< / ul >
< / ul >
< / li >
< li >
2024-12-26 04:29:22 +02:00
Time ( GMT )
2023-10-29 17:41:58 +02:00
< ul >
2024-12-26 04:29:22 +02:00
< 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 >
2023-10-29 17:41:58 +02:00
< / ul >
< / li >
< / ul >
< / article >
2024-12-15 15:39:11 +02:00
< / main > < footer > < p > { note } < / p > < 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 17:45:06 +02:00
import requests
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 16:44:31 +02:00
requests : 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
2024-12-15 18:59:14 +02:00
def update_media ( config : dict [ str , typing . Any ] ) - > int :
""" update media.json """
with open ( " media/media.json " , " w " ) as fp :
log ( " dumping media.json " )
2024-12-15 19:02:16 +02:00
json . dump ( MEDIA_INDEX , fp , indent = config [ " indent " ] if NCI else None )
2024-12-15 18:59:14 +02:00
return OK
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
2024-12-15 13:59:44 +02:00
def select_medias ( type : typing . Optional [ str ] = None ) - > tuple [ str , . . . ] :
2024-12-18 02:14:35 +02:00
if os . path . exists ( " media/media.json " ) :
with open ( " media/media.json " , " r " ) as fp :
MEDIA_INDEX . clear ( )
MEDIA_INDEX . update ( json . load ( fp ) )
2024-12-15 13:59:44 +02:00
if type :
return tuple (
map (
lambda opt : opt . split ( " | " , maxsplit = 1 ) [ 0 ] . strip ( ) ,
select_multi (
tuple (
2024-12-15 16:44:31 +02:00
f " { mdx } | { mdy [ ' type ' ] } , { mdy [ ' alt ' ] } for { mdy [ ' purpose ' ] } | { mdy [ ' title ' ] } , { mdy [ ' credit ' ] } ( { mdy [ ' license ' ] } ) "
2024-12-15 13:59:44 +02:00
for mdx , mdy in MEDIA_INDEX . items ( )
if mdy [ " type " ] == type
)
) ,
)
)
else :
return tuple (
map (
lambda opt : opt . split ( " | " , maxsplit = 1 ) [ 0 ] . strip ( ) ,
select_multi (
tuple (
f " { mdx } | { mdy [ ' type ' ] } , { mdy [ ' alt ' ] } for { mdy [ ' purpose ' ] } | { mdy [ ' title ' ] } , { mdy [ ' credit ' ] } "
for mdx , mdy in MEDIA_INDEX . items ( )
)
) ,
)
2024-12-15 03:35:24 +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 ]
2024-12-26 05:02:33 +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 " ,
2024-12-15 15:17:17 +02:00
" raw " : f " ERROR: Media ' { mistune . escape ( text ) } ' does not exist. " ,
2024-12-15 02:53:32 +02:00
}
] ,
" 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 " ,
2024-12-15 15:17:17 +02:00
" raw " : f " ERROR: Media ' { mistune . escape ( text ) } ' has an unsupported type. " ,
2024-12-15 02:53:32 +02:00
}
] ,
" attrs " : { " classes " : " media " } ,
}
)
return m . end ( )
state . append_token (
{
" type " : " block_div " ,
" children " : [
child ,
{ " type " : " linebreak " } ,
{
" type " : " emphasis " ,
2024-12-18 18:25:08 +02:00
" raw " : f " { mdx [ ' type ' ] . capitalize ( ) } ; { mdx [ ' alt ' ] } | \" { mdx [ ' title ' ] } \" by { mdx [ ' credit ' ] } ( { mdx [ ' license ' ] } ). Purpose: { mistune . escape ( mdx [ ' purpose ' ] ) } . Uploaded on { datetime . datetime . utcfromtimestamp ( mdx [ ' uploaded ' ] ) . strftime ( ' %a , %d % b % Y % H: % M: % S GMT ' ) } . " ,
2024-12-15 03:02:24 +02:00
} ,
2024-12-18 02:14:35 +02:00
{ " type " : " text " , " raw " : " " } ,
2024-12-15 03:02:24 +02:00
{
" type " : " link " ,
" children " : [ { " type " : " text " , " raw " : f " (raw media here) " } ] ,
2024-12-18 02:43:42 +02:00
" attrs " : { " url " : source , " blank " : True } ,
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-18 02:43:42 +02:00
def link (
self ,
text : str ,
url : str ,
title : typing . Optional [ str ] = None ,
blank : bool = False ,
) - > str :
s : str = ' <a href= " ' + self . safe_url ( url ) + ' " '
if title :
s + = ' title= " ' + safe_entity ( title ) + ' " '
if blank :
s + = ' target= " _blank " '
return s + " > " + text + " </a> "
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 ) :
2024-12-15 15:17:17 +02:00
return f ' <div class= " { mistune . escape ( classes ) } " > { text } </div> '
2024-12-15 02:53:32 +02:00
def image ( self , text : str , alt : str , type : str , width : int , height : int ) - > str :
2024-12-25 03:20:19 +02:00
return f ' <img title= " { mistune . escape ( alt ) } " src= " { text } " alt= " { mistune . escape ( alt ) } " type= " { type } " width= " { width } " height= " { height } " loading=lazy /> '
2024-12-15 02:53:32 +02:00
def audio ( self , text : str , alt : str , type : str ) - > str :
2024-12-15 15:17:17 +02:00
return f ' <audio controls title= " { mistune . escape ( alt ) } " > <source src= " { text } " type= " { type } " { alt } /> </audio> '
def block_code ( self , code : str , info : typing . Optional [ str ] = None ) - > str :
2024-12-19 02:30:32 +02:00
try :
lexer = get_lexer_by_name ( info ) if info else guess_lexer ( code )
except Exception :
lexer = TextLexer ( )
2024-12-15 15:17:17 +02:00
return highlight (
code ,
2024-12-19 02:30:32 +02:00
lexer ,
2024-12-15 15:17:17 +02:00
HtmlFormatter ( linenos = " table " , linenostart = 1 ) ,
)
2024-12-15 02:53:32 +02:00
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
2024-12-15 14:11:46 +02:00
2024-12-15 13:59:44 +02:00
@ecmds.new
def preview ( post : dict [ str , typing . Any ] ) - > int :
""" edit preview """
if yn ( " image preview " ) :
pv : tuple [ str , . . . ] = select_medias ( " image " )
if pv :
post [ " preview " ] = pv [ 0 ]
else :
if " preview " in post :
del post [ " preview " ]
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
2024-12-15 03:42:27 +02:00
print (
" \n " . join (
2023-08-29 10:27:14 +03:00
f " { cmd } -- { fn . __doc__ or ' no help provided ' } "
for cmd , fn in cmds . commands . items ( )
)
)
2022-03-11 13:35:37 +02:00
2024-12-15 03:42:27 +02:00
print (
"""
Custom markdown extensions ( besides the well - known ones ) :
* Titlelink : < #:Your Header Title> - links to a header
* Media embed : < @ : e3b0c4429 . . . > - embeds media """
)
return OK
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
posts [ slug ] = {
" title " : title ,
" description " : description . strip ( ) ,
" content " : content ,
" keywords " : keywords ,
2024-12-15 13:59:44 +02:00
" created " : datetime . datetime . utcnow ( ) . timestamp ( ) ,
2023-08-29 10:27:14 +03:00
}
2022-03-11 13:35:37 +02:00
2024-12-15 13:59:44 +02:00
if yn ( " image preview " ) :
pv : tuple [ str , . . . ] = select_medias ( " image " )
if pv :
posts [ slug ] [ " preview " ] = pv [ 0 ]
lnew ( f " saved blog post { slug !r} " )
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+ " )
2024-12-15 15:17:17 +02:00
code_css : str = web_mini . css . minify_css ( HtmlFormatter ( style = config [ " code-style " ] ) . get_style_defs ( ) ) # type: ignore
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
2024-12-15 13:59:44 +02:00
image_meta : str = " "
if " preview " in post and post [ " preview " ] in MEDIA_INDEX :
2024-12-15 18:05:39 +02:00
src : str = (
f " { config [ ' blog ' ] } /media/ { post [ ' preview ' ] } . { MEDIA_INDEX [ post [ ' preview ' ] ] [ ' ext ' ] } "
)
2024-12-15 17:56:38 +02:00
2024-12-15 13:59:44 +02:00
image_meta = f """ <meta property= " og:image " content= " { src } " />
2024-12-15 14:11:46 +02:00
< meta name = " twitter:card " content = " summary_large_image " / >
2024-12-15 13:59:44 +02:00
< meta name = " twitter:image " content = " {src} " / > """
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 ,
2024-12-15 18:05:39 +02:00
favicon = config [ " favicon " ] ,
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-15 13:59:44 +02:00
image_meta = image_meta ,
2024-12-15 15:17:17 +02:00
code_css = code_css ,
2024-12-15 15:39:11 +02:00
note = config [ " note " ] ,
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 ,
2024-12-15 18:05:39 +02:00
favicon = config [ " favicon " ] ,
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 " ] ,
2024-12-15 15:39:11 +02:00
note = config [ " note " ] ,
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 ,
2024-12-15 18:05:39 +02:00
favicon = config [ " favicon " ] ,
2023-10-29 17:41:58 +02:00
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 " ] ,
2024-12-15 15:39:11 +02:00
note = config [ " note " ] ,
2023-10-29 17:41:58 +02:00
)
)
)
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 " ] } / *
2025-01-02 10:06:29 +02:00
Sitemap : { config [ " blog " ] } / sitemap . xml
# We are not slaves for machines.
2025-01-02 12:12:12 +02:00
User - agent : YouBot
User - agent : Omgili
User - agent : PerplexityBot
2025-01-02 10:06:29 +02:00
User - agent : CCBot
2025-01-02 12:12:12 +02:00
User - agent : facebookexternalhit
User - agent : Amazonbot
2025-01-02 10:06:29 +02:00
User - agent : ChatGPT - User
2025-01-02 12:12:12 +02:00
User - agent : ImagesiftBot
User - agent : AI2Bot
User - agent : GPTBot
User - agent : PetalBot
User - agent : Diffbot
User - agent : Bytespider
2025-01-02 10:06:29 +02:00
User - agent : ClaudeBot
2025-01-02 12:12:12 +02:00
User - agent : OAI - SearchBot
User - agent : Applebot - Extended
2025-01-02 10:06:29 +02:00
User - agent : cohere - ai
2025-01-02 12:12:12 +02:00
User - agent : Claude - Web
2025-01-02 10:06:29 +02:00
User - agent : ICC - Crawler
2025-01-02 12:12:12 +02:00
User - agent : VelenPublicWebCrawler
2025-01-02 10:06:29 +02:00
User - agent : Omgilibot
2025-01-02 12:12:12 +02:00
User - agent : anthropic - ai
User - agent : FriendlyCrawler
2025-01-02 10:06:29 +02:00
User - agent : Scrapy
2025-01-02 12:12:12 +02:00
User - agent : img2dataset
User - agent : Google - Extended
2025-01-02 10:06:29 +02:00
User - agent : Timpibot
2025-01-02 12:12:12 +02:00
User - agent : meta - externalagent
User - agent : FacebookBot
User - agent : Ai2Bot - Dolma
2025-01-02 10:06:29 +02:00
Disallow : / """
2023-08-29 10:27:14 +03:00
)
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
)
2024-12-26 05:02:33 +02:00
etree . SubElement ( item , " description " ) . text = (
post [ " description " ]
+ (
f " [edited at { datetime . datetime . utcfromtimestamp ( created ) . strftime ( ftime ) } ] "
if created
else " "
)
+ f " - read about it at { link } :) "
)
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 = (
2024-12-26 05:15:31 +02:00
f " { config [ ' email ' ] } ( { config [ ' author ' ] } ) "
2024-03-01 00:15:51 +02:00
)
2023-08-08 17:49:12 +03:00
2024-12-26 05:02:33 +02:00
if " preview " in post and post [ " preview " ] in MEDIA_INDEX :
url : str = (
f " { config [ ' blog ' ] } /media/ { post [ ' preview ' ] } . { MEDIA_INDEX [ post [ ' preview ' ] ] [ ' ext ' ] } "
)
m = MEDIA_INDEX [ post [ " preview " ] ]
if m [ " type " ] != " image " :
continue
enc = etree . SubElement ( item , " enclosure " )
enc . set ( " url " , url )
enc . set ( " type " , m [ " mime " ] )
enc . set ( " length " , str ( m [ " size " ] ) )
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
2024-12-15 19:01:18 +02:00
update_media ( config )
2024-12-15 03:35:24 +02:00
for api in recents . name , CONFIG_FILE , " media/media.json " :
if not os . path . isfile ( api ) :
continue
2023-08-29 10:27:14 +03:00
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
2024-12-15 03:35:24 +02:00
for pattern in {
2023-08-29 10:27:14 +03:00
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 " ,
2024-12-15 03:35:24 +02:00
" media/media_json_hash.txt " ,
2023-08-29 10:27:14 +03:00
" 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 " ,
2024-12-15 03:35:24 +02:00
} :
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-18 17:37:00 +02:00
@cmds.new
def search ( config : dict [ str , typing . Any ] ) - > int :
""" search for a term """
q : str = iinput ( " query " )
for p in config [ " posts " ] . items ( ) :
if q . lower ( ) in str ( p ) . lower ( ) :
llog ( f " In post: { p [ 0 ] } " )
return OK
2024-12-15 02:53:32 +02:00
@cmds.new
def media ( config : dict [ str , typing . Any ] ) - > int :
""" add media """
2024-12-15 16:44:31 +02:00
if os . path . exists ( " licenses.json " ) :
with open ( " licenses.json " , " r " ) as fp :
licenses : tuple [ str , . . . ] = json . load ( fp )
else :
lnew ( " fetching SPDX license IDs " )
2024-12-15 17:45:06 +02:00
licenses = tuple (
license [ " licenseId " ]
for license in requests . get (
" https://raw.githubusercontent.com/spdx/license-list-data/master/json/licenses.json "
) . json ( ) [ " licenses " ]
)
2024-12-15 16:44:31 +02:00
with open ( " licenses.json " , " w " ) as fp :
json . dump ( licenses , fp )
2024-12-15 02:53:32 +02:00
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 " )
2024-12-18 02:14:35 +02:00
license : str = " , " . join (
select_multi (
[
" All-rights-reserved " ,
" Unknown " ,
]
+ licenses
)
)
2024-12-15 16:44:31 +02:00
2024-12-15 02:53:32 +02:00
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 )
# Check if it exists
2024-12-15 03:35:24 +02:00
if hash_hex in MEDIA_INDEX :
2024-12-15 02:53:32 +02:00
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/ " ) :
2024-12-15 17:45:06 +02:00
shutil . copy ( path , fpath )
2024-12-15 02:53:32 +02:00
2024-12-15 17:45:06 +02:00
if ext == " svg " :
tree : etree . ElementTree = etree . parse ( path )
root : etree . Element = tree . getroot ( )
2024-12-15 02:53:32 +02:00
2024-12-15 17:45:06 +02:00
w = root . attrib . get ( " width " )
h = root . attrib . get ( " height " )
2024-12-15 02:53:32 +02:00
2024-12-15 17:45:06 +02:00
if w is None or h is None :
view_box = root . attrib . get ( " viewBox " )
2024-12-15 02:53:32 +02:00
2024-12-15 17:45:06 +02:00
if view_box :
view_box_values = view_box . split ( )
2024-12-15 02:53:32 +02:00
2024-12-15 17:45:06 +02:00
if len ( view_box_values ) > = 4 :
w = view_box_values [ 2 ]
h = view_box_values [ 3 ]
if w is None or h is None :
width , height = 0 , 0
else :
width = int ( float ( w . replace ( " px " , " " , 1 ) ) )
height = int ( float ( h . replace ( " px " , " " , 1 ) ) )
elif ext in { " jpeg " , " png " , " gif " , " webp " , " avif " } :
img = Image . open ( path )
width , height = img . size
quality_s : str = iinput ( " image quality % (1-100) " , " 80 " )
try :
quality : typing . Union [ str , int ] = max ( min ( int ( quality_s ) , 100 ) , 1 )
except Exception :
quality = 80
img_new = Image . new ( img . mode , img . size )
img_new . putdata ( img . getdata ( ) )
img_new . save (
fpath ,
format = ext ,
quality = quality if ext in { " jpeg " , " webp " } else None ,
optimize = True ,
lossless = yn ( " lossless " , " n " ) ,
subsampling = yn ( " subsampling " ) ,
progressive = yn ( " progressive " ) ,
dpi = img . info . get ( " dpi " , ( 72 , 72 ) ) ,
)
img_new . close ( )
img . close ( )
else :
return err ( " Unkown media extension. " )
MEDIA_INDEX [ hash_hex ] = {
" type " : " image " ,
" width " : width ,
" height " : height ,
}
2024-12-15 02:53:32 +02:00
elif mime . startswith ( " audio/ " ) :
shutil . copy ( path , fpath )
2024-12-15 03:35:24 +02:00
MEDIA_INDEX [ hash_hex ] = {
2024-12-15 02:53:32 +02:00
" type " : " audio " ,
}
else :
return err ( f " unsupported MIME: { mime !r} " )
2024-12-15 03:35:24 +02:00
MEDIA_INDEX [ hash_hex ] . update (
2024-12-15 02:53:32 +02:00
{
2024-12-15 16:26:52 +02:00
" alt " : iinput ( " alt text " ) . strip ( " . " ) ,
2024-12-18 03:25:44 +02:00
" purpose " : purpose . strip ( " . " ) ,
" title " : title . strip ( " . " ) ,
2024-12-15 02:53:32 +02:00
" license " : license ,
2024-12-18 03:25:44 +02:00
" credit " : credit . strip ( " . " ) ,
2024-12-15 02:53:32 +02:00
" ext " : ext ,
" mime " : mime ,
2024-12-15 13:59:44 +02:00
" uploaded " : datetime . datetime . utcnow ( ) . timestamp ( ) ,
2024-12-26 05:02:33 +02:00
" size " : os . path . getsize ( path ) ,
2024-12-15 02:53:32 +02:00
}
)
lnew ( f " media { hash_hex } created " )
2024-12-15 18:59:14 +02:00
return update_media ( config )
2024-12-15 02:53:32 +02:00
2024-12-15 03:35:24 +02:00
@cmds.new
def lsmedia ( config : dict [ str , typing . Any ] ) - > int :
""" list media """
assert config is config , " Unused "
2024-12-15 02:53:32 +02:00
2024-12-15 03:35:24 +02:00
for mdx , mdy in MEDIA_INDEX . items ( ) :
print ( mdx )
for k , v in mdy . items ( ) :
2024-12-15 14:32:31 +02:00
print ( " " , k , " = " , v )
2024-12-15 03:35:24 +02:00
return OK
@cmds.new
def rmmedia ( config : dict [ str , typing . Any ] ) - > int :
""" remove media """
for mdx in select_medias ( ) :
imp ( f " Removing media { mdx } " )
os . remove ( f " media/ { mdx } . { MEDIA_INDEX [ mdx ] [ ' ext ' ] } " )
del MEDIA_INDEX [ mdx ]
2024-12-15 18:59:14 +02:00
return update_media ( config )
2024-12-15 03:35:24 +02:00
@cmds.new
def purgemedia ( config : dict [ str , typing . Any ] ) - > int :
""" purge unused or unindexed media """
posts : typing . Any = config [ " posts " ] . values ( )
unused : set [ str ] = set ( )
for mdx in MEDIA_INDEX :
used : bool = False
for post in posts :
2024-12-15 13:59:44 +02:00
# Seperated for readability reasons
if " preview " in post and post [ " preview " ] == mdx :
used = True
break
2024-12-18 02:43:42 +02:00
if f " <@: { mdx } > " in post [ " content " ] :
2024-12-15 03:35:24 +02:00
used = True
break
if not used :
unused . add ( mdx )
# Cannot change dict size during iteration
for mdy in unused :
log ( f " Unindexing unused media { mdy } " )
del MEDIA_INDEX [ mdy ]
for file in os . listdir ( " media " ) :
2024-12-15 13:59:44 +02:00
if file == " media.json " or file == " media_json_hash.txt " : # Ignore index file
2024-12-15 03:35:24 +02:00
continue
pid : str = os . path . splitext ( os . path . basename ( file ) ) [ 0 ]
if pid not in MEDIA_INDEX :
imp ( f " Removing unindexed media { pid } " )
os . remove ( f " media/ { file } " )
2024-12-15 02:53:32 +02:00
2024-12-15 18:59:14 +02:00
return update_media ( config )
2024-12-15 02:53:32 +02:00
2024-12-15 14:32:31 +02:00
@cmds.new
def infomedia ( config : dict [ str , typing . Any ] ) - > int :
""" get info about media """
for mdx in select_medias ( ) :
print ( mdx )
for k , v in MEDIA_INDEX [ mdx ] . items ( ) :
print ( " " , k , " = " , v )
for slug , post in config [ " posts " ] . items ( ) :
# Seperated for readability reasons
if " preview " in post and post [ " preview " ] == mdx :
print ( f " * used in { post [ ' title ' ] !r} ( { slug } ) " )
continue
2024-12-18 03:03:51 +02:00
if f " <@: { mdx } > " in post [ " content " ] :
2024-12-15 14:32:31 +02:00
print ( f " * used in { post [ ' title ' ] !r} ( { slug } ) " )
continue
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) " )
2024-12-15 03:35:24 +02:00
else :
os . makedirs ( " media " , exist_ok = True )
with open ( " media/media.json " , " w " ) as fp :
fp . write ( " {} " )
2024-12-15 02:53:32 +02:00
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 ( ) )