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/main.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
from .config import get_config, reload_config
|
|
5
|
+
|
|
6
|
+
# Import app at module level, but config will be initialized before it's used
|
|
7
|
+
from .core import app
|
|
8
|
+
|
|
9
|
+
def build_command():
|
|
10
|
+
"""CLI entry point for bloggy build command"""
|
|
11
|
+
import argparse
|
|
12
|
+
from .build import build_static_site
|
|
13
|
+
|
|
14
|
+
parser = argparse.ArgumentParser(description='Build static site from markdown files')
|
|
15
|
+
parser.add_argument('directory', nargs='?', help='Path to markdown files directory')
|
|
16
|
+
parser.add_argument('-o', '--output', help='Output directory (default: ./dist)', default='dist')
|
|
17
|
+
|
|
18
|
+
args = parser.parse_args(sys.argv[2:]) # Skip 'bloggy' and 'build'
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
output_dir = build_static_site(input_dir=args.directory, output_dir=args.output)
|
|
22
|
+
return 0
|
|
23
|
+
except Exception as e:
|
|
24
|
+
print(f"Error building static site: {e}", file=sys.stderr)
|
|
25
|
+
import traceback
|
|
26
|
+
traceback.print_exc()
|
|
27
|
+
return 1
|
|
28
|
+
|
|
29
|
+
def cli():
|
|
30
|
+
"""CLI entry point for bloggy command
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
bloggy [directory] # Run locally on 127.0.0.1:5001
|
|
34
|
+
bloggy [directory] --host 0.0.0.0 # Run on all interfaces
|
|
35
|
+
bloggy build [directory] # Build static site
|
|
36
|
+
bloggy build [directory] -o output # Build to custom output directory
|
|
37
|
+
|
|
38
|
+
Environment variables:
|
|
39
|
+
BLOGGY_ROOT: Path to markdown files
|
|
40
|
+
BLOGGY_HOST: Server host (default: 127.0.0.1)
|
|
41
|
+
BLOGGY_PORT: Server port (default: 5001)
|
|
42
|
+
|
|
43
|
+
Configuration file:
|
|
44
|
+
Create a .bloggy file (TOML format) in your blog directory
|
|
45
|
+
"""
|
|
46
|
+
import uvicorn
|
|
47
|
+
import argparse
|
|
48
|
+
|
|
49
|
+
# Check if first argument is 'build'
|
|
50
|
+
if len(sys.argv) > 1 and sys.argv[1] == 'build':
|
|
51
|
+
sys.exit(build_command())
|
|
52
|
+
|
|
53
|
+
parser = argparse.ArgumentParser(description='Run Bloggy server')
|
|
54
|
+
parser.add_argument('directory', nargs='?', help='Path to markdown files directory')
|
|
55
|
+
parser.add_argument('--host', help='Server host (default: 127.0.0.1, use 0.0.0.0 for all interfaces)')
|
|
56
|
+
parser.add_argument('--port', type=int, help='Server port (default: 5001)')
|
|
57
|
+
parser.add_argument('--no-reload', action='store_true', help='Disable auto-reload')
|
|
58
|
+
parser.add_argument('--user', help='Login username (overrides config/env)')
|
|
59
|
+
parser.add_argument('--password', help='Login password (overrides config/env)')
|
|
60
|
+
|
|
61
|
+
args = parser.parse_args()
|
|
62
|
+
|
|
63
|
+
# Set root folder from arguments or environment
|
|
64
|
+
if args.directory:
|
|
65
|
+
root = Path(args.directory).resolve()
|
|
66
|
+
if not root.exists():
|
|
67
|
+
print(f"Error: Directory {root} does not exist")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
os.environ['BLOGGY_ROOT'] = str(root)
|
|
70
|
+
|
|
71
|
+
# Initialize or reload config to pick up .bloggy file
|
|
72
|
+
# This ensures .bloggy file is loaded and config is refreshed
|
|
73
|
+
config = reload_config() if args.directory else get_config()
|
|
74
|
+
|
|
75
|
+
# Get host and port from arguments, config, or use defaults
|
|
76
|
+
host = args.host or config.get_host()
|
|
77
|
+
port = args.port or config.get_port()
|
|
78
|
+
reload = not args.no_reload
|
|
79
|
+
|
|
80
|
+
# Set login credentials from CLI if provided
|
|
81
|
+
if args.user:
|
|
82
|
+
os.environ['BLOGGY_USER'] = args.user
|
|
83
|
+
if args.password:
|
|
84
|
+
os.environ['BLOGGY_PASSWORD'] = args.password
|
|
85
|
+
|
|
86
|
+
print(f"Starting Bloggy server...")
|
|
87
|
+
print(f"Blog root: {config.get_root_folder()}")
|
|
88
|
+
print(f"Blog title: {config.get_blog_title()}")
|
|
89
|
+
print(f"Serving at: http://{host}:{port}")
|
|
90
|
+
if host == '0.0.0.0':
|
|
91
|
+
print(f"Server accessible from network at: http://<your-ip>:{port}")
|
|
92
|
+
|
|
93
|
+
uvicorn.run("bloggy.main:app", host=host, port=port, reload=reload)
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
cli()
|
bloggy/static/scripts.js
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
2
|
+
|
|
3
|
+
const mermaidStates = {};
|
|
4
|
+
const GANTT_WIDTH = 1200;
|
|
5
|
+
|
|
6
|
+
function initMermaidInteraction() {
|
|
7
|
+
document.querySelectorAll('.mermaid-wrapper').forEach(wrapper => {
|
|
8
|
+
const svg = wrapper.querySelector('svg');
|
|
9
|
+
if (!svg || mermaidStates[wrapper.id]) return;
|
|
10
|
+
|
|
11
|
+
// DEBUG: Log initial state
|
|
12
|
+
console.group(`🔍 initMermaidInteraction: ${wrapper.id}`);
|
|
13
|
+
console.log('Theme:', getCurrentTheme());
|
|
14
|
+
console.log('Wrapper computed style height:', window.getComputedStyle(wrapper).height);
|
|
15
|
+
console.log('Wrapper inline style:', wrapper.getAttribute('style'));
|
|
16
|
+
|
|
17
|
+
// Scale SVG to fit container (maintain aspect ratio, fit to width or height whichever is smaller)
|
|
18
|
+
const wrapperRect = wrapper.getBoundingClientRect();
|
|
19
|
+
const svgRect = svg.getBoundingClientRect();
|
|
20
|
+
console.log('Wrapper rect:', { width: wrapperRect.width, height: wrapperRect.height });
|
|
21
|
+
console.log('SVG rect:', { width: svgRect.width, height: svgRect.height });
|
|
22
|
+
|
|
23
|
+
const scaleX = (wrapperRect.width - 32) / svgRect.width; // 32 for p-4 padding (16px each side)
|
|
24
|
+
const scaleY = (wrapperRect.height - 32) / svgRect.height;
|
|
25
|
+
console.log('Scale factors:', { scaleX, scaleY });
|
|
26
|
+
|
|
27
|
+
// For very wide diagrams (like Gantt charts), prefer width scaling even if it exceeds height
|
|
28
|
+
const aspectRatio = svgRect.width / svgRect.height;
|
|
29
|
+
let initialScale;
|
|
30
|
+
if (aspectRatio > 3) {
|
|
31
|
+
// Wide diagram: scale to fit width, allowing vertical scroll if needed
|
|
32
|
+
initialScale = scaleX;
|
|
33
|
+
console.log('Wide diagram detected (aspect ratio > 3):', aspectRatio, 'Using scaleX:', initialScale);
|
|
34
|
+
} else {
|
|
35
|
+
// Normal diagram: fit to smaller dimension, but allow upscaling up to 3x
|
|
36
|
+
initialScale = Math.min(scaleX, scaleY, 3);
|
|
37
|
+
console.log('Normal diagram (aspect ratio <=3):', aspectRatio, 'Using min scale:', initialScale);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const state = {
|
|
41
|
+
scale: initialScale,
|
|
42
|
+
translateX: 0,
|
|
43
|
+
translateY: 0,
|
|
44
|
+
isPanning: false,
|
|
45
|
+
startX: 0,
|
|
46
|
+
startY: 0
|
|
47
|
+
};
|
|
48
|
+
mermaidStates[wrapper.id] = state;
|
|
49
|
+
console.log('Final state:', state);
|
|
50
|
+
console.groupEnd();
|
|
51
|
+
|
|
52
|
+
function updateTransform() {
|
|
53
|
+
svg.style.transform = `translate(${state.translateX}px, ${state.translateY}px) scale(${state.scale})`;
|
|
54
|
+
svg.style.transformOrigin = 'center center';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Apply initial scale
|
|
58
|
+
updateTransform();
|
|
59
|
+
|
|
60
|
+
// Mouse wheel zoom (zooms towards cursor position)
|
|
61
|
+
wrapper.addEventListener('wheel', (e) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
|
|
64
|
+
const rect = svg.getBoundingClientRect();
|
|
65
|
+
|
|
66
|
+
// Mouse position relative to SVG's current position
|
|
67
|
+
const mouseX = e.clientX - rect.left - rect.width / 2;
|
|
68
|
+
const mouseY = e.clientY - rect.top - rect.height / 2;
|
|
69
|
+
|
|
70
|
+
const zoomIntensity = 0.01;
|
|
71
|
+
const delta = e.deltaY > 0 ? 1 - zoomIntensity : 1 + zoomIntensity; // Zoom out or in speed
|
|
72
|
+
const newScale = Math.min(Math.max(0.1, state.scale * delta), 55);
|
|
73
|
+
|
|
74
|
+
// Calculate how much to adjust translation to keep point under cursor fixed
|
|
75
|
+
// With center origin, we need to account for the scale change around center
|
|
76
|
+
const scaleFactor = newScale / state.scale - 1;
|
|
77
|
+
state.translateX -= mouseX * scaleFactor;
|
|
78
|
+
state.translateY -= mouseY * scaleFactor;
|
|
79
|
+
state.scale = newScale;
|
|
80
|
+
|
|
81
|
+
updateTransform();
|
|
82
|
+
}, { passive: false });
|
|
83
|
+
|
|
84
|
+
// Pan with mouse drag
|
|
85
|
+
wrapper.addEventListener('mousedown', (e) => {
|
|
86
|
+
if (e.button !== 0) return;
|
|
87
|
+
state.isPanning = true;
|
|
88
|
+
state.startX = e.clientX - state.translateX;
|
|
89
|
+
state.startY = e.clientY - state.translateY;
|
|
90
|
+
wrapper.style.cursor = 'grabbing';
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
document.addEventListener('mousemove', (e) => {
|
|
95
|
+
if (!state.isPanning) return;
|
|
96
|
+
state.translateX = e.clientX - state.startX;
|
|
97
|
+
state.translateY = e.clientY - state.startY;
|
|
98
|
+
updateTransform();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
document.addEventListener('mouseup', () => {
|
|
102
|
+
if (state.isPanning) {
|
|
103
|
+
state.isPanning = false;
|
|
104
|
+
wrapper.style.cursor = 'grab';
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
wrapper.style.cursor = 'grab';
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
window.resetMermaidZoom = function(id) {
|
|
113
|
+
const state = mermaidStates[id];
|
|
114
|
+
if (state) {
|
|
115
|
+
state.scale = 1;
|
|
116
|
+
state.translateX = 0;
|
|
117
|
+
state.translateY = 0;
|
|
118
|
+
const svg = document.getElementById(id).querySelector('svg');
|
|
119
|
+
svg.style.transform = 'translate(0px, 0px) scale(1)';
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
window.zoomMermaidIn = function(id) {
|
|
124
|
+
const state = mermaidStates[id];
|
|
125
|
+
if (state) {
|
|
126
|
+
state.scale = Math.min(state.scale * 1.1, 10);
|
|
127
|
+
const svg = document.getElementById(id).querySelector('svg');
|
|
128
|
+
svg.style.transform = `translate(${state.translateX}px, ${state.translateY}px) scale(${state.scale})`;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
window.zoomMermaidOut = function(id) {
|
|
133
|
+
const state = mermaidStates[id];
|
|
134
|
+
if (state) {
|
|
135
|
+
state.scale = Math.max(state.scale * 0.9, 0.1);
|
|
136
|
+
const svg = document.getElementById(id).querySelector('svg');
|
|
137
|
+
svg.style.transform = `translate(${state.translateX}px, ${state.translateY}px) scale(${state.scale})`;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
window.openMermaidFullscreen = function(id) {
|
|
142
|
+
const wrapper = document.getElementById(id);
|
|
143
|
+
if (!wrapper) return;
|
|
144
|
+
|
|
145
|
+
const originalCode = wrapper.getAttribute('data-mermaid-code');
|
|
146
|
+
if (!originalCode) return;
|
|
147
|
+
|
|
148
|
+
// Decode HTML entities
|
|
149
|
+
const textarea = document.createElement('textarea');
|
|
150
|
+
textarea.innerHTML = originalCode;
|
|
151
|
+
const code = textarea.value;
|
|
152
|
+
|
|
153
|
+
// Create modal
|
|
154
|
+
const modal = document.createElement('div');
|
|
155
|
+
modal.id = 'mermaid-fullscreen-modal';
|
|
156
|
+
modal.className = 'fixed inset-0 z-[10000] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4';
|
|
157
|
+
modal.style.animation = 'fadeIn 0.2s ease-in';
|
|
158
|
+
|
|
159
|
+
// Create modal content container
|
|
160
|
+
const modalContent = document.createElement('div');
|
|
161
|
+
modalContent.className = 'relative bg-white dark:bg-slate-900 rounded-lg shadow-2xl w-full h-full max-w-[95vw] max-h-[95vh] flex flex-col';
|
|
162
|
+
|
|
163
|
+
// Create header with close button
|
|
164
|
+
const header = document.createElement('div');
|
|
165
|
+
header.className = 'flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-700';
|
|
166
|
+
|
|
167
|
+
const title = document.createElement('h3');
|
|
168
|
+
title.className = 'text-lg font-semibold text-slate-800 dark:text-slate-200';
|
|
169
|
+
title.textContent = 'Diagram';
|
|
170
|
+
|
|
171
|
+
const closeBtn = document.createElement('button');
|
|
172
|
+
closeBtn.innerHTML = '✕';
|
|
173
|
+
closeBtn.className = 'px-3 py-1 text-xl text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 rounded transition-colors';
|
|
174
|
+
closeBtn.title = 'Close (Esc)';
|
|
175
|
+
closeBtn.onclick = () => document.body.removeChild(modal);
|
|
176
|
+
|
|
177
|
+
header.appendChild(title);
|
|
178
|
+
header.appendChild(closeBtn);
|
|
179
|
+
|
|
180
|
+
// Create diagram container
|
|
181
|
+
const diagramContainer = document.createElement('div');
|
|
182
|
+
diagramContainer.className = 'flex-1 overflow-auto p-4 flex items-center justify-center';
|
|
183
|
+
|
|
184
|
+
const fullscreenId = `${id}-fullscreen`;
|
|
185
|
+
const fullscreenWrapper = document.createElement('div');
|
|
186
|
+
fullscreenWrapper.id = fullscreenId;
|
|
187
|
+
fullscreenWrapper.className = 'mermaid-wrapper w-full h-full flex items-center justify-center';
|
|
188
|
+
fullscreenWrapper.setAttribute('data-mermaid-code', originalCode);
|
|
189
|
+
|
|
190
|
+
const pre = document.createElement('pre');
|
|
191
|
+
pre.className = 'mermaid';
|
|
192
|
+
pre.textContent = code;
|
|
193
|
+
fullscreenWrapper.appendChild(pre);
|
|
194
|
+
|
|
195
|
+
diagramContainer.appendChild(fullscreenWrapper);
|
|
196
|
+
|
|
197
|
+
// Assemble modal
|
|
198
|
+
modalContent.appendChild(header);
|
|
199
|
+
modalContent.appendChild(diagramContainer);
|
|
200
|
+
modal.appendChild(modalContent);
|
|
201
|
+
document.body.appendChild(modal);
|
|
202
|
+
|
|
203
|
+
// Close on Esc key
|
|
204
|
+
const escHandler = (e) => {
|
|
205
|
+
if (e.key === 'Escape') {
|
|
206
|
+
document.body.removeChild(modal);
|
|
207
|
+
document.removeEventListener('keydown', escHandler);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
document.addEventListener('keydown', escHandler);
|
|
211
|
+
|
|
212
|
+
// Close on background click
|
|
213
|
+
modal.addEventListener('click', (e) => {
|
|
214
|
+
if (e.target === modal) {
|
|
215
|
+
document.body.removeChild(modal);
|
|
216
|
+
document.removeEventListener('keydown', escHandler);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Render mermaid in the fullscreen view
|
|
221
|
+
mermaid.run({ nodes: [pre] }).then(() => {
|
|
222
|
+
setTimeout(() => initMermaidInteraction(), 100);
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
function getCurrentTheme() {
|
|
227
|
+
return document.documentElement.classList.contains('dark') ? 'dark' : 'default';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getDynamicGanttWidth() {
|
|
231
|
+
// Check if any mermaid wrapper has custom gantt width
|
|
232
|
+
const wrappers = document.querySelectorAll('.mermaid-wrapper[data-gantt-width]');
|
|
233
|
+
if (wrappers.length > 0) {
|
|
234
|
+
// Use the first custom width found, or max width if multiple
|
|
235
|
+
const widths = Array.from(wrappers).map(w => parseInt(w.getAttribute('data-gantt-width')) || GANTT_WIDTH);
|
|
236
|
+
return Math.max(...widths);
|
|
237
|
+
}
|
|
238
|
+
return GANTT_WIDTH;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function reinitializeMermaid() {
|
|
242
|
+
console.group('🔄 reinitializeMermaid called');
|
|
243
|
+
console.log('Switching to theme:', getCurrentTheme());
|
|
244
|
+
console.log('Is initial load?', isInitialLoad);
|
|
245
|
+
|
|
246
|
+
// Skip if this is the initial load (let it render naturally first)
|
|
247
|
+
if (isInitialLoad) {
|
|
248
|
+
console.log('Skipping reinitialize on initial load');
|
|
249
|
+
console.groupEnd();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const dynamicWidth = getDynamicGanttWidth();
|
|
254
|
+
console.log('Using dynamic Gantt width:', dynamicWidth);
|
|
255
|
+
|
|
256
|
+
mermaid.initialize({
|
|
257
|
+
startOnLoad: false,
|
|
258
|
+
theme: getCurrentTheme(),
|
|
259
|
+
gantt: {
|
|
260
|
+
useWidth: dynamicWidth,
|
|
261
|
+
useMaxWidth: false
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Find all mermaid wrappers and re-render them
|
|
266
|
+
document.querySelectorAll('.mermaid-wrapper').forEach(wrapper => {
|
|
267
|
+
const originalCode = wrapper.getAttribute('data-mermaid-code');
|
|
268
|
+
if (originalCode) {
|
|
269
|
+
console.log(`Processing wrapper: ${wrapper.id}`);
|
|
270
|
+
console.log('BEFORE clear - wrapper height:', window.getComputedStyle(wrapper).height);
|
|
271
|
+
console.log('BEFORE clear - wrapper rect:', wrapper.getBoundingClientRect());
|
|
272
|
+
|
|
273
|
+
// Preserve the current computed height before clearing (height should already be set explicitly)
|
|
274
|
+
const currentHeight = wrapper.getBoundingClientRect().height;
|
|
275
|
+
console.log('Preserving height:', currentHeight);
|
|
276
|
+
wrapper.style.height = currentHeight + 'px';
|
|
277
|
+
|
|
278
|
+
// Delete the old state so it can be recreated
|
|
279
|
+
delete mermaidStates[wrapper.id];
|
|
280
|
+
|
|
281
|
+
// Decode HTML entities
|
|
282
|
+
const textarea = document.createElement('textarea');
|
|
283
|
+
textarea.innerHTML = originalCode;
|
|
284
|
+
const code = textarea.value;
|
|
285
|
+
|
|
286
|
+
// Clear the wrapper
|
|
287
|
+
wrapper.innerHTML = '';
|
|
288
|
+
console.log('AFTER clear - wrapper height:', window.getComputedStyle(wrapper).height);
|
|
289
|
+
console.log('AFTER clear - wrapper rect:', wrapper.getBoundingClientRect());
|
|
290
|
+
|
|
291
|
+
// Re-add the pre element with mermaid code
|
|
292
|
+
const newPre = document.createElement('pre');
|
|
293
|
+
newPre.className = 'mermaid';
|
|
294
|
+
newPre.textContent = code;
|
|
295
|
+
wrapper.appendChild(newPre);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Re-run mermaid
|
|
300
|
+
mermaid.run().then(() => {
|
|
301
|
+
console.log('Mermaid re-render complete, calling initMermaidInteraction in 100ms');
|
|
302
|
+
setTimeout(() => {
|
|
303
|
+
initMermaidInteraction();
|
|
304
|
+
console.groupEnd();
|
|
305
|
+
}, 100);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log('🚀 Initial Mermaid setup - Theme:', getCurrentTheme());
|
|
310
|
+
|
|
311
|
+
const initialGanttWidth = getDynamicGanttWidth();
|
|
312
|
+
console.log('Using initial Gantt width:', initialGanttWidth);
|
|
313
|
+
|
|
314
|
+
mermaid.initialize({
|
|
315
|
+
startOnLoad: true,
|
|
316
|
+
theme: getCurrentTheme(),
|
|
317
|
+
gantt: {
|
|
318
|
+
useWidth: initialGanttWidth,
|
|
319
|
+
useMaxWidth: false
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Track if this is the initial load
|
|
324
|
+
let isInitialLoad = true;
|
|
325
|
+
|
|
326
|
+
// Initialize interaction after mermaid renders
|
|
327
|
+
mermaid.run().then(() => {
|
|
328
|
+
console.log('Initial mermaid render complete');
|
|
329
|
+
setTimeout(() => {
|
|
330
|
+
console.log('Calling initial initMermaidInteraction');
|
|
331
|
+
initMermaidInteraction();
|
|
332
|
+
|
|
333
|
+
// After initial render, set explicit heights on all wrappers so theme switching works
|
|
334
|
+
document.querySelectorAll('.mermaid-wrapper').forEach(wrapper => {
|
|
335
|
+
const currentHeight = wrapper.getBoundingClientRect().height;
|
|
336
|
+
console.log(`Setting initial height for ${wrapper.id}:`, currentHeight);
|
|
337
|
+
wrapper.style.height = currentHeight + 'px';
|
|
338
|
+
});
|
|
339
|
+
isInitialLoad = false;
|
|
340
|
+
}, 100);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Reveal current file in sidebar
|
|
344
|
+
function revealInSidebar(rootElement = document) {
|
|
345
|
+
if (!window.location.pathname.startsWith('/posts/')) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const currentPath = window.location.pathname.replace(/^\/posts\//, '');
|
|
350
|
+
const activeLink = rootElement.querySelector(`.post-link[data-path="${currentPath}"]`);
|
|
351
|
+
|
|
352
|
+
if (activeLink) {
|
|
353
|
+
// Expand all parent details elements within this sidebar
|
|
354
|
+
let parent = activeLink.closest('details');
|
|
355
|
+
while (parent && rootElement.contains(parent)) {
|
|
356
|
+
parent.open = true;
|
|
357
|
+
if (parent === rootElement) {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
parent = parent.parentElement.closest('details');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Scroll to the active link
|
|
364
|
+
const scrollContainer = rootElement.querySelector('#sidebar-scroll-container');
|
|
365
|
+
if (scrollContainer) {
|
|
366
|
+
const linkRect = activeLink.getBoundingClientRect();
|
|
367
|
+
const containerRect = scrollContainer.getBoundingClientRect();
|
|
368
|
+
const scrollTop = scrollContainer.scrollTop;
|
|
369
|
+
const offset = linkRect.top - containerRect.top + scrollTop - (containerRect.height / 2) + (linkRect.height / 2);
|
|
370
|
+
|
|
371
|
+
scrollContainer.scrollTo({
|
|
372
|
+
top: offset,
|
|
373
|
+
behavior: 'smooth'
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Highlight the active link temporarily
|
|
378
|
+
activeLink.classList.add('ring-2', 'ring-blue-500', 'ring-offset-2');
|
|
379
|
+
setTimeout(() => {
|
|
380
|
+
activeLink.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-2');
|
|
381
|
+
}, 1500);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function initPostsSidebarAutoReveal() {
|
|
386
|
+
const postSidebars = document.querySelectorAll('details[data-sidebar="posts"]');
|
|
387
|
+
postSidebars.forEach((sidebar) => {
|
|
388
|
+
if (sidebar.dataset.revealBound === 'true') {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
sidebar.dataset.revealBound = 'true';
|
|
392
|
+
sidebar.addEventListener('toggle', () => {
|
|
393
|
+
if (!sidebar.open) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
revealInSidebar(sidebar);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function initFolderChevronState(rootElement = document) {
|
|
402
|
+
rootElement.querySelectorAll('details[data-folder="true"]').forEach((details) => {
|
|
403
|
+
details.classList.toggle('is-open', details.open);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
document.addEventListener('toggle', (event) => {
|
|
408
|
+
const details = event.target;
|
|
409
|
+
if (!(details instanceof HTMLDetailsElement)) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (!details.matches('details[data-folder="true"]')) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
details.classList.toggle('is-open', details.open);
|
|
416
|
+
}, true);
|
|
417
|
+
|
|
418
|
+
// Update active post link in sidebar
|
|
419
|
+
function updateActivePostLink() {
|
|
420
|
+
const currentPath = window.location.pathname.replace(/^\/posts\//, '');
|
|
421
|
+
document.querySelectorAll('.post-link').forEach(link => {
|
|
422
|
+
const linkPath = link.getAttribute('data-path');
|
|
423
|
+
if (linkPath === currentPath) {
|
|
424
|
+
link.classList.add('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-600', 'dark:text-blue-400', 'font-medium');
|
|
425
|
+
link.classList.remove('text-slate-700', 'dark:text-slate-300', 'hover:text-blue-600');
|
|
426
|
+
} else {
|
|
427
|
+
link.classList.remove('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-600', 'dark:text-blue-400', 'font-medium');
|
|
428
|
+
link.classList.add('text-slate-700', 'dark:text-slate-300', 'hover:text-blue-600');
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Update active TOC link based on scroll position
|
|
434
|
+
function updateActiveTocLink() {
|
|
435
|
+
const headings = document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]');
|
|
436
|
+
const tocLinks = document.querySelectorAll('.toc-link');
|
|
437
|
+
|
|
438
|
+
let activeHeading = null;
|
|
439
|
+
headings.forEach(heading => {
|
|
440
|
+
const rect = heading.getBoundingClientRect();
|
|
441
|
+
if (rect.top <= 100) {
|
|
442
|
+
activeHeading = heading;
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
tocLinks.forEach(link => {
|
|
447
|
+
const anchor = link.getAttribute('data-anchor');
|
|
448
|
+
if (activeHeading && anchor === activeHeading.id) {
|
|
449
|
+
link.classList.add('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-600', 'dark:text-blue-400', 'font-semibold');
|
|
450
|
+
} else {
|
|
451
|
+
link.classList.remove('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-600', 'dark:text-blue-400', 'font-semibold');
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Listen for scroll events to update active TOC link
|
|
457
|
+
let ticking = false;
|
|
458
|
+
window.addEventListener('scroll', () => {
|
|
459
|
+
if (!ticking) {
|
|
460
|
+
window.requestAnimationFrame(() => {
|
|
461
|
+
updateActiveTocLink();
|
|
462
|
+
ticking = false;
|
|
463
|
+
});
|
|
464
|
+
ticking = true;
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Re-run mermaid on HTMX content swaps
|
|
469
|
+
document.body.addEventListener('htmx:afterSwap', function() {
|
|
470
|
+
mermaid.run().then(() => {
|
|
471
|
+
setTimeout(initMermaidInteraction, 100);
|
|
472
|
+
});
|
|
473
|
+
updateActivePostLink();
|
|
474
|
+
updateActiveTocLink();
|
|
475
|
+
initMobileMenus(); // Reinitialize mobile menu handlers
|
|
476
|
+
initPostsSidebarAutoReveal();
|
|
477
|
+
initFolderChevronState();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Watch for theme changes and re-render mermaid diagrams
|
|
481
|
+
const observer = new MutationObserver((mutations) => {
|
|
482
|
+
mutations.forEach((mutation) => {
|
|
483
|
+
if (mutation.attributeName === 'class') {
|
|
484
|
+
reinitializeMermaid();
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
observer.observe(document.documentElement, {
|
|
490
|
+
attributes: true,
|
|
491
|
+
attributeFilter: ['class']
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Mobile menu toggle functionality
|
|
495
|
+
function initMobileMenus() {
|
|
496
|
+
const postsToggle = document.getElementById('mobile-posts-toggle');
|
|
497
|
+
const tocToggle = document.getElementById('mobile-toc-toggle');
|
|
498
|
+
const postsPanel = document.getElementById('mobile-posts-panel');
|
|
499
|
+
const tocPanel = document.getElementById('mobile-toc-panel');
|
|
500
|
+
const closePostsBtn = document.getElementById('close-mobile-posts');
|
|
501
|
+
const closeTocBtn = document.getElementById('close-mobile-toc');
|
|
502
|
+
|
|
503
|
+
// Open posts panel
|
|
504
|
+
if (postsToggle) {
|
|
505
|
+
postsToggle.addEventListener('click', () => {
|
|
506
|
+
if (postsPanel) {
|
|
507
|
+
postsPanel.classList.remove('-translate-x-full');
|
|
508
|
+
postsPanel.classList.add('translate-x-0');
|
|
509
|
+
// Close TOC panel if open
|
|
510
|
+
if (tocPanel) {
|
|
511
|
+
tocPanel.classList.remove('translate-x-0');
|
|
512
|
+
tocPanel.classList.add('translate-x-full');
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Open TOC panel
|
|
519
|
+
if (tocToggle) {
|
|
520
|
+
tocToggle.addEventListener('click', () => {
|
|
521
|
+
if (tocPanel) {
|
|
522
|
+
tocPanel.classList.remove('translate-x-full');
|
|
523
|
+
tocPanel.classList.add('translate-x-0');
|
|
524
|
+
// Close posts panel if open
|
|
525
|
+
if (postsPanel) {
|
|
526
|
+
postsPanel.classList.remove('translate-x-0');
|
|
527
|
+
postsPanel.classList.add('-translate-x-full');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Close posts panel
|
|
534
|
+
if (closePostsBtn) {
|
|
535
|
+
closePostsBtn.addEventListener('click', () => {
|
|
536
|
+
if (postsPanel) {
|
|
537
|
+
postsPanel.classList.remove('translate-x-0');
|
|
538
|
+
postsPanel.classList.add('-translate-x-full');
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Close TOC panel
|
|
544
|
+
if (closeTocBtn) {
|
|
545
|
+
closeTocBtn.addEventListener('click', () => {
|
|
546
|
+
if (tocPanel) {
|
|
547
|
+
tocPanel.classList.remove('translate-x-0');
|
|
548
|
+
tocPanel.classList.add('translate-x-full');
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Close panels on link click (for better mobile UX)
|
|
554
|
+
if (postsPanel) {
|
|
555
|
+
postsPanel.addEventListener('click', (e) => {
|
|
556
|
+
if (e.target.tagName === 'A' || e.target.closest('a')) {
|
|
557
|
+
setTimeout(() => {
|
|
558
|
+
postsPanel.classList.remove('translate-x-0');
|
|
559
|
+
postsPanel.classList.add('-translate-x-full');
|
|
560
|
+
}, 100);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (tocPanel) {
|
|
566
|
+
tocPanel.addEventListener('click', (e) => {
|
|
567
|
+
if (e.target.tagName === 'A' || e.target.closest('a')) {
|
|
568
|
+
setTimeout(() => {
|
|
569
|
+
tocPanel.classList.remove('translate-x-0');
|
|
570
|
+
tocPanel.classList.add('translate-x-full');
|
|
571
|
+
}, 100);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Initialize on page load
|
|
578
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
579
|
+
updateActivePostLink();
|
|
580
|
+
updateActiveTocLink();
|
|
581
|
+
initMobileMenus();
|
|
582
|
+
initPostsSidebarAutoReveal();
|
|
583
|
+
initFolderChevronState();
|
|
584
|
+
});
|