lotek 0.2.0__tar.gz
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-0.2.0/PKG-INFO +164 -0
- lotek-0.2.0/README.md +139 -0
- lotek-0.2.0/lotek/__init__.py +13 -0
- lotek-0.2.0/lotek/build.py +41 -0
- lotek-0.2.0/lotek/cli.py +99 -0
- lotek-0.2.0/lotek/cmd/__init__.py +0 -0
- lotek-0.2.0/lotek/cmd/add.py +25 -0
- lotek-0.2.0/lotek/cmd/build.py +12 -0
- lotek-0.2.0/lotek/cmd/clean.py +12 -0
- lotek-0.2.0/lotek/cmd/deploy.py +52 -0
- lotek-0.2.0/lotek/cmd/list.py +57 -0
- lotek-0.2.0/lotek/cmd/publish.py +63 -0
- lotek-0.2.0/lotek/cmd/serve.py +18 -0
- lotek-0.2.0/lotek/lib/__init__.py +1 -0
- lotek-0.2.0/lotek/lib/about.py +28 -0
- lotek-0.2.0/lotek/lib/colors.py +18 -0
- lotek-0.2.0/lotek/lib/dirs.py +26 -0
- lotek-0.2.0/lotek/lib/frontmatter.py +17 -0
- lotek-0.2.0/lotek/lib/highlight.py +25 -0
- lotek-0.2.0/lotek/lib/html_stubs.py +23 -0
- lotek-0.2.0/lotek/lib/index.py +20 -0
- lotek-0.2.0/lotek/lib/init.py +107 -0
- lotek-0.2.0/lotek/lib/pages.py +36 -0
- lotek-0.2.0/lotek/lib/posts.py +88 -0
- lotek-0.2.0/lotek/lib/render.py +65 -0
- lotek-0.2.0/lotek/lib/site_config.py +54 -0
- lotek-0.2.0/lotek/lib/site_time.py +9 -0
- lotek-0.2.0/lotek/lib/static.py +11 -0
- lotek-0.2.0/lotek/plugins/__init__.py +1 -0
- lotek-0.2.0/lotek/plugins/robots.py +31 -0
- lotek-0.2.0/lotek/plugins/rss.py +28 -0
- lotek-0.2.0/lotek/site-default.toml +38 -0
- lotek-0.2.0/lotek/static/index.html +0 -0
- lotek-0.2.0/lotek/static/pygments.css +75 -0
- lotek-0.2.0/lotek/static/style.css +264 -0
- lotek-0.2.0/lotek/templates/base.html +34 -0
- lotek-0.2.0/lotek/templates/feed.xml +10 -0
- lotek-0.2.0/lotek/templates/index.html +2 -0
- lotek-0.2.0/lotek/templates/post.html +6 -0
- lotek-0.2.0/lotek/templates/post.md +7 -0
- lotek-0.2.0/lotek.egg-info/PKG-INFO +164 -0
- lotek-0.2.0/lotek.egg-info/SOURCES.txt +53 -0
- lotek-0.2.0/lotek.egg-info/dependency_links.txt +1 -0
- lotek-0.2.0/lotek.egg-info/entry_points.txt +2 -0
- lotek-0.2.0/lotek.egg-info/requires.txt +8 -0
- lotek-0.2.0/lotek.egg-info/top_level.txt +1 -0
- lotek-0.2.0/pyproject.toml +55 -0
- lotek-0.2.0/setup.cfg +4 -0
- lotek-0.2.0/tests/test_build.py +219 -0
- lotek-0.2.0/tests/test_frontmatter.py +54 -0
- lotek-0.2.0/tests/test_highlight.py +52 -0
- lotek-0.2.0/tests/test_html_stubs.py +111 -0
- lotek-0.2.0/tests/test_render.py +113 -0
- lotek-0.2.0/tests/test_robots.py +201 -0
- lotek-0.2.0/tests/test_rss.py +153 -0
lotek-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lotek
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A static site builder
|
|
5
|
+
Author-email: Brad Arnett <brad.arnett@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://get.lotek.run
|
|
8
|
+
Project-URL: Repository, https://github.com/lotek/lotek.run
|
|
9
|
+
Keywords: static-site,builder,markdown
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: tomli>=1.2; python_version < "3.11"
|
|
21
|
+
Requires-Dist: markdown>=3.0
|
|
22
|
+
Requires-Dist: pygments>=2.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
25
|
+
|
|
26
|
+
# lotek.run
|
|
27
|
+
|
|
28
|
+
A minimal static blog. Pandoc recommended; falls back to the `markdown` Python module if not available.
|
|
29
|
+
|
|
30
|
+
## install
|
|
31
|
+
|
|
32
|
+
Install the package:
|
|
33
|
+
|
|
34
|
+
pip install .
|
|
35
|
+
|
|
36
|
+
Or install in development mode:
|
|
37
|
+
|
|
38
|
+
pip install -e .
|
|
39
|
+
|
|
40
|
+
Then use the **lotek** command:
|
|
41
|
+
|
|
42
|
+
Output goes to `output/`. Serve it with literally anything.
|
|
43
|
+
|
|
44
|
+
## cli
|
|
45
|
+
|
|
46
|
+
For day-to-day post management and operations, use the **lotek** command:
|
|
47
|
+
|
|
48
|
+
lotek init Initialize a new site in the current directory
|
|
49
|
+
lotek build Build the site
|
|
50
|
+
lotek clean Remove build output
|
|
51
|
+
lotek serve [--port N] Serve output locally (default: 8000)
|
|
52
|
+
lotek deploy Build and deploy via rsync (reads .env)
|
|
53
|
+
|
|
54
|
+
lotek list List all posts (formatted table)
|
|
55
|
+
lotek add "Title" Create new post
|
|
56
|
+
lotek publish <slug> Mark a post as published
|
|
57
|
+
lotek unpublish <slug> Mark a post as unpublished
|
|
58
|
+
|
|
59
|
+
All files are human-editable and can be edited directly. It is recommended to preface your post files with a datecode, however it is not directly enforced. Note that the `lotek add` command will automatically add a datecode to the title, as well as providing the frontmatter template needed to be recognized by the lotek command.
|
|
60
|
+
|
|
61
|
+
It might seem weird that the only content editing command lotek exposes is "add". But there's no need for any other tools, because we refuse to enforce their need. With no dependencies come no obligations:
|
|
62
|
+
|
|
63
|
+
Need to delete a file? Use `rm`. Need to search? Use `grep -r`. Want a UI? Use an IDE like Sublime Text or VS Code. Use Obsidian. Don't like any of those options? Roll your own. The pieces are all here and exposed in plain view.
|
|
64
|
+
|
|
65
|
+
## structure
|
|
66
|
+
|
|
67
|
+
content/posts/ markdown source files (YYYY-MM-DD-slug.md)
|
|
68
|
+
content/pages/ static pages (about.md, now.md, etc.)
|
|
69
|
+
templates/ html/xml templates
|
|
70
|
+
static/ css and any other static assets
|
|
71
|
+
output/ generated site (gitignored)
|
|
72
|
+
|
|
73
|
+
## frontmatter
|
|
74
|
+
|
|
75
|
+
Posts and pages share the same frontmatter schema:
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
title: Post Title
|
|
79
|
+
date: YYYY-MM-DD
|
|
80
|
+
tags: tag1, tag2
|
|
81
|
+
publish: true
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
Set `publish: false` to suppress a file from the build without deleting it. `lotek publish` and `lotek unpublish` manage this field for posts.
|
|
85
|
+
|
|
86
|
+
## pages
|
|
87
|
+
|
|
88
|
+
Any `.md` file in `content/pages/` is built as a standalone page at `/<slug>.html`. Drop a file, wire up a nav link if you want it linked, done.
|
|
89
|
+
|
|
90
|
+
content/pages/about.md → /about.html
|
|
91
|
+
content/pages/now.md → /now.html
|
|
92
|
+
|
|
93
|
+
`publish: false` in frontmatter suppresses the build, same as posts.
|
|
94
|
+
|
|
95
|
+
## 404
|
|
96
|
+
|
|
97
|
+
`output/404.html` is generated on every build with full site chrome. Point your server at it:
|
|
98
|
+
|
|
99
|
+
- Apache: `ErrorDocument 404 /404.html`
|
|
100
|
+
- nginx: `error_page 404 /404.html`
|
|
101
|
+
|
|
102
|
+
## config
|
|
103
|
+
|
|
104
|
+
Site settings live in `site-config.toml`:
|
|
105
|
+
|
|
106
|
+
[site.features]
|
|
107
|
+
robotstxt = true # robots.txt + sitemap generation
|
|
108
|
+
rss = true # RSS feed generation
|
|
109
|
+
skip_future = true # exclude posts with dates > today
|
|
110
|
+
|
|
111
|
+
[site.rss]
|
|
112
|
+
limit = 10
|
|
113
|
+
timezone = "America/Los_Angeles"
|
|
114
|
+
|
|
115
|
+
[site.site]
|
|
116
|
+
title = "lotek.run"
|
|
117
|
+
url = "https://lotek.run"
|
|
118
|
+
description = "dispatches from the margins"
|
|
119
|
+
|
|
120
|
+
[[site.nav]]
|
|
121
|
+
label = "index"
|
|
122
|
+
href = "/"
|
|
123
|
+
|
|
124
|
+
[[site.nav]]
|
|
125
|
+
label = "about"
|
|
126
|
+
href = "/about.html"
|
|
127
|
+
|
|
128
|
+
Nav links are ordered and fully configurable. Add, remove, or reorder `[[site.nav]]` blocks to change what appears in the header. If no nav is configured, the defaults (index, about, rss) are used.
|
|
129
|
+
|
|
130
|
+
## deploy
|
|
131
|
+
|
|
132
|
+
Set these in `.env` (see `.env.example`):
|
|
133
|
+
|
|
134
|
+
DEPLOY_USER=user
|
|
135
|
+
DEPLOY_HOST=example.com
|
|
136
|
+
DEPLOY_PATH=/var/www/html
|
|
137
|
+
|
|
138
|
+
Then run `lotek deploy`.
|
|
139
|
+
|
|
140
|
+
## automation
|
|
141
|
+
|
|
142
|
+
I've set up automation via gitea runners personally, but that's out of scope of this project directly. I recommend figuring it out on your own but if I get requests for tutorials I'll probably write about it.
|
|
143
|
+
|
|
144
|
+
## philosophy
|
|
145
|
+
|
|
146
|
+
No npm. No webpack. No framework. No build chain with a thousand dependencies ready to be poisoned upstream at any goddamn minute.
|
|
147
|
+
Pandoc converts markdown to HTML. A Python script assembles pages from templates.
|
|
148
|
+
The CSS is a flat file. Nothing requires Node.js. TA DA.
|
|
149
|
+
|
|
150
|
+
## requirements
|
|
151
|
+
|
|
152
|
+
Python 3.9+. Pandoc recommended; falls back to the `markdown` package if not found.
|
|
153
|
+
|
|
154
|
+
If pandoc is not installed, install the fallback:
|
|
155
|
+
|
|
156
|
+
pip install markdown
|
|
157
|
+
|
|
158
|
+
## getting started
|
|
159
|
+
|
|
160
|
+
mkdir mysite && cd mysite
|
|
161
|
+
lotek init
|
|
162
|
+
# edit site-config.toml with your title/url
|
|
163
|
+
lotek build
|
|
164
|
+
lotek serve
|
lotek-0.2.0/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# lotek.run
|
|
2
|
+
|
|
3
|
+
A minimal static blog. Pandoc recommended; falls back to the `markdown` Python module if not available.
|
|
4
|
+
|
|
5
|
+
## install
|
|
6
|
+
|
|
7
|
+
Install the package:
|
|
8
|
+
|
|
9
|
+
pip install .
|
|
10
|
+
|
|
11
|
+
Or install in development mode:
|
|
12
|
+
|
|
13
|
+
pip install -e .
|
|
14
|
+
|
|
15
|
+
Then use the **lotek** command:
|
|
16
|
+
|
|
17
|
+
Output goes to `output/`. Serve it with literally anything.
|
|
18
|
+
|
|
19
|
+
## cli
|
|
20
|
+
|
|
21
|
+
For day-to-day post management and operations, use the **lotek** command:
|
|
22
|
+
|
|
23
|
+
lotek init Initialize a new site in the current directory
|
|
24
|
+
lotek build Build the site
|
|
25
|
+
lotek clean Remove build output
|
|
26
|
+
lotek serve [--port N] Serve output locally (default: 8000)
|
|
27
|
+
lotek deploy Build and deploy via rsync (reads .env)
|
|
28
|
+
|
|
29
|
+
lotek list List all posts (formatted table)
|
|
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
|
+
All files are human-editable and can be edited directly. It is recommended to preface your post files with a datecode, however it is not directly enforced. Note that the `lotek add` command will automatically add a datecode to the title, as well as providing the frontmatter template needed to be recognized by the lotek command.
|
|
35
|
+
|
|
36
|
+
It might seem weird that the only content editing command lotek exposes is "add". But there's no need for any other tools, because we refuse to enforce their need. With no dependencies come no obligations:
|
|
37
|
+
|
|
38
|
+
Need to delete a file? Use `rm`. Need to search? Use `grep -r`. Want a UI? Use an IDE like Sublime Text or VS Code. Use Obsidian. Don't like any of those options? Roll your own. The pieces are all here and exposed in plain view.
|
|
39
|
+
|
|
40
|
+
## structure
|
|
41
|
+
|
|
42
|
+
content/posts/ markdown source files (YYYY-MM-DD-slug.md)
|
|
43
|
+
content/pages/ static pages (about.md, now.md, etc.)
|
|
44
|
+
templates/ html/xml templates
|
|
45
|
+
static/ css and any other static assets
|
|
46
|
+
output/ generated site (gitignored)
|
|
47
|
+
|
|
48
|
+
## frontmatter
|
|
49
|
+
|
|
50
|
+
Posts and pages share the same frontmatter schema:
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
title: Post Title
|
|
54
|
+
date: YYYY-MM-DD
|
|
55
|
+
tags: tag1, tag2
|
|
56
|
+
publish: true
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
Set `publish: false` to suppress a file from the build without deleting it. `lotek publish` and `lotek unpublish` manage this field for posts.
|
|
60
|
+
|
|
61
|
+
## pages
|
|
62
|
+
|
|
63
|
+
Any `.md` file in `content/pages/` is built as a standalone page at `/<slug>.html`. Drop a file, wire up a nav link if you want it linked, done.
|
|
64
|
+
|
|
65
|
+
content/pages/about.md → /about.html
|
|
66
|
+
content/pages/now.md → /now.html
|
|
67
|
+
|
|
68
|
+
`publish: false` in frontmatter suppresses the build, same as posts.
|
|
69
|
+
|
|
70
|
+
## 404
|
|
71
|
+
|
|
72
|
+
`output/404.html` is generated on every build with full site chrome. Point your server at it:
|
|
73
|
+
|
|
74
|
+
- Apache: `ErrorDocument 404 /404.html`
|
|
75
|
+
- nginx: `error_page 404 /404.html`
|
|
76
|
+
|
|
77
|
+
## config
|
|
78
|
+
|
|
79
|
+
Site settings live in `site-config.toml`:
|
|
80
|
+
|
|
81
|
+
[site.features]
|
|
82
|
+
robotstxt = true # robots.txt + sitemap generation
|
|
83
|
+
rss = true # RSS feed generation
|
|
84
|
+
skip_future = true # exclude posts with dates > today
|
|
85
|
+
|
|
86
|
+
[site.rss]
|
|
87
|
+
limit = 10
|
|
88
|
+
timezone = "America/Los_Angeles"
|
|
89
|
+
|
|
90
|
+
[site.site]
|
|
91
|
+
title = "lotek.run"
|
|
92
|
+
url = "https://lotek.run"
|
|
93
|
+
description = "dispatches from the margins"
|
|
94
|
+
|
|
95
|
+
[[site.nav]]
|
|
96
|
+
label = "index"
|
|
97
|
+
href = "/"
|
|
98
|
+
|
|
99
|
+
[[site.nav]]
|
|
100
|
+
label = "about"
|
|
101
|
+
href = "/about.html"
|
|
102
|
+
|
|
103
|
+
Nav links are ordered and fully configurable. Add, remove, or reorder `[[site.nav]]` blocks to change what appears in the header. If no nav is configured, the defaults (index, about, rss) are used.
|
|
104
|
+
|
|
105
|
+
## deploy
|
|
106
|
+
|
|
107
|
+
Set these in `.env` (see `.env.example`):
|
|
108
|
+
|
|
109
|
+
DEPLOY_USER=user
|
|
110
|
+
DEPLOY_HOST=example.com
|
|
111
|
+
DEPLOY_PATH=/var/www/html
|
|
112
|
+
|
|
113
|
+
Then run `lotek deploy`.
|
|
114
|
+
|
|
115
|
+
## automation
|
|
116
|
+
|
|
117
|
+
I've set up automation via gitea runners personally, but that's out of scope of this project directly. I recommend figuring it out on your own but if I get requests for tutorials I'll probably write about it.
|
|
118
|
+
|
|
119
|
+
## philosophy
|
|
120
|
+
|
|
121
|
+
No npm. No webpack. No framework. No build chain with a thousand dependencies ready to be poisoned upstream at any goddamn minute.
|
|
122
|
+
Pandoc converts markdown to HTML. A Python script assembles pages from templates.
|
|
123
|
+
The CSS is a flat file. Nothing requires Node.js. TA DA.
|
|
124
|
+
|
|
125
|
+
## requirements
|
|
126
|
+
|
|
127
|
+
Python 3.9+. Pandoc recommended; falls back to the `markdown` package if not found.
|
|
128
|
+
|
|
129
|
+
If pandoc is not installed, install the fallback:
|
|
130
|
+
|
|
131
|
+
pip install markdown
|
|
132
|
+
|
|
133
|
+
## getting started
|
|
134
|
+
|
|
135
|
+
mkdir mysite && cd mysite
|
|
136
|
+
lotek init
|
|
137
|
+
# edit site-config.toml with your title/url
|
|
138
|
+
lotek build
|
|
139
|
+
lotek serve
|
|
@@ -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()
|
|
@@ -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-0.2.0/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()
|
|
File without changes
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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")
|