tinyssg 0.0.8__py3-none-any.whl → 0.0.10__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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()