#!/usr/bin/env python

"""
jemdoc: light markup, see http://jemdoc.jaboc.net/.

version 0.3.3, November 2007.
"""

# Copyright (C) 2007 Jacob Mattingley.
#
# This file is part of jemdoc.
#
# jemdoc is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# jemdoc is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.

import sys
import os
import re
import time
import StringIO

class controlstruct(object):
    def __init__(self, infile, outfile=None, conf=None):
        self.inf = infile
        self.outf = outfile
        self.conf = conf
        self.linenum = 0

# better checking of arguments?
def showhelp():
    a = """Usage: jemdoc [OPTIONS] [SOURCEFILE] 
    Produces html markup from a jemdoc SOURCEFILE.

    Most of the time you can use jemdoc without any additional flags.
    For example, typing

        jemdoc index.jemdoc

    will produce an index.html, using a default configuration.  You can
    change the output file by using -o OUTFILE, for example

        jemdoc -o html/main.html index.jemdoc

    Some configuration options can be overridden by specifying a
    configuration file.  You can use

        jemdoc --show-config

    to print a sample configuration file (which includes all of the
    default options). Any or all of the configuration [blocks] can be
    overwritten by including them in a configuration file, and running,
    for example,

        jemdoc -c mywebsite.conf index.jemdoc 

    See http://jemdoc.jaboc.net/ for more details.
    """
    b = ''
    for l in a.splitlines(True):
        if l.startswith(' '*4):
            b += l[4:]
        else:
            b += l

    print b

def standardconf():
    a = """[firstbit]
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
      "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    <head>
    <meta name="generator" content="jemdoc, see http://jemdoc.jaboc.net/" />
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    
    [defaultcss]
    <link rel="stylesheet" href="jemdoc.css" type="text/css" />
    
    [windowtitle]
    # used in header for window title.
    <title>|</title>
    
    [doctitle]
    # used at top of document.
    <div id="toptitle">
    <h1>|</h1>
    
    [subtitle]
    <div id="subtitle">|</div>
    
    [doctitleend]
    </div>
    
    [bodystart]
    </head>
    <body>
    
    [menustart]
    <table summary="Table for page layout.">
    <tr valign="top">
    <td id="layout-menu">
    
    [menuend]
    </td>
    <td>
    <div id="layout-content">
    
    [menucategory]
    <div class="menu-category">|</div>
    
    [menuitem]
    <div><a href="|1">|2</a></div>
    
    [currentmenuitem]
    <div><a href="|1" class="current">|2</a></div>
    
    [nomenu]
    <div id="layout-content">
    
    [css]
    <link rel="stylesheet" href="|" type="text/css" />
    
    [menulastbit]
    </div>
    </td>
    </tr>
    </table>
    
    [nomenulastbit]
    </div>
    
    [bodyend]
    </body>
    </html>
    
    [infoblock]
    <div class="infoblock">
    
    [codeblock]
    <div class="codeblock">
    
    [blocktitle]
    <div class="blocktitle">|</div>
    
    [infoblockcontent]
    <div class="blockcontent">
    
    [codeblockcontent]
    <div class="blockcontent"><pre>
    
    [codeblockend]
    </pre></div></div>
    
    [infoblockend]
    </div></div>
    
    [footerstart]
    <div id="footer">
    <div id="footer-text">
    
    [footerend]
    </div>
    </div>
    
    [lastupdated]
    Page generated |, by <a href="http://jemdoc.jaboc.net/">jemdoc</a>.
    """
    b = ''
    for l in a.splitlines(True):
        if l.startswith('    '):
            b += l[4:]
        else:
            b += l

    return b

class JandalError(Exception):
    pass

def raisejandal(msg, line=0):
    if line == 0:
        s = "%s" % msg
    else:
        s = "line %d: %s" % (line, msg)
    raise JandalError(s)

def readnoncomment(f):
    l = f.readline()
    if l == '':
        return l
    elif l[0] == '#': # jem: be a little more generous with the comments we accept?
        return readnoncomment(f)
    else:
        return l.rstrip() + '\n' # leave just one \n and no spaces etc.

