summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbin/build-web-site5
-rwxr-xr-xlibexec/poole755
-rw-r--r--web/README2
-rw-r--r--web/macros.py10
-rw-r--r--web/page.html4
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
19prefix='/home/plugins' 19prefix='/home/plugins'
20 20
21export PATH="$prefix/libexec:/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin" 21export 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
23myself=${0##*/} 23myself=${0##*/}
24man_source_dir="$prefix/web/work/man" 24man_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#
57poole --build \ 57poole.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
26from __future__ import with_statement
27
28import codecs
29import glob
30import imp
31import optparse
32import os
33from os.path import join as opj
34from os.path import exists as opx
35import re
36import shutil
37import StringIO
38import sys
39import traceback
40import urlparse
41
42from SimpleHTTPServer import SimpleHTTPRequestHandler
43from BaseHTTPServer import HTTPServer
44
45try:
46 import markdown
47except ImportError:
48 print("abort : need python-markdown, get it from "
49 "http://www.freewisdom.org/projects/python-markdown/Installation")
50 sys.exit(1)
51
52HERE = os.path.dirname(os.path.realpath(__file__))
53
54THEME_DIR = opj(HERE, 'themes')
55
56THEME_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
66EXAMPLE_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 &middot;
141 Licensed as <a href="http://creativecommons.org/licenses/by-sa/3.0">CC-SA</a>
142 &middot;
143 <a href="http://validator.w3.org/check?uri=referer">Validate me</a>
144 </div>
145</body>
146</html>
147""",
148
149# -----------------------------------------------------------------------------
150
151opj("input", "index.md"): """
152title: home
153menu-position: 0
154---
155
156## Welcome to Poole
157
158In Poole you write your pages in [markdown][md]. It's easier to write
159markdown than HTML.
160
161Poole is made for simple websites you just want to get done, without installing
162a bunch of requirements and without learning a template engine.
163
164In a build, Poole copies every file from the *input* directory to the *output*
165directory. During that process every markdown file (ending with *md*, *mkd*,
166*mdown* or *markdown*) is converted to HTML using the project's `page.html`
167as a skeleton.
168
169[md]: http://daringfireball.net/projects/markdown/
170""",
171
172# -----------------------------------------------------------------------------
173
174opj("input", "logic.md"): """
175menu-position: 4
176---
177Poole has basic support for content generation using Python code inlined in
178page files. This is everything but a clear separation of logic and content but
179for simple sites this is just a pragmatic way to get things done fast.
180For instance the menu on this page is generated by some inlined Python code in
181the project's `page.html` file.
182
183Just ignore this feature if you don't need it :)
184
185Content generation by inlined Python code is good to add some zest to your
186site. If you use it a lot, you better go with more sophisticated site
187generators like [Hyde](http://ringce.com/hyde).
188""",
189
190# -----------------------------------------------------------------------------
191
192opj("input", "layout.md"): """
193menu-position: 3
194---
195Every page of a poole site is based on *one global template file*, `page.html`.
196All you need to adjust the site layout is to edit the page template
197`page.html`.
198""",
199
200opj("input", "blog.md"): """
201menu-position: 10
202---
203Poole 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`,
205Poole recognizes the date and post title and sets them as attributes of the
206page. These attributes can then be used to generate a list of blog posts:
207
208<!--%
209from datetime import datetime
210posts = [p for p in pages if "post" in p] # get all blog post pages
211posts.sort(key=lambda p: p.get("date"), reverse=True) # sort post pages by date
212for 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
217Have a look into `input/blog.md` to see how it works. Feel free to adjust it
218to your needs.
219""",
220
221# -----------------------------------------------------------------------------
222
223opj("input", "blog.2013-04-08.Lorem_Ipsum.md") : """
224
225---
226## {{ page["post"] }}
227
228*Posted at
229<!--%
230from datetime import datetime
231print datetime.strptime(page["date"], "%Y-%m-%d").strftime("%B %d, %Y")
232%-->*
233
234Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed sed pretium arcu.
235Nullam eu leo ut justo egestas condimentum sed id dolor. In suscipit est eu
236tellus lacinia congue. Nunc tincidunt posuere nibh vitae accumsan. Suspendisse
237quis justo quis nulla rhoncus venenatis. Cum sociis natoque penatibus et magnis
238dis parturient montes, nascetur ridiculus mus. Suspendisse potenti.
239
240Nullam luctus tortor ac libero eleifend interdum nec eget dolor. Aliquam quis
241massa metus, id fringilla odio. Fusce lobortis sollicitudin gravida. Donec
242porttitor metus aliquam diam consectetur vitae tristique ligula aliquet. Nulla
243facilisi. Mauris eleifend erat id velit eleifend facilisis. Proin orci lacus,
244imperdiet eu mollis ac, cursus sit amet ligula. Ut id neque urna, sed dignissim
245urna. Cras sit amet sodales orci. In at lacus dui. Duis mi neque, posuere ut
246congue non, ornare a magna. Fusce massa ligula, vestibulum sed vulputate quis,
247sodales at massa.
248
249No-ASCII characters like `öäüß` are no problems as long as input files are
250encoded in UTF8.
251""",
252
253# -----------------------------------------------------------------------------
254
255opj("input", "blog.2013-04-01.Holy_Grail.md"): """
256
257## {{ page["post"] }}
258
259*Posted at <!--{ page["date"] }-->.*
260
261Knights of Ni, we are but simple travelers who seek the enchanter who lives
262beyond these woods. A newt? Did you dress her up like this? On second thoughts,
263let's not go there. It is a silly place. You don't vote for kings. Knights of
264Ni, we are but simple travelers who seek the enchanter who lives beyond these
265woods.
266
267### Bridgekeeper ###
268
269Camelot! What do you mean? And this isn't my nose. This is a false one. Ah, now
270we see the violence inherent in the system!
271
272You don't frighten us, English pig-dogs! Go and boil your bottoms, sons of a
273silly person! I blow my nose at you, so-called Ah-thoor Keeng, you and all your
274silly English K-n-n-n-n-n-n-n-niggits! I don't want to talk to you no more, you
275empty-headed animal food trough water! I fart in your general direction! Your
276mother was a hamster and your father smelt of elderberries! Now leave before I
277am forced to taunt you a second time! Shh! Knights, I bid you welcome to your
278new home. Let us ride to Camelot! Now, look here, my good man.
279
280### What a strange ###
281
282She looks like one. Why do you think that she is a witch? Look, my liege! Bring
283her forward!
284
285[Ni!](http://chrisvalleskey.com/fillerama/)
286""",
287}
288
289def 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
327MKD_PATT = r'\.(?:md|mkd|mdown|markdown)$'
328
329def hx(s):
330 """
331 Replace the characters that are special within HTML (&, <, > and ")
332 with their equivalent character entity (e.g., &amp;). 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 "&": "&amp;",
345 '"': "&quot;",
346 ">": "&gt;",
347 "<": "&lt;",
348 }
349 return ''.join(escape.get(c, c) for c in s)
350
351class 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
439def 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'\\((?:(?:&lt;|<)!--|{)(?:{|%))') # 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
667def 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
683def 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
742def 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
753if __name__ == '__main__':
754
755 main()
diff --git a/web/README b/web/README
index 48e360c..365c7b2 100644
--- a/web/README
+++ b/web/README
@@ -2,7 +2,7 @@ Web Site Design
2=============== 2===============
3 3
4The Monitoring Plugins web site is generated using 4The Monitoring Plugins web site is generated using
5<https://bitbucket.org/obensonne/poole>. 5<https://hg.sr.ht/~obensonne/poole>.
6 6
7Color scheme 7Color 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 @@
1import email.utils 1import email.utils
2import math
2import os.path 3import os.path
3import time 4import 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" />