summaryrefslogtreecommitdiffstats
path: root/libexec
diff options
context:
space:
mode:
Diffstat (limited to 'libexec')
-rwxr-xr-xlibexec/poole746
1 files changed, 746 insertions, 0 deletions
diff --git a/libexec/poole b/libexec/poole
new file mode 100755
index 0000000..ea66ccb
--- /dev/null
+++ b/libexec/poole
@@ -0,0 +1,746 @@
1#!/usr/bin/env 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 # -------------------------------------------------------------------------
456 # regex patterns and replacements
457 # -------------------------------------------------------------------------
458
459 regx_escp = re.compile(r'\\((?:(?:&lt;|<)!--|{)(?:{|%))') # escaped code
460 repl_escp = r'\1'
461 regx_rurl = re.compile(r'(?<=(?:(?:\n| )src|href)=")([^#/&%].*?)(?=")')
462 repl_rurl = lambda m: urlparse.urljoin(opts.base_url, m.group(1))
463
464 regx_eval = re.compile(r'(?<!\\)(?:(?:<!--|{){)(.*?)(?:}(?:-->|}))', re.S)
465
466 def repl_eval(m):
467 """Replace a Python expression block by its evaluation."""
468
469 expr = m.group(1)
470 try:
471 repl = eval(expr, macros.copy())
472 except:
473 abort_iex(page, "expression", expr, traceback.format_exc())
474 else:
475 if not isinstance(repl, basestring): # e.g. numbers
476 repl = unicode(repl)
477 elif not isinstance(repl, unicode):
478 repl = repl.decode("utf-8")
479 return repl
480
481 regx_exec = re.compile(r'(?<!\\)(?:(?:<!--|{)%)(.*?)(?:%(?:-->|}))', re.S)
482
483 def repl_exec(m):
484 """Replace a block of Python statements by their standard output."""
485
486 stmt = m.group(1).replace("\r\n", "\n")
487
488 # base indentation
489 ind_lvl = len(re.findall(r'^(?: *\n)*( *)', stmt, re.MULTILINE)[0])
490 ind_rex = re.compile(r'^ {0,%d}' % ind_lvl, re.MULTILINE)
491 stmt = ind_rex.sub('', stmt)
492
493 # execute
494 sys.stdout = StringIO.StringIO()
495 try:
496 exec stmt in macros.copy()
497 except:
498 sys.stdout = sys.__stdout__
499 abort_iex(page, "statements", stmt, traceback.format_exc())
500 else:
501 repl = sys.stdout.getvalue()[:-1] # remove last line break
502 sys.stdout = sys.__stdout__
503 if not isinstance(repl, unicode):
504 repl = repl.decode(opts.input_enc)
505 return repl
506
507 # -------------------------------------------------------------------------
508 # preparations
509 # -------------------------------------------------------------------------
510
511 dir_in = opj(project, "input")
512 dir_out = opj(project, "output")
513 page_html = opj(project, "page.html")
514
515 # check required files and folders
516 for pelem in (page_html, dir_in, dir_out):
517 if not opx(pelem):
518 print("abort : %s does not exist, looks like project has not been "
519 "initialized" % pelem)
520 sys.exit(1)
521
522 # prepare output directory
523 for fod in glob.glob(opj(dir_out, "*")):
524 if os.path.isdir(fod):
525 shutil.rmtree(fod)
526 else:
527 os.remove(fod)
528 if not opx(dir_out):
529 os.mkdir(dir_out)
530
531 # macro module
532 fname = opj(opts.project, "macros.py")
533 macros = imp.load_source("macros", fname).__dict__ if opx(fname) else {}
534
535 macros["__encoding__"] = opts.output_enc
536 macros["options"] = opts
537 macros["project"] = project
538 macros["input"] = dir_in
539 macros["output"] = dir_out
540
541 # "builtin" items for use in macros and templates
542 macros["hx"] = hx
543 macros["htmlspecialchars"] = hx # legacy name of `htmlx` function
544 macros["Page"] = Page
545
546 # -------------------------------------------------------------------------
547 # process input files
548 # -------------------------------------------------------------------------
549
550 Page._template = macros.get("page", {})
551 Page._opts = opts
552 Page._pstrip = dir_in
553 pages = []
554 custom_converter = macros.get('converter', {})
555
556 for cwd, dirs, files in os.walk(dir_in.decode(opts.filename_enc)):
557 cwd_site = cwd[len(dir_in):].lstrip(os.path.sep)
558 for sdir in dirs[:]:
559 if re.search(opts.ignore, opj(cwd_site, sdir)):
560 dirs.remove(sdir)
561 else:
562 os.mkdir(opj(dir_out, cwd_site, sdir))
563 for f in files:
564 if re.search(opts.ignore, opj(cwd_site, f)):
565 pass
566 elif re.search(MKD_PATT, f):
567 page = Page(opj(cwd, f))
568 pages.append(page)
569 else:
570 # either use a custom converter or do a plain copy
571 for patt, (func, ext) in custom_converter.items():
572 if re.search(patt, f):
573 f_src = opj(cwd, f)
574 f_dst = opj(dir_out, cwd_site, f)
575 f_dst = '%s.%s' % (os.path.splitext(f_dst)[0], ext)
576 print('info : convert %s (%s)' % (f_src, func.__name__))
577 func(f_src, f_dst)
578 break
579 else:
580 src = opj(cwd, f)
581 try:
582 shutil.copy(src, opj(dir_out, cwd_site))
583 except OSError:
584 # some filesystems like FAT won't allow shutil.copy
585 shutil.copyfile(src, opj(dir_out, cwd_site, f))
586
587 pages.sort(key=lambda p: int(p.get("sval", "0")))
588
589 macros["pages"] = pages
590
591 # -------------------------------------------------------------------------
592 # run pre-convert hooks in macro module (named 'once' before)
593 # -------------------------------------------------------------------------
594
595 hooks = [a for a in macros if re.match(r'hook_preconvert_|once_', a)]
596 for fn in sorted(hooks):
597 macros[fn]()
598
599 # -------------------------------------------------------------------------
600 # convert pages (markdown to HTML)
601 # -------------------------------------------------------------------------
602
603 for page in pages:
604
605 print("info : convert %s" % page)
606
607 # replace expressions and statements in page source
608 macros["page"] = page
609 out = regx_eval.sub(repl_eval, page.source)
610 out = regx_exec.sub(repl_exec, out)
611
612 # convert to HTML
613 page.html = markdown.Markdown(extensions=opts.md_ext).convert(out)
614
615 # -------------------------------------------------------------------------
616 # run post-convert hooks in macro module
617 # -------------------------------------------------------------------------
618
619 hooks = [a for a in macros if a.startswith("hook_postconvert_")]
620 for fn in sorted(hooks):
621 macros[fn]()
622
623 # -------------------------------------------------------------------------
624 # render complete HTML pages
625 # -------------------------------------------------------------------------
626
627 with codecs.open(opj(project, "page.html"), 'r', opts.input_enc) as fp:
628 skeleton = fp.read()
629
630 for page in pages:
631
632 print("info : render %s" % page.url)
633
634 # replace expressions and statements in page.html
635 macros["page"] = page
636 macros["__content__"] = page.html
637 out = regx_eval.sub(repl_eval, skeleton)
638 out = regx_exec.sub(repl_exec, out)
639
640 # un-escape escaped python code blocks
641 out = regx_escp.sub(repl_escp, out)
642
643 # make relative links absolute
644 out = regx_rurl.sub(repl_rurl, out)
645
646 # write HTML page
647 fname = page.fname.replace(dir_in, dir_out)
648 fname = re.sub(MKD_PATT, ".html", fname)
649 with codecs.open(fname, 'w', opts.output_enc) as fp:
650 fp.write(out)
651
652 print("success: built project")
653
654# =============================================================================
655# serve site
656# =============================================================================
657
658def serve(project, port):
659 """Temporary serve a site project."""
660
661 root = opj(project, "output")
662 if not os.listdir(project):
663 print("abort : output dir is empty (build project first!)")
664 sys.exit(1)
665
666 os.chdir(root)
667 server = HTTPServer(('', port), SimpleHTTPRequestHandler)
668 server.serve_forever()
669
670# =============================================================================
671# options
672# =============================================================================
673
674def options():
675 """Parse and validate command line arguments."""
676
677 usage = ("Usage: %prog --init [OPTIONS] [path/to/project]\n"
678 " %prog --build [OPTIONS] [path/to/project]\n"
679 " %prog --serve [OPTIONS] [path/to/project]\n"
680 "\n"
681 " Project path is optional, '.' is used as default.")
682
683 op = optparse.OptionParser(usage=usage)
684
685 op.add_option("-i" , "--init", action="store_true", default=False,
686 help="init project")
687 op.add_option("-b" , "--build", action="store_true", default=False,
688 help="build project")
689 op.add_option("-s" , "--serve", action="store_true", default=False,
690 help="serve project")
691
692 og = optparse.OptionGroup(op, "Init options")
693 og.add_option("", "--theme", type="choice", default="minimal",
694 choices=THEME_NAMES,
695 help="theme for a new project (choices: %s)" % ', '.join(THEME_NAMES))
696 op.add_option_group(og)
697
698 og = optparse.OptionGroup(op, "Build options")
699 og.add_option("", "--base-url", default="/", metavar="URL",
700 help="base url for relative links (default: /)")
701 og.add_option("" , "--ignore", default=r"^\.|~$", metavar="REGEX",
702 help="input files to ignore (default: '^\.|~$')")
703 og.add_option("" , "--md-ext", default=[], metavar="EXT",
704 action="append", help="enable a markdown extension")
705 og.add_option("", "--input-enc", default="utf-8", metavar="ENC",
706 help="encoding of input pages (default: utf-8)")
707 og.add_option("", "--output-enc", default="utf-8", metavar="ENC",
708 help="encoding of output pages (default: utf-8)")
709 og.add_option("", "--filename-enc", default="utf-8", metavar="ENC",
710 help="encoding of file names (default: utf-8)")
711 op.add_option_group(og)
712
713 og = optparse.OptionGroup(op, "Serve options")
714 og.add_option("" , "--port", default=8080,
715 metavar="PORT", type="int",
716 help="port for serving (default: 8080)")
717 op.add_option_group(og)
718
719 opts, args = op.parse_args()
720
721 if opts.init + opts.build + opts.serve < 1:
722 op.print_help()
723 op.exit()
724
725 opts.project = args and args[0] or "."
726
727 return opts
728
729# =============================================================================
730# main
731# =============================================================================
732
733def main():
734
735 opts = options()
736
737 if opts.init:
738 init(opts.project, opts.theme)
739 if opts.build:
740 build(opts.project, opts)
741 if opts.serve:
742 serve(opts.project, opts.port)
743
744if __name__ == '__main__':
745
746 main()