def parseconf(cns):
    syntax = {}
    warn = False # jem. make configurable?
    # manually add the defaults as a file handle.
    fs = [StringIO.StringIO(standardconf())]
    for sname in cns:
        fs.append(open(sname))

    for f in fs:
        while pc(controlstruct(f)) != '':
            l = readnoncomment(f)
            r = re.match(r'\[(.*)\]\n', l)

            if r:
                tag = r.group(1)

                s = ''
                l = readnoncomment(f)
                while l not in ('\n', ''):
                    s += l
                    l = readnoncomment(f)

                syntax[tag] = s

        f.close()

    return syntax

def insertmenuitems(f, mname, current, prefix):
    m = open(mname)
    while pc(controlstruct(m)) != '':
        l = readnoncomment(m)
        l = l.strip()
        if l == '':
            continue

        r = re.match(r'\s*(.*?)\s*\[(.*)\]', l)

        if r: # then we have a link.
            link = r.group(2)
            # Don't use prefix if we have an absolute link.
            if '://' not in r.group(2):
                link = prefix + allreplace(link)

            if r.group(2) == current: 
                hb(f.outf, f.conf['currentmenuitem'], link, br(r.group(1)))
            else:
                hb(f.outf, f.conf['menuitem'], link, br(r.group(1)))

        else: # menu category.
            hb(f.outf, f.conf['menucategory'], br(l))

    m.close()

def out(f, s):
    f.write(s)

def hb(f, tag, content1, content2=None):
    """Writes out a halfblock (hb)."""
    if content2 is None:
        out(f, re.sub(r'\|', content1, tag))
    else:
        r = re.sub(r'\|1', content1, tag)
        r = re.sub(r'\|2', content2, r)
        out(f, r)

def pc(f, ditchcomments=True):
    """Peeks at next character in the file."""
    # Should only be used to look at the first character of a new line.
    c = f.inf.read(1)
    if c: # only undo forward movement if we're not at the end.
        #if c == '#': # interpret comment lines as blank.
        #    return '\n'
        if ditchcomments and c == '#':
            nl(f)

        if c in ' \t':
            return pc(f)

        f.inf.seek(-1, 1)

    return c

def nl(f, withcount=False, codemode=False):
    """Get input file line."""
    s = f.inf.readline()
    f.linenum += 1
    if not codemode:
        # remove any special characters - assume they were checked by pc()
        # before we got here.
        # remove any trailing comments.
        s = s.lstrip(' \t')
        s = re.sub(r'\s*(?<!\\)#.*', '', s)

    if withcount:
        if s[0] == '.':
            m = r'\.'
        else:
            m = s[0]

        r = re.match('(%s+) ' % m, s)
        if not r:
            raise SyntaxError("couldn't handle the jandal (code 12039) on line"
                              " %d" % linenum)

        if not codemode:
            s = s.lstrip('-.=:')

        return (s, len(r.group(1)))
    else:
        if not codemode:
            s = s.lstrip('-.=:')

        return s

def np(f, withcount=False):
    """Gets the next paragraph from the input file."""
    # New paragraph markers signalled by characters in following tuple.
    if withcount:
        (s, c) = nl(f, withcount)
    else:
        s = nl(f)

    while pc(f) not in ('\n', '-', '.', ':', '', '=', '~', '{'):
        s += nl(f)

    while pc(f) == '\n':
        nl(f) # burn blank line.

    # in both cases, ditch the trailing \n.
    if withcount:
        return (s[:-1], c)
    else:
        return s[:-1]

def quote(s, percents=True):
    if percents:
        return re.sub(r"""[\\*/+"'<>&$%\.~[\]-]""", r'\\\g<0>', s)
    else:
        return re.sub(r"""[\\*/+"'<>&$\.~[\]-]""", r'\\\g<0>', s)

