mirror of
https://git.ari.lt/ari.lt/blog.ari.lt.git
synced 2025-02-04 09:39:25 +01:00
init
Signed-off-by: Ari Archer <ari.web.xyz@gmail.com>
This commit is contained in:
commit
69a70b0b51
12 changed files with 768 additions and 0 deletions
11
.editorconfig
Normal file
11
.editorconfig
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
tab_width = 2
|
||||||
|
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/b/
|
||||||
|
*.min.css
|
||||||
|
index.html
|
||||||
|
.mypy_cache/
|
||||||
|
.blog_history
|
||||||
|
venv/
|
||||||
|
|
35
LICENSE
Normal file
35
LICENSE
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
ArAr General public license
|
||||||
|
version 2
|
||||||
|
|
||||||
|
This license is an open source license with restrictions for commercial and corporate use.
|
||||||
|
Any project under this license ...
|
||||||
|
CANNOT be used commercially
|
||||||
|
CANNOT be redistributed with any sort of fee
|
||||||
|
CANNOT be redistributed in any proprietary form (binary, obfuscated, etc.)
|
||||||
|
CANNOT be patented
|
||||||
|
CANNOT change to any more proprietary license
|
||||||
|
CANNOT depend on any proprietary code/code blobs/software
|
||||||
|
CANNOT be used by any corporate individual, company or organisation
|
||||||
|
|
||||||
|
DOESN'T have any warranty
|
||||||
|
|
||||||
|
CAN be modified
|
||||||
|
CAN be redistributed in an open source form
|
||||||
|
CAN be used privately
|
||||||
|
|
||||||
|
|
||||||
|
This license is non-corporate, individual use only.
|
||||||
|
|
||||||
|
The owner of this project holds NO LIABILITY if any unstable, redistributed or modified
|
||||||
|
versions of this project harms you in any way.
|
||||||
|
|
||||||
|
|
||||||
|
Redistributed or modified versions of this project...
|
||||||
|
MUST have the same license as the original project
|
||||||
|
MUST disclose the project's source
|
||||||
|
|
||||||
|
|
||||||
|
OWNER: Ari Archer <ari.web.xyz@gmail.com>
|
||||||
|
PROJECT NAME: blog.ari-web.xyz
|
||||||
|
YEAR: 2022
|
||||||
|
|
15
README.md
Normal file
15
README.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# [Ari-web blogs](https://blog.ari-web.xyz/)
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://img.shields.io/badge/Maintained-Yes-green?color=red&style=flat-square">
|
||||||
|
<img src="https://img.shields.io/github/last-commit/TruncatedDinosour/blog.ari-web.xyz?color=red&style=flat-square">
|
||||||
|
<img src="https://img.shields.io/github/repo-size/TruncatedDinosour/blog.ari-web.xyz?color=red&style=flat-square">
|
||||||
|
<img src="https://img.shields.io/github/issues/TruncatedDinosour/blog.ari-web.xyz?color=red&style=flat-square">
|
||||||
|
<img src="https://img.shields.io/github/stars/TruncatedDinosour/blog.ari-web.xyz?color=red&style=flat-square">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Installing dependencies
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ python3 -m pip install --user -r requirements.txt
|
||||||
|
```
|
52
blog.json
Normal file
52
blog.json
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"editor-command": "vim -- %s",
|
||||||
|
"blog-dir": "b",
|
||||||
|
"git-url": "/git",
|
||||||
|
"py-markdown-extensions": [
|
||||||
|
"markdown.extensions.abbr",
|
||||||
|
"markdown.extensions.def_list",
|
||||||
|
"markdown.extensions.fenced_code",
|
||||||
|
"markdown.extensions.footnotes",
|
||||||
|
"markdown.extensions.md_in_html",
|
||||||
|
"markdown.extensions.tables",
|
||||||
|
"markdown.extensions.admonition",
|
||||||
|
"markdown.extensions.sane_lists",
|
||||||
|
"markdown.extensions.toc",
|
||||||
|
"markdown.extensions.wikilinks",
|
||||||
|
"pymdownx.betterem",
|
||||||
|
"pymdownx.caret",
|
||||||
|
"pymdownx.magiclink",
|
||||||
|
"pymdownx.mark",
|
||||||
|
"pymdownx.tilde"
|
||||||
|
],
|
||||||
|
"default-keywords": [
|
||||||
|
"website",
|
||||||
|
"blog",
|
||||||
|
"opinion",
|
||||||
|
"article",
|
||||||
|
"ari-web",
|
||||||
|
"ari"
|
||||||
|
],
|
||||||
|
"page-title": "Ari::web -> Blog",
|
||||||
|
"page-description": "My blog page",
|
||||||
|
"colourscheme-type": "dark",
|
||||||
|
"home-keywords": [
|
||||||
|
"ari",
|
||||||
|
"ari-web",
|
||||||
|
"blog",
|
||||||
|
"ari-archer",
|
||||||
|
"foss",
|
||||||
|
"free",
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"base-homepage": "https://ari-web.xyz/",
|
||||||
|
"blogs": {
|
||||||
|
"new-blog-management-system-": {
|
||||||
|
"title": "TmV3IGJsb2cgbWFuYWdlbWVudCBzeXN0ZW0h",
|
||||||
|
"content": "SGVsbG8gd29ybGQgOikKCkkgaGF2ZSBjb21wbGV0ZWx5IHJlZG9uZSBob3cgYmxvZ3MgYXJlIG1hbmFnZWQsIG1hZGUKYW5kIHN0b3JlZCBzbyBub3cgaHR0cHM6Ly9ibG9nLmFyaS13ZWIueHl6LyAob2xkKSBpcyBtb3ZlZCB0byBodHRwczovL2xlZ2FjeS5ibG9nLmFyaS13ZWIueHl6Lwp3aGlsZSB0aGlzIG5ldyBzeXN0ZW0gaXMgb24gdGhlIG9yaWdpbmFsLCBodHRwczovL2Jsb2cuYXJpLXdlYi54eXovIHN1YmRvbWFpbiwgdGhlCmxlZ2FjeSBzdWJkb21haW4gd2lsbCBzdGlsbCBiZSBoZXJlIGFuZCB3aWxsIHN0aWxsIGJlIGJhY2t3YXJkcyBjb21wYXRpYmxlIHdpdGgKdGhlIG5ldyBvbmUsIHRob3VnaCBub3cgaXQgd2lsbCBiZSBhbiBIVFRQIHJlZGlyZWN0CgpJZiBhbnlvbmUgaXMgdXNpbmcgbXkgYmxvZyBmb3IgYW55dGhpbmcgYnV0IHZpc2l0aW5nLCBwbGVhc2UgY29uc2lkZXIKdGhlIHJlZGlyZWN0IDopCgo=",
|
||||||
|
"version": 1,
|
||||||
|
"time": 1646996956.328543,
|
||||||
|
"keywords": "management linux http-redirect new website blog opinion article ari-web ari"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
content/styles.css
Normal file
45
content/styles.css
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
:root {
|
||||||
|
color-scheme: dark !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
background-color: #262220 !important;
|
||||||
|
color: #f9f6e8 !important;
|
||||||
|
font-family: Hack, hack, monospace, sans, opensans, sans-serif !important;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: auto !important;
|
||||||
|
padding: 2rem !important;
|
||||||
|
max-width: 1200px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center !important;
|
||||||
|
margin: 1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 0.5em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none !important;
|
||||||
|
text-shadow: 0px 0px 6px white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-bar {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
45
netlify.toml
Normal file
45
netlify.toml
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
[build]
|
||||||
|
command = "python3 ./scripts/blog build"
|
||||||
|
|
||||||
|
[[redirects]]
|
||||||
|
from = "/git/*"
|
||||||
|
to = "https://ari-web.xyz/gh/blog.ari-web.xyz/:splat"
|
||||||
|
status = 301
|
||||||
|
force = true
|
||||||
|
|
||||||
|
[[redirects]]
|
||||||
|
from = "/favicon.ico"
|
||||||
|
to = "https://ari-web.xyz/favicon.ico"
|
||||||
|
status = 200
|
||||||
|
force = true
|
||||||
|
|
||||||
|
[[redirects]]
|
||||||
|
from = "/robots.txt"
|
||||||
|
to = "https://ari-web.xyz/robots.txt"
|
||||||
|
status = 200
|
||||||
|
force = true
|
||||||
|
|
||||||
|
[[redirects]]
|
||||||
|
from = "/sitemap.xml"
|
||||||
|
to = "https://ari-web.xyz/sitemap.xml"
|
||||||
|
status = 200
|
||||||
|
force = true
|
||||||
|
|
||||||
|
[[redirects]]
|
||||||
|
from = "/blogs/*"
|
||||||
|
to = "https://legacy.blog.ari-web.xyz/:splat"
|
||||||
|
status = 302
|
||||||
|
force = true
|
||||||
|
|
||||||
|
[[redirects]]
|
||||||
|
from = "/netlify.toml"
|
||||||
|
to = "https://ari-web.xyz/404.files.xyz"
|
||||||
|
status = 404
|
||||||
|
force = true
|
||||||
|
|
||||||
|
[[redirects]]
|
||||||
|
from = "/*"
|
||||||
|
to = "https://ari-web.xyz/404.files.xyz"
|
||||||
|
status = 404
|
||||||
|
force = false
|
||||||
|
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
css-html-js-minify
|
||||||
|
readline
|
||||||
|
datetime
|
||||||
|
markdown
|
||||||
|
plumbum
|
||||||
|
pyfzf
|
||||||
|
|
1
runtime.txt
Normal file
1
runtime.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
3.9
|
536
scripts/blog
Executable file
536
scripts/blog
Executable file
|
@ -0,0 +1,536 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Manage blogs"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import readline
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
from atexit import register as fn_register
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
from datetime import datetime
|
||||||
|
from html import escape as html_escape
|
||||||
|
from shutil import rmtree
|
||||||
|
from threading import Thread
|
||||||
|
from time import strftime as format_system_time
|
||||||
|
from timeit import default_timer as code_timer
|
||||||
|
from warnings import filterwarnings as filter_warnings
|
||||||
|
|
||||||
|
from css_html_js_minify import html_minify # type: ignore
|
||||||
|
from css_html_js_minify import process_single_css_file
|
||||||
|
from markdown import markdown # type: ignore
|
||||||
|
from plumbum.commands.processes import ProcessExecutionError # type: ignore
|
||||||
|
from pyfzf import FzfPrompt # type: ignore
|
||||||
|
|
||||||
|
EXIT_OK: int = 0
|
||||||
|
EXIT_ERR: int = 1
|
||||||
|
|
||||||
|
DEFAULT_CONFIG: dict = {
|
||||||
|
"editor-command": f"{os.environ.get('EDITOR', 'vim')} -- %s",
|
||||||
|
"blog-dir": "b",
|
||||||
|
"git-url": "/git",
|
||||||
|
"py-markdown-extensions": [
|
||||||
|
"markdown.extensions.abbr",
|
||||||
|
"markdown.extensions.def_list",
|
||||||
|
"markdown.extensions.fenced_code",
|
||||||
|
"markdown.extensions.footnotes",
|
||||||
|
"markdown.extensions.md_in_html",
|
||||||
|
"markdown.extensions.tables",
|
||||||
|
"markdown.extensions.admonition",
|
||||||
|
"markdown.extensions.sane_lists",
|
||||||
|
"markdown.extensions.toc",
|
||||||
|
"markdown.extensions.wikilinks",
|
||||||
|
"pymdownx.betterem",
|
||||||
|
"pymdownx.caret",
|
||||||
|
"pymdownx.magiclink",
|
||||||
|
"pymdownx.mark",
|
||||||
|
"pymdownx.tilde",
|
||||||
|
],
|
||||||
|
"default-keywords": ["website", "blog", "opinion", "article", "ari-web", "ari"],
|
||||||
|
"page-title": "Ari::web -> Blog",
|
||||||
|
"page-description": "My blog page",
|
||||||
|
"colourscheme-type": "dark",
|
||||||
|
"home-keywords": ["ari", "ari-web", "blog", "ari-archer", "foss", "free", "linux"],
|
||||||
|
"base-homepage": "https://ari-web.xyz/",
|
||||||
|
"blogs": {},
|
||||||
|
}
|
||||||
|
DEFAULT_CONFIG_FILE: str = "blog.json"
|
||||||
|
HISTORY_FILE: str = ".blog_history"
|
||||||
|
BLOG_VERSION: int = 1
|
||||||
|
|
||||||
|
BLOG_MARKDOWN_TEMPLATE = """# %s
|
||||||
|
|
||||||
|
<div class="info-bar" aria-hidden="true">
|
||||||
|
%s | <a href="%s">back</a> | <a href="/">home</a> | <a href="%s">git</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
%s"""
|
||||||
|
|
||||||
|
HTML_HEADER = """<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">
|
||||||
|
<title>{title}</title>
|
||||||
|
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
|
||||||
|
<meta name="color-scheme" content="{theme_type}">
|
||||||
|
|
||||||
|
<meta name="keywords" content="{keywords}">
|
||||||
|
<meta name="robots" content="follow"/>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/content/styles.min.css"/>"""
|
||||||
|
|
||||||
|
BLOG_HTML_TEMPLATE = f"""<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{HTML_HEADER}
|
||||||
|
<meta name="description" content="{{blog_description}}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
{{blog}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
HOME_PAGE_HTML_TEMPLATE = f"""<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{HTML_HEADER}
|
||||||
|
<meta name="description" content="{{home_page_description}}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>My blogs</h1>
|
||||||
|
<div class="info-bar" aria-hidden="true">
|
||||||
|
<p>last blog on: <code>{{lastest_blog_time}}</code> | latest blog: <a href="{{latest_blog_url}}">{{latest_blog_title}}</a> | <a href="{{git_url}}">source code</a></p>
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{content}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
def log(message: str, header: str = "ERROR", code: int = EXIT_ERR) -> int:
|
||||||
|
sys.stderr.write(f"{header}: {message}\n")
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
def sanitise_title(title: str, titleset: dict) -> str:
|
||||||
|
_title: str = ""
|
||||||
|
|
||||||
|
for char in title:
|
||||||
|
_title += char if char not in string.whitespace + string.punctuation else "-"
|
||||||
|
|
||||||
|
_title = _title.lower()
|
||||||
|
|
||||||
|
return (
|
||||||
|
_title
|
||||||
|
if _title not in titleset and _title.strip()
|
||||||
|
else sanitise_title(_title + random.choice(string.digits), titleset)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_time(timestamp: float) -> str:
|
||||||
|
return datetime.fromtimestamp(timestamp).strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S " + format_system_time("%Z")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def iinput(prompt: str, default_text: str = "") -> str:
|
||||||
|
default_text = default_text.strip()
|
||||||
|
|
||||||
|
def hook():
|
||||||
|
if not default_text:
|
||||||
|
return
|
||||||
|
|
||||||
|
readline.insert_text(default_text)
|
||||||
|
readline.redisplay()
|
||||||
|
|
||||||
|
readline.set_pre_input_hook(hook)
|
||||||
|
user_inpt: str = input(f"({prompt}) ").strip()
|
||||||
|
readline.set_pre_input_hook()
|
||||||
|
|
||||||
|
return user_inpt
|
||||||
|
|
||||||
|
|
||||||
|
def new_config() -> None:
|
||||||
|
log("Making new config...", "INFO")
|
||||||
|
|
||||||
|
with open(DEFAULT_CONFIG_FILE, "w") as cfg:
|
||||||
|
json.dump(DEFAULT_CONFIG, cfg, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def pick_blog(config: dict) -> str:
|
||||||
|
try:
|
||||||
|
blog_id: str = (
|
||||||
|
FzfPrompt()
|
||||||
|
.prompt(
|
||||||
|
map(
|
||||||
|
lambda key: f"{key} | {b64decode(config['blogs'][key]['title']).decode()!r}",
|
||||||
|
config["blogs"].keys(),
|
||||||
|
),
|
||||||
|
"--prompt='Pick blog'",
|
||||||
|
)[0]
|
||||||
|
.split()[0]
|
||||||
|
)
|
||||||
|
except ProcessExecutionError:
|
||||||
|
log("Fzf process exited unexpectedly")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if blog_id not in config["blogs"]:
|
||||||
|
log(f"Blog {blog_id!r} does not exist")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return blog_id
|
||||||
|
|
||||||
|
|
||||||
|
def new_blog(config: dict) -> tuple[int, dict]:
|
||||||
|
"""Make a new blog"""
|
||||||
|
|
||||||
|
if title := iinput("blog title"):
|
||||||
|
us_title: str = title
|
||||||
|
s_title: str = sanitise_title(us_title, config["blogs"])
|
||||||
|
|
||||||
|
blog = {
|
||||||
|
"title": b64encode(us_title.encode()).decode(),
|
||||||
|
"content": "",
|
||||||
|
"version": BLOG_VERSION,
|
||||||
|
"time": "Unknown",
|
||||||
|
"keywords": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
file: str = f"/tmp/{s_title}.md"
|
||||||
|
|
||||||
|
open(file, "w").close()
|
||||||
|
os.system(config["editor-command"] % file)
|
||||||
|
|
||||||
|
if not os.path.isfile(file):
|
||||||
|
return log(f"{file!r} does not exist"), config
|
||||||
|
|
||||||
|
with open(file, "r") as md:
|
||||||
|
blog["content"] = b64encode(md.read().encode()).decode()
|
||||||
|
|
||||||
|
if not blog["content"].strip(): # type: ignore
|
||||||
|
return log("Blog cannot be empty"), config
|
||||||
|
|
||||||
|
blog["keywords"] = html_escape(
|
||||||
|
iinput("keywords (seperated by spaces)")
|
||||||
|
+ " "
|
||||||
|
+ " ".join(config["default-keywords"])
|
||||||
|
)
|
||||||
|
|
||||||
|
blog["time"] = datetime.now().timestamp()
|
||||||
|
config["blogs"][s_title] = blog
|
||||||
|
|
||||||
|
return EXIT_OK, config
|
||||||
|
|
||||||
|
|
||||||
|
def build(config: dict) -> tuple[int, dict]:
|
||||||
|
"""Build, minimise and generate site"""
|
||||||
|
|
||||||
|
if len(config["blogs"]) < 1:
|
||||||
|
return log("Cannot build no blogs"), config
|
||||||
|
|
||||||
|
code_timestamp = code_timer()
|
||||||
|
|
||||||
|
if os.path.isdir(config["blog-dir"]):
|
||||||
|
rmtree(config["blog-dir"])
|
||||||
|
|
||||||
|
os.makedirs(config["blog-dir"], exist_ok=True)
|
||||||
|
|
||||||
|
log("Building CSS...", "INFO")
|
||||||
|
|
||||||
|
def build_css() -> None:
|
||||||
|
saved_stdout = sys.stdout
|
||||||
|
sys.stdout = open(os.devnull, "w")
|
||||||
|
|
||||||
|
if os.path.isfile("content/styles.css"):
|
||||||
|
process_single_css_file("content/styles.css")
|
||||||
|
|
||||||
|
sys.stdout.close()
|
||||||
|
sys.stdout = saved_stdout
|
||||||
|
|
||||||
|
Thread(target=build_css, daemon=True).start()
|
||||||
|
|
||||||
|
log("Building blogs...", "INFO")
|
||||||
|
|
||||||
|
def thread(blog_name: str, blog_meta: dict):
|
||||||
|
blog_dir: str = os.path.join(config["blog-dir"], blog_name)
|
||||||
|
os.makedirs(blog_dir, exist_ok=True)
|
||||||
|
|
||||||
|
with open(os.path.join(blog_dir, "index.html"), "w") as blog_html:
|
||||||
|
blog_time: str = format_time(blog_meta["time"])
|
||||||
|
|
||||||
|
blog_title: str = html_escape(b64decode(blog_meta["title"]).decode())
|
||||||
|
|
||||||
|
blog_base_html: str = markdown(
|
||||||
|
BLOG_MARKDOWN_TEMPLATE
|
||||||
|
% (
|
||||||
|
blog_title,
|
||||||
|
blog_time,
|
||||||
|
config["base-homepage"],
|
||||||
|
config["git-url"],
|
||||||
|
markdown(b64decode(blog_meta["content"]).decode())
|
||||||
|
.replace("<h1>", "<h2>")
|
||||||
|
.replace("<h1/>", "<h2/>"),
|
||||||
|
),
|
||||||
|
extensions=config["py-markdown-extensions"],
|
||||||
|
)
|
||||||
|
|
||||||
|
blog_html.write(
|
||||||
|
html_minify(
|
||||||
|
BLOG_HTML_TEMPLATE.format(
|
||||||
|
title=config["page-title"],
|
||||||
|
theme_type=config["colourscheme-type"],
|
||||||
|
keywords=blog_meta["keywords"],
|
||||||
|
blog_description=f"Blog on {blog_time} -- {blog_title}",
|
||||||
|
blog=blog_base_html,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
log(f"Finished building blog {blog_name!r}", "BUILD")
|
||||||
|
|
||||||
|
_tmp_threads: list = []
|
||||||
|
|
||||||
|
for blog_name, blog_meta in config["blogs"].items():
|
||||||
|
t: Thread = Thread(target=thread, args=(blog_name, blog_meta), daemon=True)
|
||||||
|
_tmp_threads.append(t)
|
||||||
|
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
for awaiting_thread in _tmp_threads:
|
||||||
|
awaiting_thread.join()
|
||||||
|
|
||||||
|
log("Building blog index...", "INFO")
|
||||||
|
|
||||||
|
with open("index.html", "w") as index:
|
||||||
|
latest_blog_id: str = tuple(config["blogs"].keys())[-1]
|
||||||
|
lastest_blog: dict = config["blogs"][latest_blog_id]
|
||||||
|
lastest_blog_time: str = format_time(lastest_blog["time"])
|
||||||
|
|
||||||
|
blog_list = "<ul>"
|
||||||
|
|
||||||
|
for blog_id, blog_meta in reversed(config["blogs"].items()):
|
||||||
|
blog_list += f'<li><a href="{os.path.join(config["blog-dir"], blog_id)}">{html_escape(b64decode(blog_meta["title"]).decode())}</a></li>'
|
||||||
|
|
||||||
|
blog_list += "</ul>"
|
||||||
|
|
||||||
|
index.write(
|
||||||
|
html_minify(
|
||||||
|
HOME_PAGE_HTML_TEMPLATE.format(
|
||||||
|
title=config["page-title"],
|
||||||
|
theme_type=config["colourscheme-type"],
|
||||||
|
keywords=config["home-keywords"],
|
||||||
|
home_page_description=config["page-description"],
|
||||||
|
lastest_blog_time=lastest_blog_time,
|
||||||
|
latest_blog_url=os.path.join(config["blog-dir"], latest_blog_id),
|
||||||
|
latest_blog_title=b64decode(
|
||||||
|
html_escape(lastest_blog["title"])
|
||||||
|
).decode(),
|
||||||
|
git_url=config["git-url"],
|
||||||
|
content=blog_list,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
log(
|
||||||
|
f"Successfully built blog page in {code_timer() - code_timestamp} seconds",
|
||||||
|
"TIME",
|
||||||
|
)
|
||||||
|
return EXIT_OK, config
|
||||||
|
|
||||||
|
|
||||||
|
def list_blogs(config: dict) -> tuple[int, dict]:
|
||||||
|
"""List blogs"""
|
||||||
|
|
||||||
|
for blog_id, blog_meta in config["blogs"].items():
|
||||||
|
print(
|
||||||
|
f"""ID: {blog_id}
|
||||||
|
Title: {b64decode(blog_meta['title']).decode()!r}
|
||||||
|
Version: {blog_meta['version']}
|
||||||
|
Time of creation: {format_time(blog_meta['time'])}
|
||||||
|
Keywords: {blog_meta['keywords'].replace(' ', ', ')}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
return EXIT_OK, config
|
||||||
|
|
||||||
|
|
||||||
|
def remove_blog(config: dict) -> tuple[int, dict]:
|
||||||
|
"""Remove a blog page"""
|
||||||
|
|
||||||
|
blog_id: str = pick_blog(config)
|
||||||
|
|
||||||
|
if not blog_id:
|
||||||
|
return EXIT_ERR, config
|
||||||
|
|
||||||
|
del config["blogs"][blog_id]
|
||||||
|
return EXIT_OK, config
|
||||||
|
|
||||||
|
|
||||||
|
def dummy() -> None:
|
||||||
|
"""Print help/usage information"""
|
||||||
|
|
||||||
|
|
||||||
|
def edit_title(blog: str, config: dict) -> int:
|
||||||
|
new_title: str = iinput(
|
||||||
|
"edit title", b64decode(config["blogs"][blog]["title"]).decode()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not new_title.strip():
|
||||||
|
return log("New title cannot be empty")
|
||||||
|
|
||||||
|
config["blogs"][blog]["title"] = b64encode(new_title.encode()).decode()
|
||||||
|
|
||||||
|
return EXIT_OK
|
||||||
|
|
||||||
|
|
||||||
|
def edit_keywords(blog: str, config: dict) -> int:
|
||||||
|
new_keywords: str = iinput("edit keywords", config["blogs"][blog]["keywords"])
|
||||||
|
|
||||||
|
if not new_keywords.strip():
|
||||||
|
return log("Keywords cannot be empty")
|
||||||
|
|
||||||
|
config["blogs"][blog]["keywords"] = new_keywords
|
||||||
|
|
||||||
|
return EXIT_OK
|
||||||
|
|
||||||
|
|
||||||
|
def edit_content(blog: str, config: dict) -> int:
|
||||||
|
file: str = f"/tmp/{blog}.md"
|
||||||
|
|
||||||
|
with open(file, "w") as blog_md:
|
||||||
|
blog_md.write(b64decode(config["blogs"][blog]["content"]).decode())
|
||||||
|
|
||||||
|
os.system(config["editor-command"] % (file))
|
||||||
|
|
||||||
|
with open(file, "r") as blog_md_new:
|
||||||
|
content: str = blog_md_new.read()
|
||||||
|
|
||||||
|
if not content.strip():
|
||||||
|
blog_md_new.close()
|
||||||
|
return log("Content of a blog cannot be empty")
|
||||||
|
|
||||||
|
config["blogs"][blog]["content"] = b64encode(content.encode()).decode()
|
||||||
|
|
||||||
|
return EXIT_OK
|
||||||
|
|
||||||
|
|
||||||
|
EDIT_HOOKS: dict = {
|
||||||
|
"title": edit_title,
|
||||||
|
"keywords": edit_keywords,
|
||||||
|
"content": edit_content,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def edit(config: dict) -> tuple[int, dict]:
|
||||||
|
"""Edit a blog"""
|
||||||
|
|
||||||
|
blog_id: str = pick_blog(config)
|
||||||
|
|
||||||
|
if not blog_id:
|
||||||
|
return EXIT_ERR, config
|
||||||
|
|
||||||
|
try:
|
||||||
|
hook: str = FzfPrompt().prompt(EDIT_HOOKS.keys(), "--prompt='What to edit'")[0]
|
||||||
|
|
||||||
|
if hook not in EDIT_HOOKS:
|
||||||
|
return log(f"Hook {hook!r} does not exist"), config
|
||||||
|
|
||||||
|
EDIT_HOOKS[hook](blog_id, config)
|
||||||
|
except ProcessExecutionError:
|
||||||
|
return log("Fzf unexpectedly exited"), config
|
||||||
|
|
||||||
|
return EXIT_OK, config
|
||||||
|
|
||||||
|
|
||||||
|
def gen_new_config(config: dict) -> tuple[int, dict]:
|
||||||
|
"""Make default config"""
|
||||||
|
|
||||||
|
if os.path.exists(DEFAULT_CONFIG_FILE):
|
||||||
|
if iinput("Do you want to overwite config? (y/n)").lower()[0] != "y":
|
||||||
|
return log("Not overwritting config", "INFO", EXIT_OK), config
|
||||||
|
|
||||||
|
new_config()
|
||||||
|
|
||||||
|
with open(DEFAULT_CONFIG_FILE, "r") as cfg:
|
||||||
|
config = json.load(cfg)
|
||||||
|
|
||||||
|
return EXIT_OK, config
|
||||||
|
|
||||||
|
|
||||||
|
SUBCOMMANDS: dict = {
|
||||||
|
"help": dummy,
|
||||||
|
"new": new_blog,
|
||||||
|
"build": build,
|
||||||
|
"list": list_blogs,
|
||||||
|
"rm": remove_blog,
|
||||||
|
"edit": edit,
|
||||||
|
"new-config": gen_new_config,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def usage(code: int = EXIT_ERR, config: dict = None) -> int:
|
||||||
|
sys.stderr.write(f"Usage: {sys.argv[0]} <subcommand>\n")
|
||||||
|
|
||||||
|
for subcommand, func in SUBCOMMANDS.items():
|
||||||
|
sys.stderr.write(f" {subcommand:20s}{func.__doc__ or ''}\n")
|
||||||
|
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Entry/main function"""
|
||||||
|
|
||||||
|
if not os.path.isfile(HISTORY_FILE):
|
||||||
|
open(HISTORY_FILE, "w").close()
|
||||||
|
|
||||||
|
readline.parse_and_bind("tab: complete")
|
||||||
|
|
||||||
|
fn_register(readline.write_history_file, HISTORY_FILE)
|
||||||
|
fn_register(readline.read_history_file, HISTORY_FILE)
|
||||||
|
|
||||||
|
readline.read_history_file(HISTORY_FILE)
|
||||||
|
readline.set_history_length(5000)
|
||||||
|
|
||||||
|
if not os.path.isfile(DEFAULT_CONFIG_FILE):
|
||||||
|
new_config()
|
||||||
|
return EXIT_ERR
|
||||||
|
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
return usage()
|
||||||
|
elif sys.argv[1] not in SUBCOMMANDS:
|
||||||
|
return log(f"{sys.argv[1]!r} is not a subcommand, try `{sys.argv[0]} help`")
|
||||||
|
elif sys.argv[1] == "help":
|
||||||
|
return usage(EXIT_OK)
|
||||||
|
|
||||||
|
with open(DEFAULT_CONFIG_FILE, "r") as lcfg:
|
||||||
|
code, config = SUBCOMMANDS[sys.argv[1]](config=json.load(lcfg))
|
||||||
|
|
||||||
|
log("Sorting blogs by creation time...", "CLEANUP")
|
||||||
|
config["blogs"] = dict(
|
||||||
|
map(
|
||||||
|
lambda k: (k, config["blogs"][k]),
|
||||||
|
sorted(config["blogs"], key=lambda k: config["blogs"][k]["time"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(DEFAULT_CONFIG_FILE, "w") as dcfg:
|
||||||
|
json.dump(config, dcfg, indent=4)
|
||||||
|
|
||||||
|
return code
|
||||||
|
|
||||||
|
return EXIT_OK
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
assert main.__annotations__.get("return") is int, "main() should return an integer"
|
||||||
|
|
||||||
|
filter_warnings("error", category=Warning)
|
||||||
|
sys.exit(main())
|
11
scripts/git.sh
Executable file
11
scripts/git.sh
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
set -xe
|
||||||
|
|
||||||
|
main() {
|
||||||
|
git add -A
|
||||||
|
git commit -sam "update @ $(date)"
|
||||||
|
git push -u origin "$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
3
tox.ini
Normal file
3
tox.ini
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 200
|
||||||
|
|
Loading…
Add table
Reference in a new issue