Implement variable substitution in includes
[geekigeeki.git] / geekigeeki.py
index afaebefc6a4321560a861d04f174c7d8661a304b..eb51489cddb74e6bf2185f6a0e05ac62ebfb6685 100755 (executable)
@@ -18,7 +18,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-__version__ = '$Id$'[4:12]
+__version__ = '4.0-' + '$Id$'[4:11]
 
 from time import clock
 start_time = clock()
@@ -26,13 +26,14 @@ title_done = False
 
 import cgi, sys, os, re, errno, stat
 
-# FIXME: we accept stuff like foo/../bar and we shouldn't
-file_re = re.compile(r"([A-Za-z0-9_\-][A-Za-z0-9_\.\-/]*)")
+image_ext = 'png|gif|jpg|jpeg|bmp|ico'
 video_ext = "ogg|ogv|oga" # Not supported by Firefox 3.5: mkv|mpg|mpeg|mp4|avi|asf|flv|wmv|qt
-img_re = re.compile(r".*\.(png|gif|jpg|jpeg|bmp|ico|" +  video_ext + ")", re.IGNORECASE)
+image_re = re.compile(r".*\.(" + image_ext + "|" +  video_ext + ")", re.IGNORECASE)
 video_re = re.compile(r".*\.(" + video_ext + ")", re.IGNORECASE)
-url_re = re.compile(r"[a-z]{3,8}://[^\s'\"]+\S")
-ext_re = re.compile(r"\.([^\./]+)$")
+# FIXME: we accept stuff like foo/../bar and we shouldn't
+file_re  = re.compile(r"([A-Za-z0-9_\-][A-Za-z0-9_\.\-/]*)")
+url_re   = re.compile(r"[a-z]{3,8}://[^\s'\"]+\S")
+ext_re   = re.compile(r"\.([^\./]+)$")
 
 # CGI stuff ---------------------------------------------------------
 def script_name():
@@ -82,18 +83,18 @@ def permalink(s):
 def humanlink(s):
     return re.sub(r'(?:.*[/:]|)([^:/\.]+)(?:\.[^/:]+|)$', r'\1', s.replace('_', ' '))
 
-# Split arg lists like "blah| blah blah| width=100 | align = center",
+# Split arg lists like "blah|blah blah| width=100 | align = center",
 # return a list containing anonymous arguments and a map containing the named arguments
 def parse_args(s):
     args = []
-    kwargs = {} 
+    kvargs = {}
     for arg in s.strip('<[{}]>').split('|'):
         m = re.match('\s*(\w+)\s*=\s*(.+)\s*', arg)
         if m is not None:
-            kwargs[m.group(1)] = m.group(2)
+            kvargs[m.group(1)] = m.group(2)
         else:
             args.append(arg.strip())
-    return (args, kwargs)
+    return (args, kvargs)
 
 def url_args(kvargs):
     argv = []
@@ -198,7 +199,7 @@ def send_httperror(status="403 Not Found", query=""):
 def link_tag(dest, text=None, privileged=False, **kvargs):
     if text is None:
         text = humanlink(dest)
-    elif img_re.match(text):
+    elif image_re.match(text):
         text = '<img border="0" src="' + relative_url(text) + '" alt="' + text + '" />'
 
     link_class = kvargs.get('class', kvargs.get('cssclass', None))
@@ -223,10 +224,10 @@ def link_inline(name, descr=None, kvargs={}):
     url = relative_url(name)
     if video_re.match(name):
         return '<video controls="1" src="%s">Your browser does not support the HTML5 video tag</video>' % url
-    elif img_re.match(name):
+    elif image_re.match(name):
         return '<a href="%s"><img border="0" src="%s" alt="%s" /></a>' % (url, url + url_args(kvargs), descr)
     elif file_re.match(name) and not ext_re.search(name): # FIXME: this guesses a wiki page
