#!/usr/bin/env python
"""jemdoc version 0.4.0, 2007-12-02."""
# 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 .
import sys
import os
import re
import time
import StringIO
def info():
print __doc__
print 'Platform is ' + sys.platform + '.'
print 'Using Python version %s, located at %s.' % (sys.version[:5], sys.executable)
class controlstruct(object):
def __init__(self, infile, outfile=None, conf=None, inname=None):
self.inname = inname
self.inf = infile
self.outf = outfile
self.conf = conf
self.linenum = 0
self.otherfiles = []
def pushfile(self, newfile):
self.otherfiles.insert(0, self.inf)
self.inf = open(newfile)
def nextfile(self):
self.inf.close()
self.inf = self.otherfiles.pop(0)
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
will produce an index.html from index.jemdoc, using a default
configuration.
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
You can view version and installation details with
jemdoc --version
See http://jemdoc.jaboc.net/ for many 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]
[defaultcss]
[windowtitle]
# used in header for window title.
|
[doctitle]
# used at top of document.
[footerstart]
[lastupdated]
Page generated |, by jemdoc.
[sourcelink]
(source)
"""
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 menu item.
link = r.group(2)
# Don't use prefix if we have an absolute link.
if '://' not in r.group(2):
link = prefix + allreplace(link)
# replace spaces with nbsps.
menuitem = re.sub(r'(?&$%\.~[\]-]""", r'\\\g<0>', s)
def replacequoted(b):
"""Quotes {{raw html}} 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 replacepercents(b):
# replace %sections% as +{{sections}}+. Do not replace if within a link.
r = re.compile(r'(?' % " ".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<\/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)
b = b.lstrip('-. \t') # remove leading spaces, tabs, dashes, dots.
b = replaceimages(b) # jem not sure if this is still used.
b = replacelinks(b)
b = replacepercents(b)
# Deal with /italics/ first because the '/' in other tags would otherwise
# interfere.
r = re.compile(r'(?\1', b)
# Deal with *bold*.
r = re.compile(r'(?\1', b)
# Deal with +monospace+.
r = re.compile(r'(?\1', b)
# Deal with "double quotes".
r = re.compile(r'(?', b)
# Deal with paragraph break. Caution! Should only use when we're already in
# a paragraph.
r = re.compile(r"(?
', b)
## Deal with zero width \_.
#r = re.compile(r"(?", re.M + re.S)
b = re.sub(r, r'>', b)
r = re.compile(r"(?\1', l)
if l.startswith('>>>'):
hb(f, '|\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':[], 'strings':False}
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'] = '#'
d['strings'] = True
elif lang == 'sh':
d['statement'] = putbsbs(['cd', 'ls', 'sudo'])
d['operator'] = ['>']
d['builtin'] = putbsbs(['curl', 'wget', '(?|\n', allreplace(l))
else:
l = allreplace(l)
# handle strings.
if hl['strings']:
r = re.compile(r'(".*?")')
l = r.sub(r'\1', l)
r = re.compile(r"('.*?')")
l = r.sub(r'\1', l)
# handle comments.
r = re.compile(hl['commentstart'])
l = r.sub(r'\1', l)
if hl['statement']:
r = re.compile('(' + '|'.join(hl['statement']) + ')')
l = r.sub(r'\1', l)
if hl['operator']:
r = re.compile('(' + '|'.join(hl['operator']) + ')')
l = r.sub(r'\1', l)
if hl['builtin']:
r = re.compile('(' + '|'.join(hl['builtin']) + ')')
l = r.sub(r'\1', l)
if hl['error']:
r = re.compile('(' + '|'.join(hl['error']) + ')')
l = r.sub(r'\1', 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, '
\n
')
elif newlevel < level:
for i in range(level - newlevel):
out(f.outf, '
\n
\n
')
else:
out(f.outf, '
\n
')
out(f.outf, '
' + br(s) + '
')
level = newlevel
for i in range(level):
out(f.outf, '
\n\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, '\n
')
elif newlevel < level:
for i in range(level - newlevel):
out(f.outf, '
\n\n
')
else:
out(f.outf, '
\n
')
out(f.outf, '
' + br(s) + '
')
level = newlevel
for i in range(level):
out(f.outf, '
\n\n')
def colonlist(f):
out(f.outf, '
\n')
while pc(f) == ':':
s = np(f)
r = re.compile(r'\s*{(.*?)(?|\n', br(defpart))
hb(f.outf, '
|
\n', br(rest))
out(f.outf, '
\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])
if g[1] == 'jemdoc':
out(f.outf, f.conf['codeblockcontenttt'])
else:
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)
elif g[1] == 'jemdoc':
# doing this more nicely needs python 2.5.
for x in ('#', '~', '>>>', '\~', '{'):
if str(l).lstrip().startswith(x):
out(f.outf, '
')
out(f.outf, l + '
')
break
else:
for x in (':', '.', '-'):
if str(l).lstrip().startswith(x):
out(f.outf, ' ' + prependnbsps(l))
break
else:
if str(l).lstrip().startswith('='):
out(f.outf, prependnbsps(l) + ' ')
else:
out(f.outf, l)
else:
if l.startswith('\\#include{') or l.startswith('\\#includeraw{'):
out(f.outf, l[1:])
elif l.startswith('#') and doincludes(f, l[1:]):
continue
else:
language(f.outf, l, gethl(g[1]))
if not raw:
if g[1] == 'jemdoc':
out(f.outf, f.conf['codeblockendtt'])
else:
out(f.outf, f.conf['codeblockend'])
def prependnbsps(l):
g = re.search('(^ *)(.*)', l).groups()
return g[0].replace(' ', ' ') + g[1]
def procfile(f):
f.linenum = 0
menu = None
showsourcelink = False
showlastupdated = True
showlastupdatedtime = True
nodefaultcss = False
css = []
title = None
while pc(f, False) == '#':
l = f.inf.readline()
f.linenum += 1
if doincludes(f, l[1:]):
continue
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'(? 3 or len(g) < 2:
raise SyntaxError('sidemenu error on line %d' % f.linenum)
if len(g) == 2:
menu = (f, g[0], g[1], '')
else:
menu = (f, g[0], g[1], g[2])
elif b.startswith('nodate'):
showlastupdated = False
elif b.startswith('notime'):
showlastupdatedtime = False
elif b.startswith('showsource'):
showsourcelink = True
elif b.startswith('nodefaultcss'):
nodefaultcss = True
elif b.startswith('addcss'):
r = re.compile(r'(?)|( ) *', ' ', t)
else:
t = None
if title:
hb(f.outf, f.conf['windowtitle'], title)
out(f.outf, f.conf['bodystart'])
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, '|\n' % (c, c), br(s))
# look for comments.
elif p == '#':
l = 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, '\n')
imgblock = False
nl(f)
continue
else:
if pc(f) == '{':
l = allreplace(nl(f))
r = re.compile(r'(?= 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, '
')
imgblock = True
else:
raise JandalError("couldn't handle block", f.linenum)
else:
s = br(np(f))
if s:
hb(f.outf, '
|
\n', s)
if showlastupdated or showsourcelink:
out(f.outf, f.conf['footerstart'])
if showlastupdated:
if showlastupdatedtime:
ts = '%Y-%m-%d %H:%M:%S %Z'
else:
ts = '%Y-%m-%d'
s = time.strftime(ts, time.localtime(time.time()))
hb(f.outf, f.conf['lastupdated'], s)
if showsourcelink:
hb(f.outf, f.conf['sourcelink'], f.inname)
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
if sys.argv[1] == '--version':
info()
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, inname)
procfile(f)
#
if __name__ == '__main__':
main()