def replacequoted(b):
    """Quotes {{raw html}} sections. Insert a backslash right before the end
    with &bs;, an illegal html character."""
    r = re.compile(r'\{\{(.*?)\}\}', re.M + re.S)
    m = r.search(b)
    while m:
        qb = quote(m.group(1), True)

        b = b[:m.start()] + qb + b[m.end():]

        m = r.search(b, m.start())

    r = re.compile(r'(?<!\\)\$(.*?)(?<!\\)\$', re.M + re.S)
    m = r.search(b)
    if m:
        print 'warning: $sections$ are deprecated.'

    # likewise replace %sections% as +{{sections}}+.
    r = re.compile(r'(?<!\\)%(.*?)(?<!\\)%', re.M + re.S)
    m = r.search(b)
    while m:
        qb = '+' + quote(m.group(1)) + '+'

        b = b[:m.start()] + qb + b[m.end():]

        m = r.search(b, m.start())

    return b

def replaceimages(b):
    # works with [img{width}{height}{alttext} location caption].
    r = re.compile(r'(?<!\\)\[img((?:\{.*\}){,3})\s(.*?)(?:\s(.*?))?(?<!\\)\]',
                   re.M + re.S)
    m = r.search(b)
    s = re.compile(r'{(.*?)}', re.M + re.S)
    while m:
        m1 = list(s.findall(m.group(1)))
        m1 += ['']*(3 - len(m1))

        bits = []
        link = m.group(2).strip()
        bits.append(r'src=\"%s\"' % quote(link))

        if m1[0]:
            if m1[0].isdigit():
                s = m1[0] + 'px'
            else:
                s = m1[0]
            bits.append(r'width=\"%s\"' % quote(s))
        if m1[1]:
            if m1[1].isdigit():
                s = m1[1] + 'px'
            else:
                s = m1[1]
            bits.append(r'height=\"%s\"' % quote(s))
        if m1[2]:
            bits.append(r'alt=\"%s\"' % quote(m1[2]))

        b = b[:m.start()] + r'<img %s />' % " ".join(bits) + b[m.end():]

        m = r.search(b, m.start())

    return b

def replacelinks(b):
    # works with [link.html new link style].
    r = re.compile(r'(?<!\\)\[(.*?)(?:\s(.*?))?(?<!\\)\]', re.M + re.S)
    m = r.search(b)
    while m:
        m1 = m.group(1).strip()

        if '@' in m1 and not m1.startswith('mailto:'):
            link = 'mailto:' + m1
        else:
            link = m1

        link = quote(link)

        if m.group(2):
            linkname = m.group(2).strip()
        else:
            # remove any mailto before labelling.
            linkname = re.sub('^mailto:', '', link)

        b = b[:m.start()] + r'<a href=\"%s\">%s<\/a>' % (link, linkname) + b[m.end():]

        m = r.search(b, m.start())

    return b

