-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtagy.py
executable file
·344 lines (275 loc) · 7.98 KB
/
tagy.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Generate static site
'''
import os
import yaml
import time
import shutil
import logging
import optparse
from threading import Thread
from collections import defaultdict
from jinja2 import Environment, FileSystemLoader
# Core config params
CONFIG_FILE = 'config.yaml'
CONTENT_DIR = 'content'
LAYOUT_DIR = 'layout'
STATIC_DIR = 'static'
BUILD_DIR = 'public'
# Default item params
SITE_PAGES = 'pages'
PAGE_URL = 'url'
PAGE_NAME = 'name'
PAGE_PATH = 'path'
PAGE_LAYOUT = 'layout'
PAGE_CONTENT = 'content'
log = logging.getLogger('tagy')
log.setLevel(logging.INFO)
log.addHandler(logging.StreamHandler())
def generate(silent=False):
log.info('Generate site')
site = load_site()
generate_site(site, silent)
def load_site():
# load basic configs
site = load_config()
# add pages
site[SITE_PAGES] = load_content()
# build indexes
for page in site.pages:
# check all indexes
for indexname in iter(site.indexes):
index = site.indexes[indexname]
terms = getattr(page, indexname, None)
if isinstance(terms, list):
for term in terms:
index.setdefault('terms', {}).setdefault(term, []).append(page)
return site
def load_config(path=CONFIG_FILE):
with open(path, 'r') as f:
return Config(yaml.safe_load(f))
def load_content(dir=CONTENT_DIR):
pages = []
for subdir, dirs, files in os.walk(dir):
for file in files:
if file.startswith('.'):
continue
path = os.path.join(subdir, file)
page = load_page(path)
pages.append(page)
return pages
CONFIG = '---\n'
import mistune
def load_page(path):
with open(path, 'r') as f:
try:
content = f.read()
i = content.find(CONFIG)
page = yaml.safe_load(content[:i]) if i > 0 else {}
start = i+len(CONFIG) if i > 0 else 0
content = content[start:]
if path.endswith('.html') | path.endswith('.md'):
content = mistune.markdown(content)
page[PAGE_CONTENT] = content
path = path[ : path.index('.')]
page[PAGE_NAME] = os.path.basename(path)
if path.endswith('/index'): # cut index page
path = path[ : path.index('/index')]
page[PAGE_PATH] = path[len(CONTENT_DIR + '/') : ]
return Config(page)
except Exception as e:
log.warning('Failed to read page "%s"', path, e)
# Generate logic
env = Environment(loader=FileSystemLoader(LAYOUT_DIR))
def generate_site(site, silent):
clear()
for page in site.pages:
try:
generate_page(page, site)
except Exception as e:
if silent:
log.warning('Failed to generate page "%s"', page[PAGE_PATH])
else:
raise e
for name in iter(site.indexes):
generate_index(name, site)
def clear():
# Clear dir
folder = BUILD_DIR
if os.path.exists(folder):
for name in os.listdir(folder):
path = os.path.join(folder, name)
try:
if os.path.isfile(path):
os.unlink(path)
else:
shutil.rmtree(path)
except Exception as e:
log.exception(e)
else:
os.makedirs(folder)
# Copy static dir
for file_name in os.listdir(STATIC_DIR):
full_file_name = os.path.join(STATIC_DIR, file_name)
if (os.path.isfile(full_file_name)):
shutil.copy(full_file_name, BUILD_DIR)
else:
shutil.copytree(full_file_name, os.path.join(BUILD_DIR, file_name))
def generate_page(page, site):
# render content
content = env.from_string(page.content)
page.content = content.render({'page': page})
# render layout
template = env.get_template(get_template(page))
html = template.render({'page': page, 'site': site})
# generate page
path = get_build_path(page)
with open(path, 'w') as f:
f.write(html)
def generate_index(name, site):
index = getattr(site.indexes, name)
for term in iter(index.terms):
# TODO: remove layout from yaml
template = env.get_template(get_template(index))
page = Config()
page.path = index.url + '/' + term
html = template.render({'term': term, 'pages': index.terms[term], 'site': site})
with open(get_build_path(page), 'w') as f:
f.write(html)
def get_template(page):
# TODO: impl inheritance logic
if PAGE_LAYOUT in page:
return page[PAGE_LAYOUT]
return 'index.html'
# Return fancy url for path
def get_build_path(page):
if PAGE_URL in page:
return BUILD_DIR + '/' + page.url
dir = BUILD_DIR + '/' + page.path
try:
os.stat(dir)
except:
os.makedirs(dir)
return dir + '/index.html'
def get_last_update():
'''Get last update time for content directories'''
# path = '/tmp/test_tracking/'
files = []
subdirs = []
last = None
for folder in [CONTENT_DIR, STATIC_DIR, LAYOUT_DIR]:
for root, dirs, filenames in os.walk(folder):
for f in filenames:
filename = os.path.relpath(os.path.join(root, f), folder)
file_mtime = os.path.getmtime(os.path.join(folder, filename))
if file_mtime > last or last is None:
last = file_mtime
return last
def serve(port=1313):
thread = Thread(target=watch)
thread.daemon = True
thread.start()
# start server
os.system("cd %s; python -m SimpleHTTPServer %d" % (BUILD_DIR, port))
def watch():
'''Watch file changed in infinite loop'''
updated = None
while True:
last = get_last_update()
if updated != last:
updated = last
try:
generate(silent=True)
except Exception as e:
log.exception(e)
time.sleep(1)
# Jinja filters
def breadcrumbs(path):
'''Return path chunks'''
result = []
if path == '':
return result
current = ''
parts = path.split('/')
for part in parts:
if len(current) > 0:
current = current + '/'
current = current + part
result.append(current)
return result
def where(iterator, param, value=True):
result = []
for item in iterator:
if param in item:
if isinstance(value, bool): # check item just has a param
result.append(item)
elif isinstance(value, str): # for strings check starts with
if item[param].startswith(value):
result.append(item)
elif isinstance(value, int): # for numbers check equality
if item[param] == value:
result.append(item)
return result
from PIL import Image
def get_thumbnail(value, size=(100, 100), dir=BUILD_DIR):
file_path = dir + value
file, ext = os.path.splitext(file_path)
path = (file + '-%dx%d' + ext) % size
im = Image.open(file_path)
im.thumbnail(size, Image.LANCZOS)
im.save(path, "PNG")
return path[len(dir) : ]
env.tests['equalto'] = lambda value, other : value == other
env.filters['where'] = where
env.filters['breadcrumbs'] = breadcrumbs
env.filters['thumb'] = get_thumbnail
# Model for config params
class Config(dict):
def __getattr__(self, attr):
if attr not in self:
return None
result = self[attr]
if isinstance(result, dict):
# TODO: why?
result = Config(result)
return result
def __setattr__(self, attr, value):
self[attr] = value
# =============================================================================
# options
# =============================================================================
def options():
"""Parse and validate command line arguments."""
usage = ("Usage: %prog --build [OPTIONS] [path/to/project]\n"
" %prog --serve [OPTIONS] [path/to/project]\n"
"\n"
" Project path is optional, '.' is used as default.")
op = optparse.OptionParser(usage=usage)
op.add_option("-b" , "--build", action="store_true", default=False,
help="build project")
op.add_option("-s" , "--serve", action="store_true", default=False,
help="serve project")
og = optparse.OptionGroup(op, "Serve options")
og.add_option("" , "--port", default=1313,
metavar="PORT", type="int",
help="port for serving (default: 1313)")
op.add_option_group(og)
opts, args = op.parse_args()
if opts.build + opts.serve < 1:
op.print_help()
op.exit()
opts.project = args and args[0] or "."
return opts
# =============================================================================
# main
# =============================================================================
def main():
opts = options()
if opts.build:
generate()
if opts.serve:
serve(opts.port)
if __name__ == '__main__':
main()