tinyssg 0.0.0__tar.gz
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-0.0.0/PKG-INFO +106 -0
- tinyssg-0.0.0/README.md +79 -0
- tinyssg-0.0.0/pyproject.toml +36 -0
- tinyssg-0.0.0/setup.cfg +4 -0
- tinyssg-0.0.0/tinyssg/__init__.py +716 -0
- tinyssg-0.0.0/tinyssg.egg-info/PKG-INFO +106 -0
- tinyssg-0.0.0/tinyssg.egg-info/SOURCES.txt +7 -0
- tinyssg-0.0.0/tinyssg.egg-info/dependency_links.txt +1 -0
- tinyssg-0.0.0/tinyssg.egg-info/top_level.txt +1 -0
tinyssg-0.0.0/PKG-INFO
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: tinyssg
|
3
|
+
Version: 0.0.0
|
4
|
+
Summary: A simple static site generator
|
5
|
+
Author: Uniras
|
6
|
+
Author-email: tkappeng@gmail.com
|
7
|
+
License: MIT License
|
8
|
+
Project-URL: Homepage, https://github.com/uniras/tinyssg
|
9
|
+
Project-URL: Repository, https://github.com/uniras/tinyssg
|
10
|
+
Keywords: ssg,static site generator,html
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
12
|
+
Classifier: Intended Audience :: Developers
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
15
|
+
Classifier: Programming Language :: Python :: 3.7
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
22
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
25
|
+
Classifier: Topic :: Utilities
|
26
|
+
Description-Content-Type: text/markdown
|
27
|
+
|
28
|
+
# TinySSG
|
29
|
+
|
30
|
+
## 概要
|
31
|
+
|
32
|
+
TinySSGは、ファイルベースルーティングのシンプルな静的サイトジェネレータです。
|
33
|
+
ページを簡単な構造のTinySSGPageクラスを継承したPythonコードで記述することで単純さと柔軟性を両立しています。
|
34
|
+
|
35
|
+
## インストール
|
36
|
+
|
37
|
+
```bash
|
38
|
+
pip install tinyssg
|
39
|
+
```
|
40
|
+
|
41
|
+
## 使い方
|
42
|
+
|
43
|
+
### ディレクトリ構成
|
44
|
+
|
45
|
+
以下のようにディレクトリを構成します。ディレクトリ名はオプション引数で変更可能です。
|
46
|
+
|
47
|
+
```text
|
48
|
+
--- proect
|
49
|
+
|-- pages SSGの対象となるPythonファイルを配置します
|
50
|
+
|-- libs SSG・デプロイの対象にならないPythonファイルを配置します(ライブラリなど)
|
51
|
+
|-- static SSGの対象にならない静的ファイルを配置します(css, 画像など)
|
52
|
+
|-- dist SSGの結果が出力されるディレクトリです。このディレクトリの中身をWebサーバに配置することでWebサイトとして公開できます。
|
53
|
+
|-- static staticディレクトリはこのディレクトリにコピーされます
|
54
|
+
```
|
55
|
+
|
56
|
+
pages, libs, staticディレクトリは、開発サーバー起動時に監視され、ファイルが変更されると自動的にサーバーを再起動します。
|
57
|
+
|
58
|
+
### ページの作成
|
59
|
+
|
60
|
+
`Page`ディレクトリ内にPythonファイルを作り、`TinySSGPage`クラスを継承したクラスを作成します。
|
61
|
+
|
62
|
+
```python
|
63
|
+
from tinyssg import TinySSGPage
|
64
|
+
|
65
|
+
class IndexPage(TinySSGPage):
|
66
|
+
def query(self):
|
67
|
+
return {
|
68
|
+
'title': 'Index',
|
69
|
+
'content': 'Hello, World!'
|
70
|
+
}
|
71
|
+
|
72
|
+
def template(self):
|
73
|
+
return '''
|
74
|
+
<!DOCTYPE html>
|
75
|
+
<html>
|
76
|
+
<head>
|
77
|
+
<meta charset="utf-8" />
|
78
|
+
<title>{{ title }}</title>
|
79
|
+
</head>
|
80
|
+
<body>
|
81
|
+
<h1>{{ title }}</h1>
|
82
|
+
<p>{{ content }}</p>
|
83
|
+
</body>
|
84
|
+
</html>'''
|
85
|
+
```
|
86
|
+
|
87
|
+
`query`メソッドでテンプレートに渡すデータを返し、`template`メソッドでHTMLテンプレートを返します。
|
88
|
+
TinySSGは、これらのメソッドの返り値を使ってHTMLを生成します。
|
89
|
+
|
90
|
+
`query`メソッドが返すデータは、Python辞書形式またはPython辞書のリスト形式である必要があります。
|
91
|
+
辞書形式の場合はpythonファイル名のHTMLファイルが生成され、リスト形式の場合はPythonファイル名と同じディレクトリが作成され、デフォルトでは1からの数字.htmlのファイル名でHTMLファイルが生成されます。
|
92
|
+
`return`の際にタプルとしてリストと一緒にキー名を表す文字列を返すと、そのキーに対応する値をファイル名としてHTMLファイルが生成されます。
|
93
|
+
|
94
|
+
`TinySSG`はデフォルトでは単純にテンプレートの`{{ キー名 }}`で囲まれた部分を`query`メソッドの返り値である辞書のキーに対応する値で単純に置換するだけですが、
|
95
|
+
`render`メソッドをオーバーライドすることで、より複雑な処理を行うこともできます。Jinja2などのテンプレートエンジンを使うこともできます。
|
96
|
+
|
97
|
+
また、`translate`メソッドをオーバーライドすることで、レンダー後のテキストを最終的なHTMLに変換する処理を定義することもできます。
|
98
|
+
ここでmarkdownライブラリを使って変換する処理を記述すればテンプレートをHTMLではなくMarkdownで記述することができます。
|
99
|
+
|
100
|
+
それぞれページごとに定義することになりますが、単純なPythonクラスですので複数のページに適用したい場合は共通部分を定義したクラスを作成し、それを継承することでコードをコピーすることなく簡単に適用することができます。
|
101
|
+
|
102
|
+
### HTMLの生成
|
103
|
+
|
104
|
+
```bash
|
105
|
+
python -m tinyssg gen
|
106
|
+
```
|
tinyssg-0.0.0/README.md
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# TinySSG
|
2
|
+
|
3
|
+
## 概要
|
4
|
+
|
5
|
+
TinySSGは、ファイルベースルーティングのシンプルな静的サイトジェネレータです。
|
6
|
+
ページを簡単な構造のTinySSGPageクラスを継承したPythonコードで記述することで単純さと柔軟性を両立しています。
|
7
|
+
|
8
|
+
## インストール
|
9
|
+
|
10
|
+
```bash
|
11
|
+
pip install tinyssg
|
12
|
+
```
|
13
|
+
|
14
|
+
## 使い方
|
15
|
+
|
16
|
+
### ディレクトリ構成
|
17
|
+
|
18
|
+
以下のようにディレクトリを構成します。ディレクトリ名はオプション引数で変更可能です。
|
19
|
+
|
20
|
+
```text
|
21
|
+
--- proect
|
22
|
+
|-- pages SSGの対象となるPythonファイルを配置します
|
23
|
+
|-- libs SSG・デプロイの対象にならないPythonファイルを配置します(ライブラリなど)
|
24
|
+
|-- static SSGの対象にならない静的ファイルを配置します(css, 画像など)
|
25
|
+
|-- dist SSGの結果が出力されるディレクトリです。このディレクトリの中身をWebサーバに配置することでWebサイトとして公開できます。
|
26
|
+
|-- static staticディレクトリはこのディレクトリにコピーされます
|
27
|
+
```
|
28
|
+
|
29
|
+
pages, libs, staticディレクトリは、開発サーバー起動時に監視され、ファイルが変更されると自動的にサーバーを再起動します。
|
30
|
+
|
31
|
+
### ページの作成
|
32
|
+
|
33
|
+
`Page`ディレクトリ内にPythonファイルを作り、`TinySSGPage`クラスを継承したクラスを作成します。
|
34
|
+
|
35
|
+
```python
|
36
|
+
from tinyssg import TinySSGPage
|
37
|
+
|
38
|
+
class IndexPage(TinySSGPage):
|
39
|
+
def query(self):
|
40
|
+
return {
|
41
|
+
'title': 'Index',
|
42
|
+
'content': 'Hello, World!'
|
43
|
+
}
|
44
|
+
|
45
|
+
def template(self):
|
46
|
+
return '''
|
47
|
+
<!DOCTYPE html>
|
48
|
+
<html>
|
49
|
+
<head>
|
50
|
+
<meta charset="utf-8" />
|
51
|
+
<title>{{ title }}</title>
|
52
|
+
</head>
|
53
|
+
<body>
|
54
|
+
<h1>{{ title }}</h1>
|
55
|
+
<p>{{ content }}</p>
|
56
|
+
</body>
|
57
|
+
</html>'''
|
58
|
+
```
|
59
|
+
|
60
|
+
`query`メソッドでテンプレートに渡すデータを返し、`template`メソッドでHTMLテンプレートを返します。
|
61
|
+
TinySSGは、これらのメソッドの返り値を使ってHTMLを生成します。
|
62
|
+
|
63
|
+
`query`メソッドが返すデータは、Python辞書形式またはPython辞書のリスト形式である必要があります。
|
64
|
+
辞書形式の場合はpythonファイル名のHTMLファイルが生成され、リスト形式の場合はPythonファイル名と同じディレクトリが作成され、デフォルトでは1からの数字.htmlのファイル名でHTMLファイルが生成されます。
|
65
|
+
`return`の際にタプルとしてリストと一緒にキー名を表す文字列を返すと、そのキーに対応する値をファイル名としてHTMLファイルが生成されます。
|
66
|
+
|
67
|
+
`TinySSG`はデフォルトでは単純にテンプレートの`{{ キー名 }}`で囲まれた部分を`query`メソッドの返り値である辞書のキーに対応する値で単純に置換するだけですが、
|
68
|
+
`render`メソッドをオーバーライドすることで、より複雑な処理を行うこともできます。Jinja2などのテンプレートエンジンを使うこともできます。
|
69
|
+
|
70
|
+
また、`translate`メソッドをオーバーライドすることで、レンダー後のテキストを最終的なHTMLに変換する処理を定義することもできます。
|
71
|
+
ここでmarkdownライブラリを使って変換する処理を記述すればテンプレートをHTMLではなくMarkdownで記述することができます。
|
72
|
+
|
73
|
+
それぞれページごとに定義することになりますが、単純なPythonクラスですので複数のページに適用したい場合は共通部分を定義したクラスを作成し、それを継承することでコードをコピーすることなく簡単に適用することができます。
|
74
|
+
|
75
|
+
### HTMLの生成
|
76
|
+
|
77
|
+
```bash
|
78
|
+
python -m tinyssg gen
|
79
|
+
```
|
@@ -0,0 +1,36 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["setuptools >= 61.0"]
|
3
|
+
build-backend = "setuptools.build_meta"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "tinyssg"
|
7
|
+
version = "0.0.0"
|
8
|
+
description = "A simple static site generator"
|
9
|
+
readme = "README.md"
|
10
|
+
authors = [
|
11
|
+
{name = "Uniras"},
|
12
|
+
{email = "tkappeng@gmail.com"}
|
13
|
+
]
|
14
|
+
license = {text = "MIT License"}
|
15
|
+
keywords = ["ssg", "static site generator", "html"]
|
16
|
+
classifiers = [
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
18
|
+
"Intended Audience :: Developers",
|
19
|
+
"License :: OSI Approved :: MIT License",
|
20
|
+
"Programming Language :: Python :: 3",
|
21
|
+
"Programming Language :: Python :: 3.7",
|
22
|
+
"Programming Language :: Python :: 3.8",
|
23
|
+
"Programming Language :: Python :: 3.9",
|
24
|
+
"Programming Language :: Python :: 3.10",
|
25
|
+
"Programming Language :: Python :: 3.11",
|
26
|
+
"Programming Language :: Python :: 3.12",
|
27
|
+
"Programming Language :: Python :: 3.13",
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
29
|
+
"Topic :: Software Development :: Libraries",
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
31
|
+
"Topic :: Utilities",
|
32
|
+
]
|
33
|
+
|
34
|
+
[project.urls]
|
35
|
+
Homepage = "https://github.com/uniras/tinyssg"
|
36
|
+
Repository = "https://github.com/uniras/tinyssg"
|
tinyssg-0.0.0/setup.cfg
ADDED
@@ -0,0 +1,716 @@
|
|
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
|
+
return os.path.join(args['curdir'], args[pathkey])
|
72
|
+
else:
|
73
|
+
return os.path.join(os.getcwd(), args[pathkey])
|
74
|
+
|
75
|
+
@classmethod
|
76
|
+
def clear_output(cls, output_full_path: str) -> None:
|
77
|
+
"""
|
78
|
+
Delete the output directory
|
79
|
+
"""
|
80
|
+
if os.path.exists(output_full_path):
|
81
|
+
shutil.rmtree(output_full_path)
|
82
|
+
|
83
|
+
@classmethod
|
84
|
+
def clear_start(cls, args: dict) -> None:
|
85
|
+
"""
|
86
|
+
Delete the output directory
|
87
|
+
"""
|
88
|
+
output_full_path = cls.get_fullpath(args, 'output')
|
89
|
+
cls.clear_output(output_full_path)
|
90
|
+
|
91
|
+
@classmethod
|
92
|
+
def log_print(cls, message: str) -> None:
|
93
|
+
"""
|
94
|
+
Output log message (Console Execution Only)
|
95
|
+
"""
|
96
|
+
try:
|
97
|
+
from IPython import get_ipython # type: ignore
|
98
|
+
env = get_ipython().__class__.__name__
|
99
|
+
if env == 'ZMQInteractiveShell':
|
100
|
+
return
|
101
|
+
except: # noqa: E722
|
102
|
+
pass
|
103
|
+
|
104
|
+
print(message)
|
105
|
+
|
106
|
+
def error_print(cla, message: str) -> None:
|
107
|
+
"""
|
108
|
+
Output error message
|
109
|
+
"""
|
110
|
+
print(message)
|
111
|
+
|
112
|
+
|
113
|
+
class TinySSGGenerator:
|
114
|
+
"""
|
115
|
+
Generator
|
116
|
+
"""
|
117
|
+
@classmethod
|
118
|
+
def check_duplicate_name(cls, files: list, dirs: list) -> list:
|
119
|
+
"""
|
120
|
+
Check for duplicate names in files and directories
|
121
|
+
"""
|
122
|
+
filenames = {os.path.splitext(f)[0] for f in files}
|
123
|
+
dirnames = set(dirs)
|
124
|
+
|
125
|
+
conflicts = list(filenames & dirnames)
|
126
|
+
|
127
|
+
return conflicts
|
128
|
+
|
129
|
+
@classmethod
|
130
|
+
def extract_page_classes(cls, root: str, filename: str) -> list:
|
131
|
+
"""
|
132
|
+
Extract page classes from the specified file.
|
133
|
+
"""
|
134
|
+
page_classes = []
|
135
|
+
check_base_name = TinySSGPage.__name__
|
136
|
+
|
137
|
+
if filename.endswith('.py') and filename != '__init__.py':
|
138
|
+
module_name = os.path.splitext(filename)[0] # Excluding extensions
|
139
|
+
module_path = os.path.join(root, filename)
|
140
|
+
|
141
|
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
142
|
+
if spec and spec.loader:
|
143
|
+
module = importlib.util.module_from_spec(spec)
|
144
|
+
spec.loader.exec_module(module)
|
145
|
+
for _, members in inspect.getmembers(module):
|
146
|
+
if inspect.isclass(members) and members.__module__ == module_name:
|
147
|
+
parents = [m.__name__ for m in members.__mro__]
|
148
|
+
if check_base_name in parents:
|
149
|
+
page_classes.append(members)
|
150
|
+
|
151
|
+
return page_classes, module_name, module_path
|
152
|
+
|
153
|
+
@classmethod
|
154
|
+
def check_input_file(cls, relative_path: str, filename: str, input_file: str) -> bool:
|
155
|
+
"""
|
156
|
+
Check if the input file is the same as the file being processed
|
157
|
+
"""
|
158
|
+
convert_filename = os.path.join(relative_path, os.path.splitext(filename)[0]).replace(os.sep, '/')
|
159
|
+
convert_input_file = os.path.splitext(re.sub(r'^\./', '', input_file))[0].replace(os.sep, '/')
|
160
|
+
|
161
|
+
return convert_filename == convert_input_file
|
162
|
+
|
163
|
+
@classmethod
|
164
|
+
def search_route(cls, args: dict) -> dict:
|
165
|
+
"""
|
166
|
+
Search for Page classes in the specified directory
|
167
|
+
"""
|
168
|
+
static_path = args['static']
|
169
|
+
input_file = args['input']
|
170
|
+
|
171
|
+
try:
|
172
|
+
prev_dont_write_bytecode = sys.dont_write_bytecode
|
173
|
+
sys.dont_write_bytecode = True
|
174
|
+
|
175
|
+
routes = {}
|
176
|
+
|
177
|
+
full_pages_path = TinySSGUtility.get_fullpath(args, 'page')
|
178
|
+
page_counter = 0
|
179
|
+
|
180
|
+
for root, dirs, files in os.walk(full_pages_path):
|
181
|
+
relative_path = os.path.relpath(root, full_pages_path)
|
182
|
+
|
183
|
+
conflicts = cls.check_duplicate_name(files, dirs)
|
184
|
+
if len(conflicts) > 0:
|
185
|
+
raise TinySSGException(f"The following names conflict between files and directories: {', '.join(conflicts)} in {relative_path}")
|
186
|
+
|
187
|
+
if relative_path == '.':
|
188
|
+
relative_path = ''
|
189
|
+
|
190
|
+
if relative_path == static_path:
|
191
|
+
raise TinySSGException(f"Static file directory name conflict: {os.path.join(full_pages_path, relative_path)}")
|
192
|
+
|
193
|
+
if relative_path.endswith('__pycache__'):
|
194
|
+
continue
|
195
|
+
|
196
|
+
current_dict = routes
|
197
|
+
|
198
|
+
if relative_path:
|
199
|
+
for part in relative_path.split(os.sep):
|
200
|
+
if part not in current_dict:
|
201
|
+
current_dict[part] = {}
|
202
|
+
current_dict = current_dict[part]
|
203
|
+
|
204
|
+
for filename in files:
|
205
|
+
if len(input_file) > 0 and not cls.check_input_file(relative_path, filename, input_file):
|
206
|
+
continue
|
207
|
+
|
208
|
+
if relative_path == '' and filename == f"{static_path}.py":
|
209
|
+
raise TinySSGException(f"Static file directory name conflict: {os.path.join(root, filename)}")
|
210
|
+
|
211
|
+
page_classes, module_name, module_path = cls.extract_page_classes(root, filename)
|
212
|
+
page_counter += len(page_classes)
|
213
|
+
if len(page_classes) > 1:
|
214
|
+
current_dict[module_name] = {c.__name__: c for c in page_classes}
|
215
|
+
elif len(page_classes) == 1:
|
216
|
+
current_dict[module_name] = page_classes[0]
|
217
|
+
else:
|
218
|
+
TinySSGUtility.log_print(f"warning: No Page class found in {module_path}")
|
219
|
+
|
220
|
+
if page_counter == 0:
|
221
|
+
raise TinySSGException('No Page classes found.')
|
222
|
+
finally:
|
223
|
+
sys.dont_write_bytecode = prev_dont_write_bytecode
|
224
|
+
|
225
|
+
return routes
|
226
|
+
|
227
|
+
@classmethod
|
228
|
+
def create_content(cls, page: TinySSGPage) -> str:
|
229
|
+
"""
|
230
|
+
Generate HTML content from Page class
|
231
|
+
"""
|
232
|
+
basefetch = page.query()
|
233
|
+
fetchdata, slugkey = basefetch if isinstance(basefetch, tuple) else (basefetch, None)
|
234
|
+
if isinstance(fetchdata, dict):
|
235
|
+
baselist = [fetchdata]
|
236
|
+
slugkey = None
|
237
|
+
single_page = True
|
238
|
+
elif isinstance(fetchdata, list):
|
239
|
+
if len(fetchdata) == 0:
|
240
|
+
return {}
|
241
|
+
baselist = fetchdata
|
242
|
+
for i in range(len(baselist)):
|
243
|
+
if not isinstance(baselist[i], dict):
|
244
|
+
raise TinySSGException('The query method must return a dictionary or a list of dictionaries.')
|
245
|
+
single_page = False
|
246
|
+
else:
|
247
|
+
raise TinySSGException('The query method must return a dictionary or a list of dictionaries.')
|
248
|
+
|
249
|
+
result = {}
|
250
|
+
|
251
|
+
for i in range(len(baselist)):
|
252
|
+
if isinstance(slugkey, str) and slugkey in baselist[i]:
|
253
|
+
key = baselist[i][slugkey]
|
254
|
+
else:
|
255
|
+
key = str(i + 1)
|
256
|
+
pagedata = baselist[i]
|
257
|
+
pagetemp = page.template()
|
258
|
+
basestr = page.render(pagetemp, pagedata).strip() + '\n'
|
259
|
+
htmlstr = page.translate(basestr)
|
260
|
+
if isinstance(htmlstr, str) and len(htmlstr) > 0:
|
261
|
+
result[key] = htmlstr
|
262
|
+
|
263
|
+
return result['1'] if single_page else result
|
264
|
+
|
265
|
+
@classmethod
|
266
|
+
def traverse_route(cls, route: dict, dict_path: str = '') -> dict:
|
267
|
+
"""
|
268
|
+
Traverse the route dictionary and generate HTML content
|
269
|
+
"""
|
270
|
+
result = {}
|
271
|
+
|
272
|
+
for key, value in route.items():
|
273
|
+
|
274
|
+
if isinstance(value, dict):
|
275
|
+
current_path = f"{dict_path}/{key}"
|
276
|
+
result[key] = cls.traverse_route(value, current_path)
|
277
|
+
else:
|
278
|
+
page = value()
|
279
|
+
page.name = key
|
280
|
+
if not isinstance(page.name, str) or len(page.name) == 0:
|
281
|
+
raise TinySSGException('The name must be a non-empty string.')
|
282
|
+
result[key] = cls.create_content(page)
|
283
|
+
|
284
|
+
return result
|
285
|
+
|
286
|
+
@classmethod
|
287
|
+
def generate_routes(cls, args: dict) -> dict:
|
288
|
+
"""
|
289
|
+
Generate HTML content dictionary from Page classes
|
290
|
+
"""
|
291
|
+
route = cls.search_route(args)
|
292
|
+
return cls.traverse_route(route)
|
293
|
+
|
294
|
+
@classmethod
|
295
|
+
def output_file(cls, data: dict, full_path: str) -> None:
|
296
|
+
"""
|
297
|
+
Output the HTML content dictionary to the file
|
298
|
+
"""
|
299
|
+
for key, value in data.items():
|
300
|
+
if isinstance(value, dict) and len(value) > 0:
|
301
|
+
relative_path = os.path.join(full_path, key)
|
302
|
+
if not os.path.exists(relative_path):
|
303
|
+
os.makedirs(relative_path)
|
304
|
+
cls.output_file(value, relative_path)
|
305
|
+
elif isinstance(value, str):
|
306
|
+
with open(os.path.join(full_path, key + '.html'), 'w', encoding='utf-8') as f:
|
307
|
+
f.write(value)
|
308
|
+
|
309
|
+
@classmethod
|
310
|
+
def generator_start(cls, args: dict) -> None:
|
311
|
+
"""
|
312
|
+
Generate HTML files from Page classes
|
313
|
+
"""
|
314
|
+
input_full_path = TinySSGUtility.get_fullpath(args, 'page')
|
315
|
+
|
316
|
+
if not os.path.isdir(input_full_path):
|
317
|
+
raise TinySSGException(f"The specified page directory does not exist. ({input_full_path})")
|
318
|
+
|
319
|
+
page_data = cls.generate_routes(args)
|
320
|
+
output_full_path = TinySSGUtility.get_fullpath(args, 'output')
|
321
|
+
|
322
|
+
if not os.path.exists(output_full_path):
|
323
|
+
os.makedirs(output_full_path)
|
324
|
+
|
325
|
+
cls.output_file(page_data, output_full_path)
|
326
|
+
|
327
|
+
static_full_path = TinySSGUtility.get_fullpath(args, 'static')
|
328
|
+
output_static_full_path = os.path.join(output_full_path, args['static'])
|
329
|
+
|
330
|
+
if os.path.isdir(static_full_path):
|
331
|
+
if not os.path.exists(output_static_full_path):
|
332
|
+
os.makedirs(output_static_full_path)
|
333
|
+
shutil.copytree(static_full_path, output_static_full_path, dirs_exist_ok=True)
|
334
|
+
|
335
|
+
|
336
|
+
class TinySSGDebugHTTPServer(HTTPServer):
|
337
|
+
"""
|
338
|
+
Custom HTTP server class
|
339
|
+
"""
|
340
|
+
def __init__(self, server_address: tuple, RequestHandlerClass: any, args: dict, route: dict, reload: bool) -> None:
|
341
|
+
super().__init__(server_address, RequestHandlerClass)
|
342
|
+
self.args = args
|
343
|
+
self.route = route
|
344
|
+
self.reload = reload
|
345
|
+
|
346
|
+
|
347
|
+
class TinySSGDebugHTTPHandler(SimpleHTTPRequestHandler):
|
348
|
+
"""
|
349
|
+
Custom HTTP request handler
|
350
|
+
"""
|
351
|
+
def __init__(self, *args, **kwargs) -> None:
|
352
|
+
try:
|
353
|
+
super().__init__(*args, **kwargs)
|
354
|
+
except ConnectionResetError:
|
355
|
+
pass
|
356
|
+
|
357
|
+
def end_headers(self):
|
358
|
+
self.send_header('Cache-Control', 'no-store')
|
359
|
+
return super().end_headers()
|
360
|
+
|
361
|
+
def log_message(self, format: str, *args: any) -> None:
|
362
|
+
TinySSGDebug.print_httpd_log_message(self, self.server, format, *args)
|
363
|
+
|
364
|
+
def do_GET(self) -> None:
|
365
|
+
TinySSGDebug.httpd_get_handler(self, self.server)
|
366
|
+
|
367
|
+
|
368
|
+
class TinySSGDebug:
|
369
|
+
"""
|
370
|
+
Debug Server
|
371
|
+
"""
|
372
|
+
@classmethod
|
373
|
+
def watchdog_script(cls) -> str:
|
374
|
+
"""
|
375
|
+
JavaScript code that checks for file updates from a web browser and reloads the file if there are any updates
|
376
|
+
"""
|
377
|
+
return '''
|
378
|
+
<script type="module">
|
379
|
+
let __reload_check = () => {
|
380
|
+
fetch('/change').then(response => response.json()).then(data => {
|
381
|
+
if (data.reload) {
|
382
|
+
console.log('Change detected. Reloading...');
|
383
|
+
location.reload();
|
384
|
+
} else {
|
385
|
+
setTimeout(__reload_check, 1000);
|
386
|
+
}
|
387
|
+
});
|
388
|
+
};
|
389
|
+
setTimeout(__reload_check, 1000);
|
390
|
+
</script>'''
|
391
|
+
|
392
|
+
@classmethod
|
393
|
+
def send_ok_response(cls, handler: TinySSGDebugHTTPHandler, content_type: str, content: str = '', add_headers: dict = {}) -> None:
|
394
|
+
"""
|
395
|
+
Send an OK response
|
396
|
+
"""
|
397
|
+
encoded_content = content.encode('utf-8')
|
398
|
+
handler.send_response(200)
|
399
|
+
handler.send_header('Content-type', content_type)
|
400
|
+
handler.send_header('Content-Length', len(encoded_content))
|
401
|
+
for key, value in add_headers.items():
|
402
|
+
handler.send_header(key, value)
|
403
|
+
handler.end_headers()
|
404
|
+
handler.wfile.write(encoded_content)
|
405
|
+
|
406
|
+
@classmethod
|
407
|
+
def send_no_ok_response(cls, handler: TinySSGDebugHTTPHandler, status: int, content: str = '', add_headers: dict = {}) -> None:
|
408
|
+
"""
|
409
|
+
Send a non-OK response
|
410
|
+
"""
|
411
|
+
handler.send_response(status)
|
412
|
+
for key, value in add_headers.items():
|
413
|
+
handler.send_header(key, value)
|
414
|
+
if isinstance(content, str) and len(content) > 0:
|
415
|
+
encoded_content = content.encode('utf-8')
|
416
|
+
handler.send_header('Content-type', 'text/plain')
|
417
|
+
handler.send_header('Content-Length', len(encoded_content))
|
418
|
+
handler.end_headers()
|
419
|
+
handler.wfile.write(encoded_content)
|
420
|
+
else:
|
421
|
+
handler.end_headers()
|
422
|
+
|
423
|
+
@classmethod
|
424
|
+
def print_httpd_log_message(cls, handler: TinySSGDebugHTTPHandler, server: TinySSGDebugHTTPServer, format: str, *args: any) -> None:
|
425
|
+
"""
|
426
|
+
Output the log message (HTTPServer)
|
427
|
+
"""
|
428
|
+
if not server.args['nolog'] and not str(args[0]).startswith('GET /change'):
|
429
|
+
SimpleHTTPRequestHandler.log_message(handler, format, *args)
|
430
|
+
sys.stdout.flush()
|
431
|
+
|
432
|
+
@classmethod
|
433
|
+
def httpd_get_handler(cls, handler: TinySSGDebugHTTPHandler, server: TinySSGDebugHTTPServer) -> None:
|
434
|
+
"""
|
435
|
+
Process the GET request
|
436
|
+
"""
|
437
|
+
if handler.path == '/change':
|
438
|
+
cls.send_ok_response(handler, 'application/json', json.dumps({'reload': server.reload}))
|
439
|
+
server.reload = False
|
440
|
+
elif handler.path == '/stop':
|
441
|
+
cls.send_ok_response(handler, 'text/plain', 'Server Stopped.')
|
442
|
+
server.shutdown()
|
443
|
+
elif handler.path.startswith(f"/{server.args['output']}/{server.args['static']}/"):
|
444
|
+
redirect_path = re.sub('/' + re.escape(server.args['output']), '', handler.path)
|
445
|
+
handler.path = redirect_path
|
446
|
+
SimpleHTTPRequestHandler.do_GET(handler)
|
447
|
+
elif handler.path == f"/{server.args['output']}":
|
448
|
+
cls.send_no_ok_response(handler, 301, '', {'Location': f"/{server.args['output']}/"})
|
449
|
+
elif handler.path.startswith(f"/{server.args['output']}/"):
|
450
|
+
baselen = len(f"/{server.args['output']}/")
|
451
|
+
basename = re.sub(r'\.html$', '', handler.path[baselen:])
|
452
|
+
basename = f"{basename}index" if basename.endswith('/') or basename == '' else basename
|
453
|
+
output_path = basename.split('/')
|
454
|
+
current_route = server.route
|
455
|
+
|
456
|
+
for path in output_path:
|
457
|
+
if not isinstance(current_route, dict) or path not in current_route:
|
458
|
+
cls.send_no_ok_response(handler, 404, 'Not Found')
|
459
|
+
return
|
460
|
+
current_route = current_route[path]
|
461
|
+
|
462
|
+
if isinstance(current_route, dict):
|
463
|
+
cls.send_no_ok_response(handler, 301, '', {'Location': f"{handler.path}/"})
|
464
|
+
elif not isinstance(current_route, str):
|
465
|
+
TinySSGUtility.error_print(f"Error: The Page class for {handler.path} may not be implemented correctly.")
|
466
|
+
cls.send_no_ok_response(handler, 500, 'Internal Server Error')
|
467
|
+
else:
|
468
|
+
current_route = current_route if server.args['noreload'] else re.sub(r'(\s*</head>)', f"{cls.watchdog_script()}\n\\1", current_route)
|
469
|
+
cls.send_ok_response(handler, 'text/html', current_route)
|
470
|
+
else:
|
471
|
+
cls.send_no_ok_response(handler, 404, 'Not Found')
|
472
|
+
|
473
|
+
@classmethod
|
474
|
+
def stop_server(cls, process: any) -> None:
|
475
|
+
"""
|
476
|
+
Stop the debug server
|
477
|
+
"""
|
478
|
+
process.kill()
|
479
|
+
|
480
|
+
@classmethod
|
481
|
+
def server_stop_output(cls, process) -> None:
|
482
|
+
"""
|
483
|
+
Output the server stop message
|
484
|
+
"""
|
485
|
+
TinySSGUtility.error_print('Server return code:', process.poll())
|
486
|
+
TinySSGUtility.error_print('Server Output:\n')
|
487
|
+
TinySSGUtility.error_print(process.stdout.read() if process.stdout else '')
|
488
|
+
TinySSGUtility.error_print(process.stderr.read() if process.stderr else '')
|
489
|
+
|
490
|
+
@classmethod
|
491
|
+
def server_start(cls, args: dict) -> None:
|
492
|
+
"""
|
493
|
+
Run the debug server
|
494
|
+
"""
|
495
|
+
reload = args['mode'] == 'servreload'
|
496
|
+
route = TinySSGGenerator.generate_routes(args)
|
497
|
+
server_address = ('', args['port'])
|
498
|
+
httpd = TinySSGDebugHTTPServer(server_address, TinySSGDebugHTTPHandler, args, route, reload)
|
499
|
+
TinySSGUtility.error_print(f"Starting server on http://localhost:{args['port']}/{args['output']}/")
|
500
|
+
httpd.serve_forever()
|
501
|
+
|
502
|
+
|
503
|
+
class TinySSGLauncher:
|
504
|
+
"""
|
505
|
+
Watchdog and Server Launcher
|
506
|
+
"""
|
507
|
+
@classmethod
|
508
|
+
def check_for_changes(cls, mod_time: float, args: dict, pathlits: list) -> bool:
|
509
|
+
"""
|
510
|
+
Check for changes in the specified directories
|
511
|
+
"""
|
512
|
+
path_times = []
|
513
|
+
new_mod_time = 0
|
514
|
+
|
515
|
+
try:
|
516
|
+
for path in pathlits:
|
517
|
+
time_list = [os.path.getmtime(os.path.join(root, file)) for root, _, files in os.walk(path) for file in files]
|
518
|
+
if len(time_list) > 0:
|
519
|
+
this_path_time = max(time_list)
|
520
|
+
path_times.append(this_path_time)
|
521
|
+
|
522
|
+
if len(path_times) > 0:
|
523
|
+
new_mod_time = max(path_times)
|
524
|
+
|
525
|
+
if new_mod_time > mod_time:
|
526
|
+
mod_time = new_mod_time + args['wait']
|
527
|
+
return True, mod_time
|
528
|
+
except Exception as e:
|
529
|
+
TinySSGUtility.log_print(f"update check warning: {e}")
|
530
|
+
|
531
|
+
return False, mod_time
|
532
|
+
|
533
|
+
@classmethod
|
534
|
+
def launch_server(cls, args: dict, reload: bool) -> None:
|
535
|
+
"""
|
536
|
+
Launch the server
|
537
|
+
"""
|
538
|
+
servcommand = 'serv' if not reload else 'servreload'
|
539
|
+
|
540
|
+
newargv = args.copy()
|
541
|
+
newargv['mode'] = servcommand
|
542
|
+
|
543
|
+
command = [sys.executable, __file__, '--config', f"{json.dumps(newargv)}", 'config']
|
544
|
+
|
545
|
+
process = subprocess.Popen(
|
546
|
+
command,
|
547
|
+
stdout=None if not args['nolog'] else subprocess.PIPE,
|
548
|
+
stderr=None if not args['nolog'] else subprocess.PIPE,
|
549
|
+
text=True,
|
550
|
+
encoding='utf-8'
|
551
|
+
)
|
552
|
+
|
553
|
+
time.sleep(1)
|
554
|
+
|
555
|
+
if process.poll() is None:
|
556
|
+
return process
|
557
|
+
else:
|
558
|
+
TinySSGUtility.log_print('Server start failed.')
|
559
|
+
TinySSGDebug.stop_server(process)
|
560
|
+
return None
|
561
|
+
|
562
|
+
@classmethod
|
563
|
+
def open_browser(cls, args: dict) -> None:
|
564
|
+
"""
|
565
|
+
Open the browser or Display Jupyter Iframe
|
566
|
+
"""
|
567
|
+
url = f"http://localhost:{args['port']}/{args['output']}/"
|
568
|
+
|
569
|
+
try:
|
570
|
+
from IPython import get_ipython # type: ignore
|
571
|
+
env = get_ipython().__class__.__name__
|
572
|
+
if env == 'ZMQInteractiveShell':
|
573
|
+
from IPython import display
|
574
|
+
display.display(display.IFrame(url, width=args['jwidth'], height=args['jheight']))
|
575
|
+
except: # noqa: E722
|
576
|
+
webbrowser.open(url)
|
577
|
+
|
578
|
+
@classmethod
|
579
|
+
def launcher_start(cls, args: dict) -> None:
|
580
|
+
"""
|
581
|
+
Launch the debug server and file change detection
|
582
|
+
"""
|
583
|
+
if isinstance(args['curdir'], str) and len(args['curdir']) > 0:
|
584
|
+
os.chdir(args['curdir'])
|
585
|
+
|
586
|
+
cur_dir = os.getcwd()
|
587
|
+
page_dir = os.path.join(cur_dir, args['page'])
|
588
|
+
static_dir = os.path.join(cur_dir, args['static'])
|
589
|
+
lib_dir = os.path.join(cur_dir, args['lib'])
|
590
|
+
mod_time = 0.0
|
591
|
+
should_reload = False
|
592
|
+
|
593
|
+
if not os.path.isdir(page_dir):
|
594
|
+
raise TinySSGException(f"The specified page directory does not exist. ({page_dir})")
|
595
|
+
|
596
|
+
check_dirs = [page_dir]
|
597
|
+
|
598
|
+
if os.path.isdir(static_dir):
|
599
|
+
check_dirs.append(static_dir)
|
600
|
+
|
601
|
+
if os.path.isdir(lib_dir):
|
602
|
+
check_dirs.append(lib_dir)
|
603
|
+
|
604
|
+
if not args['noreload']:
|
605
|
+
_, mod_time = cls.check_for_changes(0.0, args, check_dirs)
|
606
|
+
|
607
|
+
process = cls.launch_server(args, False)
|
608
|
+
|
609
|
+
if process is None:
|
610
|
+
return
|
611
|
+
|
612
|
+
if not args['noopen']:
|
613
|
+
cls.open_browser(args)
|
614
|
+
|
615
|
+
while True:
|
616
|
+
try:
|
617
|
+
time.sleep(1)
|
618
|
+
if process.poll() is not None:
|
619
|
+
TinySSGUtility.log_print('Server stopped.')
|
620
|
+
TinySSGDebug.server_stop_output(process)
|
621
|
+
break
|
622
|
+
if not args['noreload']:
|
623
|
+
should_reload, mod_time = cls.check_for_changes(mod_time, args, check_dirs)
|
624
|
+
if should_reload:
|
625
|
+
TinySSGUtility.log_print('File changed. Reloading...')
|
626
|
+
TinySSGDebug.stop_server(process)
|
627
|
+
time.sleep(1)
|
628
|
+
process = cls.launch_server(args, True)
|
629
|
+
except KeyboardInterrupt:
|
630
|
+
TinySSGDebug.stop_server(process)
|
631
|
+
TinySSGUtility.error_print('Server stopped.')
|
632
|
+
TinySSGDebug.server_stop_output(process)
|
633
|
+
break
|
634
|
+
|
635
|
+
|
636
|
+
class TinySSG:
|
637
|
+
"""
|
638
|
+
TinySSG Main Class
|
639
|
+
"""
|
640
|
+
@classmethod
|
641
|
+
def main(cls, args: dict) -> None:
|
642
|
+
"""
|
643
|
+
Main function
|
644
|
+
"""
|
645
|
+
exitcode = 0
|
646
|
+
|
647
|
+
try:
|
648
|
+
if args['mode'] == 'gen':
|
649
|
+
if args['input'] == '':
|
650
|
+
TinySSGUtility.clear_start(args)
|
651
|
+
TinySSGGenerator.generator_start(args)
|
652
|
+
TinySSGUtility.log_print('HTML files generated.')
|
653
|
+
elif args['mode'] == 'dev':
|
654
|
+
TinySSGLauncher.launcher_start(args)
|
655
|
+
elif args['mode'] == 'cls':
|
656
|
+
TinySSGUtility.clear_start(args)
|
657
|
+
TinySSGUtility.log_print('Output directory cleared.')
|
658
|
+
elif args['mode'] == 'serv' or args['mode'] == 'servreload':
|
659
|
+
TinySSGDebug.server_start(args)
|
660
|
+
elif args['mode'] == 'config':
|
661
|
+
config = json.loads(args['config'])
|
662
|
+
default_args = cls.get_default_arg_dict()
|
663
|
+
for key, value in default_args.items():
|
664
|
+
if key not in config:
|
665
|
+
config[key] = value
|
666
|
+
cls.main(config)
|
667
|
+
else:
|
668
|
+
raise TinySSGException('Invalid mode.')
|
669
|
+
except TinySSGException as e:
|
670
|
+
TinySSGUtility.error_print(f"Error: {e}")
|
671
|
+
exitcode = 1
|
672
|
+
|
673
|
+
sys.exit(exitcode)
|
674
|
+
|
675
|
+
@classmethod
|
676
|
+
def get_arg_parser(cls) -> argparse.ArgumentParser:
|
677
|
+
"""
|
678
|
+
Set the argument parser
|
679
|
+
"""
|
680
|
+
parser = argparse.ArgumentParser(description='TinySSG Simple Static Site Generate Tool')
|
681
|
+
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)')
|
682
|
+
parser.add_argument('--port', '-P', type=int, default=8000, help='Port number for the debug server')
|
683
|
+
parser.add_argument('--page', '-p', type=str, default='pages', help='Page file path')
|
684
|
+
parser.add_argument('--static', '-s', type=str, default='static', help='Static file path')
|
685
|
+
parser.add_argument('--lib', '-l', type=str, default='libs', help='Library file path')
|
686
|
+
parser.add_argument('--input', '-i', type=str, default='', help='Input file name (Used to generate specific files only)')
|
687
|
+
parser.add_argument('--output', '-o', type=str, default='dist', help='Output directory path')
|
688
|
+
parser.add_argument('--wait', '-w', type=int, default=5, help='Wait time for file change detection')
|
689
|
+
parser.add_argument('--nolog', '-n', action='store_true', help='Do not output debug server log')
|
690
|
+
parser.add_argument('--noreload', '-r', action='store_true', help='Do not reload the server when the file changes')
|
691
|
+
parser.add_argument('--noopen', '-N', action='store_true', help='Do not open the browser when starting the server')
|
692
|
+
parser.add_argument('--curdir', '-C', type=str, default='', help='Current directory (For Jupyter)')
|
693
|
+
parser.add_argument('--config', '-c', type=str, default='', help='Configuration json string')
|
694
|
+
parser.add_argument('--jwidth', '-jw', type=str, default='600', help='Jupyter iframe width')
|
695
|
+
parser.add_argument('--jheight', '-jh', type=str, default='600', help='Jupyter iframe height')
|
696
|
+
|
697
|
+
return parser
|
698
|
+
|
699
|
+
@classmethod
|
700
|
+
def get_default_arg_dict(cls):
|
701
|
+
parser = cls.get_arg_parser()
|
702
|
+
return vars(parser.parse_args(['dev']))
|
703
|
+
|
704
|
+
@classmethod
|
705
|
+
def cli_main(cls):
|
706
|
+
"""
|
707
|
+
Command line interface
|
708
|
+
"""
|
709
|
+
parser = cls.get_arg_parser()
|
710
|
+
parse_args = parser.parse_args()
|
711
|
+
args = vars(parse_args)
|
712
|
+
cls.main(args)
|
713
|
+
|
714
|
+
|
715
|
+
if __name__ == '__main__':
|
716
|
+
TinySSG.cli_main()
|
@@ -0,0 +1,106 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: tinyssg
|
3
|
+
Version: 0.0.0
|
4
|
+
Summary: A simple static site generator
|
5
|
+
Author: Uniras
|
6
|
+
Author-email: tkappeng@gmail.com
|
7
|
+
License: MIT License
|
8
|
+
Project-URL: Homepage, https://github.com/uniras/tinyssg
|
9
|
+
Project-URL: Repository, https://github.com/uniras/tinyssg
|
10
|
+
Keywords: ssg,static site generator,html
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
12
|
+
Classifier: Intended Audience :: Developers
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
15
|
+
Classifier: Programming Language :: Python :: 3.7
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
22
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
25
|
+
Classifier: Topic :: Utilities
|
26
|
+
Description-Content-Type: text/markdown
|
27
|
+
|
28
|
+
# TinySSG
|
29
|
+
|
30
|
+
## 概要
|
31
|
+
|
32
|
+
TinySSGは、ファイルベースルーティングのシンプルな静的サイトジェネレータです。
|
33
|
+
ページを簡単な構造のTinySSGPageクラスを継承したPythonコードで記述することで単純さと柔軟性を両立しています。
|
34
|
+
|
35
|
+
## インストール
|
36
|
+
|
37
|
+
```bash
|
38
|
+
pip install tinyssg
|
39
|
+
```
|
40
|
+
|
41
|
+
## 使い方
|
42
|
+
|
43
|
+
### ディレクトリ構成
|
44
|
+
|
45
|
+
以下のようにディレクトリを構成します。ディレクトリ名はオプション引数で変更可能です。
|
46
|
+
|
47
|
+
```text
|
48
|
+
--- proect
|
49
|
+
|-- pages SSGの対象となるPythonファイルを配置します
|
50
|
+
|-- libs SSG・デプロイの対象にならないPythonファイルを配置します(ライブラリなど)
|
51
|
+
|-- static SSGの対象にならない静的ファイルを配置します(css, 画像など)
|
52
|
+
|-- dist SSGの結果が出力されるディレクトリです。このディレクトリの中身をWebサーバに配置することでWebサイトとして公開できます。
|
53
|
+
|-- static staticディレクトリはこのディレクトリにコピーされます
|
54
|
+
```
|
55
|
+
|
56
|
+
pages, libs, staticディレクトリは、開発サーバー起動時に監視され、ファイルが変更されると自動的にサーバーを再起動します。
|
57
|
+
|
58
|
+
### ページの作成
|
59
|
+
|
60
|
+
`Page`ディレクトリ内にPythonファイルを作り、`TinySSGPage`クラスを継承したクラスを作成します。
|
61
|
+
|
62
|
+
```python
|
63
|
+
from tinyssg import TinySSGPage
|
64
|
+
|
65
|
+
class IndexPage(TinySSGPage):
|
66
|
+
def query(self):
|
67
|
+
return {
|
68
|
+
'title': 'Index',
|
69
|
+
'content': 'Hello, World!'
|
70
|
+
}
|
71
|
+
|
72
|
+
def template(self):
|
73
|
+
return '''
|
74
|
+
<!DOCTYPE html>
|
75
|
+
<html>
|
76
|
+
<head>
|
77
|
+
<meta charset="utf-8" />
|
78
|
+
<title>{{ title }}</title>
|
79
|
+
</head>
|
80
|
+
<body>
|
81
|
+
<h1>{{ title }}</h1>
|
82
|
+
<p>{{ content }}</p>
|
83
|
+
</body>
|
84
|
+
</html>'''
|
85
|
+
```
|
86
|
+
|
87
|
+
`query`メソッドでテンプレートに渡すデータを返し、`template`メソッドでHTMLテンプレートを返します。
|
88
|
+
TinySSGは、これらのメソッドの返り値を使ってHTMLを生成します。
|
89
|
+
|
90
|
+
`query`メソッドが返すデータは、Python辞書形式またはPython辞書のリスト形式である必要があります。
|
91
|
+
辞書形式の場合はpythonファイル名のHTMLファイルが生成され、リスト形式の場合はPythonファイル名と同じディレクトリが作成され、デフォルトでは1からの数字.htmlのファイル名でHTMLファイルが生成されます。
|
92
|
+
`return`の際にタプルとしてリストと一緒にキー名を表す文字列を返すと、そのキーに対応する値をファイル名としてHTMLファイルが生成されます。
|
93
|
+
|
94
|
+
`TinySSG`はデフォルトでは単純にテンプレートの`{{ キー名 }}`で囲まれた部分を`query`メソッドの返り値である辞書のキーに対応する値で単純に置換するだけですが、
|
95
|
+
`render`メソッドをオーバーライドすることで、より複雑な処理を行うこともできます。Jinja2などのテンプレートエンジンを使うこともできます。
|
96
|
+
|
97
|
+
また、`translate`メソッドをオーバーライドすることで、レンダー後のテキストを最終的なHTMLに変換する処理を定義することもできます。
|
98
|
+
ここでmarkdownライブラリを使って変換する処理を記述すればテンプレートをHTMLではなくMarkdownで記述することができます。
|
99
|
+
|
100
|
+
それぞれページごとに定義することになりますが、単純なPythonクラスですので複数のページに適用したい場合は共通部分を定義したクラスを作成し、それを継承することでコードをコピーすることなく簡単に適用することができます。
|
101
|
+
|
102
|
+
### HTMLの生成
|
103
|
+
|
104
|
+
```bash
|
105
|
+
python -m tinyssg gen
|
106
|
+
```
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
tinyssg
|