def br(b):
    """Does simple text replacements on a block of text. ('block replacements')"""
    # Deal with literal backspaces.
    b = re.sub(r'\\\\', 'jemLITerl33talBS', b)

    # Deal with {{html embedding}}.
    b = replacequoted(b)

    b = allreplace(b)

    # First do the URL thing.
    b = b.lstrip('-. \t') # remove leading spaces, tabs, dashes, dots.
    b = replaceimages(b)
    b = replacelinks(b)

    # Deal with /italics/ first because the '/' in other tags would otherwise
    # interfere.
    r = re.compile(r'(?<!\\)/(.*?)(?<!\\)/', re.M + re.S)
    b = re.sub(r, r'<i>\1</i>', b)

    # Deal with *bold*.
    r = re.compile(r'(?<!\\)\*(.*?)(?<!\\)\*', re.M + re.S)
    b = re.sub(r, r'<b>\1</b>', b)

    # Deal with +monospace+.
    r = re.compile(r'(?<!\\)\+(.*?)(?<!\\)\+', re.M + re.S)
    b = re.sub(r, r'<tt>\1</tt>', b)

    # Deal with "double quotes".
    r = re.compile(r'(?<!\\)"(.*?)(?<!\\)"', re.M + re.S)
    b = re.sub(r, r'&ldquo;\1&rdquo;', b)

    # Deal with left quote `.
    r = re.compile(r"(?<!\\)`", re.M + re.S)
    b = re.sub(r, r'&lsquo;', b)

    # Deal with apostrophe '.
    r = re.compile(r"(?<!\\)'", re.M + re.S)
    b = re.sub(r, r'&rsquo;', b)

    # Deal with em dash ---.
    r = re.compile(r"(?<!\\)---", re.M + re.S)
    b = re.sub(r, r'&mdash;', b)

    # Deal with en dash --.
    r = re.compile(r"(?<!\\)--", re.M + re.S)
    b = re.sub(r, r'&ndash;', b)

    # Deal with ellipsis ....
    r = re.compile(r"(?<!\\)\.\.\.", re.M + re.S)
    b = re.sub(r, r'&hellip;', b)

    # Deal with non-breaking space ~.
    r = re.compile(r"(?<!\\)~", re.M + re.S)
    b = re.sub(r, r'&nbsp;', b)

    # Deal with registered trademark \R.
    r = re.compile(r"(?<!\\)\\R", re.M + re.S)
    b = re.sub(r, r'&reg;', b)

    # Deal with copyright \C.
    r = re.compile(r"(?<!\\)\\C", re.M + re.S)
    b = re.sub(r, r'&copy;', b)

    # Deal with line break.
    r = re.compile(r"(?<!\\)\\n", re.M + re.S)
    b = re.sub(r, r'<br />', b)

    ## Deal with zero width \_.
    #r = re.compile(r"(?<!\\)\\_", re.M + re.S)
    #b = re.sub(r, '', b)

    # Second to last, remove any remaining quoting backslashes.
    b = re.sub(r'\\(?!\\)', '', b)

    # Deal with literal backspaces.
    b = re.sub('jemLITerl33talBS', r'\\', b)

    return b

def allreplace(b):
    """Replacements that should be done on everything."""
    r = re.compile(r"(?<!\\)&", re.M + re.S)
    b = re.sub(r, r'&amp;', b)

    r = re.compile(r"(?<!\\)>", re.M + re.S)
    b = re.sub(r, r'&gt;', b)

    r = re.compile(r"(?<!\\)<", re.M + re.S)
    b = re.sub(r, r'&lt;', b)

    return b

def pyint(f, l):
    l = l.rstrip()
    l = allreplace(l)

    r = re.compile(r'(#.*)')
    l = r.sub(r'<span class = "comment">\1</span>', l)

    if l.startswith('&gt;&gt;&gt;'):
        hb(f, '<span class="pycommand">|</span>\n', l)
    else:
        out(f, l + '\n')

def putbsbs(l):
    for i in range(len(l)):
        l[i] = '\\b' + l[i] + '\\b'

    return l

def gethl(lang):
    # disable comments by default, by choosing unlikely regex.
    d = {'statement':[], 'commentstart':'######%%%%%', 'operator':[], 'builtin':[],
         'error':[]}
    if lang in ('py', 'python'):
        d['statement'] = putbsbs(['break', 'continue', 'del', 'except', 'exec',
                                 'finally', 'pass', 'print', 'raise', 'return',
                                 'try', 'with', 'global', 'assert', 'lambda',
                                 'yield', 'def', 'class', 'for', 'while', 'if',
                                 'elif', 'else', 'import', 'from', 'as'])
        d['operator'] = putbsbs(['and', 'in', 'is', 'not', 'or'])
        d['builtin'] = putbsbs(['True', 'False', 'set', 'open', 'frozenset',
                               'enumerate', 'object', 'hasattr', 'getattr',
                               'filter', 'eval', 'zip', 'vars', 'unicode',
                               'type', 'str', 'repr', 'round'])
        d['error'] = putbsbs(['\w*Error',])
        d['commentstart'] = '#'
    elif lang == 'sh':
        d['statement'] = putbsbs(['cd', 'ls', 'sudo'])
        d['operator'] = ['&gt;']
        d['builtin'] = putbsbs(['curl', 'wget', '(?<!\.)tar(?!\.)'])
        d['commentstart'] = '#'

    return d

