lotek 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lotek/__init__.py +13 -0
- lotek/build.py +41 -0
- lotek/cli.py +99 -0
- lotek/cmd/__init__.py +0 -0
- lotek/cmd/add.py +25 -0
- lotek/cmd/build.py +12 -0
- lotek/cmd/clean.py +12 -0
- lotek/cmd/deploy.py +52 -0
- lotek/cmd/list.py +57 -0
- lotek/cmd/publish.py +63 -0
- lotek/cmd/serve.py +18 -0
- lotek/lib/__init__.py +1 -0
- lotek/lib/about.py +28 -0
- lotek/lib/colors.py +18 -0
- lotek/lib/dirs.py +26 -0
- lotek/lib/frontmatter.py +17 -0
- lotek/lib/highlight.py +25 -0
- lotek/lib/html_stubs.py +23 -0
- lotek/lib/index.py +20 -0
- lotek/lib/init.py +107 -0
- lotek/lib/pages.py +36 -0
- lotek/lib/posts.py +88 -0
- lotek/lib/render.py +65 -0
- lotek/lib/site_config.py +54 -0
- lotek/lib/site_time.py +9 -0
- lotek/lib/static.py +11 -0
- lotek/plugins/__init__.py +1 -0
- lotek/plugins/robots.py +31 -0
- lotek/plugins/rss.py +28 -0
- lotek/site-default.toml +38 -0
- lotek/static/index.html +0 -0
- lotek/static/pygments.css +75 -0
- lotek/static/style.css +264 -0
- lotek/templates/base.html +34 -0
- lotek/templates/feed.xml +10 -0
- lotek/templates/index.html +2 -0
- lotek/templates/post.html +6 -0
- lotek/templates/post.md +7 -0
- lotek-0.2.0.dist-info/METADATA +164 -0
- lotek-0.2.0.dist-info/RECORD +43 -0
- lotek-0.2.0.dist-info/WHEEL +5 -0
- lotek-0.2.0.dist-info/entry_points.txt +2 -0
- lotek-0.2.0.dist-info/top_level.txt +1 -0
lotek/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""lotek.run - A static site builder."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("lotek-run")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "unknown"
|
|
9
|
+
|
|
10
|
+
import lotek.cli as cli
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
cli.main()
|
lotek/build.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lotek.run -- static site builder
|
|
3
|
+
|
|
4
|
+
Requires: pandoc in PATH or markdown module
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from lotek.lib.site_config import config
|
|
8
|
+
from lotek.lib.site_time import now_string
|
|
9
|
+
from lotek.lib.pages import generate_pages
|
|
10
|
+
from lotek.lib.posts import generate_posts, load_posts
|
|
11
|
+
from lotek.lib.index import generate_index_landing
|
|
12
|
+
from lotek.lib.dirs import dirs
|
|
13
|
+
from lotek.lib.static import wipe_and_copy_to_output_dir
|
|
14
|
+
from lotek.plugins.rss import generate_rss
|
|
15
|
+
from lotek.plugins.robots import generate_robots
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build():
|
|
19
|
+
"""main entry point"""
|
|
20
|
+
|
|
21
|
+
out = dirs.OUTPUT
|
|
22
|
+
print(f"building lotek at {out}")
|
|
23
|
+
out.mkdir(exist_ok=True)
|
|
24
|
+
dirs.OUTPUT_POSTS.mkdir(exist_ok=True)
|
|
25
|
+
dirs.OUTPUT_STATIC.mkdir(exist_ok=True)
|
|
26
|
+
posts = load_posts(dirs.CONTENT_POSTS)
|
|
27
|
+
|
|
28
|
+
generate_posts(posts, out)
|
|
29
|
+
generate_pages(out)
|
|
30
|
+
if config.features.robotstxt:
|
|
31
|
+
print("generating robots.txt...")
|
|
32
|
+
generate_robots(posts, out)
|
|
33
|
+
if config.features.rss:
|
|
34
|
+
print("generating RSS feed...")
|
|
35
|
+
generate_rss(posts, out)
|
|
36
|
+
generate_index_landing(posts, out)
|
|
37
|
+
wipe_and_copy_to_output_dir(out)
|
|
38
|
+
|
|
39
|
+
print(f"built {len(posts)} posts -> output/")
|
|
40
|
+
last_file = out / "_last"
|
|
41
|
+
last_file.write_text(now_string())
|
lotek/cli.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lotek - operational command for lotek.run."""
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import lotek
|
|
7
|
+
|
|
8
|
+
from lotek.lib.init import init
|
|
9
|
+
from lotek.cmd.add import cmd_add
|
|
10
|
+
from lotek.cmd.build import cmd_build
|
|
11
|
+
from lotek.cmd.clean import cmd_clean
|
|
12
|
+
from lotek.cmd.deploy import cmd_deploy
|
|
13
|
+
from lotek.cmd.list import cmd_list
|
|
14
|
+
from lotek.cmd.publish import cmd_publish, cmd_unpublish
|
|
15
|
+
from lotek.cmd.serve import cmd_serve
|
|
16
|
+
|
|
17
|
+
USAGE = f"""
|
|
18
|
+
lotek - Tiny Blog Management Tool
|
|
19
|
+
ver: {getattr(lotek, "__version__", "unknown")}
|
|
20
|
+
|
|
21
|
+
Build:
|
|
22
|
+
lotek build Build the site
|
|
23
|
+
lotek clean Remove build output
|
|
24
|
+
lotek serve [--port N] Serve output locally (default: 8000)
|
|
25
|
+
lotek deploy Build and deploy via rsync (reads .env)
|
|
26
|
+
|
|
27
|
+
Content:
|
|
28
|
+
lotek init Make a new site from scratch
|
|
29
|
+
lotek list List all posts
|
|
30
|
+
lotek add <title> Create new post
|
|
31
|
+
lotek publish <slug> Mark a post as published
|
|
32
|
+
lotek unpublish <slug> Mark a post as unpublished
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def setup_cmd_parser():
|
|
36
|
+
parser = argparse.ArgumentParser(prog="lotek")
|
|
37
|
+
subs = parser.add_subparsers(dest="command")
|
|
38
|
+
|
|
39
|
+
i = subs.add_parser("init")
|
|
40
|
+
i.add_argument("path", type=str, default=".", nargs="?")
|
|
41
|
+
|
|
42
|
+
subs.add_parser("build")
|
|
43
|
+
|
|
44
|
+
subs.add_parser("clean")
|
|
45
|
+
|
|
46
|
+
p = subs.add_parser("serve")
|
|
47
|
+
p.add_argument("--port", "-p", type=int, default=8000)
|
|
48
|
+
|
|
49
|
+
p = subs.add_parser("deploy")
|
|
50
|
+
p.add_argument("--skip-build", action="store_true")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
subs.add_parser("list")
|
|
54
|
+
|
|
55
|
+
p = subs.add_parser("add")
|
|
56
|
+
p.add_argument("title", nargs="?")
|
|
57
|
+
|
|
58
|
+
p = subs.add_parser("publish")
|
|
59
|
+
p.add_argument("slug")
|
|
60
|
+
|
|
61
|
+
p = subs.add_parser("unpublish")
|
|
62
|
+
p.add_argument("slug")
|
|
63
|
+
|
|
64
|
+
args = parser.parse_args()
|
|
65
|
+
return args
|
|
66
|
+
|
|
67
|
+
def main():
|
|
68
|
+
args = setup_cmd_parser()
|
|
69
|
+
if not args.command:
|
|
70
|
+
print(USAGE)
|
|
71
|
+
return 0
|
|
72
|
+
try:
|
|
73
|
+
if args.command == "init":
|
|
74
|
+
return init(Path.absolute(Path(args.path)))
|
|
75
|
+
if args.command == "build":
|
|
76
|
+
return cmd_build()
|
|
77
|
+
if args.command == "clean":
|
|
78
|
+
return cmd_clean()
|
|
79
|
+
if args.command == "serve":
|
|
80
|
+
return cmd_serve(args.port)
|
|
81
|
+
if args.command == "deploy":
|
|
82
|
+
return cmd_deploy(skip_build=args.skip_build)
|
|
83
|
+
if args.command == "list":
|
|
84
|
+
return cmd_list()
|
|
85
|
+
if args.command == "add":
|
|
86
|
+
return cmd_add(args.title)
|
|
87
|
+
if args.command == "publish":
|
|
88
|
+
return cmd_publish(args.slug)
|
|
89
|
+
if args.command == "unpublish":
|
|
90
|
+
return cmd_unpublish(args.slug)
|
|
91
|
+
except KeyboardInterrupt:
|
|
92
|
+
print("\nInterrupted")
|
|
93
|
+
return 1
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
main()
|
lotek/cmd/__init__.py
ADDED
|
File without changes
|
lotek/cmd/add.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from lotek.lib.colors import red, green
|
|
3
|
+
from lotek.lib.dirs import dirs
|
|
4
|
+
|
|
5
|
+
def cmd_add(title):
|
|
6
|
+
if not title:
|
|
7
|
+
print(red("Title required"))
|
|
8
|
+
return 1
|
|
9
|
+
posts_dir = dirs.CONTENT_POSTS
|
|
10
|
+
posts_dir.mkdir(parents=True, exist_ok=True)
|
|
11
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
12
|
+
slug = title.lower().replace(" ", "-")
|
|
13
|
+
fname = f"{today}-{slug}.md"
|
|
14
|
+
fp = posts_dir / fname
|
|
15
|
+
if fp.exists():
|
|
16
|
+
print(red(f"Already exists: {fname}"))
|
|
17
|
+
return 1
|
|
18
|
+
template_path = dirs.TEMPLATES / "post.md"
|
|
19
|
+
if not template_path.exists():
|
|
20
|
+
print(red("Templates not found — run 'lotek init' first"))
|
|
21
|
+
return 1
|
|
22
|
+
template = template_path.read_text()
|
|
23
|
+
fp.write_text(template.replace("{title}", title).replace("{date}", today))
|
|
24
|
+
print(green(f"Created: content/posts/{fname}"))
|
|
25
|
+
return 0
|
lotek/cmd/build.py
ADDED
lotek/cmd/clean.py
ADDED
lotek/cmd/deploy.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
from lotek.lib.colors import green, red
|
|
4
|
+
from lotek.lib.dirs import dirs
|
|
5
|
+
from lotek.cmd.build import cmd_build
|
|
6
|
+
|
|
7
|
+
def read_env():
|
|
8
|
+
env_path = dirs.CWD / ".env"
|
|
9
|
+
if not env_path.exists():
|
|
10
|
+
return {}
|
|
11
|
+
env = {}
|
|
12
|
+
for line in env_path.read_text().splitlines():
|
|
13
|
+
if "=" in line and not line.startswith("#"):
|
|
14
|
+
k, _, v = line.partition("=")
|
|
15
|
+
env[k.strip()] = v.strip()
|
|
16
|
+
return env
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cmd_deploy(skip_build=False):
|
|
20
|
+
env = read_env()
|
|
21
|
+
user, host, path = (
|
|
22
|
+
env.get("DEPLOY_USER"),
|
|
23
|
+
env.get("DEPLOY_HOST"),
|
|
24
|
+
env.get("DEPLOY_PATH"),
|
|
25
|
+
)
|
|
26
|
+
if not all([user, host, path]):
|
|
27
|
+
print(red("Missing DEPLOY_USER, DEPLOY_HOST, or DEPLOY_PATH in .env"))
|
|
28
|
+
return 1
|
|
29
|
+
if not skip_build:
|
|
30
|
+
print(green("Building..."))
|
|
31
|
+
rc = cmd_build()
|
|
32
|
+
if rc != 0:
|
|
33
|
+
return rc
|
|
34
|
+
dest = f"{user}@{host}:{path}/"
|
|
35
|
+
print(green(f"Deploying to {dest}"))
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
[
|
|
38
|
+
"rsync",
|
|
39
|
+
"-avz",
|
|
40
|
+
"--exclude=.env",
|
|
41
|
+
"--exclude=*.pyc",
|
|
42
|
+
"--exclude=__pycache__",
|
|
43
|
+
"--exclude=output",
|
|
44
|
+
"output/",
|
|
45
|
+
dest,
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
if result.returncode != 0:
|
|
49
|
+
print(red("Deploy failed"))
|
|
50
|
+
return result.returncode
|
|
51
|
+
print(green("Deployed successfully"))
|
|
52
|
+
return 0
|
lotek/cmd/list.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from lotek.lib.site_config import config
|
|
4
|
+
from lotek.lib.dirs import dirs
|
|
5
|
+
from lotek.lib.frontmatter import parse_frontmatter
|
|
6
|
+
from lotek.lib.colors import green, BOLD, RESET
|
|
7
|
+
|
|
8
|
+
def _table(headers, rows):
|
|
9
|
+
widths = [len(h) for h in headers]
|
|
10
|
+
for row in rows:
|
|
11
|
+
for i, c in enumerate(row):
|
|
12
|
+
widths[i] = max(widths[i], len(str(c)))
|
|
13
|
+
hdr = "│ " + " │ ".join(h.center(w) for h, w in zip(headers, widths)) + " │"
|
|
14
|
+
print(BOLD + hdr + RESET)
|
|
15
|
+
print("-" * len(hdr))
|
|
16
|
+
for row in rows:
|
|
17
|
+
print("│ " + " │ ".join(str(c).ljust(w) for c, w in zip(row, widths)) + " │")
|
|
18
|
+
|
|
19
|
+
def cmd_list():
|
|
20
|
+
posts_dir = dirs.CONTENT_POSTS
|
|
21
|
+
if not posts_dir.exists():
|
|
22
|
+
print("No posts directory found")
|
|
23
|
+
return 0
|
|
24
|
+
today = datetime.now().date()
|
|
25
|
+
posts = []
|
|
26
|
+
for f in posts_dir.glob("*.md"):
|
|
27
|
+
meta, _ = parse_frontmatter(f.read_text())
|
|
28
|
+
if meta.get("title"):
|
|
29
|
+
posts.append((f, meta))
|
|
30
|
+
if not posts:
|
|
31
|
+
print("No posts found")
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
def sort_key(item):
|
|
35
|
+
try:
|
|
36
|
+
return datetime.strptime(item[1].get("date", ""), "%Y-%m-%d").date()
|
|
37
|
+
except ValueError:
|
|
38
|
+
return today
|
|
39
|
+
|
|
40
|
+
posts.sort(key=sort_key, reverse=True)
|
|
41
|
+
rows = []
|
|
42
|
+
for f, meta in posts:
|
|
43
|
+
date_str = meta.get("date", "")
|
|
44
|
+
if meta.get("publish", "").lower() == "false":
|
|
45
|
+
state = "hidden"
|
|
46
|
+
elif config.features.skip_future:
|
|
47
|
+
try:
|
|
48
|
+
d = datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
49
|
+
state = f"in {(d - today).days}d" if d > today else "live"
|
|
50
|
+
except ValueError:
|
|
51
|
+
state = "live"
|
|
52
|
+
else:
|
|
53
|
+
state = "live"
|
|
54
|
+
rows.append([date_str, meta.get("title", ""), f.stem, state])
|
|
55
|
+
print(green(f"{len(posts)} post(s)"))
|
|
56
|
+
_table(["date", "title", "slug", "status"], rows)
|
|
57
|
+
return 0
|
lotek/cmd/publish.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from lotek.lib.frontmatter import parse_frontmatter
|
|
4
|
+
from lotek.lib.colors import green, red
|
|
5
|
+
from lotek.lib.dirs import dirs
|
|
6
|
+
|
|
7
|
+
def _strip_datecode(stem):
|
|
8
|
+
if (
|
|
9
|
+
len(stem) > 11
|
|
10
|
+
and stem.startswith("20")
|
|
11
|
+
and stem[4] == stem[7] == "-"
|
|
12
|
+
and stem[10] == "-"
|
|
13
|
+
):
|
|
14
|
+
return stem[11:]
|
|
15
|
+
return stem
|
|
16
|
+
|
|
17
|
+
def find_post(slug):
|
|
18
|
+
posts_dir = dirs.CONTENT_POSTS
|
|
19
|
+
if not posts_dir.exists():
|
|
20
|
+
return None
|
|
21
|
+
fp = posts_dir / f"{slug}.md"
|
|
22
|
+
if fp.exists():
|
|
23
|
+
return fp
|
|
24
|
+
for f in posts_dir.glob("*.md"):
|
|
25
|
+
if slug == _strip_datecode(f.stem):
|
|
26
|
+
return f
|
|
27
|
+
matches = [
|
|
28
|
+
f for f in posts_dir.glob("*.md") if _strip_datecode(f.stem).startswith(slug)
|
|
29
|
+
]
|
|
30
|
+
if len(matches) == 1:
|
|
31
|
+
return matches[0]
|
|
32
|
+
if len(matches) > 1:
|
|
33
|
+
print(red(f"Ambiguous: {len(matches)} matches for '{slug}'"))
|
|
34
|
+
for m in matches:
|
|
35
|
+
print(f" {m.stem}")
|
|
36
|
+
sys.exit(2)
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
def _set_publish(slug, value):
|
|
40
|
+
fp = find_post(slug)
|
|
41
|
+
if not fp:
|
|
42
|
+
print(red(f"Not found: {slug}"))
|
|
43
|
+
return 1
|
|
44
|
+
meta, body = parse_frontmatter(fp.read_text())
|
|
45
|
+
if not meta.get("title"):
|
|
46
|
+
print(red("No title in frontmatter"))
|
|
47
|
+
return 1
|
|
48
|
+
meta["publish"] = value
|
|
49
|
+
fp.write_text(
|
|
50
|
+
"---\n" + "".join(f"{k}: {v}\n" for k, v in meta.items()) + "---\n\n" + body
|
|
51
|
+
)
|
|
52
|
+
print(green(f"{'Published' if value == 'true' else 'Unpublished'}: {slug}"))
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cmd_publish(slug):
|
|
57
|
+
""" i don't know if we even need these, really."""
|
|
58
|
+
return _set_publish(slug, "true")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cmd_unpublish(slug):
|
|
62
|
+
""" i don't know if we even need these, really."""
|
|
63
|
+
return _set_publish(slug, "false")
|
lotek/cmd/serve.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
from lotek.lib.colors import green, red
|
|
4
|
+
from lotek.lib.dirs import dirs
|
|
5
|
+
|
|
6
|
+
def cmd_serve(port=8000):
|
|
7
|
+
output = dirs.OUTPUT
|
|
8
|
+
if not output.exists():
|
|
9
|
+
print(red("No output/ — run 'lotek build' first."))
|
|
10
|
+
return 1
|
|
11
|
+
print(green(f"Serving at http://localhost:{port} (Ctrl-C to stop)"))
|
|
12
|
+
try:
|
|
13
|
+
subprocess.run(
|
|
14
|
+
[sys.executable, "-m", "http.server", str(port), "-d", str(output)]
|
|
15
|
+
)
|
|
16
|
+
except KeyboardInterrupt:
|
|
17
|
+
pass
|
|
18
|
+
return 0
|
lotek/lib/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core library modules for lotek.run."""
|
lotek/lib/about.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""generate the about page"""
|
|
2
|
+
|
|
3
|
+
from lotek.lib.site_config import config
|
|
4
|
+
from lotek.lib.dirs import dirs
|
|
5
|
+
from lotek.lib.render import render, render_wrap, md_to_html
|
|
6
|
+
from lotek.lib.frontmatter import parse_frontmatter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_about(out):
|
|
10
|
+
about = dirs.CONTENT_PAGES / "about.md"
|
|
11
|
+
if about.exists():
|
|
12
|
+
meta, body = parse_frontmatter(about.read_text())
|
|
13
|
+
html = md_to_html(body)
|
|
14
|
+
content = render(
|
|
15
|
+
"post.html",
|
|
16
|
+
{
|
|
17
|
+
"TITLE": meta.get("title", "About"),
|
|
18
|
+
"DATE": "",
|
|
19
|
+
"CONTENT": html,
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
(out / "about.html").write_text(
|
|
23
|
+
render_wrap(
|
|
24
|
+
content,
|
|
25
|
+
f"About - {config.site.title}",
|
|
26
|
+
url=f"{config.site.url}/about.html",
|
|
27
|
+
)
|
|
28
|
+
)
|
lotek/lib/colors.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
BOLD, RESET = "\033[1m", "\033[0m"
|
|
5
|
+
|
|
6
|
+
def _color(t, c):
|
|
7
|
+
if os.isatty(sys.stderr.fileno()):
|
|
8
|
+
return c + t + RESET
|
|
9
|
+
return t
|
|
10
|
+
|
|
11
|
+
def green(t):
|
|
12
|
+
return _color(t, "\033[32m")
|
|
13
|
+
|
|
14
|
+
def red(t):
|
|
15
|
+
return _color(t, "\033[31m")
|
|
16
|
+
|
|
17
|
+
def dim(t):
|
|
18
|
+
return _color(t, "\033[2m")
|
lotek/lib/dirs.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""lotek directory structure"""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
class Dirs:
|
|
5
|
+
"""lotek directory structure"""
|
|
6
|
+
def __init__(self, path=Path.cwd()):
|
|
7
|
+
# pylint: disable=invalid-name
|
|
8
|
+
self.CWD = path
|
|
9
|
+
self.CONTENT = self.CWD / "content"
|
|
10
|
+
self.CONTENT_POSTS = self.CONTENT / "posts"
|
|
11
|
+
self.CONTENT_PAGES = self.CONTENT / "pages"
|
|
12
|
+
|
|
13
|
+
self.STATIC = self.CWD / "static"
|
|
14
|
+
self.IMAGES = self.CWD / "static" / "img"
|
|
15
|
+
self.TEMPLATES = self.CWD / "templates"
|
|
16
|
+
|
|
17
|
+
self.OUTPUT = self.CWD / "output"
|
|
18
|
+
self.OUTPUT_POSTS = self.CWD / "output" / "posts"
|
|
19
|
+
self.OUTPUT_STATIC = self.CWD / "output" / "static"
|
|
20
|
+
|
|
21
|
+
# expected to be buried somewhere in site-packages
|
|
22
|
+
self.PKG = _pkg_path = Path(__file__).parent.parent
|
|
23
|
+
self.PKG_TEMPLATES = self.PKG / "templates"
|
|
24
|
+
self.PKG_STATIC = self.PKG / "static"
|
|
25
|
+
|
|
26
|
+
dirs = Dirs()
|
lotek/lib/frontmatter.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""frontmatter parsing for markdown files"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_frontmatter(text):
|
|
5
|
+
"""parse the frontmatter from a markdown file, returning a dict of metadata and the body text"""
|
|
6
|
+
if not text.startswith("---\n"):
|
|
7
|
+
return {}, text
|
|
8
|
+
try:
|
|
9
|
+
end = text.index("\n---\n", 4)
|
|
10
|
+
except ValueError:
|
|
11
|
+
return {}, text
|
|
12
|
+
meta = {}
|
|
13
|
+
for line in text[4:end].splitlines():
|
|
14
|
+
if ":" in line:
|
|
15
|
+
k, _, v = line.partition(":")
|
|
16
|
+
meta[k.strip()] = v.strip()
|
|
17
|
+
return meta, text[end + 5 :]
|
lotek/lib/highlight.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Syntax highlighting via Pygments."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pygments import highlight
|
|
5
|
+
from pygments.formatters import HtmlFormatter
|
|
6
|
+
from pygments.lexers import get_lexer_by_name
|
|
7
|
+
from pygments.util import ClassNotFound
|
|
8
|
+
|
|
9
|
+
_FENCE = re.compile(r"^```(\w*)\n([\s\S]*?)^```[ \t]*$", re.MULTILINE)
|
|
10
|
+
_formatter = HtmlFormatter(style="default")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def process_code_blocks(text):
|
|
14
|
+
def replace(m):
|
|
15
|
+
lang = m.group(1).strip().lower() or "text"
|
|
16
|
+
code = m.group(2)
|
|
17
|
+
if code.endswith("\n"):
|
|
18
|
+
code = code[:-1]
|
|
19
|
+
try:
|
|
20
|
+
lexer = get_lexer_by_name(lang, stripall=False)
|
|
21
|
+
except ClassNotFound:
|
|
22
|
+
lexer = get_lexer_by_name("text")
|
|
23
|
+
return "\n\n" + highlight(code, lexer, _formatter) + "\n\n"
|
|
24
|
+
|
|
25
|
+
return _FENCE.sub(replace, text)
|
lotek/lib/html_stubs.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""HTML stubs for rendering templates."""
|
|
2
|
+
|
|
3
|
+
from lotek.lib.site_config import config
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def html_stub_index(post):
|
|
7
|
+
"""Generate the HTML for a single post in the index page."""
|
|
8
|
+
return f"""
|
|
9
|
+
<li><span class="date">{post["date"]}</span>
|
|
10
|
+
<a href="posts/{post["slug"]}.html">{post["title"]}</a>
|
|
11
|
+
</li>\n
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def html_stub_feed_items(post, html):
|
|
16
|
+
"""Generate the HTML for a single post in the feed."""
|
|
17
|
+
return f"""
|
|
18
|
+
<item>
|
|
19
|
+
<title>{post['title']}</title>
|
|
20
|
+
<link>{config.site.url}/posts/{post['slug']}.html</link>
|
|
21
|
+
<pubDate>{post['date']}</pubDate>
|
|
22
|
+
<description><![CDATA[{html}]]></description>
|
|
23
|
+
</item>\n"""
|
lotek/lib/index.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""index page generator"""
|
|
2
|
+
|
|
3
|
+
from lotek.lib.site_config import config
|
|
4
|
+
from lotek.lib.site_time import now_string
|
|
5
|
+
from lotek.lib.html_stubs import html_stub_index
|
|
6
|
+
from lotek.lib.render import render, render_wrap
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_index_landing(posts, out):
|
|
10
|
+
"""Generate the index landing page."""
|
|
11
|
+
items = ""
|
|
12
|
+
for post in posts:
|
|
13
|
+
items += html_stub_index(post)
|
|
14
|
+
content = render("index.html", {"ITEMS": items, "DESC": config.site.description})
|
|
15
|
+
(out / "index.html").write_text(
|
|
16
|
+
render_wrap(content, config.site.title, url=config.site.url)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
last_file = out / "_last"
|
|
20
|
+
last_file.write_text(now_string())
|
lotek/lib/init.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Site initialization command for lotek.run."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from datetime import date
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from lotek.lib.site_config import DEFAULT_CONFIG_TEMPLATE_PATH
|
|
7
|
+
from lotek.lib.dirs import Dirs
|
|
8
|
+
|
|
9
|
+
# Get the package root directory
|
|
10
|
+
_pkg_path = Path(__file__).parent.parent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def init(site_path: Path) -> None:
|
|
14
|
+
"""Initialize a new lotek site in the given directory.
|
|
15
|
+
|
|
16
|
+
Creates the directory structure, copies templates, and generates site-config.toml.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
site_path: Path to the site directory to initialize. Uses cwd if not provided.
|
|
20
|
+
"""
|
|
21
|
+
print(f"working path is: {site_path}")
|
|
22
|
+
|
|
23
|
+
dirs: Dirs = Dirs(site_path)
|
|
24
|
+
# Create directory structure
|
|
25
|
+
site_path.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
dirs.CONTENT.mkdir(exist_ok=True)
|
|
27
|
+
dirs.CONTENT_POSTS.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
dirs.CONTENT_PAGES.mkdir(exist_ok=True)
|
|
29
|
+
dirs.STATIC.mkdir(exist_ok=True)
|
|
30
|
+
dirs.IMAGES.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
dirs.TEMPLATES.mkdir(exist_ok=True)
|
|
32
|
+
# Copy templates
|
|
33
|
+
print("Copying templates...")
|
|
34
|
+
for template in dirs.PKG_TEMPLATES.glob("*"):
|
|
35
|
+
dst = dirs.TEMPLATES / template.name
|
|
36
|
+
shutil.copy2(template, dst)
|
|
37
|
+
|
|
38
|
+
# Copy static files
|
|
39
|
+
print("Populating static directory..")
|
|
40
|
+
static_src = dirs.PKG_STATIC
|
|
41
|
+
for item in static_src.iterdir():
|
|
42
|
+
if item.is_dir():
|
|
43
|
+
shutil.copytree(item, dirs.STATIC / item.name, dirs_exist_ok=True)
|
|
44
|
+
else:
|
|
45
|
+
shutil.copy2(item, dirs.STATIC)
|
|
46
|
+
|
|
47
|
+
# Create site-config.toml from defaults
|
|
48
|
+
config_path = site_path / "site-config.toml"
|
|
49
|
+
if not config_path.exists():
|
|
50
|
+
print(f"Creating {config_path} from template...")
|
|
51
|
+
config_path.write_text(DEFAULT_CONFIG_TEMPLATE_PATH.read_text())
|
|
52
|
+
print("✓ Site configuration created")
|
|
53
|
+
|
|
54
|
+
# Create an about page
|
|
55
|
+
about_path = dirs.CONTENT_PAGES / "about.md"
|
|
56
|
+
if not about_path.exists():
|
|
57
|
+
print(f"Creating {about_path} from template...")
|
|
58
|
+
example_about = """---
|
|
59
|
+
title: About
|
|
60
|
+
date: 2026-06-15
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
lotek is a small static blog generator. It uses very little technology to do this.
|
|
64
|
+
|
|
65
|
+
The name comes from the Lo-Tek in William Gibson's Johnny Mnemonic -- an underground community that lives in the margins of the city, outside the corporate system. Not against technology. Against the assumption that more technology is always better, that the newest version is always correct, that you should replace what works because something newer exists.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
**rss**: /feed.xml
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
Built with pandoc and a Python script. No npm. No framework. No build chain.
|
|
74
|
+
"""
|
|
75
|
+
about_path.write_text(example_about)
|
|
76
|
+
print("✓ About page created")
|
|
77
|
+
|
|
78
|
+
# Create an example post
|
|
79
|
+
today = date.today().strftime("%Y-%m-%d")
|
|
80
|
+
example_post = dirs.CONTENT_POSTS / f"{today}-welcome.md"
|
|
81
|
+
if not example_post.exists():
|
|
82
|
+
example_content = f"""---
|
|
83
|
+
title: Welcome to Lotek
|
|
84
|
+
date: {today}
|
|
85
|
+
desc: Your first post
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
Congratulations! You've created a new lotek site.
|
|
89
|
+
|
|
90
|
+
This is your first post. Edit or delete this file to get started.
|
|
91
|
+
|
|
92
|
+
## Quick Start
|
|
93
|
+
|
|
94
|
+
1. Add more posts in `content/posts/`
|
|
95
|
+
2. Add static pages in `content/pages/`
|
|
96
|
+
3. Customize `site-config.toml` to change site settings
|
|
97
|
+
4. Run `lotek build` to generate the site
|
|
98
|
+
5. Serve it with `lotek serve`
|
|
99
|
+
"""
|
|
100
|
+
example_post.write_text(example_content)
|
|
101
|
+
print("✓ Example post created")
|
|
102
|
+
|
|
103
|
+
print(f"\nSite initialized at: {dirs.CWD}")
|
|
104
|
+
print("Next steps:")
|
|
105
|
+
print(" - Edit content in content/posts/ and content/pages/")
|
|
106
|
+
print(" - Customize settings in site-config.toml")
|
|
107
|
+
print(" - Run 'lotek build' to generate the site")
|