diff options
-rwxr-xr-x | bin/build-web-site | 5 | ||||
-rwxr-xr-x | libexec/poole | 755 | ||||
-rw-r--r-- | web/README | 2 | ||||
-rw-r--r-- | web/macros.py | 10 | ||||
-rw-r--r-- | web/page.html | 4 |
5 files changed, 10 insertions, 766 deletions
diff --git a/bin/build-web-site b/bin/build-web-site index cbfc8c3..3b5e4a9 100755 --- a/bin/build-web-site +++ b/bin/build-web-site | |||
@@ -18,7 +18,7 @@ umask 022 | |||
18 | 18 | ||
19 | prefix='/home/plugins' | 19 | prefix='/home/plugins' |
20 | 20 | ||
21 | export PATH="$prefix/libexec:/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin" | 21 | export PATH="$prefix/libexec:$prefix/opt/poole:$prefix/opt/poole/env/bin:/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin" |
22 | 22 | ||
23 | myself=${0##*/} | 23 | myself=${0##*/} |
24 | man_source_dir="$prefix/web/work/man" | 24 | man_source_dir="$prefix/web/work/man" |
@@ -54,9 +54,8 @@ ln -s "$output_dir" "$site_work_dir/output" | |||
54 | # See http://pythonhosted.org/Markdown/extensions/ for documentation on the | 54 | # See http://pythonhosted.org/Markdown/extensions/ for documentation on the |
55 | # extensions. | 55 | # extensions. |
56 | # | 56 | # |
57 | poole --build \ | 57 | poole.py --build \ |
58 | --md-ext='extra' \ | 58 | --md-ext='extra' \ |
59 | --md-ext='headerid' \ | ||
60 | --md-ext='toc' \ | 59 | --md-ext='toc' \ |
61 | --md-ext='wikilinks' \ | 60 | --md-ext='wikilinks' \ |
62 | "$site_work_dir" | 61 | "$site_work_dir" |
diff --git a/libexec/poole b/libexec/poole deleted file mode 100755 index eeac0b9..0000000 --- a/libexec/poole +++ /dev/null | |||
@@ -1,755 +0,0 @@ | |||
1 | #!/home/plugins/python2/bin/python | ||
2 | # -*- coding: utf-8 -*- | ||
3 | |||
4 | # ============================================================================= | ||
5 | # | ||
6 | # Poole - A damn simple static website generator. | ||
7 | # Copyright (C) 2012 Oben Sonne <obensonne@googlemail.com> | ||
8 | # | ||
9 | # This file is part of Poole. | ||
10 | # | ||
11 | # Poole is free software: you can redistribute it and/or modify | ||
12 | # it under the terms of the GNU General Public License as published by | ||
13 | # the Free Software Foundation, either version 3 of the License, or | ||
14 | # (at your option) any later version. | ||
15 | # | ||
16 | # Poole is distributed in the hope that it will be useful, | ||
17 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
18 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
19 | # GNU General Public License for more details. | ||
20 | # | ||
21 | # You should have received a copy of the GNU General Public License | ||
22 | # along with Poole. If not, see <http://www.gnu.org/licenses/>. | ||
23 | # | ||
24 | # ============================================================================= | ||
25 | |||
26 | from __future__ import with_statement | ||
27 | |||
28 | import codecs | ||
29 | import glob | ||
30 | import imp | ||
31 | import optparse | ||
32 | import os | ||
33 | from os.path import join as opj | ||
34 | from os.path import exists as opx | ||
35 | import re | ||
36 | import shutil | ||
37 | import StringIO | ||
38 | import sys | ||
39 | import traceback | ||
40 | import urlparse | ||
41 | |||
42 | from SimpleHTTPServer import SimpleHTTPRequestHandler | ||
43 | from BaseHTTPServer import HTTPServer | ||
44 | |||
45 | try: | ||
46 | import markdown | ||
47 | except ImportError: | ||
48 | print("abort : need python-markdown, get it from " | ||
49 | "http://www.freewisdom.org/projects/python-markdown/Installation") | ||
50 | sys.exit(1) | ||
51 | |||
52 | HERE = os.path.dirname(os.path.realpath(__file__)) | ||
53 | |||
54 | THEME_DIR = opj(HERE, 'themes') | ||
55 | |||
56 | THEME_NAMES = ['minimal'] + [ | ||
57 | os.path.basename(x) | ||
58 | for x in glob.glob(opj(THEME_DIR, '*')) | ||
59 | if os.path.isdir(x) | ||
60 | ] | ||
61 | |||
62 | # ============================================================================= | ||
63 | # init site | ||
64 | # ============================================================================= | ||
65 | |||
66 | EXAMPLE_FILES = { | ||
67 | |||
68 | "page.html": """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | ||
69 | <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> | ||
70 | <head> | ||
71 | <meta http-equiv="Content-Type" content="text/html; charset={{ __encoding__ }}" /> | ||
72 | <title>poole - {{ hx(page["title"]) }}</title> | ||
73 | <meta name="description" content="{{ hx(page.get("description", "a poole site")) }}" /> | ||
74 | <meta name="keywords" content="{{ hx(page.get("keywords", "poole")) }}" /> | ||
75 | <style type="text/css"> | ||
76 | body { | ||
77 | font-family: sans; | ||
78 | width: 800px; | ||
79 | margin: 1em auto; | ||
80 | color: #2e3436; | ||
81 | } | ||
82 | div#box { | ||
83 | } | ||
84 | div#header, div#menu, div#content, div#footer { | ||
85 | padding: 1em; | ||
86 | } | ||
87 | div#menu { | ||
88 | background-color: #eeeeec; | ||
89 | padding: 0.6em 0 0.6em 0; | ||
90 | } | ||
91 | #menu span { | ||
92 | font-weight: bold; | ||
93 | padding: 0.6em; | ||
94 | } | ||
95 | #menu span.current { | ||
96 | background-color: #ffffff; | ||
97 | border: 1px solid #eeeeec; | ||
98 | } | ||
99 | #menu a { | ||
100 | color: #000000; | ||
101 | text-decoration: none; | ||
102 | } | ||
103 | div#footer { | ||
104 | color: gray; | ||
105 | text-align: center; | ||
106 | font-size: small; | ||
107 | } | ||
108 | div#footer a { | ||
109 | color: gray; | ||
110 | text-decoration: none; | ||
111 | } | ||
112 | pre { | ||
113 | border: dotted black 1px; | ||
114 | background: #eeeeec; | ||
115 | font-size: small; | ||
116 | padding: 1em; | ||
117 | } | ||
118 | </style> | ||
119 | </head> | ||
120 | <body> | ||
121 | <div id="box"> | ||
122 | <div id="header"> | ||
123 | <h1>a poole site</h1> | ||
124 | <h2>{{ hx(page["title"]) }}</h2> | ||
125 | </div> | ||
126 | <div id="menu"> | ||
127 | <!--% | ||
128 | mpages = [p for p in pages if "menu-position" in p] | ||
129 | mpages.sort(key=lambda p: int(p["menu-position"])) | ||
130 | entry = '<span class="%s"><a href="%s">%s</a></span>' | ||
131 | for p in mpages: | ||
132 | style = "current" if p["title"] == page["title"] else "" | ||
133 | print(entry % (style, p["url"], hx(p["title"]))) | ||
134 | %--> | ||
135 | </div> | ||
136 | <div id="content">{{ __content__ }}</div> | ||
137 | </div> | ||
138 | <div id="footer"> | ||
139 | Built with <a href="http://bitbucket.org/obensonne/poole">Poole</a> | ||
140 | · | ||
141 | Licensed as <a href="http://creativecommons.org/licenses/by-sa/3.0">CC-SA</a> | ||
142 | · | ||
143 | <a href="http://validator.w3.org/check?uri=referer">Validate me</a> | ||
144 | </div> | ||
145 | </body> | ||
146 | </html> | ||
147 | """, | ||
148 | |||
149 | # ----------------------------------------------------------------------------- | ||
150 | |||
151 | opj("input", "index.md"): """ | ||
152 | title: home | ||
153 | menu-position: 0 | ||
154 | --- | ||
155 | |||
156 | ## Welcome to Poole | ||
157 | |||
158 | In Poole you write your pages in [markdown][md]. It's easier to write | ||
159 | markdown than HTML. | ||
160 | |||
161 | Poole is made for simple websites you just want to get done, without installing | ||
162 | a bunch of requirements and without learning a template engine. | ||
163 | |||
164 | In a build, Poole copies every file from the *input* directory to the *output* | ||
165 | directory. During that process every markdown file (ending with *md*, *mkd*, | ||
166 | *mdown* or *markdown*) is converted to HTML using the project's `page.html` | ||
167 | as a skeleton. | ||
168 | |||
169 | [md]: http://daringfireball.net/projects/markdown/ | ||
170 | """, | ||
171 | |||
172 | # ----------------------------------------------------------------------------- | ||
173 | |||
174 | opj("input", "logic.md"): """ | ||
175 | menu-position: 4 | ||
176 | --- | ||
177 | Poole has basic support for content generation using Python code inlined in | ||
178 | page files. This is everything but a clear separation of logic and content but | ||
179 | for simple sites this is just a pragmatic way to get things done fast. | ||
180 | For instance the menu on this page is generated by some inlined Python code in | ||
181 | the project's `page.html` file. | ||
182 | |||
183 | Just ignore this feature if you don't need it :) | ||
184 | |||
185 | Content generation by inlined Python code is good to add some zest to your | ||
186 | site. If you use it a lot, you better go with more sophisticated site | ||
187 | generators like [Hyde](http://ringce.com/hyde). | ||
188 | """, | ||
189 | |||
190 | # ----------------------------------------------------------------------------- | ||
191 | |||
192 | opj("input", "layout.md"): """ | ||
193 | menu-position: 3 | ||
194 | --- | ||
195 | Every page of a poole site is based on *one global template file*, `page.html`. | ||
196 | All you need to adjust the site layout is to edit the page template | ||
197 | `page.html`. | ||
198 | """, | ||
199 | |||
200 | opj("input", "blog.md"): """ | ||
201 | menu-position: 10 | ||
202 | --- | ||
203 | Poole has basic blog support. If an input page's file name has a structure like | ||
204 | `page-title.YYYY-MM-DD.post-title.md`, e.g. `blog.2010-02-27.read_this.md`, | ||
205 | Poole recognizes the date and post title and sets them as attributes of the | ||
206 | page. These attributes can then be used to generate a list of blog posts: | ||
207 | |||
208 | <!--% | ||
209 | from datetime import datetime | ||
210 | posts = [p for p in pages if "post" in p] # get all blog post pages | ||
211 | posts.sort(key=lambda p: p.get("date"), reverse=True) # sort post pages by date | ||
212 | for p in posts: | ||
213 | date = datetime.strptime(p.date, "%Y-%m-%d").strftime("%B %d, %Y") | ||
214 | print " * **[%s](%s)** - %s" % (p.post, p.url, date) # markdown list item | ||
215 | %--> | ||
216 | |||
217 | Have a look into `input/blog.md` to see how it works. Feel free to adjust it | ||
218 | to your needs. | ||
219 | """, | ||
220 | |||
221 | # ----------------------------------------------------------------------------- | ||
222 | |||
223 | opj("input", "blog.2013-04-08.Lorem_Ipsum.md") : """ | ||
224 | |||
225 | --- | ||
226 | ## {{ page["post"] }} | ||
227 | |||
228 | *Posted at | ||
229 | <!--% | ||
230 | from datetime import datetime | ||
231 | print datetime.strptime(page["date"], "%Y-%m-%d").strftime("%B %d, %Y") | ||
232 | %-->* | ||
233 | |||
234 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed sed pretium arcu. | ||
235 | Nullam eu leo ut justo egestas condimentum sed id dolor. In suscipit est eu | ||
236 | tellus lacinia congue. Nunc tincidunt posuere nibh vitae accumsan. Suspendisse | ||
237 | quis justo quis nulla rhoncus venenatis. Cum sociis natoque penatibus et magnis | ||
238 | dis parturient montes, nascetur ridiculus mus. Suspendisse potenti. | ||
239 | |||
240 | Nullam luctus tortor ac libero eleifend interdum nec eget dolor. Aliquam quis | ||
241 | massa metus, id fringilla odio. Fusce lobortis sollicitudin gravida. Donec | ||
242 | porttitor metus aliquam diam consectetur vitae tristique ligula aliquet. Nulla | ||
243 | facilisi. Mauris eleifend erat id velit eleifend facilisis. Proin orci lacus, | ||
244 | imperdiet eu mollis ac, cursus sit amet ligula. Ut id neque urna, sed dignissim | ||
245 | urna. Cras sit amet sodales orci. In at lacus dui. Duis mi neque, posuere ut | ||
246 | congue non, ornare a magna. Fusce massa ligula, vestibulum sed vulputate quis, | ||
247 | sodales at massa. | ||
248 | |||
249 | No-ASCII characters like `öäüß` are no problems as long as input files are | ||
250 | encoded in UTF8. | ||
251 | """, | ||
252 | |||
253 | # ----------------------------------------------------------------------------- | ||
254 | |||
255 | opj("input", "blog.2013-04-01.Holy_Grail.md"): """ | ||
256 | |||
257 | ## {{ page["post"] }} | ||
258 | |||
259 | *Posted at <!--{ page["date"] }-->.* | ||
260 | |||
261 | Knights of Ni, we are but simple travelers who seek the enchanter who lives | ||
262 | beyond these woods. A newt? Did you dress her up like this? On second thoughts, | ||
263 | let's not go there. It is a silly place. You don't vote for kings. Knights of | ||
264 | Ni, we are but simple travelers who seek the enchanter who lives beyond these | ||
265 | woods. | ||
266 | |||
267 | ### Bridgekeeper ### | ||
268 | |||
269 | Camelot! What do you mean? And this isn't my nose. This is a false one. Ah, now | ||
270 | we see the violence inherent in the system! | ||
271 | |||
272 | You don't frighten us, English pig-dogs! Go and boil your bottoms, sons of a | ||
273 | silly person! I blow my nose at you, so-called Ah-thoor Keeng, you and all your | ||
274 | silly English K-n-n-n-n-n-n-n-niggits! I don't want to talk to you no more, you | ||
275 | empty-headed animal food trough water! I fart in your general direction! Your | ||
276 | mother was a hamster and your father smelt of elderberries! Now leave before I | ||
277 | am forced to taunt you a second time! Shh! Knights, I bid you welcome to your | ||
278 | new home. Let us ride to Camelot! Now, look here, my good man. | ||
279 | |||
280 | ### What a strange ### | ||
281 | |||
282 | She looks like one. Why do you think that she is a witch? Look, my liege! Bring | ||
283 | her forward! | ||
284 | |||
285 | [Ni!](http://chrisvalleskey.com/fillerama/) | ||
286 | """, | ||
287 | } | ||
288 | |||
289 | def init(project, theme): | ||
290 | """Initialize a site project.""" | ||
291 | |||
292 | if not opx(project): | ||
293 | os.makedirs(project) | ||
294 | |||
295 | if os.listdir(project): | ||
296 | print("abort : project dir %s is not empty" % project) | ||
297 | sys.exit(1) | ||
298 | |||
299 | dir_in = opj(project, "input") | ||
300 | dir_out = opj(project, "output") | ||
301 | |||
302 | os.mkdir(dir_in) | ||
303 | os.mkdir(dir_out) | ||
304 | |||
305 | for fname, content in EXAMPLE_FILES.items(): | ||
306 | print('info: create example %r' % fname) | ||
307 | with open(opj(project, fname), 'w') as fp: | ||
308 | fp.write(content) | ||
309 | |||
310 | if theme != 'minimal': | ||
311 | shutil.copy(opj(THEME_DIR, theme, 'page.html'), project) | ||
312 | for fname in glob.glob(opj(THEME_DIR, theme, '*')): | ||
313 | print('info: copy theme data %r' % fname) | ||
314 | if os.path.basename(fname) == 'page.html': | ||
315 | continue | ||
316 | if os.path.isdir(fname): | ||
317 | shutil.copytree(fname, opj(dir_in, os.path.basename(fname))) | ||
318 | else: | ||
319 | shutil.copy(fname, dir_in) | ||
320 | |||
321 | print("success: initialized project") | ||
322 | |||
323 | # ============================================================================= | ||
324 | # build site | ||
325 | # ============================================================================= | ||
326 | |||
327 | MKD_PATT = r'\.(?:md|mkd|mdown|markdown)$' | ||
328 | |||
329 | def hx(s): | ||
330 | """ | ||
331 | Replace the characters that are special within HTML (&, <, > and ") | ||
332 | with their equivalent character entity (e.g., &). This should be | ||
333 | called whenever an arbitrary string is inserted into HTML (so in most | ||
334 | places where you use {{ variable }} in your templates). | ||
335 | |||
336 | Note that " is not special in most HTML, only within attributes. | ||
337 | However, since escaping it does not hurt within normal HTML, it is | ||
338 | just escaped unconditionally. | ||
339 | """ | ||
340 | if getattr(s, 'escaped', False): | ||
341 | return s | ||
342 | |||
343 | escape = { | ||
344 | "&": "&", | ||
345 | '"': """, | ||
346 | ">": ">", | ||
347 | "<": "<", | ||
348 | } | ||
349 | return ''.join(escape.get(c, c) for c in s) | ||
350 | |||
351 | class Page(dict): | ||
352 | """Abstraction of a source page.""" | ||
353 | |||
354 | _template = None # template dictionary | ||
355 | _opts = None # command line options | ||
356 | _pstrip = None # path prefix to strip from (non-virtual) page file names | ||
357 | |||
358 | _re_eom = re.compile(r'^---+ *\r?\n?$') | ||
359 | _re_vardef = re.compile(r'^([^\n:=]+?)[:=]((?:.|\n )*)', re.MULTILINE) | ||
360 | _sec_macros = "macros" | ||
361 | _modmacs = None | ||
362 | |||
363 | def __init__(self, fname, virtual=None, **attrs): | ||
364 | """Create a new page. | ||
365 | |||
366 | Page content is read from `fname`, except when `virtual` is given (a | ||
367 | string representing the raw content of a virtual page). | ||
368 | |||
369 | The filename refers to the page source file. For virtual pages, this | ||
370 | *must* be relative to a projects input directory. | ||
371 | |||
372 | Virtual pages may contain page attribute definitions similar to real | ||
373 | pages. However, it probably is easier to provide the attributes | ||
374 | directly. This may be done using arbitrary keyword arguments. | ||
375 | |||
376 | """ | ||
377 | super(Page, self).__init__() | ||
378 | |||
379 | self.update(self._template) | ||
380 | self.update(attrs) | ||
381 | |||
382 | self._virtual = virtual is not None | ||
383 | |||
384 | fname = opj(self._pstrip, fname) if virtual else fname | ||
385 | |||
386 | self["fname"] = fname | ||
387 | |||
388 | self["url"] = re.sub(MKD_PATT, ".html", fname) | ||
389 | self["url"] = self["url"][len(self._pstrip):].lstrip(os.path.sep) | ||
390 | self["url"] = self["url"].replace(os.path.sep, "/") | ||
391 | |||
392 | if virtual: | ||
393 | self.raw = virtual | ||
394 | else: | ||
395 | with codecs.open(fname, 'r', self._opts.input_enc) as fp: | ||
396 | self.raw = fp.readlines() | ||
397 | |||
398 | # split raw content into macro definitions and real content | ||
399 | vardefs = "" | ||
400 | self.source = "" | ||
401 | for line in self.raw: | ||
402 | if not vardefs and self._re_eom.match(line): | ||
403 | vardefs = self.source | ||
404 | self.source = "" # only macro defs until here, reset source | ||
405 | else: | ||
406 | self.source += line | ||
407 | |||
408 | for key, val in self._re_vardef.findall(vardefs): | ||
409 | key = key.strip() | ||
410 | val = val.strip() | ||
411 | val = re.sub(r' *\n +', ' ', val) # clean out line continuation | ||
412 | self[key] = val | ||
413 | |||
414 | basename = os.path.basename(fname) | ||
415 | |||
416 | fpatt = r'(.+?)(?:\.([0-9]+-[0-9]+-[0-9]+)(?:\.(.*))?)?%s' % MKD_PATT | ||
417 | title, date, post = re.match(fpatt, basename).groups() | ||
418 | title = title.replace("_", " ") | ||
419 | post = post and post.replace("_", " ") or None | ||
420 | self["title"] = self.get("title", title) | ||
421 | if date and "date" not in self: self["date"] = date | ||
422 | if post and "post" not in self: self["post"] = post | ||
423 | |||
424 | self.html = "" | ||
425 | |||
426 | def __getattr__(self, name): | ||
427 | """Attribute-style access to dictionary items.""" | ||
428 | try: | ||
429 | return self[name] | ||
430 | except KeyError: | ||
431 | raise AttributeError(name) | ||
432 | |||
433 | def __str__(self): | ||
434 | """Page representation by file name.""" | ||
435 | return ('%s (virtual)' % self.fname) if self._virtual else self.fname | ||
436 | |||
437 | # ----------------------------------------------------------------------------- | ||
438 | |||
439 | def build(project, opts): | ||
440 | """Build a site project.""" | ||
441 | |||
442 | # ------------------------------------------------------------------------- | ||
443 | # utilities | ||
444 | # ------------------------------------------------------------------------- | ||
445 | |||
446 | def abort_iex(page, itype, inline, exc): | ||
447 | """Abort because of an exception in inlined Python code.""" | ||
448 | print("abort : Python %s in %s failed" % (itype, page)) | ||
449 | print((" %s raising the exception " % itype).center(79, "-")) | ||
450 | print(inline) | ||
451 | print(" exception ".center(79, "-")) | ||
452 | print(exc) | ||
453 | sys.exit(1) | ||
454 | |||
455 | def copy(src, dst): | ||
456 | if os.path.islink(src): | ||
457 | linkto = os.readlink(src) | ||
458 | if os.path.isdir(dst): | ||
459 | dst = opj(dst, os.path.basename(src)) | ||
460 | os.symlink(linkto, dst) | ||
461 | else: | ||
462 | shutil.copy2(src, opj(dir_out, cwd_site)) | ||
463 | |||
464 | # ------------------------------------------------------------------------- | ||
465 | # regex patterns and replacements | ||
466 | # ------------------------------------------------------------------------- | ||
467 | |||
468 | regx_escp = re.compile(r'\\((?:(?:<|<)!--|{)(?:{|%))') # escaped code | ||
469 | repl_escp = r'\1' | ||
470 | regx_rurl = re.compile(r'(?<=(?:(?:\n| )src|href)=")([^#/&%].*?)(?=")') | ||
471 | repl_rurl = lambda m: urlparse.urljoin(opts.base_url, m.group(1)) | ||
472 | |||
473 | regx_eval = re.compile(r'(?<!\\)(?:(?:<!--|{){)(.*?)(?:}(?:-->|}))', re.S) | ||
474 | |||
475 | def repl_eval(m): | ||
476 | """Replace a Python expression block by its evaluation.""" | ||
477 | |||
478 | expr = m.group(1) | ||
479 | try: | ||
480 | repl = eval(expr, macros.copy()) | ||
481 | except: | ||
482 | abort_iex(page, "expression", expr, traceback.format_exc()) | ||
483 | else: | ||
484 | if not isinstance(repl, basestring): # e.g. numbers | ||
485 | repl = unicode(repl) | ||
486 | elif not isinstance(repl, unicode): | ||
487 | repl = repl.decode("utf-8") | ||
488 | return repl | ||
489 | |||
490 | regx_exec = re.compile(r'(?<!\\)(?:(?:<!--|{)%)(.*?)(?:%(?:-->|}))', re.S) | ||
491 | |||
492 | def repl_exec(m): | ||
493 | """Replace a block of Python statements by their standard output.""" | ||
494 | |||
495 | stmt = m.group(1).replace("\r\n", "\n") | ||
496 | |||
497 | # base indentation | ||
498 | ind_lvl = len(re.findall(r'^(?: *\n)*( *)', stmt, re.MULTILINE)[0]) | ||
499 | ind_rex = re.compile(r'^ {0,%d}' % ind_lvl, re.MULTILINE) | ||
500 | stmt = ind_rex.sub('', stmt) | ||
501 | |||
502 | # execute | ||
503 | sys.stdout = StringIO.StringIO() | ||
504 | try: | ||
505 | exec stmt in macros.copy() | ||
506 | except: | ||
507 | sys.stdout = sys.__stdout__ | ||
508 | abort_iex(page, "statements", stmt, traceback.format_exc()) | ||
509 | else: | ||
510 | repl = sys.stdout.getvalue()[:-1] # remove last line break | ||
511 | sys.stdout = sys.__stdout__ | ||
512 | if not isinstance(repl, unicode): | ||
513 | repl = repl.decode(opts.input_enc) | ||
514 | return repl | ||
515 | |||
516 | # ------------------------------------------------------------------------- | ||
517 | # preparations | ||
518 | # ------------------------------------------------------------------------- | ||
519 | |||
520 | dir_in = opj(project, "input") | ||
521 | dir_out = opj(project, "output") | ||
522 | page_html = opj(project, "page.html") | ||
523 | |||
524 | # check required files and folders | ||
525 | for pelem in (page_html, dir_in, dir_out): | ||
526 | if not opx(pelem): | ||
527 | print("abort : %s does not exist, looks like project has not been " | ||
528 | "initialized" % pelem) | ||
529 | sys.exit(1) | ||
530 | |||
531 | # prepare output directory | ||
532 | for fod in glob.glob(opj(dir_out, "*")): | ||
533 | if os.path.isdir(fod): | ||
534 | shutil.rmtree(fod) | ||
535 | else: | ||
536 | os.remove(fod) | ||
537 | if not opx(dir_out): | ||
538 | os.mkdir(dir_out) | ||
539 | |||
540 | # macro module | ||
541 | fname = opj(opts.project, "macros.py") | ||
542 | macros = imp.load_source("macros", fname).__dict__ if opx(fname) else {} | ||
543 | |||
544 | macros["__encoding__"] = opts.output_enc | ||
545 | macros["options"] = opts | ||
546 | macros["project"] = project | ||
547 | macros["input"] = dir_in | ||
548 | macros["output"] = dir_out | ||
549 | |||
550 | # "builtin" items for use in macros and templates | ||
551 | macros["hx"] = hx | ||
552 | macros["htmlspecialchars"] = hx # legacy name of `htmlx` function | ||
553 | macros["Page"] = Page | ||
554 | |||
555 | # ------------------------------------------------------------------------- | ||
556 | # process input files | ||
557 | # ------------------------------------------------------------------------- | ||
558 | |||
559 | Page._template = macros.get("page", {}) | ||
560 | Page._opts = opts | ||
561 | Page._pstrip = dir_in | ||
562 | pages = [] | ||
563 | custom_converter = macros.get('converter', {}) | ||
564 | |||
565 | for cwd, dirs, files in os.walk(dir_in.decode(opts.filename_enc)): | ||
566 | cwd_site = cwd[len(dir_in):].lstrip(os.path.sep) | ||
567 | for sdir in dirs[:]: | ||
568 | if re.search(opts.ignore, opj(cwd_site, sdir)): | ||
569 | dirs.remove(sdir) | ||
570 | else: | ||
571 | os.mkdir(opj(dir_out, cwd_site, sdir)) | ||
572 | for f in files: | ||
573 | if re.search(opts.ignore, opj(cwd_site, f)): | ||
574 | pass | ||
575 | elif re.search(MKD_PATT, f): | ||
576 | page = Page(opj(cwd, f)) | ||
577 | pages.append(page) | ||
578 | else: | ||
579 | # either use a custom converter or do a plain copy | ||
580 | for patt, (func, ext) in custom_converter.items(): | ||
581 | if re.search(patt, f): | ||
582 | f_src = opj(cwd, f) | ||
583 | f_dst = opj(dir_out, cwd_site, f) | ||
584 | f_dst = '%s.%s' % (os.path.splitext(f_dst)[0], ext) | ||
585 | print('info : convert %s (%s)' % (f_src, func.__name__)) | ||
586 | func(f_src, f_dst) | ||
587 | break | ||
588 | else: | ||
589 | src = opj(cwd, f) | ||
590 | try: | ||
591 | copy(src, opj(dir_out, cwd_site)) | ||
592 | except OSError: | ||
593 | # some filesystems like FAT won't allow shutil.copy | ||
594 | shutil.copyfile(src, opj(dir_out, cwd_site, f)) | ||
595 | |||
596 | pages.sort(key=lambda p: int(p.get("sval", "0"))) | ||
597 | |||
598 | macros["pages"] = pages | ||
599 | |||
600 | # ------------------------------------------------------------------------- | ||
601 | # run pre-convert hooks in macro module (named 'once' before) | ||
602 | # ------------------------------------------------------------------------- | ||
603 | |||
604 | hooks = [a for a in macros if re.match(r'hook_preconvert_|once_', a)] | ||
605 | for fn in sorted(hooks): | ||
606 | macros[fn]() | ||
607 | |||
608 | # ------------------------------------------------------------------------- | ||
609 | # convert pages (markdown to HTML) | ||
610 | # ------------------------------------------------------------------------- | ||
611 | |||
612 | for page in pages: | ||
613 | |||
614 | print("info : convert %s" % page) | ||
615 | |||
616 | # replace expressions and statements in page source | ||
617 | macros["page"] = page | ||
618 | out = regx_eval.sub(repl_eval, page.source) | ||
619 | out = regx_exec.sub(repl_exec, out) | ||
620 | |||
621 | # convert to HTML | ||
622 | page.html = markdown.Markdown(extensions=opts.md_ext).convert(out) | ||
623 | |||
624 | # ------------------------------------------------------------------------- | ||
625 | # run post-convert hooks in macro module | ||
626 | # ------------------------------------------------------------------------- | ||
627 | |||
628 | hooks = [a for a in macros if a.startswith("hook_postconvert_")] | ||
629 | for fn in sorted(hooks): | ||
630 | macros[fn]() | ||
631 | |||
632 | # ------------------------------------------------------------------------- | ||
633 | # render complete HTML pages | ||
634 | # ------------------------------------------------------------------------- | ||
635 | |||
636 | with codecs.open(opj(project, "page.html"), 'r', opts.input_enc) as fp: | ||
637 | skeleton = fp.read() | ||
638 | |||
639 | for page in pages: | ||
640 | |||
641 | print("info : render %s" % page.url) | ||
642 | |||
643 | # replace expressions and statements in page.html | ||
644 | macros["page"] = page | ||
645 | macros["__content__"] = page.html | ||
646 | out = regx_eval.sub(repl_eval, skeleton) | ||
647 | out = regx_exec.sub(repl_exec, out) | ||
648 | |||
649 | # un-escape escaped python code blocks | ||
650 | out = regx_escp.sub(repl_escp, out) | ||
651 | |||
652 | # make relative links absolute | ||
653 | out = regx_rurl.sub(repl_rurl, out) | ||
654 | |||
655 | # write HTML page | ||
656 | fname = page.fname.replace(dir_in, dir_out) | ||
657 | fname = re.sub(MKD_PATT, ".html", fname) | ||
658 | with codecs.open(fname, 'w', opts.output_enc) as fp: | ||
659 | fp.write(out) | ||
660 | |||
661 | print("success: built project") | ||
662 | |||
663 | # ============================================================================= | ||
664 | # serve site | ||
665 | # ============================================================================= | ||
666 | |||
667 | def serve(project, port): | ||
668 | """Temporary serve a site project.""" | ||
669 | |||
670 | root = opj(project, "output") | ||
671 | if not os.listdir(project): | ||
672 | print("abort : output dir is empty (build project first!)") | ||
673 | sys.exit(1) | ||
674 | |||
675 | os.chdir(root) | ||
676 | server = HTTPServer(('', port), SimpleHTTPRequestHandler) | ||
677 | server.serve_forever() | ||
678 | |||
679 | # ============================================================================= | ||
680 | # options | ||
681 | # ============================================================================= | ||
682 | |||
683 | def options(): | ||
684 | """Parse and validate command line arguments.""" | ||
685 | |||
686 | usage = ("Usage: %prog --init [OPTIONS] [path/to/project]\n" | ||
687 | " %prog --build [OPTIONS] [path/to/project]\n" | ||
688 | " %prog --serve [OPTIONS] [path/to/project]\n" | ||
689 | "\n" | ||
690 | " Project path is optional, '.' is used as default.") | ||
691 | |||
692 | op = optparse.OptionParser(usage=usage) | ||
693 | |||
694 | op.add_option("-i" , "--init", action="store_true", default=False, | ||
695 | help="init project") | ||
696 | op.add_option("-b" , "--build", action="store_true", default=False, | ||
697 | help="build project") | ||
698 | op.add_option("-s" , "--serve", action="store_true", default=False, | ||
699 | help="serve project") | ||
700 | |||
701 | og = optparse.OptionGroup(op, "Init options") | ||
702 | og.add_option("", "--theme", type="choice", default="minimal", | ||
703 | choices=THEME_NAMES, | ||
704 | help="theme for a new project (choices: %s)" % ', '.join(THEME_NAMES)) | ||
705 | op.add_option_group(og) | ||
706 | |||
707 | og = optparse.OptionGroup(op, "Build options") | ||
708 | og.add_option("", "--base-url", default="/", metavar="URL", | ||
709 | help="base url for relative links (default: /)") | ||
710 | og.add_option("" , "--ignore", default=r"^\.|~$", metavar="REGEX", | ||
711 | help="input files to ignore (default: '^\.|~$')") | ||
712 | og.add_option("" , "--md-ext", default=[], metavar="EXT", | ||
713 | action="append", help="enable a markdown extension") | ||
714 | og.add_option("", "--input-enc", default="utf-8", metavar="ENC", | ||
715 | help="encoding of input pages (default: utf-8)") | ||
716 | og.add_option("", "--output-enc", default="utf-8", metavar="ENC", | ||
717 | help="encoding of output pages (default: utf-8)") | ||
718 | og.add_option("", "--filename-enc", default="utf-8", metavar="ENC", | ||
719 | help="encoding of file names (default: utf-8)") | ||
720 | op.add_option_group(og) | ||
721 | |||
722 | og = optparse.OptionGroup(op, "Serve options") | ||
723 | og.add_option("" , "--port", default=8080, | ||
724 | metavar="PORT", type="int", | ||
725 | help="port for serving (default: 8080)") | ||
726 | op.add_option_group(og) | ||
727 | |||
728 | opts, args = op.parse_args() | ||
729 | |||
730 | if opts.init + opts.build + opts.serve < 1: | ||
731 | op.print_help() | ||
732 | op.exit() | ||
733 | |||
734 | opts.project = args and args[0] or "." | ||
735 | |||
736 | return opts | ||
737 | |||
738 | # ============================================================================= | ||
739 | # main | ||
740 | # ============================================================================= | ||
741 | |||
742 | def main(): | ||
743 | |||
744 | opts = options() | ||
745 | |||
746 | if opts.init: | ||
747 | init(opts.project, opts.theme) | ||
748 | if opts.build: | ||
749 | build(opts.project, opts) | ||
750 | if opts.serve: | ||
751 | serve(opts.project, opts.port) | ||
752 | |||
753 | if __name__ == '__main__': | ||
754 | |||
755 | main() | ||
@@ -2,7 +2,7 @@ Web Site Design | |||
2 | =============== | 2 | =============== |
3 | 3 | ||
4 | The Monitoring Plugins web site is generated using | 4 | The Monitoring Plugins web site is generated using |
5 | <https://bitbucket.org/obensonne/poole>. | 5 | <https://hg.sr.ht/~obensonne/poole>. |
6 | 6 | ||
7 | Color scheme | 7 | Color scheme |
8 | ------------ | 8 | ------------ |
diff --git a/web/macros.py b/web/macros.py index 3ef93e6..9dfadb2 100644 --- a/web/macros.py +++ b/web/macros.py | |||
@@ -1,4 +1,5 @@ | |||
1 | import email.utils | 1 | import email.utils |
2 | import math | ||
2 | import os.path | 3 | import os.path |
3 | import time | 4 | import time |
4 | 5 | ||
@@ -68,9 +69,8 @@ def hook_postconvert_rss(): | |||
68 | desc = 'Announcements published by the Monitoring Plugins Development Team.' | 69 | desc = 'Announcements published by the Monitoring Plugins Development Team.' |
69 | date = email.utils.formatdate() | 70 | date = email.utils.formatdate() |
70 | rss = _RSS % (title, link, desc, date, date, items) | 71 | rss = _RSS % (title, link, desc, date, date, items) |
71 | fp = open(os.path.join(output, 'rss.xml'), 'w') | 72 | with open(os.path.join(output, 'rss.xml'), 'w', encoding='utf-8') as fp: |
72 | fp.write(rss.encode('utf-8')) | 73 | fp.write(rss) |
73 | fp.close() | ||
74 | 74 | ||
75 | # | 75 | # |
76 | # News | 76 | # News |
@@ -80,7 +80,7 @@ def hook_preconvert_news(): | |||
80 | posts_per_page = 10 | 80 | posts_per_page = 10 |
81 | posts = [p for p in pages if 'date' in p] | 81 | posts = [p for p in pages if 'date' in p] |
82 | posts.sort(key=lambda p: p.date, reverse=True) | 82 | posts.sort(key=lambda p: p.date, reverse=True) |
83 | n_news_pages = len(posts) / posts_per_page | 83 | n_news_pages = math.ceil(len(posts) / posts_per_page) |
84 | if len(posts) % posts_per_page > 0: | 84 | if len(posts) % posts_per_page > 0: |
85 | n_news_pages += 1 | 85 | n_news_pages += 1 |
86 | for i, chunk in enumerate(next_news_chunk(posts, posts_per_page)): | 86 | for i, chunk in enumerate(next_news_chunk(posts, posts_per_page)): |
@@ -108,7 +108,7 @@ def make_news_page(posts, current_index): | |||
108 | teaser = ['# ' + title] | 108 | teaser = ['# ' + title] |
109 | for p in posts: | 109 | for p in posts: |
110 | source = list() | 110 | source = list() |
111 | author = p.author.encode('ascii', 'xmlcharrefreplace') | 111 | author = p.author |
112 | timestamp = time.strptime(p.date, '%Y-%m-%d') | 112 | timestamp = time.strptime(p.date, '%Y-%m-%d') |
113 | author_date = author + ', ' + time.strftime('%B %-e, %Y', timestamp) | 113 | author_date = author + ', ' + time.strftime('%B %-e, %Y', timestamp) |
114 | teaser.append('## %s' % p.title) | 114 | teaser.append('## %s' % p.title) |
diff --git a/web/page.html b/web/page.html index 8999af9..ee36fb2 100644 --- a/web/page.html +++ b/web/page.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <?xml version="1.0" encoding="{{ __encoding__ }}"?> | 1 | <?xml version="1.0" encoding="UTF-8"?> |
2 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" | 2 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" |
3 | "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> | 3 | "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> |
4 | 4 | ||
@@ -6,7 +6,7 @@ | |||
6 | 6 | ||
7 | <head> | 7 | <head> |
8 | <title>Monitoring Plugins - {{ hx(page.title) }}</title> | 8 | <title>Monitoring Plugins - {{ hx(page.title) }}</title> |
9 | <meta http-equiv="Content-Type" content="text/html; charset={{ __encoding__ }}" /> | 9 | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> |
10 | <meta name="description" content="{{ hx(page.description) }}" /> | 10 | <meta name="description" content="{{ hx(page.description) }}" /> |
11 | <meta name="keywords" content="{{ hx(page.keywords) }}" /> | 11 | <meta name="keywords" content="{{ hx(page.keywords) }}" /> |
12 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | 12 | <meta name="viewport" content="width=device-width, initial-scale=1" /> |