def language(f, l, hl):
    # also handle strings and comments.
    l = l.rstrip()
    if l.startswith(hl['commentstart']):
        hb(f, '<span class="comment">|</span>\n', allreplace(l))
    else:
        l = allreplace(l)
        # handle strings.
        r = re.compile(r'(".*?")')
        l = r.sub(r'<span CLASS="string">\1</span>', l)
        r = re.compile(r"('.*?')")
        l = r.sub(r'<span CLASS="string">\1</span>', l)

        # handle comments.
        r = re.compile(hl['commentstart'])
        l = r.sub(r'<span CLASS="comment">\1</span>', l)

        if hl['statement']:
            r = re.compile('(' + '|'.join(hl['statement']) + ')')
            l = r.sub(r'<span CLASS="statement">\1</span>', l)

        if hl['operator']:
            r = re.compile('(' + '|'.join(hl['operator']) + ')')
            l = r.sub(r'<span CLASS="operator">\1</span>', l)

        if hl['builtin']:
            r = re.compile('(' + '|'.join(hl['builtin']) + ')')
            l = r.sub(r'<span CLASS="builtin">\1</span>', l)

        if hl['error']:
            r = re.compile('(' + '|'.join(hl['error']) + ')')
            l = r.sub(r'<span CLASS="error">\1</span>', l)

        l = re.sub('CLASS', 'class', l)

        out(f, l + '\n')

def dashlist(f):
    level = 0

    while pc(f) == '-':
        (s, newlevel) = np(f, True)

        # first adjust list number as appropriate.
        if newlevel > level:
            for i in range(newlevel - level):
                if newlevel > 1:
                    out(f.outf, '\n')
                out(f.outf, '<ul>\n<li>')
        elif newlevel < level:
            for i in range(level - newlevel):
                out(f.outf, '</li>\n</ul>\n</li><li>')
        else:
            out(f.outf, '</li>\n<li>')

        out(f.outf, br(s))
        level = newlevel

    for i in range(level):
        out(f.outf, '</li>\n</ul>\n')

def dotlist(f):
    level = 0

    while pc(f) == '.':
        (s, newlevel) = np(f, True)

        # first adjust list number as appropriate.
        if newlevel > level:
            for i in range(newlevel - level):
                if newlevel > 1:
                    out(f.outf, '\n')
                out(f.outf, '<ol>\n<li>')
        elif newlevel < level:
            for i in range(level - newlevel):
                out(f.outf, '</li>\n</ol>\n</li><li>')
        else:
            out(f.outf, '</li>\n<li>')

        out(f.outf, br(s))
        level = newlevel

    for i in range(level):
        out(f.outf, '</li>\n</ol>\n')

def colonlist(f):
    out(f.outf, '<dl>\n')
    while pc(f) == ':':
        s = np(f)
        r = re.compile(r'\s*{(.*?)(?<!\\)}(.*)', re.M + re.S)
        g = re.match(r, s)

        if not g or len(g.groups()) != 2:
            raise SyntaxError("couldn't handle the jandal (invalid deflist "
                             "format) on line %d" % linenum)
        # split into definition / non-definition part.
        defpart = g.group(1)
        rest = g.group(2)

        hb(f.outf, '<dt>|</dt>\n', br(defpart))
        hb(f.outf, '<dd>|</dd>\n', br(rest))

    out(f.outf, '</dl>\n')

