Hi! I'm the second draft.
--------------------------------------------------------------------------------
/sites/basic-example/content/posts/blog/en/2015-10-13-mmu-my-first-post.html:
--------------------------------------------------------------------------------
1 | Interesting story. Some new content.
4 |
--------------------------------------------------------------------------------
/sites/basic-example/content/posts/blog/en/2015-12-15-jsm-first-post.html:
--------------------------------------------------------------------------------
1 | Hey guys! I'm John Smith. {{ tree_localized }}
4 | Hey guys! I'm John Smith. {{ tree_localized }}
4 |
--------------------------------------------------------------------------------
/sites/basic-example/design/blog-items.html:
--------------------------------------------------------------------------------
1 |
21 | {{{ content }}}
22 |
23 |
24 |
--------------------------------------------------------------------------------
/sites/minimal/content/config.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/sites/minimal/content/pages/en/index.html:
--------------------------------------------------------------------------------
1 | hello world!
--------------------------------------------------------------------------------
/sites/minimal/design/template.html:
--------------------------------------------------------------------------------
1 | {{{ content }}}
--------------------------------------------------------------------------------
/tools/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muellermichel/guetzli/21279ff0ec42ab8804a30c4c56efb078ec7b7c48/tools/__init__.py
--------------------------------------------------------------------------------
/tools/guetzli.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: UTF-8 -*-
3 |
4 | # Copyright (C) 2016 Michel Müller
5 |
6 | # This file is part of Guetzli.
7 |
8 | # Guetzli is free software: you can redistribute it and/or modify
9 | # it under the terms of the GNU Lesser General Public License as published by
10 | # the Free Software Foundation, either version 3 of the License, or
11 | # (at your option) any later version.
12 |
13 | # Guetzli is distributed in the hope that it will be useful,
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | # GNU Lesser General Public License for more details.
17 |
18 | # You should have received a copy of the GNU Lesser General Public License
19 | # along with Guetzli. If not, see .
20 |
21 | from __future__ import division
22 | import os, codecs, re, math, logging, json
23 | import pystache
24 | from flask import url_for, request, Blueprint
25 |
26 | _file_content_by_path = {}
27 | _file_modification_date_by_path = {}
28 | _checked_path_components = {}
29 | _content_config = {}
30 | _post_filenames_by_subdirectory_and_language = {}
31 | _post_directory_modification_dates_by_subdirectory_and_language = {}
32 | _site = "basic-example"
33 |
34 | class NotFoundError(Exception):
35 | pass
36 |
37 | class UsageError(Exception):
38 | pass
39 |
40 | class NotAllowedError(Exception):
41 | pass
42 |
43 | class Extension(Blueprint):
44 | def __init__(self, name):
45 | super(Extension, self).__init__(name, "extensions.%s" %(name))
46 |
47 | def set_site(site):
48 | global _site
49 | _site = site
50 |
51 | def get_site():
52 | return _site
53 |
54 | def get_repo_path():
55 | return os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
56 |
57 | def get_site_path():
58 | return os.path.join(get_repo_path(), 'sites', _site)
59 |
60 | def get_template_path(template_name="template"):
61 | return os.path.join(get_site_path(), 'design', template_name) + '.html'
62 |
63 | def get_page_path(pagename, language):
64 | return os.path.join(get_site_path(), 'content', 'pages', language, pagename) + '.html'
65 |
66 | def get_post_path(posts_subdirectory, language, post_id=None):
67 | posts_path = os.path.join(get_site_path(), 'content', 'posts', posts_subdirectory, language)
68 | if post_id == None:
69 | return posts_path
70 | return os.path.join(posts_path, post_id) + '.html'
71 |
72 | def get_file_content(path):
73 | if not os.path.isfile(path):
74 | if path in _file_content_by_path:
75 | del _file_content_by_path[path]
76 | if path in _file_modification_date_by_path:
77 | del _file_modification_date_by_path[path]
78 | raise NotFoundError(path)
79 | previous_modification_date = _file_modification_date_by_path.get(path)
80 | curr_modification_date = os.path.getmtime(path)
81 | if previous_modification_date and curr_modification_date <= previous_modification_date:
82 | return _file_content_by_path[path], False
83 | with codecs.open(path, encoding="utf-8") as f:
84 | file_content = f.read()
85 | _file_content_by_path[path] = file_content
86 | _file_modification_date_by_path[path] = curr_modification_date
87 | return file_content, True
88 |
89 | def render_file_content(path, ctx):
90 | return pystache.render(get_file_content(path)[0], ctx)
91 |
92 | def get_content_config():
93 | config_path = os.path.join(get_site_path(), 'content', 'config') + '.json'
94 | file_content, has_changed = get_file_content(config_path)
95 | global _content_config
96 | if not has_changed:
97 | return _content_config
98 | try:
99 | _content_config = json.loads(file_content)
100 | except Exception as e:
101 | raise UsageError("Wrong format or syntax error in %s, please compare with basic-example." %(config_path))
102 | return _content_config
103 |
104 | def get_post_content(ctx, content_config, posts_path, posts_subdirectory, file_name):
105 | match = re.match(r'(\d{4}-\d{2}-\d{2})-(\w*)-?(.*?)\.\w*', file_name)
106 | author_shortname = None
107 | publishing_date = None
108 | if match:
109 | publishing_date = match.group(1)
110 | author_shortname = match.group(2)
111 | author = content_config.get('authors_by_shortname', {}).get(author_shortname, author_shortname)
112 | ctx.update({
113 | "post_path": url_for(
114 | 'page_view',
115 | pagename=posts_subdirectory,
116 | language=ctx["language"],
117 | post_id=file_name.split('.')[0]
118 | ),
119 | "author": author,
120 | "publishing_date": publishing_date
121 | })
122 | return render_file_content(os.path.join(posts_path, file_name), ctx)
123 |
124 | def get_posts_page_and_page_info(ctx, content_config, posts_subdirectory, items_per_page, page_number):
125 | posts_path = get_post_path(posts_subdirectory, ctx['language'])
126 | if not os.path.isdir(posts_path):
127 | raise NotFoundError(posts_path)
128 | identifier_tuple = (posts_subdirectory, ctx['language'])
129 | previous_directory_modification_date = _post_directory_modification_dates_by_subdirectory_and_language.get(
130 | identifier_tuple
131 | )
132 | curr_directory_modification_date = os.path.getmtime(posts_path)
133 | file_names = None
134 | if previous_directory_modification_date \
135 | and curr_directory_modification_date <= previous_directory_modification_date:
136 | file_names = _post_filenames_by_subdirectory_and_language.get(identifier_tuple)
137 | else:
138 | file_names = sorted(
139 | [file_name for file_name in os.listdir(posts_path) if os.path.isfile(os.path.join(
140 | posts_path,
141 | file_name
142 | ))],
143 | reverse=True
144 | )
145 | _post_filenames_by_subdirectory_and_language[identifier_tuple] = file_names
146 | _post_directory_modification_dates_by_subdirectory_and_language[
147 | identifier_tuple
148 | ] = curr_directory_modification_date
149 | number_of_pages = int(math.ceil(len(file_names) / items_per_page))
150 | start_item = (page_number - 1) * items_per_page
151 | page_info = {
152 | "first_item_number": start_item + 1,
153 | "last_item_number": min(start_item + items_per_page, len(file_names)),
154 | "number_of_items": len(file_names),
155 | "number_of_pages": number_of_pages,
156 | "next_page": page_number + 1 if page_number < number_of_pages else None,
157 | "previous_page": page_number - 1 if page_number > 1 else None
158 | }
159 | page = []
160 | for file_name in file_names[start_item:start_item + items_per_page]:
161 | page.append({
162 | "item": get_post_content(ctx, content_config, posts_path, posts_subdirectory, file_name)
163 | })
164 | return page, page_info
165 |
166 | def get_posts_listing(ctx, content_config, posts_subdirectory, items_per_page, page_number=1):
167 | try:
168 | page, page_info = get_posts_page_and_page_info(ctx, content_config, posts_subdirectory, items_per_page, page_number)
169 | ctx[posts_subdirectory + "_items"] = page
170 | ctx.update(page_info)
171 | except NotFoundError:
172 | pass
173 | return render_file_content(get_template_path(posts_subdirectory + "-items"), ctx)
174 |
175 | def get_page_content(ctx, content_config):
176 | for entry in content_config.get("active_post_types_by_pagename", {}).get(ctx["pagename"], []):
177 | posts_subdirectory = entry["posts_directory"]
178 | items_per_page = entry.get("items_per_page")
179 | if not isinstance(items_per_page, int) or items_per_page <= 0:
180 | items_per_page = 5
181 | ctx[posts_subdirectory + "_listing"] = get_posts_listing(
182 | ctx,
183 | content_config,
184 | posts_subdirectory,
185 | items_per_page,
186 | ctx["page_number"]
187 | )
188 | return render_file_content(get_page_path(ctx["pagename"], ctx["language"]), ctx)
189 |
190 | def get_menu(language, content_config):
191 | menu = []
192 | for page in content_config.get('pages_by_language', {}).get(language, []):
193 | menu.append({
194 | "title": page["title"],
195 | "url": url_for("page_view", pagename=page["name"], language=language),
196 | "pagename_class": "menu-" + page["name"]
197 | })
198 | return menu
199 |
200 | def get_page_title(pagename, language, content_config):
201 | def first_item(list_or_none):
202 | return list_or_none[0] if list_or_none else None
203 |
204 | #TODO: Cache prefixes and titles after content config reload
205 | title_prefix = first_item([
206 | language_hash.get("title_prefix")
207 | for language_hash in content_config.get('active_languages', [])
208 | if language_hash['id'] == language
209 | ])
210 | for page in content_config.get('pages_by_language', {}).get(language, []):
211 | if page["name"] == pagename:
212 | pagetitle = page["title"]
213 | if title_prefix and not title_prefix in pagetitle:
214 | pagetitle = "%s: %s" %(title_prefix, pagetitle)
215 | return pagetitle
216 | return title_prefix
217 |
218 | def get_context_with_rendered_content(language, page_or_post_type, post_id=None, additional_context={}):
219 | if not is_valid_path_component(page_or_post_type) \
220 | or not is_valid_path_component(language) \
221 | or not is_valid_path_component(post_id):
222 | raise NotAllowedError()
223 |
224 | content_config = get_content_config()
225 | active_languages = content_config.get('active_languages', [])
226 | default_pagename = content_config.get('default_pagename', 'index')
227 | if language == None:
228 | language = request.accept_languages.best_match([
229 | language_hash['id'] for language_hash in active_languages
230 | ])
231 | if language == None:
232 | language = content_config.get('default_language', 'en')
233 | if page_or_post_type == None:
234 | page_or_post_type = default_pagename
235 |
236 | ctx = {
237 | "page_number": request.args.get('page_number', 1, type=int),
238 | "pagename": page_or_post_type,
239 | "pagetitle": get_page_title(page_or_post_type, language, content_config),
240 | "language": language,
241 | "menu": get_menu(language, content_config),
242 | "languages": content_config.get('active_languages', []),
243 | "current_path": url_for('page_view', pagename=page_or_post_type, language=language)
244 | }
245 | ctx.update({
246 | key: lang_dict.get(language)
247 | for key, lang_dict in content_config.get("strings_by_template_reference", {}).iteritems()
248 | })
249 | ctx.update(additional_context)
250 | if post_id != None:
251 | ctx["content"] = get_post_content(
252 | ctx,
253 | content_config,
254 | get_post_path(page_or_post_type, language),
255 | page_or_post_type,
256 | post_id + '.html'
257 | )
258 | else:
259 | ctx["content"] = get_page_content(ctx, content_config)
260 | return ctx
261 |
262 | def render_with_template(context):
263 | return render_file_content(get_template_path(), context)
264 |
265 | def is_valid_path_component(component):
266 | '''making sure that the client cannot manipulate his way to parts of the file system where we don't want him to'''
267 | if not component:
268 | return True
269 | if component in _checked_path_components:
270 | return True
271 | if re.match(r'^[A-Za-z0-9_-]*$', component):
272 | _checked_path_components[component] = None
273 | return True
274 | return False
275 |
276 | def is_valid_email_address(visitor_email):
277 | return re.match(r'^[^@\s]+@[^@\s]+\.[^@\s]+$', visitor_email) != None
278 |
279 | def send_mail(recipients, sender, subject, text, reply_to=None, files=[], server="localhost"):
280 | import smtplib
281 | from email.header import Header
282 | from email.MIMEMultipart import MIMEMultipart
283 | from email.MIMEBase import MIMEBase
284 | from email.MIMEText import MIMEText
285 | from email.Utils import COMMASPACE, formatdate
286 | from email import Encoders
287 | if type(recipients) != list:
288 | raise UsageError("recipients must be a list")
289 | if type(files) != list:
290 | raise UsageError("files must be a list")
291 | msg = MIMEText(text.encode('utf-8'), 'plain', 'utf-8')
292 | msg['From'] = sender
293 | msg['To'] = COMMASPACE.join(recipients)
294 | msg['Date'] = formatdate(localtime=True)
295 | msg['Subject'] = Header(subject, 'utf-8')
296 | if reply_to:
297 | msg.add_header('reply-to', reply_to)
298 | for f in files:
299 | part = MIMEBase('application', "octet-stream")
300 | part.set_payload(open(f,"rb").read())
301 | Encoders.encode_base64(part)
302 | part.add_header('Content-Disposition', 'attachment; filename="%s"' %(os.path.basename(f)))
303 | msg.attach(part)
304 | smtp = smtplib.SMTP(server)
305 | smtp.sendmail(sender, recipients, msg.as_string() )
306 | smtp.close()
307 |
--------------------------------------------------------------------------------