-        return Page(name).send_naked()
+        return Page(name).send_naked(kvargs)
     else:
         return '<iframe width="100%%" scrolling="auto" frameborder="0" src="%s"><a href="%s">%s</a></iframe>' \
             % (url, url, name)
@@ -336,16 +337,14 @@ def send_footer(mod_string=None):
         cgi.print_arguments()
         cgi.print_form(form)
         cgi.print_environ()
-    print('''
-<div id="footer"><hr />
-<p class="copyright">
-<a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/"><img class="license" alt="Creative Commons License" src="%s" /></a>
-<span class="benchmark">generated in %0.3fs</span> by <a href="http://www.codewiz.org/wiki/GeekiGeeki">GeekiGeeki</a> version %s
-</p>
-''' % (relative_url('cc-by-sa.png'), clock() - start_time, __version__))
-    if mod_string:
-        print('<p class="modified">last modified %s</p>' % mod_string)
-    print('</div></body></html>')
+    link_inline("sys/footer", kvargs= { 'LAST_MODIFIED': mod_string })
+    print("</body></html>")
+
+def _macro_ELAPSED_TIME(*args, **kvargs):
+    return "%03f" % (clock() - start_time)
+
+def _macro_VERSION(*args, **kvargs):
+    return __version__
 
 class WikiFormatter:
     """Object that turns Wiki markup into HTML.
@@ -353,8 +352,9 @@ class WikiFormatter:
     All formatting commands can be parsed one line at a time, though
     some state is carried over between lines.
     """
-    def __init__(self, raw):
+    def __init__(self, raw, kvargs=None):
         self.raw = raw
+        self.kvargs = kvargs or {}
         self.h_level = 0
         self.in_pre = self.in_html = self.in_table = self.in_li = False
         self.in_header = True
@@ -366,6 +366,7 @@ class WikiFormatter:
             "**":  ["b",   False],
             "##":  ["tt",  False],
             "__":  ["u",   False],
+            "--":  ["del", False],
             "^^":  ["sup", False],
             ",,":  ["sub", False],
             "''":  ["em",  False], # LEGACY
@@ -378,6 +379,9 @@ class WikiFormatter:
         style[1] = not style[1]
         return ['</', '<'][style[1]] + style[0] + '>'
 
+    def _glyph_repl(self, word):
+        return '&mdash;'
+
     def _tit_repl(self, word):
         if self.h_level:
             result = '</h%d><p>\n' % self.h_level
@@ -396,14 +400,16 @@ class WikiFormatter:
 
     def _macro_repl(self, word):
         try:
-            args, kwargs = parse_args(word)
+            args, kvargs = parse_args(word)
+            if args[0] in self.kvargs:
+                return self.kvargs[args[0]]
             macro = globals().get('_macro_' + args[0])
             if not macro:
-                exec(open("macros/" + name + ".py").read(), globals())
-                macro = globals().get('_macro_' + name)
-            return macro(*args, **kwargs)
-        except Exception:
-            msg = cgi.escape(word)
+                exec(open("macros/" + args[0] + ".py").read(), globals())
+                macro = globals().get('_macro_' + args[0])
+            return macro(*args, **kvargs)
+        except Exception, e:
+            msg = cgi.escape(word) + ": " + cgi.escape(e.message)
             if not self.in_html:
                 msg = '<strong class="error">' + msg + '</strong>'
             return msg
