diff options
-rwxr-xr-x | libexec/poole | 746 |
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 | |||
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 | # ------------------------------------------------------------------------- | ||
456 | # regex patterns and replacements | ||
457 | # ------------------------------------------------------------------------- | ||
458 | |||
459 | regx_escp = re.compile(r'\\((?:(?:<|<)!--|{)(?:{|%))') # 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 | |||
658 | def 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 | |||
674 | def 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 | |||
733 | def 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 | |||
744 | if __name__ == '__main__': | ||
745 | |||
746 | main() | ||