bloggy 0.1.40__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.
- bloggy/__init__.py +5 -0
- bloggy/build.py +608 -0
- bloggy/config.py +134 -0
- bloggy/core.py +1618 -0
- bloggy/main.py +96 -0
- bloggy/static/scripts.js +584 -0
- bloggy/static/sidenote.css +21 -0
- bloggy-0.1.40.dist-info/METADATA +926 -0
- bloggy-0.1.40.dist-info/RECORD +13 -0
- bloggy-0.1.40.dist-info/WHEEL +5 -0
- bloggy-0.1.40.dist-info/entry_points.txt +2 -0
- bloggy-0.1.40.dist-info/licenses/LICENSE +201 -0
- bloggy-0.1.40.dist-info/top_level.txt +1 -0
bloggy/__init__.py
ADDED
bloggy/build.py
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
"""Static site generator for Bloggy
|
|
2
|
+
|
|
3
|
+
This module provides functionality to convert a folder of markdown files
|
|
4
|
+
into a standalone static website with HTML, CSS, and JavaScript files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import shutil
|
|
9
|
+
from functools import partial
|
|
10
|
+
import mistletoe as mst
|
|
11
|
+
from fasthtml.common import *
|
|
12
|
+
from monsterui.all import *
|
|
13
|
+
from .core import (
|
|
14
|
+
parse_frontmatter, get_post_title, slug_to_title,
|
|
15
|
+
from_md, extract_toc, build_toc_items, text_to_anchor,
|
|
16
|
+
build_post_tree, ContentRenderer, extract_footnotes,
|
|
17
|
+
preprocess_super_sub, preprocess_tabs,
|
|
18
|
+
get_bloggy_config, order_bloggy_entries
|
|
19
|
+
)
|
|
20
|
+
from .config import get_config, reload_config
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def generate_static_html(title, body_content, blog_title):
|
|
24
|
+
"""Generate complete static HTML page"""
|
|
25
|
+
|
|
26
|
+
# Static CSS (inline critical styles)
|
|
27
|
+
static_css = """
|
|
28
|
+
<style>
|
|
29
|
+
body { font-family: 'IBM Plex Sans', sans-serif; margin: 0; padding: 0; }
|
|
30
|
+
code, pre { font-family: 'IBM Plex Mono', monospace; }
|
|
31
|
+
.folder-chevron {
|
|
32
|
+
display: inline-block;
|
|
33
|
+
width: 0.45rem;
|
|
34
|
+
height: 0.45rem;
|
|
35
|
+
border-right: 2px solid rgb(148 163 184);
|
|
36
|
+
border-bottom: 2px solid rgb(148 163 184);
|
|
37
|
+
transform: rotate(-45deg);
|
|
38
|
+
transition: transform 0.2s;
|
|
39
|
+
}
|
|
40
|
+
details.is-open > summary .folder-chevron { transform: rotate(45deg); }
|
|
41
|
+
details { border: none !important; box-shadow: none !important; }
|
|
42
|
+
h1, h2, h3, h4, h5, h6 { scroll-margin-top: 7rem; }
|
|
43
|
+
|
|
44
|
+
/* Ultra thin scrollbar styles */
|
|
45
|
+
* { scrollbar-width: thin; scrollbar-color: rgb(203 213 225) transparent; }
|
|
46
|
+
*::-webkit-scrollbar { width: 3px; height: 3px; }
|
|
47
|
+
*::-webkit-scrollbar-track { background: transparent; }
|
|
48
|
+
*::-webkit-scrollbar-thumb { background-color: rgb(203 213 225); border-radius: 2px; }
|
|
49
|
+
*::-webkit-scrollbar-thumb:hover { background-color: rgb(148 163 184); }
|
|
50
|
+
.dark *::-webkit-scrollbar-thumb { background-color: rgb(71 85 105); }
|
|
51
|
+
.dark *::-webkit-scrollbar-thumb:hover { background-color: rgb(100 116 139); }
|
|
52
|
+
.dark * { scrollbar-color: rgb(71 85 105) transparent; }
|
|
53
|
+
|
|
54
|
+
/* Tabs styles */
|
|
55
|
+
.tabs-container {
|
|
56
|
+
margin: 2rem 0;
|
|
57
|
+
border: 1px solid rgb(226 232 240);
|
|
58
|
+
border-radius: 0.5rem;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
|
61
|
+
}
|
|
62
|
+
.dark .tabs-container {
|
|
63
|
+
border-color: rgb(51 65 85);
|
|
64
|
+
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.tabs-header {
|
|
68
|
+
display: flex;
|
|
69
|
+
background: rgb(248 250 252);
|
|
70
|
+
border-bottom: 1px solid rgb(226 232 240);
|
|
71
|
+
gap: 0;
|
|
72
|
+
}
|
|
73
|
+
.dark .tabs-header {
|
|
74
|
+
background: rgb(15 23 42);
|
|
75
|
+
border-bottom-color: rgb(51 65 85);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.tab-button {
|
|
79
|
+
flex: 1;
|
|
80
|
+
padding: 0.875rem 1.5rem;
|
|
81
|
+
background: transparent;
|
|
82
|
+
border: none;
|
|
83
|
+
border-bottom: 3px solid transparent;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
font-weight: 500;
|
|
86
|
+
font-size: 0.9375rem;
|
|
87
|
+
color: rgb(100 116 139);
|
|
88
|
+
transition: all 0.15s ease;
|
|
89
|
+
position: relative;
|
|
90
|
+
margin-bottom: -1px;
|
|
91
|
+
}
|
|
92
|
+
.dark .tab-button { color: rgb(148 163 184); }
|
|
93
|
+
|
|
94
|
+
.tab-button:hover:not(.active) {
|
|
95
|
+
background: rgb(241 245 249);
|
|
96
|
+
color: rgb(51 65 85);
|
|
97
|
+
}
|
|
98
|
+
.dark .tab-button:hover:not(.active) {
|
|
99
|
+
background: rgb(30 41 59);
|
|
100
|
+
color: rgb(226 232 240);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.tab-button.active {
|
|
104
|
+
color: rgb(15 23 42);
|
|
105
|
+
border-bottom-color: rgb(15 23 42);
|
|
106
|
+
background: white;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
}
|
|
109
|
+
.dark .tab-button.active {
|
|
110
|
+
color: rgb(248 250 252);
|
|
111
|
+
border-bottom-color: rgb(248 250 252);
|
|
112
|
+
background: rgb(2 6 23);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.tabs-content {
|
|
116
|
+
background: white;
|
|
117
|
+
position: relative;
|
|
118
|
+
}
|
|
119
|
+
.dark .tabs-content {
|
|
120
|
+
background: rgb(2 6 23);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.tab-panel {
|
|
124
|
+
padding: 1rem 1rem;
|
|
125
|
+
animation: fadeIn 0.2s ease-in;
|
|
126
|
+
position: absolute;
|
|
127
|
+
top: 0;
|
|
128
|
+
left: 0;
|
|
129
|
+
right: 0;
|
|
130
|
+
opacity: 0;
|
|
131
|
+
visibility: hidden;
|
|
132
|
+
pointer-events: none;
|
|
133
|
+
}
|
|
134
|
+
.tab-panel.active {
|
|
135
|
+
position: relative;
|
|
136
|
+
opacity: 1;
|
|
137
|
+
visibility: visible;
|
|
138
|
+
pointer-events: auto;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@keyframes fadeIn {
|
|
142
|
+
from { opacity: 0; }
|
|
143
|
+
to { opacity: 1; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Remove extra margins from first/last elements in tabs */
|
|
147
|
+
.tab-panel > *:first-child { margin-top: 0 !important; }
|
|
148
|
+
.tab-panel > *:last-child { margin-bottom: 0 !important; }
|
|
149
|
+
|
|
150
|
+
/* Ensure code blocks in tabs look good */
|
|
151
|
+
.tab-panel pre {
|
|
152
|
+
border-radius: 0.375rem;
|
|
153
|
+
font-size: 0.875rem;
|
|
154
|
+
}
|
|
155
|
+
.tab-panel code {
|
|
156
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
157
|
+
}
|
|
158
|
+
</style>
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
# JavaScript for interactivity
|
|
162
|
+
static_js = """
|
|
163
|
+
<script>
|
|
164
|
+
// Theme toggle functionality
|
|
165
|
+
(function() {
|
|
166
|
+
const stored = localStorage.getItem('__FRANKEN__');
|
|
167
|
+
const franken = stored ? JSON.parse(stored) : {mode: 'light'};
|
|
168
|
+
if (franken.mode === 'dark') {
|
|
169
|
+
document.documentElement.classList.add('dark');
|
|
170
|
+
}
|
|
171
|
+
})();
|
|
172
|
+
|
|
173
|
+
function toggleTheme() {
|
|
174
|
+
const html = document.documentElement;
|
|
175
|
+
html.classList.toggle('dark');
|
|
176
|
+
const stored = localStorage.getItem('__FRANKEN__');
|
|
177
|
+
const franken = stored ? JSON.parse(stored) : {mode: 'light'};
|
|
178
|
+
franken.mode = html.classList.contains('dark') ? 'dark' : 'light';
|
|
179
|
+
localStorage.setItem('__FRANKEN__', JSON.stringify(franken));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Tab switching functionality
|
|
183
|
+
function switchTab(tabsId, index) {
|
|
184
|
+
const container = document.querySelector('.tabs-container[data-tabs-id="' + tabsId + '"]');
|
|
185
|
+
if (!container) return;
|
|
186
|
+
|
|
187
|
+
// Update buttons
|
|
188
|
+
const buttons = container.querySelectorAll('.tab-button');
|
|
189
|
+
buttons.forEach(function(btn, i) {
|
|
190
|
+
if (i === index) {
|
|
191
|
+
btn.classList.add('active');
|
|
192
|
+
} else {
|
|
193
|
+
btn.classList.remove('active');
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Update panels
|
|
198
|
+
const panels = container.querySelectorAll('.tab-panel');
|
|
199
|
+
panels.forEach(function(panel, i) {
|
|
200
|
+
if (i === index) {
|
|
201
|
+
panel.classList.add('active');
|
|
202
|
+
panel.style.position = 'relative';
|
|
203
|
+
panel.style.visibility = 'visible';
|
|
204
|
+
panel.style.opacity = '1';
|
|
205
|
+
panel.style.pointerEvents = 'auto';
|
|
206
|
+
} else {
|
|
207
|
+
panel.classList.remove('active');
|
|
208
|
+
panel.style.position = 'absolute';
|
|
209
|
+
panel.style.visibility = 'hidden';
|
|
210
|
+
panel.style.opacity = '0';
|
|
211
|
+
panel.style.pointerEvents = 'none';
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
window.switchTab = switchTab;
|
|
216
|
+
|
|
217
|
+
// Set tab container heights based on tallest panel
|
|
218
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
document.querySelectorAll('.tabs-container').forEach(container => {
|
|
221
|
+
const panels = container.querySelectorAll('.tab-panel');
|
|
222
|
+
let maxHeight = 0;
|
|
223
|
+
|
|
224
|
+
panels.forEach(panel => {
|
|
225
|
+
const wasActive = panel.classList.contains('active');
|
|
226
|
+
panel.style.position = 'relative';
|
|
227
|
+
panel.style.visibility = 'visible';
|
|
228
|
+
panel.style.opacity = '1';
|
|
229
|
+
panel.style.pointerEvents = 'auto';
|
|
230
|
+
|
|
231
|
+
const height = panel.offsetHeight;
|
|
232
|
+
if (height > maxHeight) maxHeight = height;
|
|
233
|
+
|
|
234
|
+
if (!wasActive) {
|
|
235
|
+
panel.style.position = 'absolute';
|
|
236
|
+
panel.style.visibility = 'hidden';
|
|
237
|
+
panel.style.opacity = '0';
|
|
238
|
+
panel.style.pointerEvents = 'none';
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const tabsContent = container.querySelector('.tabs-content');
|
|
243
|
+
if (tabsContent && maxHeight > 0) {
|
|
244
|
+
tabsContent.style.minHeight = maxHeight + 'px';
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}, 100);
|
|
248
|
+
|
|
249
|
+
// Initialize KaTeX rendering
|
|
250
|
+
if (window.renderMathInElement) {
|
|
251
|
+
renderMathInElement(document.body, {
|
|
252
|
+
delimiters: [
|
|
253
|
+
{left: '$$', right: '$$', display: true},
|
|
254
|
+
{left: '$', right: '$', display: false}
|
|
255
|
+
],
|
|
256
|
+
throwOnError: false
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Sidenote interactions
|
|
262
|
+
document.addEventListener('click', function(e) {
|
|
263
|
+
if (e.target.classList.contains('sidenote-ref')) {
|
|
264
|
+
const id = e.target.id.replace('snref-', 'sn-');
|
|
265
|
+
const sidenote = document.getElementById(id);
|
|
266
|
+
if (sidenote) {
|
|
267
|
+
if (window.innerWidth >= 1280) {
|
|
268
|
+
sidenote.classList.add('hl');
|
|
269
|
+
setTimeout(() => sidenote.classList.remove('hl'), 1000);
|
|
270
|
+
} else {
|
|
271
|
+
e.target.classList.toggle('open');
|
|
272
|
+
sidenote.classList.toggle('show');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
</script>
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
html = f"""<!DOCTYPE html>
|
|
281
|
+
<html lang="en">
|
|
282
|
+
<head>
|
|
283
|
+
<meta charset="UTF-8">
|
|
284
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
285
|
+
<title>{title}</title>
|
|
286
|
+
|
|
287
|
+
<!-- TailwindCSS and MonsterUI -->
|
|
288
|
+
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,container-queries"></script>
|
|
289
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.16.14/dist/css/uikit.min.css" />
|
|
290
|
+
<script src="https://cdn.jsdelivr.net/npm/uikit@3.16.14/dist/js/uikit.min.js"></script>
|
|
291
|
+
<script src="https://cdn.jsdelivr.net/npm/uikit@3.16.14/dist/js/uikit-icons.min.js"></script>
|
|
292
|
+
|
|
293
|
+
<!-- Fonts -->
|
|
294
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
295
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
296
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono&display=swap" rel="stylesheet">
|
|
297
|
+
|
|
298
|
+
<!-- Syntax Highlighting -->
|
|
299
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
|
300
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
301
|
+
|
|
302
|
+
<!-- Math Rendering -->
|
|
303
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
|
304
|
+
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
|
305
|
+
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
|
306
|
+
|
|
307
|
+
<!-- Hyperscript for interactions -->
|
|
308
|
+
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
|
|
309
|
+
|
|
310
|
+
<!-- Static assets -->
|
|
311
|
+
<link rel="stylesheet" href="/static/sidenote.css">
|
|
312
|
+
|
|
313
|
+
{static_css}
|
|
314
|
+
</head>
|
|
315
|
+
<body>
|
|
316
|
+
{body_content}
|
|
317
|
+
|
|
318
|
+
<!-- Mermaid diagrams -->
|
|
319
|
+
<script type="module">
|
|
320
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
321
|
+
mermaid.initialize({{ startOnLoad: true, theme: 'default' }});
|
|
322
|
+
</script>
|
|
323
|
+
<script src="/static/scripts.js" type="module"></script>
|
|
324
|
+
|
|
325
|
+
{static_js}
|
|
326
|
+
</body>
|
|
327
|
+
</html>"""
|
|
328
|
+
|
|
329
|
+
return html
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def build_post_tree_static(folder, root_folder):
|
|
333
|
+
"""Build post tree with static .html links instead of HTMX"""
|
|
334
|
+
items = []
|
|
335
|
+
try:
|
|
336
|
+
index_file = None
|
|
337
|
+
if folder == root_folder:
|
|
338
|
+
for candidate in root_folder.iterdir():
|
|
339
|
+
if candidate.is_file() and candidate.suffix == '.md' and candidate.stem.lower() == 'index':
|
|
340
|
+
index_file = candidate
|
|
341
|
+
break
|
|
342
|
+
if index_file is None:
|
|
343
|
+
for candidate in root_folder.iterdir():
|
|
344
|
+
if candidate.is_file() and candidate.suffix == '.md' and candidate.stem.lower() == 'readme':
|
|
345
|
+
index_file = candidate
|
|
346
|
+
break
|
|
347
|
+
|
|
348
|
+
entries = []
|
|
349
|
+
for item in folder.iterdir():
|
|
350
|
+
if item.name == ".bloggy":
|
|
351
|
+
continue
|
|
352
|
+
if item.is_dir():
|
|
353
|
+
if item.name.startswith('.'):
|
|
354
|
+
continue
|
|
355
|
+
entries.append(item)
|
|
356
|
+
elif item.suffix == '.md':
|
|
357
|
+
if index_file and item.resolve() == index_file.resolve():
|
|
358
|
+
continue
|
|
359
|
+
entries.append(item)
|
|
360
|
+
entries = order_bloggy_entries(entries, get_bloggy_config(folder))
|
|
361
|
+
except (OSError, PermissionError):
|
|
362
|
+
return items
|
|
363
|
+
|
|
364
|
+
for item in entries:
|
|
365
|
+
if item.is_dir():
|
|
366
|
+
if item.name.startswith('.'):
|
|
367
|
+
continue
|
|
368
|
+
sub_items = build_post_tree_static(item, root_folder)
|
|
369
|
+
if sub_items:
|
|
370
|
+
folder_title = slug_to_title(item.name)
|
|
371
|
+
items.append(Li(Details(
|
|
372
|
+
Summary(
|
|
373
|
+
Span(Span(cls="folder-chevron"), cls="w-4 mr-2 flex items-center justify-center shrink-0"),
|
|
374
|
+
Span(UkIcon("folder", cls="text-blue-500 w-4 h-4"), cls="w-4 mr-2 flex items-center justify-center shrink-0"),
|
|
375
|
+
Span(folder_title, cls="truncate min-w-0", title=folder_title),
|
|
376
|
+
cls="flex items-center font-medium cursor-pointer py-1 px-2 hover:text-blue-600 select-none list-none rounded hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors min-w-0"),
|
|
377
|
+
Ul(*sub_items, cls="ml-4 pl-2 space-y-1 border-l border-slate-100 dark:border-slate-800"),
|
|
378
|
+
data_folder="true"), cls="my-1"))
|
|
379
|
+
elif item.suffix == '.md':
|
|
380
|
+
slug = str(item.relative_to(root_folder).with_suffix(''))
|
|
381
|
+
title = get_post_title(item)
|
|
382
|
+
|
|
383
|
+
# Use .html extension for static links
|
|
384
|
+
items.append(Li(A(
|
|
385
|
+
Span(cls="w-4 mr-2 shrink-0"),
|
|
386
|
+
Span(UkIcon("file-text", cls="text-slate-400 w-4 h-4"), cls="w-4 mr-2 flex items-center justify-center shrink-0"),
|
|
387
|
+
Span(title, cls="truncate min-w-0", title=title),
|
|
388
|
+
href=f'/posts/{slug}.html', # Add .html extension
|
|
389
|
+
cls="flex items-center py-1 px-2 rounded hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-300 hover:text-blue-600 transition-colors min-w-0")))
|
|
390
|
+
return items
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def static_layout(content_html, blog_title, page_title, nav_tree, toc_items=None, current_path=None):
|
|
394
|
+
"""Generate complete static page layout"""
|
|
395
|
+
|
|
396
|
+
# Theme toggle button
|
|
397
|
+
theme_toggle = '''
|
|
398
|
+
<button onclick="toggleTheme()" class="p-1 hover:scale-110 shadow-none" type="button">
|
|
399
|
+
<span uk-icon="moon" class="dark:hidden"></span>
|
|
400
|
+
<span uk-icon="sun" class="hidden dark:block"></span>
|
|
401
|
+
</button>
|
|
402
|
+
'''
|
|
403
|
+
|
|
404
|
+
# Navbar
|
|
405
|
+
navbar = f'''
|
|
406
|
+
<div class="flex items-center justify-between bg-slate-900 text-white p-4 my-4 rounded-lg shadow-md dark:bg-slate-800">
|
|
407
|
+
<a href="/index.html">{blog_title}</a>
|
|
408
|
+
{theme_toggle}
|
|
409
|
+
</div>
|
|
410
|
+
'''
|
|
411
|
+
|
|
412
|
+
# Build navigation sidebar
|
|
413
|
+
nav_html = to_xml(Ul(*nav_tree, cls="mt-2 list-none"))
|
|
414
|
+
posts_sidebar = f'''
|
|
415
|
+
<aside id="posts-sidebar" class="hidden md:block w-64 shrink-0 sticky top-24 self-start max-h-[calc(100vh-10rem)] overflow-hidden z-[1000]">
|
|
416
|
+
<details open>
|
|
417
|
+
<summary class="flex items-center font-semibold cursor-pointer py-2 px-3 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg select-none list-none bg-white dark:bg-slate-950 z-10">
|
|
418
|
+
<span uk-icon="menu" class="w-5 h-5 mr-2"></span>
|
|
419
|
+
Posts
|
|
420
|
+
</summary>
|
|
421
|
+
<div class="mt-2 p-3 bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 overflow-y-auto max-h-[calc(100vh-16rem)]">
|
|
422
|
+
{nav_html}
|
|
423
|
+
</div>
|
|
424
|
+
</details>
|
|
425
|
+
</aside>
|
|
426
|
+
'''
|
|
427
|
+
|
|
428
|
+
# Build TOC sidebar
|
|
429
|
+
toc_html = ""
|
|
430
|
+
if toc_items:
|
|
431
|
+
toc_list_html = to_xml(Ul(*toc_items, cls="mt-2 list-none"))
|
|
432
|
+
toc_html = f'''
|
|
433
|
+
<aside id="toc-sidebar" class="hidden md:block w-64 shrink-0 sticky top-24 self-start max-h-[calc(100vh-10rem)] overflow-hidden z-[1000]">
|
|
434
|
+
<details open>
|
|
435
|
+
<summary class="flex items-center font-semibold cursor-pointer py-2 px-3 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg select-none list-none bg-white dark:bg-slate-950 z-10">
|
|
436
|
+
<span uk-icon="list" class="w-5 h-5 mr-2"></span>
|
|
437
|
+
Contents
|
|
438
|
+
</summary>
|
|
439
|
+
<div class="mt-2 p-3 bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 overflow-y-auto max-h-[calc(100vh-16rem)]">
|
|
440
|
+
{toc_list_html}
|
|
441
|
+
</div>
|
|
442
|
+
</details>
|
|
443
|
+
</aside>
|
|
444
|
+
'''
|
|
445
|
+
|
|
446
|
+
# Main content area
|
|
447
|
+
main_content = f'''
|
|
448
|
+
<main id="main-content" class="flex-1 min-w-0 px-6 py-8 space-y-8">
|
|
449
|
+
{content_html}
|
|
450
|
+
</main>
|
|
451
|
+
'''
|
|
452
|
+
|
|
453
|
+
# Footer
|
|
454
|
+
footer = '''
|
|
455
|
+
<footer class="w-full max-w-7xl mx-auto px-6 mt-auto mb-6">
|
|
456
|
+
<div class="bg-slate-900 text-white rounded-lg p-4 my-4 dark:bg-slate-800 text-right">
|
|
457
|
+
Powered by Bloggy
|
|
458
|
+
</div>
|
|
459
|
+
</footer>
|
|
460
|
+
'''
|
|
461
|
+
|
|
462
|
+
# Complete body
|
|
463
|
+
body = f'''
|
|
464
|
+
<div id="page-container" class="flex flex-col min-h-screen">
|
|
465
|
+
<div class="w-full max-w-7xl mx-auto px-4 sticky top-0 z-50 mt-4">
|
|
466
|
+
{navbar}
|
|
467
|
+
</div>
|
|
468
|
+
<div class="w-full max-w-7xl mx-auto px-4 flex gap-6 flex-1">
|
|
469
|
+
{posts_sidebar}
|
|
470
|
+
{main_content}
|
|
471
|
+
{toc_html}
|
|
472
|
+
</div>
|
|
473
|
+
{footer}
|
|
474
|
+
</div>
|
|
475
|
+
'''
|
|
476
|
+
|
|
477
|
+
return generate_static_html(page_title, body, blog_title)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def build_static_site(input_dir=None, output_dir=None):
|
|
481
|
+
"""
|
|
482
|
+
Build a complete static site from markdown files
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
input_dir: Path to markdown files (defaults to BLOGGY_ROOT or current dir)
|
|
486
|
+
output_dir: Path to output directory (defaults to ./dist)
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
# Initialize config
|
|
490
|
+
if input_dir:
|
|
491
|
+
import os
|
|
492
|
+
os.environ['BLOGGY_ROOT'] = str(Path(input_dir).resolve())
|
|
493
|
+
reload_config()
|
|
494
|
+
|
|
495
|
+
config = get_config()
|
|
496
|
+
root_folder = config.get_root_folder()
|
|
497
|
+
blog_title = config.get_blog_title()
|
|
498
|
+
|
|
499
|
+
# Set default output directory
|
|
500
|
+
if output_dir is None:
|
|
501
|
+
output_dir = Path.cwd() / 'dist'
|
|
502
|
+
else:
|
|
503
|
+
output_dir = Path(output_dir)
|
|
504
|
+
|
|
505
|
+
print(f"Building static site...")
|
|
506
|
+
print(f" Source: {root_folder}")
|
|
507
|
+
print(f" Output: {output_dir}")
|
|
508
|
+
print(f" Blog title: {blog_title}")
|
|
509
|
+
|
|
510
|
+
# Create output directory
|
|
511
|
+
if output_dir.exists():
|
|
512
|
+
shutil.rmtree(output_dir)
|
|
513
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
514
|
+
|
|
515
|
+
# Build navigation tree with static .html links
|
|
516
|
+
nav_tree = build_post_tree_static(root_folder, root_folder)
|
|
517
|
+
|
|
518
|
+
# Find all markdown files (only in the specified root folder, not parent directories)
|
|
519
|
+
md_files = []
|
|
520
|
+
for md_file in root_folder.rglob('*.md'):
|
|
521
|
+
# Only include files that are actually inside root_folder
|
|
522
|
+
try:
|
|
523
|
+
relative_path = md_file.relative_to(root_folder)
|
|
524
|
+
md_files.append(md_file)
|
|
525
|
+
except ValueError:
|
|
526
|
+
# Skip files outside root_folder
|
|
527
|
+
continue
|
|
528
|
+
|
|
529
|
+
print(f"\nFound {len(md_files)} markdown files")
|
|
530
|
+
|
|
531
|
+
# Process each markdown file
|
|
532
|
+
for md_file in md_files:
|
|
533
|
+
relative_path = md_file.relative_to(root_folder)
|
|
534
|
+
print(f" Processing: {relative_path}")
|
|
535
|
+
|
|
536
|
+
# Parse frontmatter and content
|
|
537
|
+
metadata, raw_content = parse_frontmatter(md_file)
|
|
538
|
+
post_title = metadata.get('title', get_post_title(md_file))
|
|
539
|
+
|
|
540
|
+
# Render markdown to HTML
|
|
541
|
+
content_div = from_md(raw_content)
|
|
542
|
+
title_html = f'<h1 class="text-4xl font-bold mb-8">{post_title}</h1>'
|
|
543
|
+
content_html = title_html + to_xml(content_div)
|
|
544
|
+
|
|
545
|
+
# Extract TOC
|
|
546
|
+
toc_headings = extract_toc(raw_content)
|
|
547
|
+
toc_items = build_toc_items(toc_headings)
|
|
548
|
+
|
|
549
|
+
# Generate full page
|
|
550
|
+
full_html = static_layout(
|
|
551
|
+
content_html=content_html,
|
|
552
|
+
blog_title=blog_title,
|
|
553
|
+
page_title=f"{post_title} - {blog_title}",
|
|
554
|
+
nav_tree=nav_tree,
|
|
555
|
+
toc_items=toc_items,
|
|
556
|
+
current_path=str(relative_path.with_suffix(''))
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# Determine output path
|
|
560
|
+
if md_file.stem.lower() in ['index', 'readme'] and md_file.parent == root_folder:
|
|
561
|
+
# Root index/readme becomes index.html
|
|
562
|
+
output_path = output_dir / 'index.html'
|
|
563
|
+
else:
|
|
564
|
+
# Other files go in posts/ directory
|
|
565
|
+
output_path = output_dir / 'posts' / relative_path.with_suffix('.html')
|
|
566
|
+
|
|
567
|
+
# Create directory and write file
|
|
568
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
569
|
+
output_path.write_text(full_html, encoding='utf-8')
|
|
570
|
+
|
|
571
|
+
# Copy static assets
|
|
572
|
+
static_src = Path(__file__).parent / 'static'
|
|
573
|
+
if static_src.exists():
|
|
574
|
+
static_dst = output_dir / 'static'
|
|
575
|
+
print(f"\nCopying static assets...")
|
|
576
|
+
shutil.copytree(static_src, static_dst, dirs_exist_ok=True)
|
|
577
|
+
|
|
578
|
+
# Generate index.html if it doesn't exist
|
|
579
|
+
index_path = output_dir / 'index.html'
|
|
580
|
+
if not index_path.exists():
|
|
581
|
+
print("\nGenerating default index.html...")
|
|
582
|
+
welcome_content = f'''
|
|
583
|
+
<h1 class="text-4xl font-bold tracking-tight mb-8">Welcome to {blog_title}!</h1>
|
|
584
|
+
<p class="text-lg text-slate-600 dark:text-slate-400 mb-4">Your personal blogging platform.</p>
|
|
585
|
+
<p class="text-base text-slate-600 dark:text-slate-400">
|
|
586
|
+
Browse your posts using the sidebar, or create an <strong>index.md</strong> or
|
|
587
|
+
<strong>README.md</strong> file in your blog directory to customize this page.
|
|
588
|
+
</p>
|
|
589
|
+
'''
|
|
590
|
+
|
|
591
|
+
full_html = static_layout(
|
|
592
|
+
content_html=welcome_content,
|
|
593
|
+
blog_title=blog_title,
|
|
594
|
+
page_title=f"Home - {blog_title}",
|
|
595
|
+
nav_tree=nav_tree,
|
|
596
|
+
toc_items=None
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
index_path.write_text(full_html, encoding='utf-8')
|
|
600
|
+
|
|
601
|
+
print(f"\nā
Static site built successfully!")
|
|
602
|
+
print(f"š Output directory: {output_dir}")
|
|
603
|
+
print(f"\nTo preview the site:")
|
|
604
|
+
print(f" cd {output_dir}")
|
|
605
|
+
print(f" python -m http.server 8000")
|
|
606
|
+
print(f" Open http://localhost:8000")
|
|
607
|
+
|
|
608
|
+
return output_dir
|