@@ -533,29 +539,30 @@ class WikiFormatter:
         print('<div class="wiki"><p>')
 
         scan_re = re.compile(r"""(?:
-            # Styles and formatting
-              (?P<b>     \*\*|'''|//|''|\#\#|``|__|\^\^|,,)
+            # Styles and formatting ("--" must cling to a word to disambiguate it from the dash)
+              (?P<b>     \*\* | // | \#\# | __ | --\b | \b-- | \^\^ | ,, | ''' | '' | `` )
             | (?P<tit>   \={2,6})
             | (?P<br>    \\\\)
             | (?P<rule>  ^-{3,})
             | (?P<hi>    \b( FIXME | TODO | DONE )\b )
+            | (?P<glyph> --)
 
             # Links
             | (?P<macro> \<\<([^\s\|\>]+)(?:\s*\|\s*([^\>]+)|)\>\>)
             | (?P<hurl>  \[\[([^\s\|]+)(?:\s*\|\s*([^\]]+)|)\]\])
 
             # Inline HTML
-            | (?P<html>  <(br|hr|div|span|form|iframe|input|textarea|a|img|h[1-5])\b )
+            | (?P<html>             <(br|hr|div|span|form|iframe|input|textarea|a|img|h[1-5])\b )
             | (?P<htmle> ( /\s*> | </(br|hr|div|span|form|iframe|input|textarea|a|img|h[1-5])> ) )
             | (?P<ent>   [<>&] )
 
             # Auto links (LEGACY)
-            | (?P<img>   \b[a-zA-Z0-9_/-]+\.(png|gif|jpg|jpeg|bmp|ico|ogm|ogg|mkv|mpg|mpeg|mp4|avi|asf|flv|wmv|qt))
+            | (?P<img>   \b[a-zA-Z0-9_/-]+\.(""" + image_ext + "|" + video_ext + r"""))
             | (?P<word>  \b(?:[A-Z][a-z]+){2,}\b)
             | (?P<url>   (http|https|ftp|mailto)\:[^\s'\"]+\S)
             | (?P<email> [-\w._+]+\@[\w.-]+)
 
-            # Lists, divs, spans
+            # Lists, divs, spans and inline objects
             | (?P<li>    ^\s+[\*\#]\s+)
             | (?P<pre>   \{\{\{|\s*\}\}\})
             | (?P<inl>   \{\{([^\s\|]+)(?:\s*\|\s*([^\]]+)|)\}\})
@@ -635,7 +642,7 @@ class Page:
         except IOError, err:
             if err.errno == errno.ENOENT:
                 if default is None:
-                    default = '//[[%s|Describe %s|action=edit]]//' % (self.page_name, self.page_name)
+                    default = '//[[?a=edit&q=%s|Describe %s]]//' % (self.page_name, self.page_name)
                 return default
             if err.errno == errno.EISDIR:
                 return self.format_dir()
@@ -644,13 +651,13 @@ class Page:
     def format_dir(self):
         out = '== '
         pathname = ''
-        for dirname in self.page_name.split('/'):
+        for dirname in self.page_name.strip('/').split('/'):
             pathname = (pathname + '/' + dirname) if pathname else dirname
             out += '[[' + pathname + '|' + dirname + ']]/'
         out += ' ==\n'
  
         for filename in page_list(self._filename(), file_re):
-            if img_re.match(filename):
+            if image_re.match(filename):
                 if image_maxwidth:
                     maxwidth_arg = ' | maxwidth=' + str(image_maxwidth)
                 out += '{{' + self.page_name + '/' + filename + ' | ' + humanlink(filename) + maxwidth_arg + ' | class=thumbleft}}\n'
@@ -699,9 +706,9 @@ class Page:
     def can_read(self):
         return self.can("read", True)
 
-    def send_naked(self):
+    def send_naked(self, kvargs=None):
         if self.can_read():
-            WikiFormatter(self.get_raw_body()).print_html()
+            WikiFormatter(self.get_raw_body(), kvargs).print_html()
         else:
             send_guru("Read access denied by ACLs", "notice")
 
@@ -794,6 +801,9 @@ class Page:
                 os.remove(name)
             except OSError, err:
                 if err.errno != errno.ENOENT: raise err
+        path = os.path.split(name)[0]
+        if not os.path.exists(path):
+            os.makedirs(path)
         os.rename(tmp_filename, name)
 
     def save(self, newdata, changelog):