tinyssg 0.0.8__py3-none-any.whl → 0.0.10__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.
- tinyssg/__main__.py +1 -721
- tinyssg/tinyssg.py +731 -0
- {tinyssg-0.0.8.dist-info → tinyssg-0.0.10.dist-info}/METADATA +1 -1
- tinyssg-0.0.10.dist-info/RECORD +6 -0
- tinyssg-0.0.8.dist-info/RECORD +0 -5
- {tinyssg-0.0.8.dist-info → tinyssg-0.0.10.dist-info}/WHEEL +0 -0
- {tinyssg-0.0.8.dist-info → tinyssg-0.0.10.dist-info}/top_level.txt +0 -0
tinyssg/tinyssg.py
ADDED
@@ -0,0 +1,731 @@
|
|
1
|
+
import argparse
|
2
|
+
import importlib.util
|
3
|
+
import inspect
|
4
|
+
import json
|
5
|
+
import os
|
6
|
+
import re
|
7
|
+
import shutil
|
8
|
+
import subprocess
|
9
|
+
import sys
|
10
|
+
import time
|
11
|
+
import webbrowser
|
12
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
13
|
+
|
14
|
+
|
15
|
+
class TinySSGPage:
|
16
|
+
"""
|
17
|
+
Base class for HTML page generation
|
18
|
+
"""
|
19
|
+
def render(self, src: str, data: dict) -> str:
|
20
|
+
"""
|
21
|
+
Template rendering process
|
22
|
+
"""
|
23
|
+
return TinySSGUtility.render_variables(src, data)
|
24
|
+
|
25
|
+
def translate(self, basestr: str) -> str:
|
26
|
+
"""
|
27
|
+
Process to convert rendered text to HTML
|
28
|
+
"""
|
29
|
+
return basestr
|
30
|
+
|
31
|
+
def query(self) -> dict:
|
32
|
+
"""
|
33
|
+
Data Acquisition Process
|
34
|
+
"""
|
35
|
+
return {}
|
36
|
+
|
37
|
+
def template(self) -> str:
|
38
|
+
"""
|
39
|
+
Template string
|
40
|
+
"""
|
41
|
+
raise TinySSGException(f"The Page class corresponding to {self.__class__.__name__} does not appear to be implemented correctly.")
|
42
|
+
|
43
|
+
|
44
|
+
class TinySSGException(Exception):
|
45
|
+
"""
|
46
|
+
TinySSG Exception Class
|
47
|
+
"""
|
48
|
+
pass
|
49
|
+
|
50
|
+
|
51
|
+
class TinySSGUtility:
|
52
|
+
"""
|
53
|
+
TinySSG Utility Class
|
54
|
+
"""
|
55
|
+
@classmethod
|
56
|
+
def render_variables(cls, src: str, data: dict, start_delimiter: str = r'\{\{\s?', end_delimiter: str = r'\s?\}\}') -> str:
|
57
|
+
"""
|
58
|
+
Replace variables in the template with the values in the dictionary
|
59
|
+
"""
|
60
|
+
result = src
|
61
|
+
for key, value in data.items():
|
62
|
+
result = re.sub(start_delimiter + re.escape(key) + end_delimiter, str(value), result)
|
63
|
+
return result
|
64
|
+
|
65
|
+
@classmethod
|
66
|
+
def get_fullpath(cls, args: dict, pathkey: str = '') -> str:
|
67
|
+
"""
|
68
|
+
Get the full path from the relative path
|
69
|
+
"""
|
70
|
+
if isinstance(args['curdir'], str) and len(args['curdir']) > 0:
|
71
|
+
basedir = args['curdir']
|
72
|
+
else:
|
73
|
+
basedir = os.getcwd()
|
74
|
+
|
75
|
+
if isinstance(args[pathkey], str) and len(args[pathkey]) > 0:
|
76
|
+
result = os.path.join(basedir, args[pathkey])
|
77
|
+
else:
|
78
|
+
result = basedir
|
79
|
+
|
80
|
+
return result
|
81
|
+
|
82
|
+
@classmethod
|
83
|
+
def clear_output(cls, output_full_path: str) -> None:
|
84
|
+
"""
|
85
|
+
Delete the output directory
|
86
|
+
"""
|
87
|
+
if os.path.exists(output_full_path):
|
88
|
+
shutil.rmtree(output_full_path)
|
89
|
+
|
90
|
+
@classmethod
|
91
|
+
def clear_start(cls, args: dict) -> None:
|
92
|
+
"""
|
93
|
+
Delete the output directory
|
94
|
+
"""
|
95
|
+
output_full_path = cls.get_fullpath(args, 'output')
|
96
|
+
cls.clear_output(output_full_path)
|
97
|
+
|
98
|
+
@classmethod
|
99
|
+
def log_print(cls, message: str) -> None:
|
100
|
+
"""
|
101
|
+
Output log message (Console Execution Only)
|
102
|
+
"""
|
103
|
+
try:
|
104
|
+
from IPython import get_ipython # type: ignore
|
105
|
+
env = get_ipython().__class__.__name__
|
106
|
+
if env == 'ZMQInteractiveShell':
|
107
|
+
return
|
108
|
+
except: # noqa: E722
|
109
|
+
pass
|
110
|
+
|
111
|
+
print(message)
|
112
|
+
|
113
|
+
@classmethod
|
114
|
+
def error_print(cls, message: str) -> None:
|
115
|
+
"""
|
116
|
+
Output error message
|
117
|
+
"""
|
118
|
+
print(message)
|
119
|
+
|
120
|
+
|
121
|
+
class TinySSGGenerator:
|
122
|
+
"""
|
123
|
+
Generator
|
124
|
+
"""
|
125
|
+
@classmethod
|
126
|
+
def check_duplicate_name(cls, files: list, dirs: list) -> list:
|
127
|
+
"""
|
128
|
+
Check for duplicate names in files and directories
|
129
|
+
"""
|
130
|
+
filenames = {os.path.splitext(f)[0] for f in files}
|
131
|
+
dirnames = set(dirs)
|
132
|
+
|
133
|
+
conflicts = list(filenames & dirnames)
|
134
|
+
|
135
|
+
return conflicts
|
136
|
+
|
137
|
+
@classmethod
|
138
|
+
def extract_page_classes(cls, root: str, filename: str) -> list:
|
139
|
+
"""
|
140
|
+
Extract page classes from the specified file.
|
141
|
+
"""
|
142
|
+
page_classes = []
|
143
|
+
check_base_name = TinySSGPage.__name__
|
144
|
+
|
145
|
+
if filename.endswith('.py') and filename != '__init__.py':
|
146
|
+
module_name = os.path.splitext(filename)[0] # Excluding extensions
|
147
|
+
module_path = os.path.join(root, filename)
|
148
|
+
|
149
|
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
150
|
+
if spec and spec.loader:
|
151
|
+
module = importlib.util.module_from_spec(spec)
|
152
|
+
spec.loader.exec_module(module)
|
153
|
+
for _, members in inspect.getmembers(module):
|
154
|
+
if inspect.isclass(members) and members.__module__ == module_name:
|
155
|
+
parents = [m.__name__ for m in members.__mro__]
|
156
|
+
if check_base_name in parents:
|
157
|
+
page_classes.append(members)
|
158
|
+
|
159
|
+
return page_classes, module_name, module_path
|
160
|
+
|
161
|
+
@classmethod
|
162
|
+
def check_input_file(cls, relative_path: str, filename: str, input_file: str) -> bool:
|
163
|
+
"""
|
164
|
+
Check if the input file is the same as the file being processed
|
165
|
+
"""
|
166
|
+
convert_filename = os.path.join(relative_path, os.path.splitext(filename)[0]).replace(os.sep, '/')
|
167
|
+
convert_input_file = os.path.splitext(re.sub(r'^\./', '', input_file))[0].replace(os.sep, '/')
|
168
|
+
|
169
|
+
return convert_filename == convert_input_file
|
170
|
+
|
171
|
+
@classmethod
|
172
|
+
def search_route(cls, args: dict) -> dict:
|
173
|
+
"""
|
174
|
+
Search for Page classes in the specified directory
|
175
|
+
"""
|
176
|
+
static_path = args['static']
|
177
|
+
input_file = args['input']
|
178
|
+
|
179
|
+
try:
|
180
|
+
prev_dont_write_bytecode = sys.dont_write_bytecode
|
181
|
+
sys.dont_write_bytecode = True
|
182
|
+
|
183
|
+
routes = {}
|
184
|
+
|
185
|
+
full_pages_path = TinySSGUtility.get_fullpath(args, 'page')
|
186
|
+
page_counter = 0
|
187
|
+
|
188
|
+
for root, dirs, files in os.walk(full_pages_path):
|
189
|
+
relative_path = os.path.relpath(root, full_pages_path)
|
190
|
+
|
191
|
+
conflicts = cls.check_duplicate_name(files, dirs)
|
192
|
+
if len(conflicts) > 0:
|
193
|
+
raise TinySSGException(f"The following names conflict between files and directories: {', '.join(conflicts)} in {relative_path}")
|
194
|
+
|
195
|
+
if relative_path == '.':
|
196
|
+
relative_path = ''
|
197
|
+
|
198
|
+
if relative_path == static_path:
|
199
|
+
raise TinySSGException(f"Static file directory name conflict: {os.path.join(full_pages_path, relative_path)}")
|
200
|
+
|
201
|
+
if relative_path.endswith('__pycache__'):
|
202
|
+
continue
|
203
|
+
|
204
|
+
current_dict = routes
|
205
|
+
|
206
|
+
if relative_path:
|
207
|
+
for part in relative_path.split(os.sep):
|
208
|
+
if part not in current_dict:
|
209
|
+
current_dict[part] = {}
|
210
|
+
current_dict = current_dict[part]
|
211
|
+
|
212
|
+
for filename in files:
|
213
|
+
if len(input_file) > 0 and not cls.check_input_file(relative_path, filename, input_file):
|
214
|
+
continue
|
215
|
+
|
216
|
+
if relative_path == '' and filename == f"{static_path}.py":
|
217
|
+
raise TinySSGException(f"Static file directory name conflict: {os.path.join(root, filename)}")
|
218
|
+
|
219
|
+
page_classes, module_name, module_path = cls.extract_page_classes(root, filename)
|
220
|
+
page_counter += len(page_classes)
|
221
|
+
if len(page_classes) > 1:
|
222
|
+
current_dict[module_name] = {c.__name__: c for c in page_classes}
|
223
|
+
elif len(page_classes) == 1:
|
224
|
+
current_dict[module_name] = page_classes[0]
|
225
|
+
else:
|
226
|
+
TinySSGUtility.log_print(f"warning: No Page class found in {module_path}")
|
227
|
+
|
228
|
+
if page_counter == 0:
|
229
|
+
raise TinySSGException('No Page classes found.')
|
230
|
+
finally:
|
231
|
+
sys.dont_write_bytecode = prev_dont_write_bytecode
|
232
|
+
|
233
|
+
return routes
|
234
|
+
|
235
|
+
@classmethod
|
236
|
+
def create_content(cls, page: TinySSGPage) -> str:
|
237
|
+
"""
|
238
|
+
Generate HTML content from Page class
|
239
|
+
"""
|
240
|
+
basefetch = page.query()
|
241
|
+
fetchdata, slugkey = basefetch if isinstance(basefetch, tuple) else (basefetch, None)
|
242
|
+
if isinstance(fetchdata, dict):
|
243
|
+
baselist = [fetchdata]
|
244
|
+
slugkey = None
|
245
|
+
single_page = True
|
246
|
+
elif isinstance(fetchdata, list):
|
247
|
+
if len(fetchdata) == 0:
|
248
|
+
return {}
|
249
|
+
baselist = fetchdata
|
250
|
+
for i in range(len(baselist)):
|
251
|
+
if not isinstance(baselist[i], dict):
|
252
|
+
raise TinySSGException('The query method must return a dictionary or a list of dictionaries.')
|
253
|
+
single_page = False
|
254
|
+
else:
|
255
|
+
raise TinySSGException('The query method must return a dictionary or a list of dictionaries.')
|
256
|
+
|
257
|
+
result = {}
|
258
|
+
|
259
|
+
for i in range(len(baselist)):
|
260
|
+
if isinstance(slugkey, str) and slugkey in baselist[i]:
|
261
|
+
key = baselist[i][slugkey]
|
262
|
+
else:
|
263
|
+
key = str(i + 1)
|
264
|
+
pagedata = baselist[i]
|
265
|
+
pagetemp = page.template()
|
266
|
+
basestr = page.render(pagetemp, pagedata).strip() + '\n'
|
267
|
+
htmlstr = page.translate(basestr)
|
268
|
+
if isinstance(htmlstr, str) and len(htmlstr) > 0:
|
269
|
+
result[key] = htmlstr
|
270
|
+
|
271
|
+
return result['1'] if single_page else result
|
272
|
+
|
273
|
+
@classmethod
|
274
|
+
def traverse_route(cls, route: dict, dict_path: str = '') -> dict:
|
275
|
+
"""
|
276
|
+
Traverse the route dictionary and generate HTML content
|
277
|
+
"""
|
278
|
+
result = {}
|
279
|
+
|
280
|
+
for key, value in route.items():
|
281
|
+
|
282
|
+
if isinstance(value, dict):
|
283
|
+
current_path = f"{dict_path}/{key}"
|
284
|
+
result[key] = cls.traverse_route(value, current_path)
|
285
|
+
else:
|
286
|
+
page = value()
|
287
|
+
page.name = key
|
288
|
+
if not isinstance(page.name, str) or len(page.name) == 0:
|
289
|
+
raise TinySSGException('The name must be a non-empty string.')
|
290
|
+
result[key] = cls.create_content(page)
|
291
|
+
|
292
|
+
return result
|
293
|
+
|
294
|
+
@classmethod
|
295
|
+
def generate_routes(cls, args: dict) -> dict:
|
296
|
+
"""
|
297
|
+
Generate HTML content dictionary from Page classes
|
298
|
+
"""
|
299
|
+
route = cls.search_route(args)
|
300
|
+
return cls.traverse_route(route)
|
301
|
+
|
302
|
+
@classmethod
|
303
|
+
def output_file(cls, data: dict, full_path: str) -> None:
|
304
|
+
"""
|
305
|
+
Output the HTML content dictionary to the file
|
306
|
+
"""
|
307
|
+
for key, value in data.items():
|
308
|
+
if isinstance(value, dict) and len(value) > 0:
|
309
|
+
relative_path = os.path.join(full_path, key)
|
310
|
+
if not os.path.exists(relative_path):
|
311
|
+
os.makedirs(relative_path)
|
312
|
+
cls.output_file(value, relative_path)
|
313
|
+
elif isinstance(value, str):
|
314
|
+
with open(os.path.join(full_path, key + '.html'), 'w', encoding='utf-8') as f:
|
315
|
+
f.write(value)
|
316
|
+
|
317
|
+
@classmethod
|
318
|
+
def generator_start(cls, args: dict) -> None:
|
319
|
+
"""
|
320
|
+
Generate HTML files from Page classes
|
321
|
+
"""
|
322
|
+
input_full_path = TinySSGUtility.get_fullpath(args, 'page')
|
323
|
+
|
324
|
+
if not os.path.isdir(input_full_path):
|
325
|
+
raise TinySSGException(f"The specified page directory does not exist. ({input_full_path})")
|
326
|
+
|
327
|
+
page_data = cls.generate_routes(args)
|
328
|
+
output_full_path = TinySSGUtility.get_fullpath(args, 'output')
|
329
|
+
|
330
|
+
if not os.path.exists(output_full_path):
|
331
|
+
os.makedirs(output_full_path)
|
332
|
+
|
333
|
+
cls.output_file(page_data, output_full_path)
|
334
|
+
|
335
|
+
static_full_path = TinySSGUtility.get_fullpath(args, 'static')
|
336
|
+
output_static_full_path = os.path.join(output_full_path, args['static'])
|
337
|
+
|
338
|
+
if os.path.isdir(static_full_path):
|
339
|
+
if not os.path.exists(output_static_full_path):
|
340
|
+
os.makedirs(output_static_full_path)
|
341
|
+
shutil.copytree(static_full_path, output_static_full_path, dirs_exist_ok=True)
|
342
|
+
|
343
|
+
|
344
|
+
class TinySSGDebugHTTPServer(HTTPServer):
|
345
|
+
"""
|
346
|
+
Custom HTTP server class
|
347
|
+
"""
|
348
|
+
def __init__(self, server_address: tuple, RequestHandlerClass: any, args: dict, route: dict, reload: bool) -> None:
|
349
|
+
super().__init__(server_address, RequestHandlerClass)
|
350
|
+
self.args = args
|
351
|
+
self.route = route
|
352
|
+
self.reload = reload
|
353
|
+
|
354
|
+
|
355
|
+
class TinySSGDebugHTTPHandler(SimpleHTTPRequestHandler):
|
356
|
+
"""
|
357
|
+
Custom HTTP request handler
|
358
|
+
"""
|
359
|
+
def __init__(self, *args, **kwargs) -> None:
|
360
|
+
try:
|
361
|
+
super().__init__(*args, **kwargs)
|
362
|
+
except ConnectionResetError:
|
363
|
+
pass
|
364
|
+
|
365
|
+
def end_headers(self):
|
366
|
+
self.send_header('Cache-Control', 'no-store')
|
367
|
+
return super().end_headers()
|
368
|
+
|
369
|
+
def log_message(self, format: str, *args: any) -> None:
|
370
|
+
TinySSGDebug.print_httpd_log_message(self, self.server, format, *args)
|
371
|
+
|
372
|
+
def do_GET(self) -> None:
|
373
|
+
TinySSGDebug.httpd_get_handler(self, self.server)
|
374
|
+
|
375
|
+
|
376
|
+
class TinySSGDebug:
|
377
|
+
"""
|
378
|
+
Debug Server
|
379
|
+
"""
|
380
|
+
@classmethod
|
381
|
+
def watchdog_script(cls) -> str:
|
382
|
+
"""
|
383
|
+
JavaScript code that checks for file updates from a web browser and reloads the file if there are any updates
|
384
|
+
"""
|
385
|
+
return '''
|
386
|
+
<script type="module">
|
387
|
+
let __reload_check = () => {
|
388
|
+
fetch('/change').then(response => response.json()).then(data => {
|
389
|
+
if (data.reload) {
|
390
|
+
console.log('Change detected. Reloading...');
|
391
|
+
location.reload();
|
392
|
+
} else {
|
393
|
+
setTimeout(__reload_check, 1000);
|
394
|
+
}
|
395
|
+
});
|
396
|
+
};
|
397
|
+
setTimeout(__reload_check, 1000);
|
398
|
+
</script>'''
|
399
|
+
|
400
|
+
@classmethod
|
401
|
+
def send_ok_response(cls, handler: TinySSGDebugHTTPHandler, content_type: str, content: str = '', add_headers: dict = {}) -> None:
|
402
|
+
"""
|
403
|
+
Send an OK response
|
404
|
+
"""
|
405
|
+
encoded_content = content.encode('utf-8')
|
406
|
+
handler.send_response(200)
|
407
|
+
handler.send_header('Content-type', content_type)
|
408
|
+
handler.send_header('Content-Length', len(encoded_content))
|
409
|
+
for key, value in add_headers.items():
|
410
|
+
handler.send_header(key, value)
|
411
|
+
handler.end_headers()
|
412
|
+
handler.wfile.write(encoded_content)
|
413
|
+
|
414
|
+
@classmethod
|
415
|
+
def send_no_ok_response(cls, handler: TinySSGDebugHTTPHandler, status: int, content: str = '', add_headers: dict = {}) -> None:
|
416
|
+
"""
|
417
|
+
Send a non-OK response
|
418
|
+
"""
|
419
|
+
handler.send_response(status)
|
420
|
+
for key, value in add_headers.items():
|
421
|
+
handler.send_header(key, value)
|
422
|
+
if isinstance(content, str) and len(content) > 0:
|
423
|
+
encoded_content = content.encode('utf-8')
|
424
|
+
handler.send_header('Content-type', 'text/plain')
|
425
|
+
handler.send_header('Content-Length', len(encoded_content))
|
426
|
+
handler.end_headers()
|
427
|
+
handler.wfile.write(encoded_content)
|
428
|
+
else:
|
429
|
+
handler.end_headers()
|
430
|
+
|
431
|
+
@classmethod
|
432
|
+
def print_httpd_log_message(cls, handler: TinySSGDebugHTTPHandler, server: TinySSGDebugHTTPServer, format: str, *args: any) -> None:
|
433
|
+
"""
|
434
|
+
Output the log message (HTTPServer)
|
435
|
+
"""
|
436
|
+
if not server.args['nolog'] and not str(args[0]).startswith('GET /change'):
|
437
|
+
SimpleHTTPRequestHandler.log_message(handler, format, *args)
|
438
|
+
sys.stdout.flush()
|
439
|
+
|
440
|
+
@classmethod
|
441
|
+
def httpd_get_handler(cls, handler: TinySSGDebugHTTPHandler, server: TinySSGDebugHTTPServer) -> None:
|
442
|
+
"""
|
443
|
+
Process the GET request
|
444
|
+
"""
|
445
|
+
if handler.path == '/change':
|
446
|
+
cls.send_ok_response(handler, 'application/json', json.dumps({'reload': server.reload}))
|
447
|
+
server.reload = False
|
448
|
+
elif handler.path == '/stop':
|
449
|
+
cls.send_ok_response(handler, 'text/plain', 'Server Stopped.')
|
450
|
+
server.shutdown()
|
451
|
+
elif handler.path.startswith(f"/{server.args['output']}/{server.args['static']}/"):
|
452
|
+
redirect_path = re.sub('/' + re.escape(server.args['output']), '', handler.path)
|
453
|
+
handler.path = redirect_path
|
454
|
+
SimpleHTTPRequestHandler.do_GET(handler)
|
455
|
+
elif handler.path == f"/{server.args['output']}":
|
456
|
+
cls.send_no_ok_response(handler, 301, '', {'Location': f"/{server.args['output']}/"})
|
457
|
+
elif handler.path.startswith(f"/{server.args['output']}/"):
|
458
|
+
baselen = len(f"/{server.args['output']}/")
|
459
|
+
basename = re.sub(r'\.html$', '', handler.path[baselen:])
|
460
|
+
basename = f"{basename}index" if basename.endswith('/') or basename == '' else basename
|
461
|
+
output_path = basename.split('/')
|
462
|
+
current_route = server.route
|
463
|
+
|
464
|
+
for path in output_path:
|
465
|
+
if not isinstance(current_route, dict) or path not in current_route:
|
466
|
+
cls.send_no_ok_response(handler, 404, 'Not Found')
|
467
|
+
return
|
468
|
+
current_route = current_route[path]
|
469
|
+
|
470
|
+
if isinstance(current_route, dict):
|
471
|
+
cls.send_no_ok_response(handler, 301, '', {'Location': f"{handler.path}/"})
|
472
|
+
elif not isinstance(current_route, str):
|
473
|
+
TinySSGUtility.error_print(f"Error: The Page class for {handler.path} may not be implemented correctly.")
|
474
|
+
cls.send_no_ok_response(handler, 500, 'Internal Server Error')
|
475
|
+
else:
|
476
|
+
current_route = current_route if server.args['noreload'] else re.sub(r'(\s*</head>)', f"{cls.watchdog_script()}\n\\1", current_route)
|
477
|
+
cls.send_ok_response(handler, 'text/html', current_route)
|
478
|
+
else:
|
479
|
+
cls.send_no_ok_response(handler, 404, 'Not Found')
|
480
|
+
|
481
|
+
@classmethod
|
482
|
+
def stop_server(cls, process: any) -> None:
|
483
|
+
"""
|
484
|
+
Stop the debug server
|
485
|
+
"""
|
486
|
+
process.kill()
|
487
|
+
|
488
|
+
@classmethod
|
489
|
+
def server_stop_output(cls, process) -> None:
|
490
|
+
"""
|
491
|
+
Output the server stop message
|
492
|
+
"""
|
493
|
+
TinySSGUtility.error_print(f"Server return code:{process.poll()}")
|
494
|
+
TinySSGUtility.error_print('Server Output:\n')
|
495
|
+
TinySSGUtility.error_print(process.stdout.read() if process.stdout else '')
|
496
|
+
TinySSGUtility.error_print(process.stderr.read() if process.stderr else '')
|
497
|
+
|
498
|
+
@classmethod
|
499
|
+
def server_start(cls, args: dict) -> None:
|
500
|
+
"""
|
501
|
+
Run the debug server
|
502
|
+
"""
|
503
|
+
reload = args['mode'] == 'servreload'
|
504
|
+
route = TinySSGGenerator.generate_routes(args)
|
505
|
+
server_address = ('', args['port'])
|
506
|
+
httpd = TinySSGDebugHTTPServer(server_address, TinySSGDebugHTTPHandler, args, route, reload)
|
507
|
+
TinySSGUtility.error_print(f"Starting server on http://localhost:{args['port']}/{args['output']}/")
|
508
|
+
httpd.serve_forever()
|
509
|
+
|
510
|
+
|
511
|
+
class TinySSGLauncher:
|
512
|
+
"""
|
513
|
+
Watchdog and Server Launcher
|
514
|
+
"""
|
515
|
+
@classmethod
|
516
|
+
def check_for_changes(cls, mod_time: float, args: dict, pathlits: list) -> bool:
|
517
|
+
"""
|
518
|
+
Check for changes in the specified directories
|
519
|
+
"""
|
520
|
+
path_times = []
|
521
|
+
new_mod_time = 0
|
522
|
+
|
523
|
+
try:
|
524
|
+
for path in pathlits:
|
525
|
+
time_list = [os.path.getmtime(os.path.join(root, file)) for root, _, files in os.walk(path) for file in files]
|
526
|
+
if len(time_list) > 0:
|
527
|
+
this_path_time = max(time_list)
|
528
|
+
path_times.append(this_path_time)
|
529
|
+
|
530
|
+
if len(path_times) > 0:
|
531
|
+
new_mod_time = max(path_times)
|
532
|
+
|
533
|
+
if new_mod_time > mod_time:
|
534
|
+
mod_time = new_mod_time + args['wait']
|
535
|
+
return True, mod_time
|
536
|
+
except Exception as e:
|
537
|
+
TinySSGUtility.log_print(f"update check warning: {e}")
|
538
|
+
|
539
|
+
return False, mod_time
|
540
|
+
|
541
|
+
@classmethod
|
542
|
+
def launch_server(cls, args: dict, reload: bool) -> None:
|
543
|
+
"""
|
544
|
+
Launch the server
|
545
|
+
"""
|
546
|
+
servcommand = 'serv' if not reload else 'servreload'
|
547
|
+
|
548
|
+
newargv = args.copy()
|
549
|
+
newargv['mode'] = servcommand
|
550
|
+
|
551
|
+
command = [sys.executable, __file__, '--config', f"{json.dumps(newargv)}", 'config']
|
552
|
+
|
553
|
+
process = subprocess.Popen(
|
554
|
+
command,
|
555
|
+
stdout=None if not args['nolog'] else subprocess.PIPE,
|
556
|
+
stderr=None if not args['nolog'] else subprocess.PIPE,
|
557
|
+
text=True,
|
558
|
+
encoding='utf-8'
|
559
|
+
)
|
560
|
+
|
561
|
+
time.sleep(1)
|
562
|
+
|
563
|
+
if process.poll() is None:
|
564
|
+
return process
|
565
|
+
else:
|
566
|
+
TinySSGUtility.log_print('Server start failed.')
|
567
|
+
TinySSGDebug.stop_server(process)
|
568
|
+
return None
|
569
|
+
|
570
|
+
@classmethod
|
571
|
+
def open_browser(cls, args: dict) -> None:
|
572
|
+
"""
|
573
|
+
Open the browser or Display Jupyter Iframe
|
574
|
+
"""
|
575
|
+
url = f"http://localhost:{args['port']}/{args['output']}/"
|
576
|
+
|
577
|
+
is_jupyter = False
|
578
|
+
|
579
|
+
try:
|
580
|
+
from IPython import get_ipython # type: ignore
|
581
|
+
env = get_ipython().__class__.__name__
|
582
|
+
if env == 'ZMQInteractiveShell':
|
583
|
+
is_jupyter = True
|
584
|
+
except: # noqa: E722
|
585
|
+
pass
|
586
|
+
|
587
|
+
if is_jupyter:
|
588
|
+
from IPython import display
|
589
|
+
display.display(display.IFrame(url, width=args['jwidth'], height=args['jheight']))
|
590
|
+
else:
|
591
|
+
webbrowser.open(url)
|
592
|
+
|
593
|
+
@classmethod
|
594
|
+
def launcher_start(cls, args: dict) -> None:
|
595
|
+
"""
|
596
|
+
Launch the debug server and file change detection
|
597
|
+
"""
|
598
|
+
if isinstance(args['curdir'], str) and len(args['curdir']) > 0:
|
599
|
+
os.chdir(args['curdir'])
|
600
|
+
|
601
|
+
cur_dir = os.getcwd()
|
602
|
+
page_dir = os.path.join(cur_dir, args['page'])
|
603
|
+
static_dir = os.path.join(cur_dir, args['static'])
|
604
|
+
lib_dir = os.path.join(cur_dir, args['lib'])
|
605
|
+
mod_time = 0.0
|
606
|
+
should_reload = False
|
607
|
+
|
608
|
+
if not os.path.isdir(page_dir):
|
609
|
+
raise TinySSGException(f"The specified page directory does not exist. ({page_dir})")
|
610
|
+
|
611
|
+
check_dirs = [page_dir]
|
612
|
+
|
613
|
+
if os.path.isdir(static_dir):
|
614
|
+
check_dirs.append(static_dir)
|
615
|
+
|
616
|
+
if os.path.isdir(lib_dir):
|
617
|
+
check_dirs.append(lib_dir)
|
618
|
+
|
619
|
+
if not args['noreload']:
|
620
|
+
_, mod_time = cls.check_for_changes(0.0, args, check_dirs)
|
621
|
+
|
622
|
+
process = cls.launch_server(args, False)
|
623
|
+
|
624
|
+
if process is None:
|
625
|
+
return
|
626
|
+
|
627
|
+
if not args['noopen']:
|
628
|
+
cls.open_browser(args)
|
629
|
+
|
630
|
+
while True:
|
631
|
+
try:
|
632
|
+
time.sleep(1)
|
633
|
+
if process.poll() is not None:
|
634
|
+
TinySSGUtility.log_print('Server stopped.')
|
635
|
+
TinySSGDebug.server_stop_output(process)
|
636
|
+
break
|
637
|
+
if not args['noreload']:
|
638
|
+
should_reload, mod_time = cls.check_for_changes(mod_time, args, check_dirs)
|
639
|
+
if should_reload:
|
640
|
+
TinySSGUtility.log_print('File changed. Reloading...')
|
641
|
+
TinySSGDebug.stop_server(process)
|
642
|
+
time.sleep(1)
|
643
|
+
process = cls.launch_server(args, True)
|
644
|
+
except KeyboardInterrupt:
|
645
|
+
TinySSGDebug.stop_server(process)
|
646
|
+
TinySSGUtility.error_print('Server stopped.')
|
647
|
+
TinySSGDebug.server_stop_output(process)
|
648
|
+
break
|
649
|
+
|
650
|
+
|
651
|
+
class TinySSG:
|
652
|
+
"""
|
653
|
+
TinySSG Main Class
|
654
|
+
"""
|
655
|
+
@classmethod
|
656
|
+
def main(cls, args: dict) -> None:
|
657
|
+
"""
|
658
|
+
Main function
|
659
|
+
"""
|
660
|
+
exitcode = 0
|
661
|
+
|
662
|
+
try:
|
663
|
+
if args['mode'] == 'gen':
|
664
|
+
if args['input'] == '':
|
665
|
+
TinySSGUtility.clear_start(args)
|
666
|
+
TinySSGGenerator.generator_start(args)
|
667
|
+
TinySSGUtility.log_print('HTML files generated.')
|
668
|
+
elif args['mode'] == 'dev':
|
669
|
+
TinySSGLauncher.launcher_start(args)
|
670
|
+
elif args['mode'] == 'cls':
|
671
|
+
TinySSGUtility.clear_start(args)
|
672
|
+
TinySSGUtility.log_print('Output directory cleared.')
|
673
|
+
elif args['mode'] == 'serv' or args['mode'] == 'servreload':
|
674
|
+
TinySSGDebug.server_start(args)
|
675
|
+
elif args['mode'] == 'config':
|
676
|
+
config = json.loads(args['config'])
|
677
|
+
default_args = cls.get_default_arg_dict()
|
678
|
+
for key, value in default_args.items():
|
679
|
+
if key not in config:
|
680
|
+
config[key] = value
|
681
|
+
cls.main(config)
|
682
|
+
else:
|
683
|
+
raise TinySSGException('Invalid mode.')
|
684
|
+
except TinySSGException as e:
|
685
|
+
TinySSGUtility.error_print(f"Error: {e}")
|
686
|
+
exitcode = 1
|
687
|
+
|
688
|
+
sys.exit(exitcode)
|
689
|
+
|
690
|
+
@classmethod
|
691
|
+
def get_arg_parser(cls) -> argparse.ArgumentParser:
|
692
|
+
"""
|
693
|
+
Set the argument parser
|
694
|
+
"""
|
695
|
+
parser = argparse.ArgumentParser(prog='python -m tinyssg', description='TinySSG Simple Static Site Generate Tool')
|
696
|
+
parser.add_argument('mode', choices=['dev', 'gen', 'cls', 'serv', 'servreload', 'config'], help='Select the mode to run (gen = Generate HTML files, dev = Run the debug server)')
|
697
|
+
parser.add_argument('--port', '-P', type=int, default=8000, help='Port number for the debug server')
|
698
|
+
parser.add_argument('--page', '-p', type=str, default='pages', help='Page file path')
|
699
|
+
parser.add_argument('--static', '-s', type=str, default='static', help='Static file path')
|
700
|
+
parser.add_argument('--lib', '-l', type=str, default='libs', help='Library file path')
|
701
|
+
parser.add_argument('--input', '-i', type=str, default='', help='Input file name (Used to generate specific files only)')
|
702
|
+
parser.add_argument('--output', '-o', type=str, default='dist', help='Output directory path')
|
703
|
+
parser.add_argument('--wait', '-w', type=int, default=5, help='Wait time for file change detection')
|
704
|
+
parser.add_argument('--nolog', '-n', action='store_true', help='Do not output debug server log')
|
705
|
+
parser.add_argument('--noreload', '-r', action='store_true', help='Do not reload the server when the file changes')
|
706
|
+
parser.add_argument('--noopen', '-N', action='store_true', help='Do not open the browser when starting the server')
|
707
|
+
parser.add_argument('--curdir', '-C', type=str, default='', help='Current directory (For Jupyter)')
|
708
|
+
parser.add_argument('--config', '-c', type=str, default='', help='Configuration json string')
|
709
|
+
parser.add_argument('--jwidth', '-jw', type=str, default='600', help='Jupyter iframe width')
|
710
|
+
parser.add_argument('--jheight', '-jh', type=str, default='600', help='Jupyter iframe height')
|
711
|
+
|
712
|
+
return parser
|
713
|
+
|
714
|
+
@classmethod
|
715
|
+
def get_default_arg_dict(cls):
|
716
|
+
parser = cls.get_arg_parser()
|
717
|
+
return vars(parser.parse_args(['dev']))
|
718
|
+
|
719
|
+
@classmethod
|
720
|
+
def cli_main(cls):
|
721
|
+
"""
|
722
|
+
Command line interface
|
723
|
+
"""
|
724
|
+
parser = cls.get_arg_parser()
|
725
|
+
parse_args = parser.parse_args()
|
726
|
+
args = vars(parse_args)
|
727
|
+
cls.main(args)
|
728
|
+
|
729
|
+
|
730
|
+
if __name__ == '__main__':
|
731
|
+
TinySSG.cli_main()
|