def codeblock(f, g):
    if g[1] == 'raw':
        raw = True
    else:
        raw = False
        out(f.outf, f.conf['codeblock'])
        if g[0]:
            hb(f.outf, f.conf['blocktitle'], g[0])
        out(f.outf, f.conf['codeblockcontent'])

    # Now we are handling code.
    # Handle \~ and ~ differently.
    while 1: # wait for EOF.
        l = nl(f, codemode=True)
        if not l:
            break
        elif l.startswith('~'):
            break
        elif l.startswith('\\~'):
            l = l[1:]
        elif l.startswith('\\{'):
            l = l[1:]

        # jem revise pyint out of the picture.
        if g[1] == 'pyint':
            pyint(f.outf, l)
        else:
            if raw:
                out(f.outf, l)
            else:
                language(f.outf, l, gethl(g[1]))

    if not raw:
        out(f.outf, f.conf['codeblockend'])

def procfile(f):
    linenum = 0

    menu = None
    footer = True
    nodefaultcss = False
    css = []
    if pc(f, False) == '#':
        l = f.inf.readline()
        linenum += 1
        if l.startswith('# jemdoc: '):
            l = l[len('# jemdoc: '):]
            a = l.split(',')
            # jem only handle one argument for now.
            for b in a:
                b = b.strip()
                if b.startswith('menu'):
                    sidemenu = True
                    r = re.compile(r'(?<!\\){(.*?)(?<!\\)}', re.M + re.S)
                    g = re.findall(r, b)
                    if len(g) > 3 or len(g) < 2:
                        raise SyntaxError('sidemenu error on line %d' % linenum)

                    if len(g) == 2:
                        menu = (f, g[0], g[1], '')
                    else:
                        menu = (f, g[0], g[1], g[2])

                elif b.startswith('nodate'):
                    footer = False

                elif b.startswith('nodefaultcss'):
                    nodefaultcss = True

                elif b.startswith('addcss'):
                    r = re.compile(r'(?<!\\){(.*?)(?<!\\)}', re.M + re.S)
                    css += re.findall(r, b)

    # Get the file started with the firstbit.
    out(f.outf, f.conf['firstbit'])

    if not nodefaultcss:
        out(f.outf, f.conf['defaultcss'])

    # Add per-file css lines here.
    for i in range(len(css)):
        if '.' not in css[i]:
            css[i] += '.css'

    for x in css:
        hb(f.outf, f.conf['css'], x)

    # Look for a title.
    if pc(f) == '=': # don't check exact number f.outf '=' here jem.
        t = br(nl(f))[:-1]
        hb(f.outf, f.conf['windowtitle'], t)
        out(f.outf, f.conf['bodystart'])

    else:
        out(f.outf, f.conf['bodystart'])
        t = None

    if menu:
        out(f.outf, f.conf['menustart'])
        insertmenuitems(*menu)
        out(f.outf, f.conf['menuend'])
    else:
        out(f.outf, f.conf['nomenu'])

    if t is not None:
        hb(f.outf, f.conf['doctitle'], t)

        # Look for a subtitle.
        if pc(f) != '\n':
            hb(f.outf, f.conf['subtitle'], br(np(f)))

        hb(f.outf, f.conf['doctitleend'], t)

    infoblock = False
    imgblock = False
    while 1: # wait for EOF.
        p = pc(f)

        if p == '':
            break

        # look for lists.
        elif p == '-':
            dashlist(f)

        elif p == '.':
            dotlist(f)

        elif p == ':':
            colonlist(f)

        # look for titles.
        elif p == '=':
            (s, c) = nl(f, True)
            # trim trailing \n.
            s = s[:-1]
            hb(f.outf, '<h%d>|</h%d>\n' % (c, c), br(s))

        # look for comments.
        elif p == '#':
            nl(f)

        elif p == '\n':
            nl(f)

        # look for blocks.
        elif p == '~':
            nl(f)
            if infoblock:
                out(f.outf, f.conf['infoblockend'])
                infoblock = False
                nl(f)
                continue
            elif imgblock:
                out(f.outf, '</td></tr></table>\n')
                imgblock = False
                nl(f)
                continue
            else:
                if pc(f) == '{':
                    l = allreplace(nl(f))
                    r = re.compile(r'(?<!\\){(.*?)(?<!\\)}', re.M + re.S)
                    g = re.findall(r, l)
                else:
                    g = []

                # process jemdoc markup in titles.
                if len(g) >= 1:
                    g[0] = br(g[0])

                if len(g) in (0, 1): # info block.
                    out(f.outf, f.conf['infoblock'])
                    infoblock = True
                    
                    if len(g) == 1: # info block.
                        hb(f.outf, f.conf['blocktitle'], g[0])

                    out(f.outf, f.conf['infoblockcontent'])

                elif len(g) == 2:
                    codeblock(f, g)

                elif len(g) >= 4 and g[1] == 'img_left':
                    # handles
                    # {}{img_left}{source}{alttext}{width}{height}{linktarget}.
                    g += ['']*(7 - len(g))
                    
                    if g[4].isdigit():
                        g[4] += 'px'

                    if g[5].isdigit():
                        g[5] += 'px'

                    out(f.outf, '<table><tr><td>\n')
                    if g[6]:
                        out(f.outf, '<a href="%s">' % g[6])
                    out(f.outf, '<img src="%s"' % g[2])
                    out(f.outf, ' alt="%s"' % g[3])
                    if g[4]:
                        out(f.outf, ' width="%s"' % g[4])
                    if g[5]:
                        out(f.outf, ' height="%s"' % g[5])
                    out(f.outf, ' />')
                    if g[6]:
                        out(f.outf, '</a>')
                    out(f.outf, '&nbsp;</td>\n<td align="left">')
                    imgblock = True

                else:
                    raise JandalError("couldn't handle block", linenum)

        else:
            s = br(np(f))
            if s:
                hb(f.outf, '<p>|</p>\n', s)

    if footer:
        s = time.strftime('%F %R:%S %Z', time.localtime(time.time()))
        out(f.outf, f.conf['footerstart'])
        hb(f.outf, f.conf['lastupdated'], s)
        out(f.outf, f.conf['footerend'])

    if menu:
        out(f.outf, f.conf['menulastbit'])
    else:
        out(f.outf, f.conf['nomenulastbit'])

    out(f.outf, f.conf['bodyend'])

    if f.outf is not sys.stdout:
        f.outf.close()

