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/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()