def main():
    if len(sys.argv) == 1 or sys.argv[1] in ('--help', '-h'):
        showhelp()
        raise SystemExit
    if sys.argv[1] == '--show-config':
        print standardconf()
        raise SystemExit

    outoverride = False
    confoverride = False
    outname = None
    confnames = []
    for i in range(1, len(sys.argv), 2):
        if sys.argv[i] == '-o':
            if outoverride:
                raise RuntimeError("only one output file / directory, please")
            outname = sys.argv[i+1]
            outoverride = True
        elif sys.argv[i] == '-c':
            if confoverride:
                raise RuntimeError("only one config file, please")
            confnames.append(sys.argv[i+1])
            confoverride = True
        elif sys.argv[i].startswith('-'):
            raise RuntimeError('unrecognised argument %s, try --help' % sys.argv[i])
        else:
            break

    conf = parseconf(confnames)

    innames = []
    for j in range(i, len(sys.argv)):
        # First, if not a file and no dot, try opening .jemdoc. Otherwise, fall back
        # to just doing exactly as asked.
        inname = sys.argv[j]
        if not os.path.isfile(inname) and '.' not in inname:
            inname += '.jemdoc'

        innames.append(inname)

    if outname is not None and not os.path.isdir(outname) and len(innames) > 1:
        raise RuntimeError('cannot handle one outfile with multiple infiles')

    for inname in innames:
        if outname is None:
            thisout = re.sub(r'.jemdoc$', '', inname) + '.html'
        elif os.path.isdir(outname):
            # if directory, prepend directory to automatically generated name.
            thisout = outname + re.sub(r'.jemdoc$', '', inname) + '.html'
        else:
            thisout = outname

        infile = open(inname)
        outfile = open(thisout, 'w')

        f = controlstruct(infile, outfile, conf)
        procfile(f)

#
if __name__ == '__main__':
    main()
